diff --git a/TunnelKit/Sources/AppExtension/GenericSocket.swift b/TunnelKit/Sources/AppExtension/GenericSocket.swift index cf40b1f..a30156c 100644 --- a/TunnelKit/Sources/AppExtension/GenericSocket.swift +++ b/TunnelKit/Sources/AppExtension/GenericSocket.swift @@ -36,30 +36,69 @@ import Foundation +/// Receives events from a `GenericSocket`. public protocol GenericSocketDelegate: class { + + /** + The socket timed out. + **/ func socketDidTimeout(_ socket: GenericSocket) + /** + The socket became active. + **/ func socketDidBecomeActive(_ socket: GenericSocket) + /** + The socket shut down. + + - Parameter failure: `true` if the shutdown was caused by a failure. + **/ func socket(_ socket: GenericSocket, didShutdownWithFailure failure: Bool) + /** + The socket has a better path. + **/ func socketHasBetterPath(_ socket: GenericSocket) } +/// An opaque socket implementation. public protocol GenericSocket { + + /// The address of the remote endpoint. var remoteAddress: String? { get } + /// `true` if the socket has a better path. var hasBetterPath: Bool { get } + /// `true` if the socket was shut down. var isShutdown: Bool { get } + /// The optional delegate for events. var delegate: GenericSocketDelegate? { get set } + /** + Observes socket events. + + - Parameter queue: The queue to observe events in. + - Parameter activeTimeout: The timeout in milliseconds for socket activity. + **/ func observe(queue: DispatchQueue, activeTimeout: Int) + /** + Stops observing socket events. + **/ func unobserve() - + + /** + Shuts down the socket + **/ func shutdown() + /** + Returns an upgraded socket if available (e.g. when a better path exists). + + - Returns: An upgraded socket if any. + **/ func upgraded() -> GenericSocket? } diff --git a/TunnelKit/Sources/AppExtension/InterfaceObserver.swift b/TunnelKit/Sources/AppExtension/InterfaceObserver.swift index 1013c8e..3dc0629 100644 --- a/TunnelKit/Sources/AppExtension/InterfaceObserver.swift +++ b/TunnelKit/Sources/AppExtension/InterfaceObserver.swift @@ -41,16 +41,24 @@ import SwiftyBeaver private let log = SwiftyBeaver.self extension NSNotification.Name { - static let __InterfaceObserverDidDetectWifiChange = NSNotification.Name("__InterfaceObserverDidDetectWifiChange") + + /// A change in Wi-Fi state occurred. + public static let InterfaceObserverDidDetectWifiChange = NSNotification.Name("InterfaceObserverDidDetectWifiChange") } +/// Observes changes in the current Wi-Fi network. public class InterfaceObserver: NSObject { private var queue: DispatchQueue? private var timer: DispatchSourceTimer? private var lastWifiName: String? - + + /** + Starts observing Wi-Fi updates. + + - Parameter queue: The `DispatchQueue` to deliver notifications to. + **/ public func start(queue: DispatchQueue) { self.queue = queue @@ -63,7 +71,10 @@ public class InterfaceObserver: NSObject { self.timer = timer } - + + /** + Stops observing Wi-Fi updates. + **/ public func stop() { timer?.cancel() timer = nil @@ -77,7 +88,7 @@ public class InterfaceObserver: NSObject { log.debug("SSID is now '\(current.maskedDescription)'") if let last = lastWifiName, (current != last) { queue?.async { - NotificationCenter.default.post(name: .__InterfaceObserverDidDetectWifiChange, object: nil) + NotificationCenter.default.post(name: .InterfaceObserverDidDetectWifiChange, object: nil) } } } else { @@ -87,6 +98,11 @@ public class InterfaceObserver: NSObject { lastWifiName = currentWifiName } + /** + Returns the current Wi-Fi SSID if any. + + - Returns: The current Wi-Fi SSID if any. + **/ public func currentWifiNetworkName() -> String? { #if os(iOS) guard let interfaceNames = CNCopySupportedInterfaces() as? [CFString] else { diff --git a/TunnelKit/Sources/AppExtension/Keychain.swift b/TunnelKit/Sources/AppExtension/Keychain.swift index cd3a53c..03f97c7 100644 --- a/TunnelKit/Sources/AppExtension/Keychain.swift +++ b/TunnelKit/Sources/AppExtension/Keychain.swift @@ -36,29 +36,49 @@ import Foundation +/// Error raised by `Keychain` methods. public enum KeychainError: Error { + + /// Unable to add. case add + /// Item not found. case notFound - case typeMismatch +// /// Unexpected item type returned. +// case typeMismatch } +/// Wrapper for easy keychain access and modification. public class Keychain { private let service: String? private let accessGroup: String? + /// :nodoc: public init() { service = Bundle.main.bundleIdentifier accessGroup = nil } + /** + Creates a keychain in an App Group. + + - Parameter group: The App Group. + - Precondition: Proper App Group entitlements. + **/ public init(group: String) { service = nil accessGroup = group } + /** + Creates a keychain in an App Group and a Team ID prefix. + + - Parameter team: The Team ID prefix. + - Parameter group: The App Group. + - Precondition: Proper App Group entitlements. + **/ public init(team: String, group: String) { service = nil accessGroup = "\(team).\(group)" @@ -66,6 +86,14 @@ public class Keychain { // MARK: Password + /** + Sets a password. + + - Parameter password: The password to set. + - Parameter username: The username to set the password for. + - Parameter label: An optional label. + - Throws: `KeychainError.add` if unable to add the password to the keychain. + **/ public func set(password: String, for username: String, label: String? = nil) throws { do { let currentPassword = try self.password(for: username) @@ -94,6 +122,12 @@ public class Keychain { } } + /** + Removes a password. + + - Parameter username: The username to remove the password for. + - Returns: `true` if the password was successfully removed. + **/ @discardableResult public func removePassword(for username: String) -> Bool { var query = [String: Any]() setScope(query: &query) @@ -104,6 +138,13 @@ public class Keychain { return (status == errSecSuccess) } + /** + Gets a password. + + - Parameter username: The username to get the password for. + - Returns: The password for the input username. + - Throws: `KeychainError.notFound` if unable to find the password in the keychain. + **/ public func password(for username: String) throws -> String { var query = [String: Any]() setScope(query: &query) @@ -126,6 +167,13 @@ public class Keychain { return password } + /** + Gets a password reference. + + - Parameter username: The username to get the password for. + - Returns: The password reference for the input username. + - Throws: `KeychainError.notFound` if unable to find the password in the keychain. + **/ public func passwordReference(for username: String) throws -> Data { var query = [String: Any]() setScope(query: &query) @@ -145,6 +193,14 @@ public class Keychain { return data } + /** + Gets a password associated with a password reference. + + - Parameter username: The username to get the password for. + - Parameter reference: The password reference. + - Returns: The password for the input username and reference. + - Throws: `KeychainError.notFound` if unable to find the password in the keychain. + **/ public static func password(for username: String, reference: Data) throws -> String { var query = [String: Any]() query[kSecClass as String] = kSecClassGenericPassword @@ -170,6 +226,14 @@ public class Keychain { // https://forums.developer.apple.com/thread/13748 + /** + Adds a public key. + + - Parameter identifier: The unique identifier. + - Parameter data: The public key data. + - Returns: The `SecKey` object representing the public key. + - Throws: `KeychainError.add` if unable to add the public key to the keychain. + **/ public func add(publicKeyWithIdentifier identifier: String, data: Data) throws -> SecKey { var query = [String: Any]() query[kSecClass as String] = kSecClassKey @@ -188,6 +252,13 @@ public class Keychain { return try publicKey(withIdentifier: identifier) } + /** + Gets a public key. + + - Parameter identifier: The unique identifier. + - Returns: The `SecKey` object representing the public key. + - Throws: `KeychainError.notFound` if unable to find the public key in the keychain. + **/ public func publicKey(withIdentifier identifier: String) throws -> SecKey { var query = [String: Any]() query[kSecClass as String] = kSecClassKey @@ -211,6 +282,12 @@ public class Keychain { return result as! SecKey } + /** + Removes a public key. + + - Parameter identifier: The unique identifier. + - Returns: `true` if the public key was successfully removed. + **/ @discardableResult public func remove(publicKeyWithIdentifier identifier: String) -> Bool { var query = [String: Any]() query[kSecClass as String] = kSecClassKey diff --git a/TunnelKit/Sources/AppExtension/LinkProducer.swift b/TunnelKit/Sources/AppExtension/LinkProducer.swift index 967cac6..3ef4904 100644 --- a/TunnelKit/Sources/AppExtension/LinkProducer.swift +++ b/TunnelKit/Sources/AppExtension/LinkProducer.swift @@ -25,6 +25,13 @@ import Foundation +/// Entity able to produce a `LinkInterface`. public protocol LinkProducer { + + /** + Returns a `LinkInterface`. + + - Parameter mtu: The MTU value. + **/ func link(withMTU mtu: Int) -> LinkInterface } diff --git a/TunnelKit/Sources/AppExtension/MemoryDestination.swift b/TunnelKit/Sources/AppExtension/MemoryDestination.swift index 913603d..f38b77b 100644 --- a/TunnelKit/Sources/AppExtension/MemoryDestination.swift +++ b/TunnelKit/Sources/AppExtension/MemoryDestination.swift @@ -37,22 +37,35 @@ import Foundation import SwiftyBeaver +/// Implements a `SwiftyBeaver.BaseDestination` logging to a memory buffer. public class MemoryDestination: BaseDestination, CustomStringConvertible { private var buffer: [String] = [] - + + /// Max number of retained lines. public var maxLines: Int? - + + /// :nodoc: public override init() { super.init() asynchronously = false } - public func start(with existing: [String]) { + /** + Starts logging. Optionally prepend an array of lines. + + - Parameter existing: The optional lines to prepend (none by default). + **/ + public func start(with existing: [String] = []) { execute(synchronously: true) { self.buffer = existing } } - + + /** + Flushes the log content to an URL. + + - Parameter url: The URL to write the log content to. + **/ public func flush(to url: URL) { execute(synchronously: true) { let content = self.buffer.joined(separator: "\n") @@ -63,6 +76,7 @@ public class MemoryDestination: BaseDestination, CustomStringConvertible { // MARK: BaseDestination // XXX: executed in SwiftyBeaver queue. DO NOT invoke execute* here (sync in sync would crash otherwise) + /// :nodoc: public override func send(_ level: SwiftyBeaver.Level, msg: String, thread: String, file: String, function: String, line: Int, context: Any?) -> String? { guard let message = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line) else { return nil @@ -78,6 +92,7 @@ public class MemoryDestination: BaseDestination, CustomStringConvertible { // MARK: CustomStringConvertible + /// :nodoc: public var description: String { return executeSynchronously { return self.buffer.joined(separator: "\n") diff --git a/TunnelKit/Sources/AppExtension/Transport/NETCPSocket.swift b/TunnelKit/Sources/AppExtension/Transport/NETCPSocket.swift index d023d0f..997ec8b 100644 --- a/TunnelKit/Sources/AppExtension/Transport/NETCPSocket.swift +++ b/TunnelKit/Sources/AppExtension/Transport/NETCPSocket.swift @@ -40,11 +40,14 @@ import SwiftyBeaver private let log = SwiftyBeaver.self +/// TCP implementation of a `GenericSocket` via NetworkExtension. public class NETCPSocket: NSObject, GenericSocket { private static var linkContext = 0 + /// :nodoc: public let impl: NWTCPConnection + /// :nodoc: public init(impl: NWTCPConnection) { self.impl = impl isActive = false @@ -57,18 +60,23 @@ public class NETCPSocket: NSObject, GenericSocket { private var isActive: Bool + /// :nodoc: public private(set) var isShutdown: Bool + /// :nodoc: public var remoteAddress: String? { return (impl.remoteAddress as? NWHostEndpoint)?.hostname } + /// :nodoc: public var hasBetterPath: Bool { return impl.hasBetterPath } + /// :nodoc: public weak var delegate: GenericSocketDelegate? + /// :nodoc: public func observe(queue: DispatchQueue, activeTimeout: Int) { isActive = false @@ -86,16 +94,19 @@ public class NETCPSocket: NSObject, GenericSocket { impl.addObserver(self, forKeyPath: #keyPath(NWTCPConnection.hasBetterPath), options: .new, context: &NETCPSocket.linkContext) } + /// :nodoc: public func unobserve() { impl.removeObserver(self, forKeyPath: #keyPath(NWTCPConnection.state), context: &NETCPSocket.linkContext) impl.removeObserver(self, forKeyPath: #keyPath(NWTCPConnection.hasBetterPath), context: &NETCPSocket.linkContext) } + /// :nodoc: public func shutdown() { impl.writeClose() impl.cancel() } + /// :nodoc: public func upgraded() -> GenericSocket? { guard impl.hasBetterPath else { return nil @@ -105,6 +116,7 @@ public class NETCPSocket: NSObject, GenericSocket { // MARK: Connection KVO (any queue) + /// :nodoc: public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard (context == &NETCPSocket.linkContext) else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) diff --git a/TunnelKit/Sources/AppExtension/Transport/NETunnelInterface.swift b/TunnelKit/Sources/AppExtension/Transport/NETunnelInterface.swift index f6d353c..387175d 100644 --- a/TunnelKit/Sources/AppExtension/Transport/NETunnelInterface.swift +++ b/TunnelKit/Sources/AppExtension/Transport/NETunnelInterface.swift @@ -37,11 +37,13 @@ import Foundation import NetworkExtension +/// `TunnelInterface` implementation via NetworkExtension. public class NETunnelInterface: TunnelInterface { private weak var impl: NEPacketTunnelFlow? private let protocolNumber: NSNumber - + + /// :nodoc: public init(impl: NEPacketTunnelFlow, isIPv6: Bool) { self.impl = impl protocolNumber = (isIPv6 ? AF_INET6 : AF_INET) as NSNumber @@ -49,12 +51,14 @@ public class NETunnelInterface: TunnelInterface { // MARK: TunnelInterface + /// :nodoc: public var isPersistent: Bool { return false } // MARK: IOInterface + /// :nodoc: public func setReadHandler(queue: DispatchQueue, _ handler: @escaping ([Data]?, Error?) -> Void) { loopReadPackets(queue, handler) } @@ -70,11 +74,13 @@ public class NETunnelInterface: TunnelInterface { } } + /// :nodoc: public func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) { impl?.writePackets([packet], withProtocols: [protocolNumber]) completionHandler?(nil) } + /// :nodoc: public func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { let protocols = [NSNumber](repeating: protocolNumber, count: packets.count) impl?.writePackets(packets, withProtocols: protocols) diff --git a/TunnelKit/Sources/AppExtension/Transport/NEUDPSocket.swift b/TunnelKit/Sources/AppExtension/Transport/NEUDPSocket.swift index 6172af4..f22a485 100644 --- a/TunnelKit/Sources/AppExtension/Transport/NEUDPSocket.swift +++ b/TunnelKit/Sources/AppExtension/Transport/NEUDPSocket.swift @@ -40,11 +40,14 @@ import SwiftyBeaver private let log = SwiftyBeaver.self +/// UDP implementation of a `GenericSocket` via NetworkExtension. public class NEUDPSocket: NSObject, GenericSocket { private static var linkContext = 0 + /// :nodoc: public let impl: NWUDPSession + /// :nodoc: public init(impl: NWUDPSession) { self.impl = impl @@ -58,18 +61,23 @@ public class NEUDPSocket: NSObject, GenericSocket { private var isActive: Bool + /// :nodoc: public private(set) var isShutdown: Bool + /// :nodoc: public var remoteAddress: String? { return (impl.resolvedEndpoint as? NWHostEndpoint)?.hostname } + /// :nodoc: public var hasBetterPath: Bool { return impl.hasBetterPath } + /// :nodoc: public weak var delegate: GenericSocketDelegate? + /// :nodoc: public func observe(queue: DispatchQueue, activeTimeout: Int) { isActive = false @@ -87,15 +95,18 @@ public class NEUDPSocket: NSObject, GenericSocket { impl.addObserver(self, forKeyPath: #keyPath(NWUDPSession.hasBetterPath), options: .new, context: &NEUDPSocket.linkContext) } + /// :nodoc: public func unobserve() { impl.removeObserver(self, forKeyPath: #keyPath(NWUDPSession.state), context: &NEUDPSocket.linkContext) impl.removeObserver(self, forKeyPath: #keyPath(NWUDPSession.hasBetterPath), context: &NEUDPSocket.linkContext) } + /// :nodoc: public func shutdown() { impl.cancel() } + /// :nodoc: public func upgraded() -> GenericSocket? { guard impl.hasBetterPath else { return nil @@ -105,6 +116,7 @@ public class NEUDPSocket: NSObject, GenericSocket { // MARK: Connection KVO (any queue) + /// :nodoc: public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard (context == &NEUDPSocket.linkContext) else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) diff --git a/TunnelKit/Sources/Core/DNSResolver.swift b/TunnelKit/Sources/Core/DNSResolver.swift index ca32701..8985ce3 100644 --- a/TunnelKit/Sources/Core/DNSResolver.swift +++ b/TunnelKit/Sources/Core/DNSResolver.swift @@ -36,9 +36,18 @@ import Foundation +/// Convenient methods for DNS resolution. public class DNSResolver { private static let queue = DispatchQueue(label: "DNSResolver") + /** + Resolves a hostname asynchronously. + + - Parameter hostname: The hostname to resolve. + - Parameter timeout: The timeout in milliseconds. + - Parameter queue: The queue to execute the `completionHandler` in. + - Parameter completionHandler: The completion handler with the resolved addresses and an optional error. + */ public static func resolve(_ hostname: String, timeout: Int, queue: DispatchQueue, completionHandler: @escaping ([String]?, Error?) -> Void) { var pendingHandler: (([String]?, Error?) -> Void)? = completionHandler let host = CFHostCreateWithName(nil, hostname as CFString).takeRetainedValue() @@ -94,6 +103,12 @@ public class DNSResolver { completionHandler(ipAddresses, nil) } + /** + Returns a `String` representation from a numeric IPv4 address. + + - Parameter ipv4: The IPv4 address as a 32-bit number. + - Returns: The string representation of `ipv4`. + */ public static func string(fromIPv4 ipv4: UInt32) -> String { var addr = in_addr(s_addr: CFSwapInt32HostToBig(ipv4)) var buf = Data(count: Int(INET_ADDRSTRLEN)) @@ -110,6 +125,12 @@ public class DNSResolver { return String(cString: result) } + /** + Returns a numeric representation from an IPv4 address. + + - Parameter string: The IPv4 address as a string. + - Returns: The numeric representation of `string`. + */ public static func ipv4(fromString string: String) -> UInt32? { var addr = in_addr() let result = string.withCString {