Simplified process startup on macOS and Linux

This commit is contained in:
Conrad Kramer 2024-01-21 16:18:13 -08:00
parent 9e03c9680c
commit b462f3fb6c
40 changed files with 1343 additions and 1157 deletions

View file

@ -1,133 +0,0 @@
import Foundation
import Network
import os
final class LineProtocol: NWProtocolFramerImplementation {
static let definition = NWProtocolFramer.Definition(implementation: LineProtocol.self)
static let label = "Lines"
init(framer: NWProtocolFramer.Instance) { }
func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready }
func stop(framer: NWProtocolFramer.Instance) -> Bool { true }
func wakeup(framer: NWProtocolFramer.Instance) { }
func cleanup(framer: NWProtocolFramer.Instance) { }
func lines(from buffer: UnsafeMutableRawBufferPointer?) -> (lines: [Data], size: Int)? {
guard let buffer = buffer else { return nil }
let lines = buffer
.split(separator: 10)
guard !lines.isEmpty else { return nil }
let size = lines
.lazy
.map(\.count)
.reduce(0, +) + lines.count
let strings = lines
.lazy
.map { Data($0) }
return (lines: Array(strings), size: size)
}
func handleInput(framer: NWProtocolFramer.Instance) -> Int {
var result: [Data] = []
_ = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in
guard let (lines, size) = lines(from: buffer) else {
return 0
}
result = lines
return size
}
for line in result {
framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true)
}
return 0
}
func handleOutput(
framer: NWProtocolFramer.Instance,
message: NWProtocolFramer.Message,
messageLength: Int,
isComplete: Bool
) {
do {
try framer.writeOutputNoCopy(length: messageLength)
} catch {
}
}
}
extension NWConnection {
func receiveMessage() async throws -> (Data?, NWConnection.ContentContext?, Bool) {
try await withUnsafeThrowingContinuation { continuation in
receiveMessage { completeContent, contentContext, isComplete, error in
if let error = error {
continuation.resume(throwing: error)
}
continuation.resume(returning: (completeContent, contentContext, isComplete))
}
}
}
func send_raw(_ request: Data) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
let comp: NWConnection.SendCompletion = .contentProcessed {error in
if let error = error {
continuation.resume(with: .failure(error))
} else {
continuation.resume(with: .success(request))
}
}
self.send(content: request, completion: comp)
}
}
}
final class BurrowIpc {
let connection: NWConnection
private var generator = SystemRandomNumberGenerator()
private var logger: Logger
init(logger: Logger) {
let params = NWParameters.tcp
params.defaultProtocolStack
.applicationProtocols
.insert(NWProtocolFramer.Options(definition: LineProtocol.definition), at: 0)
let connection = NWConnection(to: .unix(path: "burrow.sock"), using: params)
connection.start(queue: .global())
self.connection = connection
self.logger = logger
}
func send<T: Request, U: Decodable>(_ request: T) async throws -> U {
do {
let id: UInt = generator.next(upperBound: UInt.max)
var copy = request
copy.id = id
var data = try JSONEncoder().encode(request)
data.append(contentsOf: [10])
_ = try await self.connection.send_raw(data)
return try JSONDecoder().decode(Response<U>.self, from: data).result
} catch {
throw error
}
}
func receive_raw() async throws -> Data {
let (completeContent, _, _) = try await connection.receiveMessage()
self.logger.info("Received raw message response")
guard let data = completeContent else {
throw BurrowError.resultIsNone
}
return data
}
func request<U: Decodable>(_ request: any Request, type: U.Type) async throws -> U {
do {
var data: Data = try JSONEncoder().encode(request)
data.append(contentsOf: [10])
_ = try await self.connection.send_raw(data)
self.logger.debug("message sent")
let receivedData = try await receive_raw()
self.logger.info("Received result: \(String(decoding: receivedData, as: UTF8.self))")
return try self.parse_response(receivedData)
} catch {
throw error
}
}
func parse_response<U: Decodable>(_ response: Data) throws -> U {
try JSONDecoder().decode(U.self, from: response)
}
}

View file

@ -0,0 +1,60 @@
import BurrowShared
import Foundation
import Network
final class Client {
let connection: NWConnection
private let logger: Logger = Logger.logger(for: Client.self)
private var generator = SystemRandomNumberGenerator()
convenience init() throws {
self.init(url: try Constants.socketURL)
}
init(url: URL) {
let endpoint: NWEndpoint
if url.isFileURL {
endpoint = .unix(path: url.path(percentEncoded: false))
} else {
endpoint = .url(url)
}
let parameters = NWParameters.tcp
parameters.defaultProtocolStack
.applicationProtocols
.insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0)
connection = NWConnection(to: endpoint, using: parameters)
connection.start(queue: .global())
}
func request<U: Decodable>(_ request: any Request, type: U.Type = U.self) async throws -> U {
do {
var copy = request
copy.id = generator.next(upperBound: UInt.max)
let content = try JSONEncoder().encode(copy)
logger.debug("> \(String(decoding: content, as: UTF8.self))")
try await self.connection.send(content: content)
let (response, _, _) = try await connection.receiveMessage()
logger.debug("< \(String(decoding: response, as: UTF8.self))")
return try JSONDecoder().decode(U.self, from: response)
} catch {
logger.error("\(error, privacy: .public)")
throw error
}
}
deinit {
connection.cancel()
}
}
extension Constants {
static var socketURL: URL {
get throws {
try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory)
}
}
}

View file

@ -8,10 +8,11 @@ enum BurrowError: Error {
case resultIsNone
}
protocol Request: Codable where CommandT: Codable {
associatedtype CommandT
protocol Request: Codable where Command: Codable {
associatedtype Command
var id: UInt { get set }
var command: CommandT { get set }
var command: Command { get set }
}
struct BurrowSingleCommand: Request {
@ -38,13 +39,6 @@ struct BurrowStartRequest: Codable {
let Start: StartOptions
}
func start_req_fd(id: UInt) -> BurrowRequest<BurrowStartRequest> {
let command = BurrowStartRequest(Start: BurrowStartRequest.StartOptions(
tun: BurrowStartRequest.TunOptions(name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil)
))
return BurrowRequest(id: id, command: command)
}
struct Response<T>: Decodable where T: Decodable {
var id: UInt
var result: T

View file

@ -0,0 +1,32 @@
import Foundation
import Network
extension NWConnection {
// swiftlint:disable:next large_tuple
func receiveMessage() async throws -> (Data, NWConnection.ContentContext?, Bool) {
try await withUnsafeThrowingContinuation { continuation in
receiveMessage { completeContent, contentContext, isComplete, error in
if let error {
continuation.resume(throwing: error)
} else {
guard let completeContent = completeContent else {
fatalError("Both error and completeContent were nil")
}
continuation.resume(returning: (completeContent, contentContext, isComplete))
}
}
}
}
func send(content: Data) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
send(content: content, completion: .contentProcessed { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
})
}
}
}

View file

@ -2,17 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(APP_GROUP_IDENTIFIER)</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View file

@ -6,6 +6,6 @@ PRODUCT_BUNDLE_IDENTIFIER = $(NETWORK_EXTENSION_BUNDLE_IDENTIFIER)
INFOPLIST_FILE = NetworkExtension/Info.plist
CODE_SIGN_ENTITLEMENTS = NetworkExtension/NetworkExtension-iOS.entitlements
CODE_SIGN_ENTITLEMENTS[sdk=macos*] = NetworkExtension/NetworkExtension-macOS.entitlements
CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = NetworkExtension/NetworkExtension-macOS.entitlements
SWIFT_INCLUDE_PATHS = $(inherited) $(PROJECT_DIR)/NetworkExtension

View file

@ -0,0 +1,54 @@
import Foundation
import Network
final class NewlineProtocolFramer: NWProtocolFramerImplementation {
private static let delimeter: UInt8 = 10 // `\n`
static let definition = NWProtocolFramer.Definition(implementation: NewlineProtocolFramer.self)
static let label = "Lines"
init(framer: NWProtocolFramer.Instance) { }
func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready }
func stop(framer: NWProtocolFramer.Instance) -> Bool { true }
func wakeup(framer: NWProtocolFramer.Instance) { }
func cleanup(framer: NWProtocolFramer.Instance) { }
func handleInput(framer: NWProtocolFramer.Instance) -> Int {
while true {
var result: [Data] = []
let parsed = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in
guard let buffer else { return 0 }
var lines = buffer
.split(separator: Self.delimeter, omittingEmptySubsequences: false)
.map { Data($0) }
guard lines.count > 1 else { return 0 }
_ = lines.popLast()
result = lines
return lines.reduce(lines.count) { $0 + $1.count }
}
guard parsed && !result.isEmpty else { break }
for line in result {
framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true)
}
}
return 0
}
func handleOutput(
framer: NWProtocolFramer.Instance,
message: NWProtocolFramer.Message,
messageLength: Int,
isComplete: Bool
) {
do {
try framer.writeOutputNoCopy(length: messageLength)
framer.writeOutput(data: [Self.delimeter])
} catch {
}
}
}

View file

@ -1,51 +1,49 @@
import BurrowShared
import libburrow
import NetworkExtension
import os
class PacketTunnelProvider: NEPacketTunnelProvider {
let logger = Logger(subsystem: "com.hackclub.burrow", category: "frontend")
var client: BurrowIpc?
var osInitialized = false
private let logger = Logger.logger(for: PacketTunnelProvider.self)
override func startTunnel(options: [String: NSObject]? = nil) async throws {
logger.log("Starting tunnel")
if !osInitialized {
libburrow.initialize_oslog()
osInitialized = true
}
libburrow.start_srv()
client = BurrowIpc(logger: logger)
logger.info("Started server")
do {
let command = BurrowSingleCommand(id: 0, command: "ServerConfig")
guard let data = try await client?.request(command, type: Response<BurrowResult<ServerConfigData>>.self)
else {
throw BurrowError.cantParseResult
}
libburrow.spawnInProcess(socketPath: try Constants.socketURL.path)
let client = try Client()
let command = BurrowRequest(id: 0, command: "ServerConfig")
let data = try await client.request(command, type: Response<BurrowResult<ServerConfigData>>.self)
let encoded = try JSONEncoder().encode(data.result)
self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))")
guard let serverconfig = data.result.Ok else {
throw BurrowError.resultIsError
}
guard let tunNs = self.generateTunSettings(from: serverconfig) else {
guard let tunNs = generateTunSettings(from: serverconfig) else {
throw BurrowError.addrDoesntExist
}
try await self.setTunnelNetworkSettings(tunNs)
self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)")
// let tunFd = self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int;
// self.logger.info("Found File Descriptor: \(tunFd)")
let startCommand = start_req_fd(id: 1)
guard let data = try await client?.request(startCommand, type: Response<BurrowResult<String>>.self)
else {
throw BurrowError.cantParseResult
}
let encodedStartRes = try JSONEncoder().encode(data.result)
self.logger.log("Received start server response: \(String(decoding: encodedStartRes, as: UTF8.self))")
let startRequest = BurrowRequest(
id: .random(in: (.min)..<(.max)),
command: BurrowStartRequest(
Start: BurrowStartRequest.StartOptions(
tun: BurrowStartRequest.TunOptions(
name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil
)
)
)
)
let response = try await client.request(startRequest, type: Response<BurrowResult<String>>.self)
self.logger.log("Received start server response: \(String(describing: response.result))")
} catch {
self.logger.error("An error occurred: \(error)")
self.logger.error("Failed to start tunnel: \(error)")
throw error
}
}
private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? {
let cfig = from.ServerConfig
guard let addr = cfig.address else {
@ -57,13 +55,4 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)")
return nst
}
override func stopTunnel(with reason: NEProviderStopReason) async {
}
override func handleAppMessage(_ messageData: Data) async -> Data? {
messageData
}
override func sleep() async {
}
override func wake() {
}
}

View file

@ -1,2 +1,2 @@
void start_srv();
void initialize_oslog();
__attribute__((__swift_name__("spawnInProcess(socketPath:)")))
extern void spawn_in_process(const char * __nullable path);