Skip to content

compat: populate IPRange in IPAM config when listing networks#28396

Open
crawfordxx wants to merge 2 commits intocontainers:mainfrom
crawfordxx:compat-add-iprange-to-ipam-config
Open

compat: populate IPRange in IPAM config when listing networks#28396
crawfordxx wants to merge 2 commits intocontainers:mainfrom
crawfordxx:compat-add-iprange-to-ipam-config

Conversation

@crawfordxx
Copy link
Copy Markdown
Contributor

Summary

When converting a libpod network to a Docker-compatible network in
convertLibpodNetworktoDockerNetwork, the IPAMConfig.IPRange field
was left empty with a // TODO add range comment.

This PR resolves the TODO by reconstructing the CIDR prefix from the
LeaseRange stored on each subnet. The forward conversion (Docker
IPRange CIDR → LeaseRange{StartIP, EndIP}) already exists in the
createNetwork path. This adds the reverse direction:

  • Given StartIP and EndIP from LeaseRange, check whether they
    form a valid aligned IPv4 CIDR block (power-of-two size, aligned
    start address).
  • If so, return the corresponding netip.Prefix as IPAMConfig.IPRange.
  • Otherwise (non-CIDR-aligned ranges, IPv6) leave IPRange empty.

This means docker network inspect now shows the IPRange for
networks created with --ip-range (or the Docker compat API equivalent),
restoring round-trip fidelity for the IPAM configuration.

Fixes #28378

@github-actions github-actions bot added the kind/api-change Change to remote API; merits scrutiny label Mar 28, 2026
When converting a libpod network to a Docker-compat network, the
IPAMConfig.IPRange field was left empty (TODO). This field should
reflect the lease range configured on each subnet so that Docker
clients see the full network config on inspect.

The conversion from IPRange CIDR to LeaseRange is already done in
the createNetwork path (IPRange -> FirstIP/LastIP). This commit
adds the reverse: reconstruct the CIDR prefix from the stored
StartIP/EndIP for IPv4 networks where the range forms a valid
aligned CIDR block.

Fixes containers#28378

Signed-off-by: crawfordxx <crawfordxx@users.noreply.github.com>
@crawfordxx crawfordxx force-pushed the compat-add-iprange-to-ipam-config branch from 7c965d3 to a388d72 Compare March 29, 2026 04:08
@crawfordxx
Copy link
Copy Markdown
Contributor Author

The MacOS arm64 build failure appears to be a transient Cirrus CI issue — the change only touches pkg/api/handlers/compat/networks.go which has //go:build !remote && (linux || freebsd) at the top, so it is excluded from macOS compilation entirely. All Linux/cross-compilation builds passed.

@baude
Copy link
Copy Markdown
Member

baude commented Mar 30, 2026

@crawfordxx i was able to rerun the test in question. it passed. Ive now asked for all failed jobs to retry. thanks for the PR

@baude
Copy link
Copy Markdown
Member

baude commented Mar 30, 2026

ok, api test failures here now have been revealed.

FirstIPInSubnet returns network+1 (the first usable host) rather than
the network address itself, so the stored LeaseRange.StartIP is one
greater than the network address. The previous implementation compared
[StartIP, EndIP] = [network+1, broadcast] directly, treating them as
a CIDR block. For a /25 (128 hosts) this yields count=127, which is
not a power of two, causing the function to return false.

Fix by recovering the network address (StartIP - 1), then computing
the block size as EndIP - networkAddr + 1. This correctly handles all
aligned IPv4 ranges: /25 yields size=128, /24 yields size=256, etc.

The /32 case is handled separately since FirstIPInSubnet returns the
address unchanged (no increment) for a single-host subnet.

Signed-off-by: crawfordxx <crawfordxx@users.noreply.github.com>
@crawfordxx
Copy link
Copy Markdown
Contributor Author

Fixed. The bug was in leaseRangeToIPRangePrefix: FirstIPInSubnet returns network+1 (first usable host), not the network address itself. So for 10.10.61.128/25 the stored StartIP is 10.10.61.129 and EndIP is 10.10.61.255. The old code computed count = 127, which is not a power of two, and returned false.

The fix recovers the network address as StartIP - 1, then computes size = EndIP - networkAddr + 1 = 128, which passes all checks and returns the correct 10.10.61.128/25 prefix.

@packit-as-a-service
Copy link
Copy Markdown

[NON-BLOCKING] Packit jobs failed. @containers/packit-build please check. Everyone else, feel free to ignore.

// 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/api-change Change to remote API; merits scrutiny

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add IPRange to IPAMConfig in convertLibpodNetworkToDockerNetwork

3 participants