Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion pkg/api/handlers/compat/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ func convertLibpodNetworktoDockerNetwork(runtime *libpod.Runtime, statuses []abi
ipamConfig := dockerNetwork.IPAMConfig{
Subnet: subnet,
Gateway: gateway,
// TODO add range
}
if ipRange, ok := leaseRangeToIPRangePrefix(sub.LeaseRange); ok {
ipamConfig.IPRange = ipRange
}
ipamConfigs = append(ipamConfigs, ipamConfig)
}
Expand Down Expand Up @@ -506,3 +508,53 @@ func Prune(w http.ResponseWriter, r *http.Request) {
}
utils.WriteResponse(w, http.StatusOK, response{NetworksDeleted: prunedNetworks})
}

// leaseRangeToIPRangePrefix converts a LeaseRange back to a CIDR prefix for the Docker
// compat API. This is the reverse of the conversion done in createNetwork, where an
// IPRange CIDR (e.g. "192.168.0.128/25") is expanded to [FirstIP, LastIP] via
// FirstIPInSubnet/LastIPInSubnet. FirstIPInSubnet returns network+1 (first usable host),
// so StartIP is one greater than the network address. LastIPInSubnet returns the broadcast.
// Only succeeds for IPv4 ranges that form a valid aligned CIDR block.
func leaseRangeToIPRangePrefix(lr *nettypes.LeaseRange) (netip.Prefix, bool) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few thoughts on this!

First, this function could be a lot shorter and cleaner. I'm not a huge fan of using raw bitwise operations here; we can make the logic much more elegant and readable. Here is a quick pseudocode mockup of what I mean:

FUNCTION leaseRangeToIPRangePrefix(leaseRange):

    // 1. Initial Validation
    IF leaseRange IS NULL:
        RETURN Null, False
        
    startIP = ParseIPv4(leaseRange.StartIP)
    endIP = ParseIPv4(leaseRange.EndIP)
    
    // Ensure IPs are valid and mathematically start <= end
    IF startIP IS INVALID OR endIP IS INVALID OR startIP > endIP:
        RETURN Null, False

    // 2. Calculate the total number of IPs in the range
    startInt = ConvertTo32BitInteger(startIP)
    endInt = ConvertTo32BitInteger(endIP)
    
    totalIPs = (endInt - startInt) + 1

    // 3. Verify the range size is a valid subnet size
    // A valid subnet size must be a perfect power of 2 (1, 2, 4, 8, 16, etc.)
    // In binary, a perfect power of 2 has exactly one "1" bit.
    IF CountSetBits(totalIPs) != 1:
        RETURN Null, False

    // 4. Calculate the subnet mask / prefix length
    // The number of trailing zeros in the total IPs equals the host bits
    numberOfHostBits = CountTrailingZeros(totalIPs)
    networkPrefixLength = 32 - numberOfHostBits
    
    proposedPrefix = CreateSubnetPrefix(startIP, networkPrefixLength)

    // 5. Verify network boundary alignment
    // The start IP must perfectly match the base network address of the subnet
    IF proposedPrefix DOES NOT EQUAL ApplyNetworkMask(proposedPrefix):
        RETURN Null, False

    // 6. Success
    RETURN proposedPrefix, True

Second, we definitely need to add unit tests for this function.

Lastly (and this isn't a blocker), something in the back of my mind is screaming that this function actually belongs in libnetwork under container-libs. WDYT? @Luap99

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with having this code here, I don't think we will need it elsewhere. most of that libnetwork code is going into netavark now so it is not like we could resuse it from there.

if lr == nil || lr.StartIP == nil || lr.EndIP == nil {
return netip.Prefix{}, false
}
start4 := lr.StartIP.To4()
end4 := lr.EndIP.To4()
if start4 == nil || end4 == nil {
// IPv6 not handled: no lossless round-trip without storing the original CIDR
return netip.Prefix{}, false
}
startU := uint32(start4[0])<<24 | uint32(start4[1])<<16 | uint32(start4[2])<<8 | uint32(start4[3])
endU := uint32(end4[0])<<24 | uint32(end4[1])<<16 | uint32(end4[2])<<8 | uint32(end4[3])
if endU < startU {
return netip.Prefix{}, false
}
// For /32, FirstIPInSubnet returns the address unchanged; StartIP == EndIP.
if startU == endU {
startAddr, _ := netip.AddrFromSlice(start4)
return netip.PrefixFrom(startAddr.Unmap(), 32), true
}
// For all other subnets, FirstIPInSubnet increments the network address by 1,
// so recover the network address by subtracting 1 from StartIP.
if startU == 0 {
return netip.Prefix{}, false
}
netU := startU - 1
size := endU - netU + 1
// Verify size is a power of two (required for a valid CIDR block).
if size&(size-1) != 0 {
return netip.Prefix{}, false
}
// Verify the network address is aligned to the block size.
if netU&(size-1) != 0 {
return netip.Prefix{}, false
}
prefixBits := 32
for n := size; n > 1; n >>= 1 {
prefixBits--
}
netBytes := []byte{byte(netU >> 24), byte(netU >> 16), byte(netU >> 8), byte(netU)}
netAddr, _ := netip.AddrFromSlice(netBytes)
return netip.PrefixFrom(netAddr.Unmap(), prefixBits), true
}
6 changes: 6 additions & 0 deletions test/apiv2/35-networks.at
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,10 @@ t POST networks/netcon/connect Container=c1 200 OK
# cleanup
podman network rm -f netcon

# IPRange is populated in IPAM config when a network is created with --ip-range
podman network create --subnet 10.10.61.0/24 --ip-range 10.10.61.128/25 iprange-test
t GET networks/iprange-test 200 \
.IPAM.Config[0].IPRange="10.10.61.128/25"
podman network rm -f iprange-test

# vim: filetype=sh