Skip to content

Commit b5fc254

Browse files
authored
Align CIDR containment and bounds with CIDRv4 and CIDRv6
`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.
1 parent ff44a5b commit b5fc254

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

Sources/Containerization/VmnetNetwork.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ public struct VmnetNetwork: Network {
291291

292292
private static func configurePrefixV6(_ config: vmnet_network_configuration_ref, prefixV6: CIDRv6) throws {
293293
var p = in6_addr()
294-
inet_pton(AF_INET6, prefixV6.lower.description, &p)
294+
// `inet_pton` rejects a zone suffix, so render the network address on
295+
// its own. A vmnet prefix is never link-scoped, so dropping the zone
296+
// here loses nothing.
297+
inet_pton(AF_INET6, IPv6Address(prefixV6.lower.value).description, &p)
295298

296299
guard vmnet_network_configuration_set_ipv6_prefix(config, &p, prefixV6.prefix.length) == .VMNET_SUCCESS else {
297300
throw ContainerizationError(.internalError, message: "failed to set IPv6 prefix \(prefixV6) for network")

Sources/ContainerizationExtras/CIDR.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public enum CIDR: CustomStringConvertible, Equatable, Sendable, Hashable {
9191
case (.v4(let addr, let prefix)):
9292
return .v4(IPv4Address(addr.value & prefix.prefixMask32))
9393
case (.v6(let addr, let prefix)):
94-
return .v6(IPv6Address(addr.value & prefix.prefixMask128))
94+
return .v6(IPv6Address(addr.value & prefix.prefixMask128, zone: addr.zone))
9595
}
9696
}
9797

@@ -113,9 +113,9 @@ public enum CIDR: CustomStringConvertible, Equatable, Sendable, Hashable {
113113
public func contains(_ ip: IPAddress) -> Bool {
114114
switch (self, ip) {
115115
case (.v4(let network, let prefix), .v4(let ip)):
116-
return network.value == (ip.value & prefix.prefixMask32)
116+
return (network.value & prefix.prefixMask32) == (ip.value & prefix.prefixMask32)
117117
case (.v6(let network, let prefix), .v6(let ip)):
118-
return (network.zone == ip.zone) && (network.value == (ip.value & prefix.prefixMask128))
118+
return (network.zone == ip.zone) && ((network.value & prefix.prefixMask128) == (ip.value & prefix.prefixMask128))
119119
default:
120120
return false
121121
}

Sources/ContainerizationExtras/CIDRv6.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable {
7878
/// The lowest address in this CIDR block
7979
@inlinable
8080
public var lower: IPv6Address {
81-
IPv6Address(address.value & prefix.prefixMask128)
81+
IPv6Address(address.value & prefix.prefixMask128, zone: address.zone)
8282
}
8383

8484
/// The highest address in this CIDR block (broadcast address).

Tests/ContainerizationExtrasTests/TestCIDR.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,102 @@ struct TestCIDR {
146146
#expect(!cidr.contains(.v6(ip)))
147147
}
148148

149+
// MARK: - Wrapper Agrees With The Concrete Types
150+
151+
// `CIDR` keeps the address exactly as parsed, so a block written from a
152+
// host address ("192.168.1.100/24") stores a value with host bits set.
153+
// Containment has to mask both sides, the way `CIDRv4`/`CIDRv6` do, or the
154+
// block reports that it excludes its own members.
155+
@Test(arguments: [
156+
("192.168.1.100/24", "192.168.1.100"),
157+
("192.168.1.100/24", "192.168.1.0"),
158+
("192.168.1.100/24", "192.168.1.255"),
159+
("10.1.2.3/16", "10.1.99.99"),
160+
("2001:db8::1234/64", "2001:db8::1"),
161+
("2001:db8::1234/64", "2001:db8::1234"),
162+
])
163+
func testWrapperContainsMembersOfAHostAddressBlock(cidr: String, ip: String) throws {
164+
let block = try CIDR(cidr)
165+
#expect(block.contains(try IPAddress(ip)))
166+
}
167+
168+
@Test(arguments: [
169+
("192.168.1.100/24", "192.168.2.1"),
170+
("10.1.2.3/16", "10.2.0.1"),
171+
("2001:db8::1234/64", "2001:db9::1"),
172+
])
173+
func testWrapperStillExcludesNonMembers(cidr: String, ip: String) throws {
174+
let block = try CIDR(cidr)
175+
#expect(!block.contains(try IPAddress(ip)))
176+
}
177+
178+
// The wrapper must never disagree with the type it wraps.
179+
@Test(arguments: ["192.168.1.100/24", "10.1.2.3/16", "192.168.1.0/24", "1.2.3.4/32"])
180+
func testWrapperMatchesCIDRv4(cidr: String) throws {
181+
let wrapper = try CIDR(cidr)
182+
let concrete = try CIDRv4(cidr)
183+
for probe in ["192.168.1.100", "192.168.1.0", "10.1.99.99", "1.2.3.4", "8.8.8.8"] {
184+
let ip = try IPv4Address(probe)
185+
#expect(wrapper.contains(.v4(ip)) == concrete.contains(ip), "disagreed on \(probe) for \(cidr)")
186+
}
187+
#expect(wrapper.lower.description == concrete.lower.description)
188+
#expect(wrapper.upper.description == concrete.upper.description)
189+
}
190+
191+
@Test(arguments: ["2001:db8::1234/64", "fe80::1%eth0/64", "2001:db8::/32"])
192+
func testWrapperMatchesCIDRv6(cidr: String) throws {
193+
let wrapper = try CIDR(cidr)
194+
let concrete = try CIDRv6(cidr)
195+
for probe in ["2001:db8::1", "2001:db8::1234", "fe80::1%eth0", "fe80::1", "2001:db9::1"] {
196+
let ip = try IPv6Address(probe)
197+
#expect(wrapper.contains(.v6(ip)) == concrete.contains(ip), "disagreed on \(probe) for \(cidr)")
198+
}
199+
#expect(wrapper.lower.description == concrete.lower.description)
200+
#expect(wrapper.upper.description == concrete.upper.description)
201+
}
202+
203+
// MARK: - Zone Preservation in Bounds
204+
205+
// `contains` compares zones, so a bound that drops the zone is reported as
206+
// outside its own block. Both bounds have to carry the address's zone for
207+
// the block to contain them.
208+
@Test(arguments: ["fe80::1%eth0/64", "fe80::1%eth0/128", "2001:db8::5%lo0/126"])
209+
func testZonedBlockContainsItsOwnBounds(cidr: String) throws {
210+
let block = try CIDRv6(cidr)
211+
#expect(block.contains(block.lower))
212+
#expect(block.contains(block.upper))
213+
}
214+
215+
@Test(arguments: ["fe80::1%eth0/64", "2001:db8::5%lo0/126"])
216+
func testBoundsAgreeOnZone(cidr: String) throws {
217+
let block = try CIDRv6(cidr)
218+
#expect(block.lower.zone == block.address.zone)
219+
#expect(block.upper.zone == block.address.zone)
220+
}
221+
222+
@Test func testZonedLowerRendersItsZone() throws {
223+
let block = try CIDRv6("2001:db8::5%lo0/126")
224+
#expect(block.lower.description == "2001:db8::4%lo0")
225+
#expect(block.upper.description == "2001:db8::7%lo0")
226+
}
227+
228+
// The same bounds live on the `CIDR` wrapper, which carries its own copy.
229+
@Test func testZonedWrapperContainsItsOwnBounds() throws {
230+
let block = try CIDR("fe80::1%eth0/64")
231+
#expect(block.contains(block.lower))
232+
#expect(block.contains(block.upper))
233+
#expect(block.lower.description == "fe80::%eth0")
234+
}
235+
236+
// An unzoned block must keep rendering without a zone suffix.
237+
@Test func testUnzonedBoundsCarryNoZone() throws {
238+
let block = try CIDRv6("2001:db8::5/126")
239+
#expect(block.lower.zone == nil)
240+
#expect(block.upper.zone == nil)
241+
#expect(block.lower.description == "2001:db8::4")
242+
#expect(try CIDR("2001:db8::5/126").lower.description == "2001:db8::4")
243+
}
244+
149245
// MARK: - Range Constructor
150246

151247
@Test func testRangeConstructorFindsSmallestBlock() throws {

0 commit comments

Comments
 (0)