diff options
Diffstat (limited to 'Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift')
-rw-r--r-- | Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift b/Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift new file mode 100644 index 0000000..e4fead3 --- /dev/null +++ b/Sources/WireGuardKit/PacketTunnelSettingsGenerator.swift @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import Foundation +import Network +import NetworkExtension +import WireGuardKitC + +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() { + wgSettings.append("public_key=\(peer.publicKey.hexKey)\n") + // TODO: log the error returned by `withReresolvedIP` + if let endpoint = try? 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 = "" + wgSettings.append("private_key=\(tunnelConfiguration.interface.privateKey.hexKey)\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() { + wgSettings.append("public_key=\(peer.publicKey.hexKey)\n") + if let preSharedKey = peer.preSharedKey?.hexKey { + wgSettings.append("preshared_key=\(preSharedKey)\n") + } + if let endpoint = try? 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) + } +} |