Skip to content

Align CIDR containment and bounds with CIDRv4 and CIDRv6 - #827

Open
vyncint wants to merge 1 commit into
apple:mainfrom
vyncint:fix/cidr-contains-masks-network
Open

Align CIDR containment and bounds with CIDRv4 and CIDRv6#827
vyncint wants to merge 1 commit into
apple:mainfrom
vyncint:fix/cidr-contains-masks-network

Conversation

@vyncint

@vyncint vyncint commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #826.

What

CIDR keeps the address exactly as parsed, so a block written from a host address keeps its host bits set. contains compared that unmasked stored value against the masked probe, so the wrapper reported that a block excluded its own members while the types it wraps answered the same query correctly.

expression before CIDRv4 / CIDRv6
192.168.1.100/24 contains 192.168.1.100 false true
192.168.1.100/24 contains 192.168.1.0 false true
10.1.2.3/16 contains 10.1.99.99 false true
2001:db8::1234/64 contains 2001:db8::1 false true

Masking both sides matches what CIDRv4.contains and CIDRv6.contains already do. A block written in network form was unaffected, which is why this went unnoticed: every containment case in TestCIDR.swift used a network address.

The same accessors carried a second defect. lower dropped the IPv6 zone while upper kept it, and because contains compares zones, a zoned block excluded its own lower bound:

2001:db8::5%lo0/126  ->  lower = 2001:db8::4       (zone lost)
                         upper = 2001:db8::7%lo0

lower now carries the address's zone the way upper already did, in both copies of the logic: the CIDR wrapper and CIDRv6.

One caller needed a matching change. VmnetNetwork.configurePrefixV6 renders lower into inet_pton, which rejects a zone suffix, so it was relying on lower stripping it. It now renders the network address on its own, leaving the vmnet path unchanged.

Verification

  • 9 new tests in TestCIDR.swift: containment across v4 and v6 host-address blocks, non-members still excluded, the wrapper agreeing with CIDRv4/CIDRv6 over a probe set, zoned bounds contained in their own block, zone rendering, and unzoned bounds still carrying no zone.
  • Negative control: reverting only CIDR.swift and CIDRv6.swift while keeping the tests fails 7 of the 9, with the wrong values visible in the output, for example (block → 2001:db8::5%lo0/126).contains(block.lower → 2001:db8::4) and (wrapper.contains(.v4(ip)) → false) == (concrete.contains(ip) → true). The 2 that still pass are the controls written to pass either way: non-members stay excluded, and unzoned bounds are untouched.
  • Full suite passes: 591 tests in 81 suites.
  • swift format lint --strict --recursive --configuration .swift-format-nolint Sources Tests exits 0, and swift format leaves all four files unchanged.
  • No new files, so license headers are unchanged.

The public shape of CIDR, CIDRv4 and CIDRv6 is untouched; only the values these accessors compute change.

`CIDR` keeps the address exactly as parsed, so a block written from a host
address keeps its host bits. `contains` compared that unmasked value against
the masked probe, so the block reported that it excluded its own members.
Mask both sides, as `CIDRv4` and `CIDRv6` already do.

`lower` also dropped the IPv6 zone while `upper` kept it, which put a zoned
block's lower bound outside the block because `contains` compares zones.

The vmnet prefix helper rendered `lower` into `inet_pton`, which rejects a
zone suffix, so it now renders the network address on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: CIDR.contains excludes members of a block written from a host address

1 participant