Skip to content

Commit

Permalink
#3: Add common extensions for VPN config
Browse files Browse the repository at this point in the history
  • Loading branch information
lika-vorobeva committed Sep 28, 2022
1 parent 1ab2459 commit 6c07409
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// Serializer.swift
// SOLARdVPNCommunityCoreiOS
//
// Created by Lika Vorobeva on 28.09.2022.
//

import Foundation

enum Serializer {
static func toData<T: Encodable>(from object: T) -> Data? {
if let data = try? JSONEncoder().encode(object) {
return data
}
if let dataConvertible = object as? DataConvertible, let data = dataConvertible.data {
return data
}
return nil
}

static func fromData<T: Decodable>(_ data: Data, withType type: T.Type) -> T? {
if let object = try? JSONDecoder().decode(type.self, from: data) {
return object
}
if let Type = T.self as? DataConvertible.Type, let object = Type.init(data: data) as? T {
return object
}
return nil
}
}

// MARK: - DataConvertible

protocol DataConvertible {
init?(data: Data)
var data: Data? { get }
}

extension DataConvertible {
init?(data: Data) {
guard data.count == MemoryLayout<Self>.size else { return nil }
self = data.withUnsafeBytes { $0.load(as: Self.self) }
}

var data: Data? {
return withUnsafeBytes(of: self) { Data($0) }
}
}

// MARK: - DataConvertible implementation

extension Int: DataConvertible { }
extension Float: DataConvertible { }
extension Double: DataConvertible { }
extension Bool: DataConvertible { }
extension String: DataConvertible {
init?(data: Data) {
self.init(data: data, encoding: .utf8)
}

var data: Data? {
return data(using: .utf8)
}
}

extension Encodable {
func toData() -> Data? {
return Serializer.toData(from: self)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// NETunnelProviderManager+Ext.swift
// SOLARdVPNCommunityCoreiOS
//
// Created by Lika Vorobeva on 28.09.2022.
//

import Foundation
import WireGuardKit
import NetworkExtension

extension NETunnelProviderManager {
private static var cachedKey: UInt8 = 0

var providerSession: NETunnelProviderSession? {
connection as? NETunnelProviderSession
}

var provider: NETunnelProviderProtocol? {
protocolConfiguration as? NETunnelProviderProtocol
}

var tunnelConfiguration: TunnelConfiguration? {
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedKey) as? TunnelConfiguration {
return cached
}

guard let provider = protocolConfiguration as? NETunnelProviderProtocol,
let config = provider.asTunnelConfiguration(with: localizedDescription) else {
return nil
}

objc_setAssociatedObject(
self, &NETunnelProviderManager.cachedKey,
config,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)

return config
}

func set(tunnelConfiguration: TunnelConfiguration) {
protocolConfiguration = NETunnelProviderProtocol(
tunnelConfiguration: tunnelConfiguration,
previouslyFrom: protocolConfiguration
)
localizedDescription = tunnelConfiguration.name
objc_setAssociatedObject(
self, &NETunnelProviderManager.cachedKey,
tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}

func isEquivalent(to tunnel: TunnelContainer) -> Bool {
localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// NEVPNManager+Ext.swift
// SOLARdVPNCommunityCoreiOS
//
// Created by Lika Vorobeva on 28.09.2022.
//

import NetworkExtension

extension NEVPNManager {
var tunnelBundleIdentifier: String? {
guard let proto = protocolConfiguration as? NETunnelProviderProtocol else {
return nil
}
return proto.providerBundleIdentifier
}

func isTunnel(withIdentifier bundleIdentifier: String) -> Bool {
return tunnelBundleIdentifier == bundleIdentifier
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// NEVPNStatus+Ext.swift
// SOLARdVPNCommunityCoreiOS
//
// Created by Lika Vorobeva on 28.09.2022.
//

import Foundation
import NetworkExtension

extension NEVPNStatus {
var description: String {
switch self {
case .connected:
return "connected"
case .connecting:
return "connecting"
case .disconnected:
return "disconnected"
case .disconnecting:
return "disconnecting"
case .invalid:
return "invalid"
case .reasserting:
return "reasserting"
@unknown default:
return "@unknown default"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//
// TunnelConfiguration+Ext.swift
// SOLARdVPNCommunityCoreiOS
//
// Created by Lika Vorobeva on 28.09.2022.
//

import Foundation
import WireGuardKit

private struct Constants {
let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
let peerSectionKeys: Set<String> = [
"public_key", "preshared_key", "allowed_ip", "endpoint",
"persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec",
"rx_bytes", "tx_bytes", "protocol_version"
]
}

private let constants = Constants()

extension TunnelConfiguration {
convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]()

var lines = uapiConfig.split(separator: "\n")
lines.append("")

var parserState = ConfigurationParserState.inInterfaceSection
var attributes = [String: String]()

for line in lines {
var key = ""
var value = ""

if !line.isEmpty {
guard let equalsIndex = line.firstIndex(of: "=") else {
throw ConfigurationParseError.invalidLine(line)
}
key = String(line[..<equalsIndex])
value = String(line[line.index(equalsIndex, offsetBy: 1)...])
}

if line.isEmpty || key == "public_key" {
// Previous section has ended; process the attributes collected so far
if parserState == .inInterfaceSection {
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
guard interfaceConfiguration == nil else { throw ConfigurationParseError.multipleInterfaces }
interfaceConfiguration = interface
parserState = .inPeerSection
} else if parserState == .inPeerSection {
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
peerConfigurations.append(peer)
}
attributes.removeAll()
if line.isEmpty {
break
}
}

if let presentValue = attributes[key] {
if key == "allowed_ip" {
attributes[key] = presentValue + "," + value
} else {
throw ConfigurationParseError.multipleEntriesForKey(key)
}
} else {
attributes[key] = value
}

if parserState == .inInterfaceSection {
guard constants.interfaceSectionKeys.contains(key) else {
throw ConfigurationParseError.interfaceHasUnrecognizedKey(key)
}
}
if parserState == .inPeerSection {
guard constants.peerSectionKeys.contains(key) else {
throw ConfigurationParseError.peerHasUnrecognizedKey(key)
}
}
}

let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ConfigurationParseError.multiplePeersWithSamePublicKey
}

interfaceConfiguration?.addresses = base?.interface.addresses ?? []
interfaceConfiguration?.dns = base?.interface.dns ?? []
interfaceConfiguration?.dnsSearch = base?.interface.dnsSearch ?? []
interfaceConfiguration?.mtu = base?.interface.mtu

if let interfaceConfiguration = interfaceConfiguration {
self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
} else {
throw ConfigurationParseError.noInterface
}
}
}

private extension TunnelConfiguration {
static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
guard let privateKeyString = attributes["private_key"] else {
throw ConfigurationParseError.interfaceHasNoPrivateKey
}
guard let privateKey = PrivateKey(hexKey: privateKeyString) else {
throw ConfigurationParseError.interfaceHasInvalidPrivateKey(privateKeyString)
}
var interface = InterfaceConfiguration(privateKey: privateKey)
if let listenPortString = attributes["listen_port"] {
guard let listenPort = UInt16(listenPortString) else {
throw ConfigurationParseError.interfaceHasInvalidListenPort(listenPortString)
}
if listenPort != 0 {
interface.listenPort = listenPort
}
}
return interface
}

static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
guard let publicKeyString = attributes["public_key"] else {
throw ConfigurationParseError.peerHasNoPublicKey
}
guard let publicKey = PublicKey(hexKey: publicKeyString) else {
throw ConfigurationParseError.peerHasInvalidPublicKey(publicKeyString)
}
var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["preshared_key"] {
guard let preSharedKey = PreSharedKey(hexKey: preSharedKeyString) else {
throw ConfigurationParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
}
// TODO(zx2c4): does the compiler optimize this away?
var accumulator: UInt8 = 0
for index in 0..<preSharedKey.rawValue.count {
accumulator |= preSharedKey.rawValue[index]
}
if accumulator != 0 {
peer.preSharedKey = preSharedKey
}
}
if let allowedIPsString = attributes["allowed_ip"] {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
throw ConfigurationParseError.peerHasInvalidAllowedIP(allowedIPString)
}
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else {
throw ConfigurationParseError.peerHasInvalidEndpoint(endpointString)
}
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
throw ConfigurationParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
}
if persistentKeepAlive != 0 {
peer.persistentKeepAlive = persistentKeepAlive
}
}
if let rxBytesString = attributes["rx_bytes"] {
guard let rxBytes = UInt64(rxBytesString) else {
throw ConfigurationParseError.peerHasInvalidTransferBytes(rxBytesString)
}
if rxBytes != 0 {
peer.rxBytes = rxBytes
}
}
if let txBytesString = attributes["tx_bytes"] {
guard let txBytes = UInt64(txBytesString) else {
throw ConfigurationParseError.peerHasInvalidTransferBytes(txBytesString)
}
if txBytes != 0 {
peer.txBytes = txBytes
}
}
if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
var lastHandshakeTimeSince1970: TimeInterval = 0
guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
throw ConfigurationParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
}
if lastHandshakeTimeSec != 0 {
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
throw ConfigurationParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
}
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
}
peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
}
}
return peer
}
}
Loading

0 comments on commit 6c07409

Please sign in to comment.