Files
byre/mac/Sources/Byre/Proxy.swift
T

197 lines
6.6 KiB
Swift

import Foundation
import Network
#if os(iOS)
import UIKit
#endif
private let succeeded: [UInt8] = [5, 0, 0, 1, 0, 0, 0, 0, 0, 0]
private let hostUnreachable: UInt8 = 4
private let commandNotSupported: UInt8 = 7
private let addressNotSupported: UInt8 = 8
final class TailnetProxy: @unchecked Sendable {
let credential = (0..<32).map { _ in String(UInt8.random(in: 0..<16), radix: 16) }.joined()
private(set) var port: UInt16 = 0
private let queue = DispatchQueue(label: "run.fc.byre.proxy")
private var listener: NWListener?
private var splices: [ObjectIdentifier: Splice] = [:]
init() {
let bound = DispatchSemaphore(value: 0)
queue.async {
self.listen {
bound.signal()
}
}
bound.wait()
#if os(iOS)
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [queue] _ in
queue.async {
self.listen()
}
}
#endif
}
// iOS reclaims a suspended app's listening sockets, so the listener is bound again on the same port each time the app returns to the foreground.
private func listen(_ bound: (@Sendable () -> Void)? = nil) {
self.listener?.cancel()
self.listener = nil
let params = NWParameters.tcp
params.allowLocalEndpointReuse = true
params.requiredLocalEndpoint = .hostPort(host: .ipv4(.loopback), port: port == 0 ? .any : NWEndpoint.Port(rawValue: port)!)
let listener: NWListener
do {
listener = try NWListener(using: params)
} catch {
failed(nil, error)
return
}
self.listener = listener
listener.stateUpdateHandler = { [weak self, weak listener] state in
guard let self, let listener, self.listener === listener else {
return
}
switch state {
case .ready:
if self.port == 0 {
self.port = listener.port!.rawValue
}
bound?()
case .failed(let error):
self.failed(listener, error)
default:
break
}
}
listener.newConnectionHandler = { [weak self] conn in
self?.accept(conn)
}
listener.start(queue: queue)
}
private func failed(_ listener: NWListener?, _ error: any Error) {
if port == 0 {
fatalError("tailnet proxy listener: \(error)")
}
log.error("tailnet proxy listener on port \(self.port): \(String(describing: error), privacy: .public)")
listener?.cancel()
queue.asyncAfter(deadline: .now() + 1) {
if self.listener === listener {
self.listen()
}
}
}
private func accept(_ conn: NWConnection) {
conn.start(queue: queue)
read(conn, 2) { head in
self.read(conn, Int(head[1])) { methods in
guard head[0] == 5, methods.contains(2) else {
self.send(conn, [5, 0xff], close: true)
return
}
self.send(conn, [5, 2])
self.authenticate(conn)
}
}
}
private func authenticate(_ conn: NWConnection) {
read(conn, 2) { head in
self.read(conn, Int(head[1])) { _ in
self.read(conn, 1) { length in
self.read(conn, Int(length[0])) { password in
guard head[0] == 1, String(decoding: password, as: UTF8.self) == self.credential else {
self.send(conn, [1, 1], close: true)
return
}
self.send(conn, [1, 0])
self.request(conn)
}
}
}
}
}
private func request(_ conn: NWConnection) {
read(conn, 4) { head in
guard head[0] == 5, head[1] == 1 else {
self.refuse(conn, commandNotSupported)
return
}
let connect = { @Sendable (host: String) in
self.read(conn, 2) { port in
self.connect(conn, "\(host):\(Int(port[0]) << 8 | Int(port[1]))")
}
}
switch head[3] {
case 1:
self.read(conn, 4) { ip in
connect(ip.map(String.init).joined(separator: "."))
}
case 3:
self.read(conn, 1) { length in
self.read(conn, Int(length[0])) { name in
connect(String(decoding: name, as: UTF8.self))
}
}
case 4:
self.read(conn, 16) { ip in
connect("[\(IPv6Address(Data(ip))!)]")
}
default:
self.refuse(conn, addressNotSupported)
}
}
}
private func connect(_ conn: NWConnection, _ address: String) {
DispatchQueue.global().async {
do {
let fd = try tailnetNode.dial(address)
self.queue.async {
self.send(conn, succeeded)
let splice = Splice(conn, fd, self.queue)
let id = ObjectIdentifier(splice)
self.splices[id] = splice
splice.start {
self.splices[id] = nil
}
}
} catch {
log.error("tailnet proxy: \(errorText(error), privacy: .public)")
self.queue.async {
self.refuse(conn, hostUnreachable)
}
}
}
}
private func refuse(_ conn: NWConnection, _ code: UInt8) {
send(conn, [5, code, 0, 1, 0, 0, 0, 0, 0, 0], close: true)
}
private func read(_ conn: NWConnection, _ count: Int, _ then: @escaping @Sendable ([UInt8]) -> Void) {
if count == 0 {
then([])
return
}
conn.receive(minimumIncompleteLength: count, maximumLength: count) { data, _, _, error in
guard error == nil, let data, data.count == count else {
conn.cancel()
return
}
then([UInt8](data))
}
}
private func send(_ conn: NWConnection, _ bytes: [UInt8], close: Bool = false) {
conn.send(content: Data(bytes), completion: .contentProcessed { _ in
if close {
conn.cancel()
}
})
}
}