WireGuardKit: Move shared structs to WireGuardKit

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
This commit is contained in:
Andrej Mihajlov
2020-11-25 13:41:56 +01:00
parent 57f66f16f8
commit a03df7d8cc
8 changed files with 0 additions and 0 deletions
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Network
import Foundation
class DNSResolver {
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
for endpoint in endpoints {
guard let endpoint = endpoint else { continue }
if !endpoint.hasHostAsIPAddress() {
return false
}
}
return true
}
static func resolveSync(endpoints: [Endpoint?]) -> [Endpoint?]? {
let dispatchGroup = DispatchGroup()
if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints
}
var resolvedEndpoints: [Endpoint?] = Array(repeating: nil, count: endpoints.count)
for (index, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint else { continue }
if endpoint.hasHostAsIPAddress() {
resolvedEndpoints[index] = endpoint
} else {
let workItem = DispatchWorkItem {
resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
}
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
}
}
dispatchGroup.wait() // TODO: Timeout?
var hostnamesWithDnsResolutionFailure = [String]()
assert(endpoints.count == resolvedEndpoints.count)
for tuple in zip(endpoints, resolvedEndpoints) {
let endpoint = tuple.0
let resolvedEndpoint = tuple.1
if let endpoint = endpoint {
if resolvedEndpoint == nil {
guard let hostname = endpoint.hostname() else { fatalError() }
hostnamesWithDnsResolutionFailure.append(hostname)
}
}
}
if !hostnamesWithDnsResolutionFailure.isEmpty {
wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure.joined(separator: ", "))")
return nil
}
return resolvedEndpoints
}
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
switch endpoint.host {
case .name(let name, _):
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo(
ai_flags: AI_ALL, // We set this to ALL so that we get v4 addresses even on DNS64 networks
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
if getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer) != 0 {
return nil
}
var next = resultPointer
var ipv4Address: IPv4Address?
var ipv6Address: IPv6Address?
while next != nil {
let result = next!.pointee
next = result.ai_next
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
ipv4Address = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
break // If we found an IPv4 address, we can stop
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
ipv6Address = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
continue // If we already have an IPv6 address, we can skip this one
}
}
freeaddrinfo(resultPointer)
// We prefer an IPv4 address over an IPv6 address
if let ipv4Address = ipv4Address {
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6Address = ipv6Address {
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
return nil
}
default:
return endpoint
}
}
}
extension Endpoint {
func withReresolvedIP() -> Endpoint {
#if os(iOS)
var ret = self
let hostname: String
switch host {
case .name(let name, _):
hostname = name
case .ipv4(let address):
hostname = "\(address)"
case .ipv6(let address):
hostname = "\(address)"
@unknown default:
fatalError()
}
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo(
ai_flags: 0, // We set this to zero so that we actually resolve this using DNS64
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
if getaddrinfo(hostname, "\(port)", &hints, &resultPointer) != 0 || resultPointer == nil {
return ret
}
let result = resultPointer!.pointee
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
let addr = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
ret = Endpoint(host: .ipv4(addr!), port: port)
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
let addr = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
ret = Endpoint(host: .ipv6(addr!), port: port)
}
freeaddrinfo(resultPointer)
if ret.host != host {
wg_log(.debug, message: "DNS64: mapped \(host) to \(ret.host)")
} else {
wg_log(.debug, message: "DNS64: mapped \(host) to itself.")
}
return ret
#elseif os(macOS)
return self
#else
#error("Unimplemented")
#endif
}
}
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct DNSServer {
let address: IPAddress
init(address: IPAddress) {
self.address = address
}
}
extension DNSServer: Equatable {
static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue
}
}
extension DNSServer {
var stringRepresentation: String {
return "\(address)"
}
init?(from addressString: String) {
if let addr = IPv4Address(addressString) {
address = addr
} else if let addr = IPv6Address(addressString) {
address = addr
} else {
return nil
}
}
}
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct Endpoint {
let host: NWEndpoint.Host
let port: NWEndpoint.Port
init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
self.host = host
self.port = port
}
}
extension Endpoint: Equatable {
static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
return lhs.host == rhs.host && lhs.port == rhs.port
}
}
extension Endpoint: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(host)
hasher.combine(port)
}
}
extension Endpoint {
var stringRepresentation: String {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
@unknown default:
fatalError()
}
}
init?(from string: String) {
// Separation of host and port is based on 'parse_endpoint' function in
// https://git.zx2c4.com/wireguard-tools/tree/src/config.c
guard !string.isEmpty else { return nil }
let startOfPort: String.Index
let hostString: String
if string.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = string.index(after: string.startIndex)
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
let afterEndOfHost = string.index(after: endOfHost)
guard string[afterEndOfHost] == ":" else { return nil }
startOfPort = string.index(after: afterEndOfHost)
hostString = String(string[startOfHost ..< endOfHost])
} else {
// Look for an IPv4-style endpoint, like 127.0.0.1:80
guard let endOfHost = string.firstIndex(of: ":") else { return nil }
startOfPort = string.index(after: endOfHost)
hostString = String(string[string.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard invalidCharacterIndex == nil else { return nil }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
}
extension Endpoint {
func hasHostAsIPAddress() -> Bool {
switch host {
case .name:
return false
case .ipv4:
return true
case .ipv6:
return true
@unknown default:
fatalError()
}
}
func hostname() -> String? {
switch host {
case .name(let hostname, _):
return hostname
case .ipv4:
return nil
case .ipv6:
return nil
@unknown default:
fatalError()
}
}
}
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct IPAddressRange {
let address: IPAddress
var networkPrefixLength: UInt8
init(address: IPAddress, networkPrefixLength: UInt8) {
self.address = address
self.networkPrefixLength = networkPrefixLength
}
}
extension IPAddressRange: Equatable {
static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
}
}
extension IPAddressRange: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(address.rawValue)
hasher.combine(networkPrefixLength)
}
}
extension IPAddressRange {
var stringRepresentation: String {
return "\(address)/\(networkPrefixLength)"
}
init?(from string: String) {
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
address = parsed.0
networkPrefixLength = parsed.1
}
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
let addressString = String(string[string.startIndex ..< endOfIPAddress])
let address: IPAddress
if let addr = IPv4Address(addressString) {
address = addr
} else if let addr = IPv6Address(addressString) {
address = addr
} else {
return nil
}
let maxNetworkPrefixLength: UInt8 = address is IPv4Address ? 32 : 128
var networkPrefixLength: UInt8
if endOfIPAddress < string.endIndex { // "/" was located
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
networkPrefixLength = min(npl, maxNetworkPrefixLength)
} else {
networkPrefixLength = maxNetworkPrefixLength
}
return (address, networkPrefixLength)
}
}
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct InterfaceConfiguration {
var privateKey: Data
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns = [DNSServer]()
init(privateKey: Data) {
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
self.privateKey = privateKey
}
}
extension InterfaceConfiguration: Equatable {
static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
return lhs.privateKey == rhs.privateKey &&
lhsAddresses == rhsAddresses &&
lhs.listenPort == rhs.listenPort &&
lhs.mtu == rhs.mtu &&
lhs.dns == rhs.dns
}
}
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
class PacketTunnelSettingsGenerator {
let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?]
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
self.tunnelConfiguration = tunnelConfiguration
self.resolvedEndpoints = resolvedEndpoints
}
func endpointUapiConfiguration() -> String {
var wgSettings = ""
for (index, peer) in tunnelConfiguration.peers.enumerated() {
if let publicKey = peer.publicKey.hexKey() {
wgSettings.append("public_key=\(publicKey)\n")
}
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation)\n")
}
}
return wgSettings
}
func uapiConfiguration() -> String {
var wgSettings = ""
if let privateKey = tunnelConfiguration.interface.privateKey.hexKey() {
wgSettings.append("private_key=\(privateKey)\n")
}
if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n")
}
if !tunnelConfiguration.peers.isEmpty {
wgSettings.append("replace_peers=true\n")
}
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (index, peer) in tunnelConfiguration.peers.enumerated() {
if let publicKey = peer.publicKey.hexKey() {
wgSettings.append("public_key=\(publicKey)\n")
}
if let preSharedKey = peer.preSharedKey?.hexKey() {
wgSettings.append("preshared_key=\(preSharedKey)\n")
}
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation)\n")
}
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n")
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation)\n") }
}
}
return wgSettings
}
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation }
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
networkSettings.dnsSettings = dnsSettings
let mtu = tunnelConfiguration.interface.mtu ?? 0
/* 0 means automatic MTU. In theory, we should just do
* `networkSettings.tunnelOverheadBytes = 80` but in
* practice there are too many broken networks out there.
* Instead set it to 1280. Boohoo. Maybe someday we'll
* add a nob, maybe, or iOS will do probing for us.
*/
if mtu == 0 {
#if os(iOS)
networkSettings.mtu = NSNumber(value: 1280)
#elseif os(macOS)
networkSettings.tunnelOverheadBytes = 80
#else
#error("Unimplemented")
#endif
} else {
networkSettings.mtu = NSNumber(value: mtu)
}
let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes
networkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
networkSettings.ipv6Settings = ipv6Settings
return networkSettings
}
private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let length: UInt8 = addressRange.networkPrefixLength
assert(length <= 32)
var octets: [UInt8] = [0, 0, 0, 0]
let subnetMask: UInt32 = length > 0 ? ~UInt32(0) << (32 - length) : UInt32(0)
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
return octets.map { String($0) }.joined(separator: ".")
}
private func routes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4Routes = [NEIPv4Route]()
var ipv6Routes = [NEIPv6Route]()
for addressRange in tunnelConfiguration.interface.addresses {
if addressRange.address is IPv4Address {
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
* very bad, if various network parameters were actually relying on that subnet being
* intentionally small. TODO: talk about this with upstream iOS devs.
*/
ipv6Routes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: min(120, addressRange.networkPrefixLength))))
}
}
return (ipv4Routes, ipv6Routes)
}
private func includedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if addressRange.address is IPv4Address {
ipv4IncludedRoutes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
ipv6IncludedRoutes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength)))
}
}
}
return (ipv4IncludedRoutes, ipv6IncludedRoutes)
}
}
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
struct PeerConfiguration {
var publicKey: Data
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if value.count != TunnelConfiguration.keyLength {
fatalError("Invalid preshared key")
}
}
}
}
var allowedIPs = [IPAddressRange]()
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
var rxBytes: UInt64?
var txBytes: UInt64?
var lastHandshakeTime: Date?
init(publicKey: Data) {
self.publicKey = publicKey
if publicKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid public key")
}
}
}
extension PeerConfiguration: Equatable {
static func == (lhs: PeerConfiguration, rhs: PeerConfiguration) -> Bool {
return lhs.publicKey == rhs.publicKey &&
lhs.preSharedKey == rhs.preSharedKey &&
Set(lhs.allowedIPs) == Set(rhs.allowedIPs) &&
lhs.endpoint == rhs.endpoint &&
lhs.persistentKeepAlive == rhs.persistentKeepAlive
}
}
extension PeerConfiguration: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(publicKey)
hasher.combine(preSharedKey)
hasher.combine(Set(allowedIPs))
hasher.combine(endpoint)
hasher.combine(persistentKeepAlive)
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
final class TunnelConfiguration {
var name: String?
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength = 32
init(name: String?, interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
self.name = name
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
}
extension TunnelConfiguration: Equatable {
static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
return lhs.name == rhs.name &&
lhs.interface == rhs.interface &&
Set(lhs.peers) == Set(rhs.peers)
}
}