Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
117 changes: 102 additions & 15 deletions Sources/SwiftNetwork/Utilities/IPAddress+CIDR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ extension IPv4Address {
}
return matchesDomainPattern(debugDescription, pattern: pattern)
}

/// Whether this address falls inside an already-parsed IPv4 CIDR block.
func matchesCIDR(network: UInt32, mask: UInt32) -> Bool {
(addressValue & mask) == network
}
}

@available(Network 0.1.0, *)
Expand All @@ -147,32 +152,114 @@ extension IPv6Address {
/// representation matches `pattern` as a domain pattern.
func matches(pattern: String) -> Bool {
if let cidr = parseCIDRv6(pattern) {
let (a0, a1, a2, a3) = addressValue
let (n0, n1, n2, n3) = cidr.network
let (m0, m1, m2, m3) = cidr.mask
return (a0 & m0) == n0 && (a1 & m1) == n1 && (a2 & m2) == n2 && (a3 & m3) == n3
return matchesCIDR(network: cidr.network, mask: cidr.mask)
}
return matchesDomainPattern(debugDescription, pattern: pattern)
}

/// Whether this address falls inside an already-parsed IPv6 CIDR block.
func matchesCIDR(
network: (UInt32, UInt32, UInt32, UInt32),
mask: (UInt32, UInt32, UInt32, UInt32)
) -> Bool {
let (a0, a1, a2, a3) = addressValue
let (n0, n1, n2, n3) = network
let (m0, m1, m2, m3) = mask
return (a0 & m0) == n0 && (a1 & m1) == n1 && (a2 & m2) == n2 && (a3 & m3) == n3
}
}

// MARK: - Endpoint pattern matching

/// How a proxy-exception pattern should be interpreted, decided by the pattern's own shape.
/// Patterns are tried in this order: wildcard, address literal, CIDR, domain.
@available(Network 0.1.0, *)
private enum ProxyPatternKind {
case wildcard
case v4Literal(IPv4Address)
case v6Literal(IPv6Address)
case v4CIDR(network: UInt32, mask: UInt32)
case v6CIDR(network: (UInt32, UInt32, UInt32, UInt32), mask: (UInt32, UInt32, UInt32, UInt32))
case domain

init(_ pattern: String) {
if pattern == "*" {
self = .wildcard
} else if let v4 = IPv4Address(pattern) {
self = .v4Literal(v4)
} else if let v6 = IPv6Address(pattern) {
self = .v6Literal(v6)
} else if let cidr = parseCIDRv4(pattern) {
self = .v4CIDR(network: cidr.network, mask: cidr.mask)
} else if let cidr = parseCIDRv6(pattern) {
self = .v6CIDR(network: cidr.network, mask: cidr.mask)
} else {
self = .domain
}
}
}

@available(Network 0.1.0, *)
extension Endpoint {
/// Returns true if this endpoint matches `pattern`. `"*"` matches all endpoints. Host endpoints
/// are matched by hostname; address endpoints are matched by IP address or CIDR block.
/// Returns true if this endpoint matches `pattern`. The pattern's form decides how they're compared.
///
/// - Address and CIDR patterns only match address endpoints, so `"1.2.3.4"` doesn't match a
/// host named `1.2.3.4`.
/// - Addresses are compared by value, so different spellings of the same IPv6 address match.
/// - Wildcard text matching applies to IPv4 addresses only, so `"2001:db8:*"` matches nothing.
func matchesPattern(_ pattern: String) -> Bool {
if pattern == "*" { return true }
// Only host and address endpoints can match, and that's checked before the pattern,
// so even "*" does not match a bonjour, URL, or service endpoint.
let addressEndpoint: AddressEndpoint?
switch type {
case .host(let hostEndpoint):
return matchesDomainPattern(hostEndpoint.name, pattern: pattern)
case .address(let addressEndpoint):
switch addressEndpoint.type {
case .v4(let ipv4, _): return ipv4.matches(pattern: pattern)
case .v6(let ipv6, _): return ipv6.matches(pattern: pattern)
default: return false
}
case .address(let endpoint):
addressEndpoint = endpoint
case .host:
addressEndpoint = nil
default:
return false
}

switch ProxyPatternKind(pattern) {
case .wildcard:
return true

case .v4Literal(let patternAddress):
guard let addressEndpoint, case .v4(let ipv4, _) = addressEndpoint.type else { return false }
return ipv4.addressValue == patternAddress.addressValue

case .v6Literal(let patternAddress):
guard let addressEndpoint, case .v6(let ipv6, _) = addressEndpoint.type else { return false }
// The scope must match too, and a bare literal carries no scope.
return ipv6.addressValue == patternAddress.addressValue && addressEndpoint.scope == 0

case .v4CIDR(let network, let mask):
guard let addressEndpoint, case .v4(let ipv4, _) = addressEndpoint.type else { return false }
return ipv4.matchesCIDR(network: network, mask: mask)

case .v6CIDR(let network, let mask):
guard let addressEndpoint, case .v6(let ipv6, _) = addressEndpoint.type else { return false }
return ipv6.matchesCIDR(network: network, mask: mask)

case .domain:
if case .host(let hostEndpoint) = type {
return matchesDomainPattern(hostEndpoint.name, pattern: pattern)
}
guard let addressEndpoint else { return false }
#if NETWORK_PRIVATE
// An address resolved from a hostname carries that name as its policy domain, and
// domain patterns match against it.
if let policyDomain = domainForPolicy,
matchesDomainPattern(policyDomain, pattern: pattern)
{
return true
}
#endif
// IPv4 only, so that wildcard forms like "17.42.*.10" still work.
if case .v4(let ipv4, _) = addressEndpoint.type {
return matchesDomainPattern(ipv4.debugDescription, pattern: pattern)
}
return false
}
}
}
62 changes: 62 additions & 0 deletions Tests/SwiftNetworkTests/SwiftNetworkCIDRTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,66 @@ final class SwiftNetworkCIDRTests: NetTestCase {
XCTAssertTrue(ep.matchesPattern("2001:db8::/32"))
XCTAssertFalse(ep.matchesPattern("2001:db9::/32"))
}

// MARK: - Endpoint.matchesPattern

func testEndpointMatchesPattern_ipv6LiteralExact() {
let ep = Endpoint(address: IPv6Address.loopback, port: 443)
XCTAssertTrue(ep.matchesPattern("::1"))
XCTAssertFalse(ep.matchesPattern("::2"))
}

func testEndpointMatchesPattern_ipv6LiteralNonCanonicalSpellingStillMatches() {
// One IPv6 address has many spellings, and an exception list may
// use any of them.
let ep = Endpoint(address: IPv6Address.loopback, port: 443)
XCTAssertTrue(ep.matchesPattern("0:0:0:0:0:0:0:1"))
XCTAssertTrue(ep.matchesPattern("0000:0000:0000:0000:0000:0000:0000:0001"))
}

func testEndpointMatchesPattern_ipv6WildcardPatternRefused() {
// Refuse domain-style matching for IPv6
let addr = IPv6Address([
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
])!
let ep = Endpoint(address: addr, port: 443)
XCTAssertFalse(ep.matchesPattern("2001:db8:*"))
}

func testEndpointMatchesPattern_ipv4WildcardInAddressTextStillWorks() {
// IPv4 keeps it, which is what makes "17.42.*.10" style entries work.
let ep = Endpoint(address: IPv4Address([17, 42, 9, 10])!, port: 443)
XCTAssertTrue(ep.matchesPattern("17.42.*.10"))
XCTAssertFalse(ep.matchesPattern("17.43.*.10"))
}

func testEndpointMatchesPattern_addressLiteralDoesNotMatchHostEndpoint() {
// An address-literal pattern must not fall through to domain matching.
let ep = Endpoint(hostname: "1.2.3.4", port: 80)
XCTAssertFalse(ep.matchesPattern("1.2.3.4"))
}

func testEndpointMatchesPattern_cidrDoesNotMatchHostEndpoint() {
let ep = Endpoint(hostname: "example.com", port: 80)
XCTAssertFalse(ep.matchesPattern("192.168.1.0/24"))
XCTAssertFalse(ep.matchesPattern("2001:db8::/32"))
}

func testEndpointMatchesPattern_ipv4LiteralDoesNotMatchIPv6Endpoint() {
let ep = Endpoint(address: IPv6Address.loopback, port: 443)
XCTAssertFalse(ep.matchesPattern("1.2.3.4"))
}

func testEndpointMatchesPattern_unsupportedEndpointTypeNeverMatches() {
// Reject anything that is not a host or address before reading the pattern, so
// even the wildcard does not match.
guard let urlEndpoint = URLEndpoint(url: URL(string: "https://example.com/x")!) else {
XCTFail("could not build URL endpoint")
return
}
let ep = Endpoint(urlEndpoint)
XCTAssertFalse(ep.matchesPattern("*"))
XCTAssertFalse(ep.matchesPattern("example.com"))
}
}
Loading