From 8f6e0fa72066441529869c87928ffeaca7234e5c Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 1 Oct 2023 11:49:00 +0800 Subject: [PATCH] Generate NetworkSettings with IPC This generates and applies NetworkSettings object with unix socket IPC. - domain socket, json-rpc based communication - switches to anyhow for burrow crate - adds support for starting daemons on macos --- .gitignore | 3 + Apple/Burrow.xcodeproj/project.pbxproj | 8 + Apple/NetworkExtension/BurrowIpc.swift | 142 ++++++++++++ Apple/NetworkExtension/DataTypes.swift | 40 ++++ .../NetworkExtension-macOS.entitlements | 2 + .../PacketTunnelProvider.swift | 63 +++-- Apple/NetworkExtension/libburrow/libburrow.h | 3 +- Cargo.lock | 217 ++++++++++++++++-- burrow/Cargo.toml | 11 +- burrow/src/apple.rs | 15 ++ burrow/src/daemon/command.rs | 23 +- burrow/src/daemon/instance.rs | 66 ++++-- burrow/src/daemon/mod.rs | 13 +- burrow/src/daemon/net/apple.rs | 24 ++ burrow/src/daemon/net/mod.rs | 12 +- burrow/src/daemon/net/systemd.rs | 12 +- burrow/src/daemon/net/unix.rs | 99 ++++++-- burrow/src/daemon/net/windows.rs | 2 +- burrow/src/daemon/response.rs | 109 +++++++++ ...ommand__daemoncommand_serialization-2.snap | 5 + ...ommand__daemoncommand_serialization-3.snap | 5 + ...ommand__daemoncommand_serialization-4.snap | 5 + ..._command__daemoncommand_serialization.snap | 5 + ...n__response__response_serialization-2.snap | 5 + ...n__response__response_serialization-3.snap | 5 + ...n__response__response_serialization-4.snap | 5 + ...mon__response__response_serialization.snap | 5 + burrow/src/lib.rs | 11 + burrow/src/main.rs | 100 ++++++-- tun/Cargo.toml | 3 +- tun/src/options.rs | 2 +- 31 files changed, 906 insertions(+), 114 deletions(-) create mode 100644 Apple/NetworkExtension/BurrowIpc.swift create mode 100644 Apple/NetworkExtension/DataTypes.swift create mode 100644 burrow/src/apple.rs create mode 100644 burrow/src/daemon/net/apple.rs create mode 100644 burrow/src/daemon/response.rs create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap create mode 100644 burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap diff --git a/.gitignore b/.gitignore index 102ee0dd..11c6ec92 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ xcuserdata # Rust target/ + +.DS_STORE +.idea/ \ No newline at end of file diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index f9c74545..7548f3e5 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; + 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */; }; 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; @@ -46,6 +48,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; + 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowIpc.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; @@ -122,6 +126,8 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */, + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, + 0B46E8DF2AC918CA00BA2A3C /* BurrowIpc.swift */, D0B98FD729FDDB57004E7149 /* libburrow */, ); path = NetworkExtension; @@ -304,6 +310,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */, + 0B46E8E02AC918CA00BA2A3C /* BurrowIpc.swift in Sources */, D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Apple/NetworkExtension/BurrowIpc.swift b/Apple/NetworkExtension/BurrowIpc.swift new file mode 100644 index 00000000..0992bfc8 --- /dev/null +++ b/Apple/NetworkExtension/BurrowIpc.swift @@ -0,0 +1,142 @@ +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)) + } + } + } +} + +final class BurrowIpc { + let connection: NWConnection + private var generator = SystemRandomNumberGenerator() + private var continuations: [UInt: UnsafeContinuation] = [:] + 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(_ request: T) async throws -> U { + let data: Data = try await withUnsafeThrowingContinuation { continuation in + let id: UInt = generator.next(upperBound: UInt.max) + continuations[id] = continuation + var copy = request + copy.id = id + do { + var data = try JSONEncoder().encode(request) + data.append(contentsOf: [10]) + let completion: NWConnection.SendCompletion = .contentProcessed { error in + guard let error = error else { return } + continuation.resume(throwing: error) + } + connection.send(content: data, completion: completion) + } catch { + continuation.resume(throwing: error) + return + } + } + return try JSONDecoder().decode(Response.self, from: data).result + } + 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.connection.send(content: request, completion: comp) + } + } + + 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(_ request: Request, type: U.Type) async throws -> U { + do { + var data: Data = try JSONEncoder().encode(request) + data.append(contentsOf: [10]) + try await 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(_ response: Data) throws -> U { + try JSONDecoder().decode(U.self, from: response) + } +} diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift new file mode 100644 index 00000000..b228d778 --- /dev/null +++ b/Apple/NetworkExtension/DataTypes.swift @@ -0,0 +1,40 @@ +import Foundation + +enum BurrowError: Error { + case addrDoesntExist + case resultIsError + case cantParseResult + case resultIsNone +} + +protocol Request: Codable { + var id: UInt { get set } + var command: String { get set } +} + +struct BurrowRequest: Request { + var id: UInt + var command: String +} + +struct Response: Decodable where T: Decodable { + var id: UInt + var result: T +} + +// swiftlint:disable identifier_name +struct BurrowResult: Codable where T: Codable { + var Ok: T? + var Err: String? +} + +struct ServerConfigData: Codable { + struct InternalConfig: Codable { + let address: String? + let name: String? + let mtu: Int32? + } + let ServerConfig: InternalConfig +} + +// swiftlint:enable identifier_name diff --git a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements index 02ee960d..c3d6dc29 100644 --- a/Apple/NetworkExtension/NetworkExtension-macOS.entitlements +++ b/Apple/NetworkExtension/NetworkExtension-macOS.entitlements @@ -2,6 +2,8 @@ + com.apple.security.network.client + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 5ca4e931..d3a3e5dc 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,39 +1,66 @@ import libburrow import NetworkExtension -import OSLog +import os class PacketTunnelProvider: NEPacketTunnelProvider { - let logger = Logger(subsystem: "com.hackclub.burrow", category: "General") + let logger = Logger(subsystem: "com.hackclub.burrow", category: "frontend") + var client: BurrowIpc? + var osInitialized = false override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { - let fild = libburrow.retrieve() - if fild == -1 { - // Not sure if this is the right way to return an error - logger.error("Failed to retrieve file descriptor for burrow.") - let err = NSError( - domain: "com.hackclub.burrow", - code: 1_010, - userInfo: [NSLocalizedDescriptionKey: "Failed to find TunInterface"] - ) - completionHandler(err) + logger.log("Starting tunnel") + if !osInitialized { + libburrow.initialize_oslog() + osInitialized = true + } + libburrow.start_srv() + client = BurrowIpc(logger: logger) + logger.info("Started server") + Task { + do { + let command = BurrowRequest(id: 0, command: "ServerConfig") + guard let data = try await client?.request(command, type: Response>.self) + else { + throw BurrowError.cantParseResult + } + 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 { + throw BurrowError.addrDoesntExist + } + try await self.setTunnelNetworkSettings(tunNs) + self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") + completionHandler(nil) + } catch { + self.logger.error("An error occurred: \(error)") + completionHandler(error) + } } - logger.info("fd: \(fild)") - completionHandler(nil) } - + private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { + let cfig = from.ServerConfig + guard let addr = cfig.address else { + return nil + } + // Using a makeshift remote tunnel address + var nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") + nst.ipv4Settings = NEIPv4Settings(addresses: [addr], subnetMasks: ["255.255.255.0"]) + logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") + return nst + } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { completionHandler() } - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { if let handler = completionHandler { handler(messageData) } } - override func sleep(completionHandler: @escaping () -> Void) { completionHandler() } - override func wake() { } } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 1057c90d..32d1d3b1 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1 +1,2 @@ -int retrieve(); +void start_srv(); +void initialize_oslog(); diff --git a/Cargo.lock b/Cargo.lock index f7cf03ad..6716320b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,16 +39,15 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -78,9 +77,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -92,6 +91,17 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -202,12 +212,15 @@ name = "burrow" version = "0.1.0" dependencies = [ "anyhow", + "async-channel", "caps", "clap", "env_logger", + "insta", "libsystemd", "log", "nix", + "schemars", "serde", "serde_json", "tokio", @@ -309,20 +322,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.10" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.10" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -332,9 +344,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.2" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", @@ -354,6 +366,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -424,12 +457,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "dyn-clone" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" + [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -473,6 +518,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.9.0" @@ -811,6 +862,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -918,6 +983,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -946,6 +1017,15 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1068,6 +1148,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.31.1" @@ -1153,7 +1243,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -1260,9 +1350,24 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.2" @@ -1360,6 +1465,30 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "schemars" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1409,6 +1538,17 @@ dependencies = [ "syn 2.0.22", ] +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_json" version = "1.0.99" @@ -1480,6 +1620,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "slab" version = "0.4.8" @@ -1656,6 +1802,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2", "tokio-macros", @@ -1779,10 +1926,14 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -1808,6 +1959,7 @@ dependencies = [ "log", "nix", "reqwest", + "schemars", "serde", "socket2", "ssri", @@ -2041,7 +2193,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -2059,13 +2211,37 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2182,6 +2358,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index ddce4b1c..ab19e5c8 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,18 +10,20 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util"] } -tun = { version = "0.1", path = "../tun", features = ["serde"] } +tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread"] } +tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.3.2", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" tracing-journald = "0.3" tracing-oslog = {git = "https://github.com/Stormshield-robinc/tracing-oslog"} -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"]} env_logger = "0.10" log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +async-channel = "1.9" +schemars = "0.8" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5.5" @@ -30,6 +32,9 @@ libsystemd = "0.6" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.26.2" } +[dev-dependencies] +insta = { version = "1.32.0", features = ["yaml"] } + [package.metadata.generate-rpm] assets = [ { source = "target/release/burrow", dest = "/usr/bin/burrow", mode = "755" }, diff --git a/burrow/src/apple.rs b/burrow/src/apple.rs new file mode 100644 index 00000000..0a968779 --- /dev/null +++ b/burrow/src/apple.rs @@ -0,0 +1,15 @@ +use tracing::{debug, Subscriber}; +use tracing::instrument::WithSubscriber; +use tracing_oslog::OsLogger; +use tracing_subscriber::FmtSubscriber; +use tracing_subscriber::layer::SubscriberExt; + +pub use crate::daemon::start_srv; + +#[no_mangle] +pub extern "C" fn initialize_oslog() { + let collector = tracing_subscriber::registry() + .with(OsLogger::new("com.hackclub.burrow", "backend")); + tracing::subscriber::set_global_default(collector).unwrap(); + debug!("Initialized oslog tracing in libburrow rust FFI"); +} \ No newline at end of file diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/command.rs index d786a99c..a5a1f30a 100644 --- a/burrow/src/daemon/command.rs +++ b/burrow/src/daemon/command.rs @@ -1,13 +1,32 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tun::TunOptions; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub enum DaemonCommand { Start(DaemonStartOptions), + ServerInfo, + ServerConfig, Stop, } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct DaemonStartOptions { pub(super) tun: TunOptions, } + +#[test] +fn test_daemoncommand_serialization() { + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::ServerInfo).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::Stop).unwrap() + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonCommand::ServerConfig).unwrap() + ) +} \ No newline at end of file diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index d1849d02..db9e1ac7 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,40 +1,70 @@ +use tracing::{debug, info, warn}; +use DaemonResponse; +use crate::daemon::response::{DaemonResponseData, ServerConfig, ServerInfo}; use super::*; pub struct DaemonInstance { - rx: mpsc::Receiver, + rx: async_channel::Receiver, + sx: async_channel::Sender, tun_interface: Option, } impl DaemonInstance { - pub fn new(rx: mpsc::Receiver) -> Self { + pub fn new(rx: async_channel::Receiver, sx: async_channel::Sender) -> Self { Self { rx, + sx, tun_interface: None, } } - pub async fn run(&mut self) -> Result<()> { - while let Some(command) = self.rx.recv().await { - match command { - DaemonCommand::Start(options) => { - if self.tun_interface.is_none() { - self.tun_interface = Some(options.tun.open()?); - eprintln!("Daemon starting tun interface."); - } else { - eprintln!("Got start, but tun interface already up."); - } + async fn proc_command(&mut self, command: DaemonCommand) -> Result { + info!("Daemon got command: {:?}", command); + match command { + DaemonCommand::Start(st) => { + if self.tun_interface.is_none() { + debug!("Daemon attempting start tun interface."); + self.tun_interface = Some(st.tun.open()?); + info!("Daemon started tun interface"); + } else { + warn!("Got start, but tun interface already up."); } - DaemonCommand::Stop => { - if self.tun_interface.is_some() { - self.tun_interface = None; - eprintln!("Daemon stopping tun interface."); - } else { - eprintln!("Got stop, but tun interface is not up.") + Ok(DaemonResponseData::None) + } + DaemonCommand::ServerInfo => { + match &self.tun_interface { + None => {Ok(DaemonResponseData::None)} + Some(ti) => { + info!("{:?}", ti); + Ok( + DaemonResponseData::ServerInfo( + ServerInfo::try_from(ti)? + ) + ) } } } + DaemonCommand::Stop => { + if self.tun_interface.is_some() { + self.tun_interface = None; + info!("Daemon stopping tun interface."); + } else { + warn!("Got stop, but tun interface is not up.") + } + Ok(DaemonResponseData::None) + } + DaemonCommand::ServerConfig => { + Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) + } } + } + pub async fn run(&mut self) -> Result<()> { + while let Ok(command) = self.rx.recv().await { + let response = self.proc_command(command).await; + info!("Daemon response: {:?}", response); + self.sx.send(DaemonResponse::new(response)).await?; + } Ok(()) } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 5fcf8ee7..b59ad7f8 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -4,6 +4,7 @@ use tokio::sync::mpsc; mod command; mod instance; mod net; +mod response; use instance::DaemonInstance; use net::listen; @@ -11,9 +12,15 @@ use net::listen; pub use command::{DaemonCommand, DaemonStartOptions}; pub use net::DaemonClient; +#[cfg(target_vendor = "apple")] +pub use net::start_srv; + +pub use response::{DaemonResponseData, DaemonResponse, ServerInfo}; + pub async fn daemon_main() -> Result<()> { - let (tx, rx) = mpsc::channel(2); - let mut inst = DaemonInstance::new(rx); + let (commands_tx, commands_rx) = async_channel::unbounded(); + let (response_tx, response_rx) = async_channel::unbounded(); + let mut inst = DaemonInstance::new(commands_rx, response_tx); - tokio::try_join!(inst.run(), listen(tx)).map(|_| ()) + tokio::try_join!(inst.run(), listen(commands_tx, response_rx)).map(|_| ()) } diff --git a/burrow/src/daemon/net/apple.rs b/burrow/src/daemon/net/apple.rs new file mode 100644 index 00000000..e53bdaad --- /dev/null +++ b/burrow/src/daemon/net/apple.rs @@ -0,0 +1,24 @@ +use std::thread; +use tokio::runtime::Runtime; +use tracing::error; +use crate::daemon::{daemon_main, DaemonClient}; + +#[no_mangle] +pub extern "C" fn start_srv(){ + let _handle = thread::spawn(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + if let Err(e) = daemon_main().await { + error!("Error when starting rpc server: {}", e); + } + }); + }); + let rt = Runtime::new().unwrap(); + rt.block_on(async { + loop { + if let Ok(_) = DaemonClient::new().await{ + break + } + } + }); +} \ No newline at end of file diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index d8cc5fad..b5e0736d 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -13,17 +13,19 @@ pub use systemd::{listen, DaemonClient}; #[cfg(target_os = "windows")] mod windows; + #[cfg(target_os = "windows")] pub use windows::{listen, DaemonClient}; +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_vendor = "apple")] +pub use apple::start_srv; + #[derive(Clone, Serialize, Deserialize)] pub struct DaemonRequest { pub id: u32, pub command: DaemonCommand, } -#[derive(Clone, Serialize, Deserialize)] -pub struct DaemonResponse { - // Error types can't be serialized, so this is the second best option. - result: std::result::Result<(), String>, -} diff --git a/burrow/src/daemon/net/systemd.rs b/burrow/src/daemon/net/systemd.rs index f67888ed..446c2593 100644 --- a/burrow/src/daemon/net/systemd.rs +++ b/burrow/src/daemon/net/systemd.rs @@ -1,16 +1,16 @@ use super::*; use std::os::fd::IntoRawFd; -pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { - if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone()).await.is_err() { - unix::listen(cmd_tx).await?; +pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + if !libsystemd::daemon::booted() || listen_with_systemd(cmd_tx.clone(), rsp_rx.clone()).await.is_err() { + unix::listen(cmd_tx, rsp_rx).await?; } Ok(()) } -async fn listen_with_systemd(cmd_tx: mpsc::Sender) -> Result<()> { - let fds = libsystemd::activation::receive_descriptors(false).unwrap(); - super::unix::listen_with_optional_fd(cmd_tx, Some(fds[0].clone().into_raw_fd())).await +async fn listen_with_systemd(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + let fds = libsystemd::activation::receive_descriptors(false)?; + super::unix::listen_with_optional_fd(cmd_tx, rsp_rx,Some(fds[0].clone().into_raw_fd())).await } pub type DaemonClient = unix::DaemonClient; diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index a2837a33..c193a7bc 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -1,22 +1,51 @@ use super::*; -use std::{ - os::fd::{FromRawFd, RawFd}, - os::unix::net::UnixListener as StdUnixListener, - path::Path, -}; +use std::{ascii, io, os::fd::{FromRawFd, RawFd}, os::unix::net::UnixListener as StdUnixListener, path::Path}; +use std::hash::Hash; +use std::path::PathBuf; +use anyhow::anyhow; +use log::log; +use tracing::info; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; +use tracing::debug; +#[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; -pub async fn listen(cmd_tx: mpsc::Sender) -> Result<()> { - listen_with_optional_fd(cmd_tx, None).await +#[cfg(target_vendor = "apple")] +const UNIX_SOCKET_PATH: &str = "burrow.sock"; + +#[cfg(target_os = "macos")] +fn fetch_socket_path() -> Option{ + let tries = vec![ + "burrow.sock".to_string(), + format!("{}/Library/Containers/com.hackclub.burrow.network/Data/burrow.sock", + std::env::var("HOME").unwrap_or_default()) + .to_string(), + ]; + for path in tries{ + let path = PathBuf::from(path); + if path.exists(){ + return Some(path); + } + } + None +} + +#[cfg(not(target_os = "macos"))] +fn fetch_socket_path() -> Option{ + Some(Path::new(UNIX_SOCKET_PATH).to_path_buf()) +} + +pub async fn listen(cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver) -> Result<()> { + listen_with_optional_fd(cmd_tx, rsp_rx, None).await } pub(crate) async fn listen_with_optional_fd( - cmd_tx: mpsc::Sender, + cmd_tx: async_channel::Sender, + rsp_rx: async_channel::Receiver, raw_fd: Option, ) -> Result<()> { let path = Path::new(UNIX_SOCKET_PATH); @@ -32,7 +61,16 @@ pub(crate) async fn listen_with_optional_fd( listener } else { // Won't help all that much, if we use the async version of fs. - std::fs::remove_file(path)?; + if let Some(par) = path.parent(){ + std::fs::create_dir_all( + par + )?; + } + match std::fs::remove_file(path){ + Err(e) if e.kind()==io::ErrorKind::NotFound => {Ok(())} + stuff => stuff + }?; + info!("Relative path: {}", path.to_string_lossy()); UnixListener::bind(path)? }; loop { @@ -41,29 +79,35 @@ pub(crate) async fn listen_with_optional_fd( // I'm pretty sure we won't need to manually join / shut this down, // `lines` will return Err during dropping, and this task should exit gracefully. - tokio::task::spawn(async { + let rsp_rxc = rsp_rx.clone(); + tokio::task::spawn(async move { let cmd_tx = cmd_tx; let mut stream = stream; let (mut read_stream, mut write_stream) = stream.split(); let buf_reader = BufReader::new(&mut read_stream); let mut lines = buf_reader.lines(); while let Ok(Some(line)) = lines.next_line().await { - let mut res = DaemonResponse { result: Ok(()) }; - let command = match serde_json::from_str::(&line) { - Ok(req) => Some(req.command), + info!("Got line: {}", line); + debug!("Line raw data: {:?}", line.as_bytes()); + let mut res : DaemonResponse = DaemonResponseData::None.into(); + let req = match serde_json::from_str::(&line) { + Ok(req) => Some(req), Err(e) => { - res.result = Err(format!("{}", e)); + res.result = Err(e.to_string()); None } }; let mut res = serde_json::to_string(&res).unwrap(); res.push('\n'); - write_stream.write_all(res.as_bytes()).await.unwrap(); - // I want this to come at the very end so that we always send a reponse back. - if let Some(command) = command { - cmd_tx.send(command).await.unwrap(); + if let Some(req) = req { + cmd_tx.send(req.command).await.unwrap(); + let res = rsp_rxc.recv().await.unwrap().with_id(req.id); + let mut retres = serde_json::to_string(&res).unwrap(); + retres.push('\n'); + info!("Sending response: {}", retres); + write_stream.write_all(retres.as_bytes()).await.unwrap(); } } }); @@ -76,7 +120,12 @@ pub struct DaemonClient { impl DaemonClient { pub async fn new() -> Result { - Self::new_with_path(UNIX_SOCKET_PATH).await + let path = fetch_socket_path() + .ok_or(anyhow!("Failed to find socket path"))?; + // debug!("found path: {:?}", path); + let connection = UnixStream::connect(path).await?; + debug!("connected to socket"); + Ok(Self { connection }) } pub async fn new_with_path(path: &str) -> Result { @@ -86,17 +135,19 @@ impl DaemonClient { Ok(Self { connection }) } - pub async fn send_command(&mut self, command: DaemonCommand) -> Result<()> { + pub async fn send_command(&mut self, command: DaemonCommand) -> Result { let mut command = serde_json::to_string(&DaemonRequest { id: 0, command })?; command.push('\n'); self.connection.write_all(command.as_bytes()).await?; let buf_reader = BufReader::new(&mut self.connection); let mut lines = buf_reader.lines(); - // This unwrap *should* never cause issues. - let response = lines.next_line().await?.unwrap(); + let response = lines + .next_line() + .await? + .ok_or(anyhow!("Failed to read response"))?; + debug!("Got raw response: {}", response); let res: DaemonResponse = serde_json::from_str(&response)?; - res.result.unwrap(); - Ok(()) + Ok(res) } } diff --git a/burrow/src/daemon/net/windows.rs b/burrow/src/daemon/net/windows.rs index c858613f..3f9d5132 100644 --- a/burrow/src/daemon/net/windows.rs +++ b/burrow/src/daemon/net/windows.rs @@ -1,6 +1,6 @@ use super::*; -pub async fn listen(_: mpsc::Sender) -> Result<()> { +pub async fn listen(_cmd_tx: async_channel::Sender, _rsp_rx: async_channel::Receiver) -> Result<()> { unimplemented!("This platform does not currently support daemon mode.") } diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs new file mode 100644 index 00000000..da471504 --- /dev/null +++ b/burrow/src/daemon/response.rs @@ -0,0 +1,109 @@ +use anyhow::anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use tun::TunInterface; + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +pub struct DaemonResponse { + // Error types can't be serialized, so this is the second best option. + pub result: Result, + pub id: u32 +} + +impl DaemonResponse{ + pub fn new(result: Result) -> Self{ + Self{ + result: result.map_err(|e| e.to_string()), + id: 0 + } + } +} + +impl Into for DaemonResponseData{ + fn into(self) -> DaemonResponse{ + DaemonResponse::new(Ok::(self)) + } +} + +impl DaemonResponse{ + pub fn with_id(self, id: u32) -> Self{ + Self { + id, + ..self + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ServerInfo { + pub name: Option, + pub ip: Option, + pub mtu: Option +} + +impl TryFrom<&TunInterface> for ServerInfo{ + type Error = anyhow::Error; + + #[cfg(any(target_os="linux",target_vendor="apple"))] + fn try_from(server: &TunInterface) -> anyhow::Result { + Ok( + ServerInfo{ + name: server.name().ok(), + ip: server.ipv4_addr().ok().map(|ip| ip.to_string()), + mtu: server.mtu().ok() + } + ) + } + + #[cfg(not(any(target_os="linux",target_vendor="apple")))] + fn try_from(server: &TunInterface) -> anyhow::Result { + Err(anyhow!("Not implemented in this platform")) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ServerConfig { + pub address: Option, + pub name: Option, + pub mtu: Option +} + +impl Default for ServerConfig { + fn default() -> Self { + Self{ + address: Some("10.0.0.1".to_string()), // Dummy remote address + name: None, + mtu: None + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum DaemonResponseData{ + ServerInfo(ServerInfo), + ServerConfig(ServerConfig), + None +} + +#[test] +fn test_response_serialization() -> anyhow::Result<()>{ + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo{ + name: Some("burrow".to_string()), + ip: None, + mtu: Some(1500) + }))))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Err::("error".to_string())))? + ); + insta::assert_snapshot!( + serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig( + ServerConfig::default() + ))))? + ); + Ok(()) +} \ No newline at end of file diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap new file mode 100644 index 00000000..80b9e24c --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" +--- +"ServerInfo" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap new file mode 100644 index 00000000..8dc1b8b2 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" +--- +"Stop" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap new file mode 100644 index 00000000..9334ecef --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +--- +"ServerConfig" diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap new file mode 100644 index 00000000..2f8af667 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/command.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" +--- +{"Start":{"tun":{"name":null,"no_pi":null,"tun_excl":null}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap new file mode 100644 index 00000000..3787cd19 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo {\n name: Some(\"burrow\".to_string()),\n ip: None,\n mtu: Some(1500),\n }))))?" +--- +{"result":{"Ok":{"ServerInfo":{"name":"burrow","ip":null,"mtu":1500}}},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap new file mode 100644 index 00000000..4ef9575e --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Err::(\"error\".to_string())))?" +--- +{"result":{"Err":"error"},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap new file mode 100644 index 00000000..cf254193 --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" +--- +{"result":{"Ok":{"ServerConfig":{"address":"1.1.1.1","name":null,"mtu":null}}},"id":0} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap new file mode 100644 index 00000000..647d01cd --- /dev/null +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))?" +--- +{"result":{"Ok":"None"},"id":0} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index 1032e975..f66c4ac0 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,6 +1,8 @@ #![deny(missing_debug_implementations)] pub mod ensureroot; +use anyhow::Result; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] use std::{ mem, @@ -11,6 +13,15 @@ use tun::TunInterface; // TODO Separate start and retrieve functions +mod daemon; +pub use daemon::{DaemonCommand, DaemonResponseData, DaemonStartOptions, DaemonResponse, ServerInfo}; + +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_vendor = "apple")] +pub use apple::*; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[no_mangle] pub extern "C" fn retrieve() -> i32 { diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 1f70b1c5..f89ee391 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -4,12 +4,12 @@ use std::mem; use std::os::fd::FromRawFd; use clap::{Args, Parser, Subcommand}; -use tracing::instrument; +use tracing::{instrument, Level}; use tracing_log::LogTracer; use tracing_oslog::OsLogger; -use tracing_subscriber::{prelude::*, FmtSubscriber}; -use tokio::io::Result; +use tracing_subscriber::{prelude::*, FmtSubscriber, EnvFilter}; +use anyhow::Result; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use burrow::retrieve; use tun::TunInterface; @@ -17,6 +17,7 @@ use tun::TunInterface; mod daemon; use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; +use crate::daemon::DaemonResponseData; #[derive(Parser)] #[command(name = "Burrow")] @@ -44,6 +45,10 @@ enum Commands { Stop, /// Start Burrow daemon Daemon(DaemonArgs), + /// Server Info + ServerInfo, + /// Server config + ServerConfig, } #[derive(Args)] @@ -61,24 +66,35 @@ async fn try_start() -> Result<()> { client .send_command(DaemonCommand::Start(DaemonStartOptions::default())) .await + .map(|_| ()) } #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[instrument] async fn try_retrieve() -> Result<()> { - LogTracer::init().context("Failed to initialize LogTracer").unwrap(); + burrow::ensureroot::ensure_root(); + let iface2 = retrieve(); + tracing::info!("{}", iface2); + Ok(()) +} - if cfg!(target_os = "linux") || cfg!(target_vendor = "apple") { - let maybe_layer = system_log().unwrap(); +async fn initialize_tracing() -> Result<()> { + LogTracer::init().context("Failed to initialize LogTracer")?; + + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + { + let maybe_layer = system_log()?; if let Some(layer) = maybe_layer { - let logger = layer.with_subscriber(FmtSubscriber::new()); - tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber").unwrap(); + let logger = layer.with_subscriber( + FmtSubscriber::builder() + .with_line_number(true) + .with_env_filter(EnvFilter::from_default_env()) + .finish() + ); + tracing::subscriber::set_global_default(logger).context("Failed to set the global tracing subscriber")?; } } - burrow::ensureroot::ensure_root(); - let iface2 = retrieve(); - tracing::info!("{}", iface2); Ok(()) } @@ -89,6 +105,44 @@ async fn try_stop() -> Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverinfo() -> Result<()>{ + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerInfo).await?; + match res.result { + Ok(DaemonResponseData::ServerInfo(si)) => { + println!("Got Result! {:?}", si); + } + Ok(DaemonResponseData::None) => { + println!("Server not started.") + } + Ok(res) => {println!("Unexpected Response: {:?}", res)} + Err(e) => { + println!("Error when retrieving from server: {}", e) + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverconfig() -> Result<()>{ + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerConfig).await?; + match res.result { + Ok(DaemonResponseData::ServerConfig(cfig)) => { + println!("Got Result! {:?}", cfig); + } + Ok(DaemonResponseData::None) => { + println!("Server not started.") + } + Ok(res) => {println!("Unexpected Response: {:?}", res)} + Err(e) => { + println!("Error when retrieving from server: {}", e) + } + } + Ok(()) +} + #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] async fn try_start() -> Result<()> { Ok(()) @@ -104,24 +158,40 @@ async fn try_stop() -> Result<()> { Ok(()) } +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_serverinfo() -> Result<()> { + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] +async fn try_serverconfig() -> Result<()> { + Ok(()) +} #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { + initialize_tracing().await?; tracing::info!("Platform: {}", std::env::consts::OS); let cli = Cli::parse(); match &cli.command { Commands::Start(..) => { - try_start().await.unwrap(); + try_start().await?; tracing::info!("FINISHED"); } Commands::Retrieve(..) => { - try_retrieve().await.unwrap(); + try_retrieve().await?; tracing::info!("FINISHED"); } Commands::Stop => { - try_stop().await.unwrap(); + try_stop().await?; } Commands::Daemon(_) => daemon::daemon_main().await?, + Commands::ServerInfo => { + try_serverinfo().await? + } + Commands::ServerConfig => { + try_serverconfig().await? + } } Ok(()) @@ -141,5 +211,5 @@ fn system_log() -> anyhow::Result> { #[cfg(target_vendor = "apple")] fn system_log() -> anyhow::Result> { - Ok(Some(OsLogger::new("com.hackclub.burrow", "default"))) + Ok(Some(OsLogger::new("com.hackclub.burrow", "burrow-cli"))) } diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 2979c184..b95c1bf2 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -13,11 +13,12 @@ byteorder = "1.4" tracing = "0.1" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +schemars = { version = "0.8", optional = true } futures = { version = "0.3.28", optional = true } [features] -serde = ["dep:serde"] +serde = ["dep:serde", "dep:schemars"] tokio = ["tokio/net", "dep:futures"] [target.'cfg(feature = "tokio")'.dev-dependencies] diff --git a/tun/src/options.rs b/tun/src/options.rs index e766be80..e74afe37 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -4,7 +4,7 @@ use std::io::Error; use super::TunInterface; #[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema))] pub struct TunOptions { /// (Windows + Linux) Name the tun interface. pub(crate) name: Option,