Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Conrad Kramer
85640ffce1 Switch to gRPC client in Swift app
Some checks are pending
Build AppImage / Build AppImage (push) Waiting to run
Build Apple Apps / Build App (iOS) (push) Waiting to run
Build Apple Apps / Build App (iOS Simulator) (push) Waiting to run
Build Apple Apps / Build App (macOS) (push) Waiting to run
Build Docker / Build Docker Image (push) Waiting to run
Build Rust Crate / Build Crate (macOS (Intel)) (push) Waiting to run
Build Rust Crate / Build Crate (macOS) (push) Waiting to run
Build Rust Crate / Build Crate (Linux) (push) Waiting to run
Build Rust Crate / Build Crate (Windows) (push) Waiting to run
2024-09-09 10:38:13 -07:00
Conrad Kramer
25a0f7c421 Add Developer ID Profiles to build 2024-09-07 20:35:28 -07:00
Jett Chen
e4b0f1660b GRPC Server Support
- Deprecates old json-rpc system
- Add GRPC daemon over uds
2024-09-08 11:33:11 +08:00
119 changed files with 2746 additions and 1492 deletions

View file

@ -1,7 +1,7 @@
name: Build Apple Apps name: Build Apple Apps
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
@ -39,6 +39,7 @@ jobs:
- aarch64-apple-darwin - aarch64-apple-darwin
env: env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -54,6 +55,9 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ join(matrix.rust-targets, ', ') }} targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Build - name: Build
id: build id: build
uses: ./.github/actions/build-for-testing uses: ./.github/actions/build-for-testing
@ -82,4 +86,4 @@ jobs:
destination: ${{ matrix.destination }} destination: ${{ matrix.destination }}
test-plan: ${{ matrix.xcode-ui-test }} test-plan: ${{ matrix.xcode-ui-test }}
artifact-prefix: ui-tests-${{ matrix.sdk-name }} artifact-prefix: ui-tests-${{ matrix.sdk-name }}
check-name: Xcode UI Tests (${{ matrix.platform }}) check-name: Xcode UI Tests (${{ matrix.platform }})

View file

@ -48,6 +48,7 @@ jobs:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
RUST_BACKTRACE: short RUST_BACKTRACE: short
PROTOC_VERSION: 3.25.1
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -64,6 +65,10 @@ jobs:
if: matrix.os == 'windows-2022' if: matrix.os == 'windows-2022'
shell: bash shell: bash
run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }}
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@ -77,4 +82,4 @@ jobs:
run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }}
- name: Test - name: Test
shell: bash shell: bash
run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }}

View file

@ -22,6 +22,7 @@ jobs:
- aarch64-apple-darwin - aarch64-apple-darwin
env: env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -38,10 +39,18 @@ jobs:
app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
- name: Install Provisioning Profiles
shell: bash
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/
cp -f Apple/Profiles/* ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ join(matrix.rust-targets, ', ') }} targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Configure Version - name: Configure Version
id: version id: version
shell: bash shell: bash

8
.gitignore vendored
View file

@ -1,9 +1,17 @@
# Xcode # Xcode
xcuserdata xcuserdata
# Swift
Apple/Package/.swiftpm/
# Rust # Rust
target/ target/
.env .env
.DS_STORE .DS_STORE
.idea/ .idea/
tmp/
*.db
*.sock

View file

@ -30,7 +30,6 @@ opt_in_rules:
- function_default_parameter_at_end - function_default_parameter_at_end
- ibinspectable_in_extension - ibinspectable_in_extension
- identical_operands - identical_operands
- implicitly_unwrapped_optional
- indentation_width - indentation_width
- joined_default_parameter - joined_default_parameter
- last_where - last_where

View file

@ -15,5 +15,12 @@
"rust-analyzer.inlayHints.typeHints.enable": false, "rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.linkedProjects": [ "rust-analyzer.linkedProjects": [
"./burrow/Cargo.toml" "./burrow/Cargo.toml"
] ],
"[yaml]": {
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.autoIndent": "advanced",
"diffEditor.ignoreTrimWhitespace": false,
"editor.formatOnSave": false
}
} }

View file

@ -1,5 +1,6 @@
#if os(macOS) #if os(macOS)
import AppKit import AppKit
import BurrowUI
import SwiftUI import SwiftUI
@main @main

View file

@ -1,6 +1,7 @@
#if !os(macOS)
import BurrowUI
import SwiftUI import SwiftUI
#if !os(macOS)
@MainActor @MainActor
@main @main
struct BurrowApp: App { struct BurrowApp: App {

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23077.2" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23091" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23077.2"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23091"/>
</dependencies> </dependencies>
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View file

@ -1,10 +0,0 @@
import SwiftUI
protocol Network {
associatedtype Label: View
var id: String { get }
var backgroundColor: Color { get }
var label: Label { get }
}

View file

@ -1,50 +0,0 @@
import SwiftUI
protocol Tunnel {
var status: TunnelStatus { get }
func start()
func stop()
func enable()
}
enum TunnelStatus: Equatable, Hashable {
case unknown
case permissionRequired
case disabled
case connecting
case connected(Date)
case disconnecting
case disconnected
case reasserting
case invalid
case configurationReadWriteFailed
}
struct TunnelKey: EnvironmentKey {
static let defaultValue: any Tunnel = NetworkExtensionTunnel()
}
extension EnvironmentValues {
var tunnel: any Tunnel {
get { self[TunnelKey.self] }
set { self[TunnelKey.self] = newValue }
}
}
#if DEBUG
@Observable
class PreviewTunnel: Tunnel {
var status: TunnelStatus = .permissionRequired
func start() {
status = .connected(.now)
}
func stop() {
status = .disconnected
}
func enable() {
status = .disconnected
}
}
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
{
"originHash" : "fa512b990383b7e309c5854a5279817052294a8191a6d3c55c49cfb38e88c0c3",
"pins" : [
{
"identity" : "grpc-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift.git",
"state" : {
"revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1",
"version" : "1.23.0"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20",
"version" : "1.0.1"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.1.3"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd",
"version" : "1.3.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "9746cf80e29edfef2a39924a66731249223f42a3",
"version" : "2.72.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "d1ead62745cc3269e482f1c51f27608057174379",
"version" : "1.24.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "b5f7062b60e4add1e8c343ba4eb8da2e324b3a94",
"version" : "1.34.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "7b84abbdcef69cc3be6573ac12440220789dcd69",
"version" : "2.27.2"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "38ac8221dd20674682148d6451367f89c2652980",
"version" : "1.21.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5",
"version" : "1.28.1"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5",
"version" : "1.3.2"
}
}
],
"version" : 3
}

View file

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "1600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "1600"
wasCreatedForAppExtension = "YES"
version = "2.0"> version = "2.0">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"

View file

@ -1,10 +1,8 @@
LD_EXPORT_SYMBOLS = NO
SKIP_INSTALL = NO SKIP_INSTALL = NO
MERGED_BINARY_TYPE = manual
LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
ENABLE_PREVIEWS = YES

View file

@ -1,16 +1,17 @@
#include "Identity.xcconfig" #include "Identity.xcconfig"
#include "Debug.xcconfig"
#include "Version.xcconfig" #include "Version.xcconfig"
SDKROOT = auto SDKROOT = auto
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_VERSION = 6.0
IPHONEOS_DEPLOYMENT_TARGET = 17.0 IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 14.0 MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
SUPPORTS_MACCATALYST = NO SUPPORTS_MACCATALYST = NO
ALWAYS_SEARCH_USER_PATHS = NO
PRODUCT_NAME = $(TARGET_NAME:c99extidentifier) PRODUCT_NAME = $(TARGET_NAME:c99extidentifier)
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(PRODUCT_NAME) PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(PRODUCT_NAME)
MARKETING_VERSION = 0.1 MARKETING_VERSION = 0.1
@ -19,43 +20,25 @@ SKIP_INSTALL = YES
CODE_SIGN_IDENTITY = Apple Development CODE_SIGN_IDENTITY = Apple Development
INFOPLIST_FILE = Configuration/Info.plist
GENERATE_INFOPLIST_FILE = YES GENERATE_INFOPLIST_FILE = YES
INFOPLIST_FILE = Configuration/Info.plist
INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club
INFOPLIST_KEY_CFBundleDisplayName = Burrow INFOPLIST_KEY_CFBundleDisplayName = Burrow
ENABLE_BITCODE = NO
ENABLE_APP_SANDBOX[sdk=macosx*] = YES ENABLE_APP_SANDBOX[sdk=macosx*] = YES
ENABLE_HARDENED_RUNTIME[sdk=macosx*] = YES
COMBINE_HIDPI_IMAGES = YES
COPY_PHASE_STRIP = NO
ENABLE_BITCODE = NO
ALWAYS_SEARCH_USER_PATHS = NO
COMBINE_HIDPI_IMAGES = YES
EAGER_LINKING = YES
FUSE_BUILD_SCRIPT_PHASES = YES FUSE_BUILD_SCRIPT_PHASES = YES
SWIFT_EMIT_LOC_STRINGS = YES
LOCALIZATION_PREFERS_STRING_CATALOGS = YES
ENABLE_DEBUG_DYLIB = NO
APP_GROUP_IDENTIFIER = group.$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER = group.$(APP_BUNDLE_IDENTIFIER)
APP_GROUP_IDENTIFIER[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER)
NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network
// Swift // https://github.com/grpc/grpc-swift/issues/683#issuecomment-1130118953
SWIFT_VERSION = 5.0 OTHER_SWIFT_FLAGS = $(inherited) -Xcc -fmodule-map-file=$(GENERATED_MODULEMAP_DIR)/CNIOAtomics.modulemap -Xcc -fmodule-map-file=$(GENERATED_MODULEMAP_DIR)/CNIODarwin.modulemap -Xcc -fmodule-map-file=$(GENERATED_MODULEMAP_DIR)/CGRPCZlib.modulemap
SWIFT_EMIT_LOC_STRINGS = YES
// Release
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
SWIFT_COMPILATION_MODE = wholemodule
SWIFT_OPTIMIZATION_LEVEL = -Osize
LLVM_LTO = YES
DEAD_CODE_STRIPPING = YES
VALIDATE_PRODUCT = YES
// Debug
ONLY_ACTIVE_ARCH[config=Debug] = YES
DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf
ENABLE_TESTABILITY[config=Debug] = YES
SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG
SWIFT_COMPILATION_MODE[config=Debug] = singlefile
LLVM_LTO[config=Debug] = NO
DEAD_CODE_STRIPPING[config=Debug] = NO
VALIDATE_PRODUCT[config=Debug] = NO

View file

@ -1,5 +1,4 @@
PRODUCT_NAME = BurrowShared #include "Framework.xcconfig"
MERGEABLE_LIBRARY = YES
SWIFT_INCLUDE_PATHS = $(PROJECT_DIR)/Shared/Constants SWIFT_INCLUDE_PATHS = $(PROJECT_DIR)/Configuration/Constants
GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) NETWORK_EXTENSION_BUNDLE_IDENTIFIER=$(NETWORK_EXTENSION_BUNDLE_IDENTIFIER) GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) NETWORK_EXTENSION_BUNDLE_IDENTIFIER=$(NETWORK_EXTENSION_BUNDLE_IDENTIFIER)

View file

@ -1,4 +1,5 @@
@_implementationOnly import Constants @_implementationOnly import CConstants
import OSLog
public enum Constants { public enum Constants {
enum Error: Swift.Error { enum Error: Swift.Error {
@ -9,25 +10,29 @@ public enum Constants {
public static let appGroupIdentifier = AppGroupIdentifier public static let appGroupIdentifier = AppGroupIdentifier
public static let networkExtensionBundleIdentifier = NetworkExtensionBundleIdentifier public static let networkExtensionBundleIdentifier = NetworkExtensionBundleIdentifier
public static var groupContainerURL: URL {
get throws { try _groupContainerURL.get() }
}
private static let _groupContainerURL: Result<URL, Error> = {
guard let groupContainerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
return .failure(.invalidAppGroupIdentifier)
}
return .success(groupContainerURL)
}()
public static var socketURL: URL { public static var socketURL: URL {
get throws { get throws {
try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory)
} }
} }
public static var dbURL: URL { public static var databaseURL: URL {
get throws { get throws {
try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory) try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory)
} }
} }
private static var groupContainerURL: URL {
get throws { try _groupContainerURL.get() }
}
private static let _groupContainerURL: Result<URL, Error> = {
switch FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
case .some(let url): .success(url)
case .none: .failure(.invalidAppGroupIdentifier)
}
}()
}
extension Logger {
@_dynamicReplacement(for: subsystem)
public static var subsystem: String { Constants.bundleIdentifier }
} }

View file

@ -1,4 +1,4 @@
module Constants { module CConstants {
header "Constants.h" header "Constants.h"
export * export *
} }

View file

@ -0,0 +1,26 @@
// Release
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
SWIFT_COMPILATION_MODE = wholemodule
SWIFT_OPTIMIZATION_LEVEL = -Osize
LLVM_LTO = YES
DEAD_CODE_STRIPPING = YES
STRIP_INSTALLED_PRODUCT = YES
STRIP_SWIFT_SYMBOLS = YES
COPY_PHASE_STRIP = NO
VALIDATE_PRODUCT = YES
ENABLE_MODULE_VERIFIER = YES
// Debug
ONLY_ACTIVE_ARCH[config=Debug] = YES
DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf
ENABLE_TESTABILITY[config=Debug] = YES
GCC_PREPROCESSOR_DEFINITIONS[config=Debug] = DEBUG=1 $(inherited)
SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG
SWIFT_COMPILATION_MODE[config=Debug] = singlefile
LLVM_LTO[config=Debug] = NO
DEAD_CODE_STRIPPING[config=Debug] = NO
VALIDATE_PRODUCT[config=Debug] = NO
STRIP_INSTALLED_PRODUCT[config=Debug] = NO
STRIP_SWIFT_SYMBOLS[config=Debug] = NO
ENABLE_MODULE_VERIFIER[config=Debug] = NO

View file

@ -1,4 +1,6 @@
MERGED_BINARY_TYPE = manual LD_EXPORT_SYMBOLS = NO
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -disable-autolink-framework -Xfrontend UIKit -Xfrontend -disable-autolink-framework -Xfrontend AppKit -Xfrontend -disable-autolink-framework -Xfrontend SwiftUI
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macos*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks

View file

@ -0,0 +1,14 @@
PRODUCT_NAME = Burrow$(TARGET_NAME:c99extidentifier)
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(TARGET_NAME:c99extidentifier)
APPLICATION_EXTENSION_API_ONLY = YES
SWIFT_INSTALL_OBJC_HEADER = NO
SWIFT_SKIP_AUTOLINKING_FRAMEWORKS = YES
SWIFT_SKIP_AUTOLINKING_LIBRARIES = YES
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks
DYLIB_INSTALL_NAME_BASE = @rpath
DYLIB_COMPATIBILITY_VERSION = 1
DYLIB_CURRENT_VERSION = 1
VERSIONING_SYSTEM =

32
Apple/Core/Client.swift Normal file
View file

@ -0,0 +1,32 @@
import GRPC
import NIOTransportServices
public typealias TunnelClient = Burrow_TunnelAsyncClient
public typealias NetworksClient = Burrow_NetworksAsyncClient
public protocol Client {
init(channel: GRPCChannel)
}
extension Client {
public static func unix(socketURL: URL) -> Self {
let group = NIOTSEventLoopGroup()
let configuration = ClientConnection.Configuration.default(
target: .unixDomainSocket(socketURL.path),
eventLoopGroup: group
)
return Self(channel: ClientConnection(configuration: configuration))
}
}
extension TunnelClient: Client {
public init(channel: any GRPCChannel) {
self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none)
}
}
extension NetworksClient: Client {
public init(channel: any GRPCChannel) {
self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none)
}
}

View file

@ -0,0 +1 @@
../../../proto/burrow.proto

View file

@ -0,0 +1,11 @@
{
"invocations": [
{
"protoFiles": [
"burrow.proto",
],
"server": false,
"visibility": "public"
}
]
}

View file

@ -0,0 +1,10 @@
{
"invocations": [
{
"protoFiles": [
"burrow.proto",
],
"visibility": "public"
}
]
}

View file

@ -4,7 +4,7 @@ import os
extension Logger { extension Logger {
private static let loggers: OSAllocatedUnfairLock<[String: Logger]> = OSAllocatedUnfairLock(initialState: [:]) private static let loggers: OSAllocatedUnfairLock<[String: Logger]> = OSAllocatedUnfairLock(initialState: [:])
public static let subsystem = Constants.bundleIdentifier public dynamic static var subsystem: String { "com.hackclub.burrow" }
public static func logger(for type: Any.Type) -> Logger { public static func logger(for type: Any.Type) -> Logger {
let category = String(describing: type) let category = String(describing: type)

View file

@ -1,92 +1,74 @@
import BurrowShared import AsyncAlgorithms
import BurrowConfiguration
import BurrowCore
import libburrow import libburrow
import NetworkExtension import NetworkExtension
import os import os
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
enum Error: Swift.Error {
case missingTunnelConfiguration
}
private let logger = Logger.logger(for: PacketTunnelProvider.self) private let logger = Logger.logger(for: PacketTunnelProvider.self)
private var client: Client?
private var client: TunnelClient {
get throws { try _client.get() }
}
private let _client: Result<TunnelClient, Swift.Error> = Result {
try TunnelClient.unix(socketURL: Constants.socketURL)
}
override init() { override init() {
do { do {
libburrow.spawnInProcess( libburrow.spawnInProcess(
socketPath: try Constants.socketURL.path(percentEncoded: false), socketPath: try Constants.socketURL.path(percentEncoded: false),
dbPath: try Constants.dbURL.path(percentEncoded: false) databasePath: try Constants.databaseURL.path(percentEncoded: false)
) )
} catch { } catch {
logger.error("Failed to spawn: \(error)") logger.error("Failed to spawn networking thread: \(error)")
} }
} }
override func startTunnel(options: [String: NSObject]? = nil) async throws { override func startTunnel(options: [String: NSObject]? = nil) async throws {
do { do {
let client = try Client() let configuration = try await Array(client.tunnelConfiguration(.init()).prefix(1)).first
self.client = client guard let settings = configuration?.settings else {
register_events(client) throw Error.missingTunnelConfiguration
}
_ = try await self.loadTunSettings() try await setTunnelNetworkSettings(settings)
let startRequest = Start( _ = try await client.tunnelStart(.init())
tun: Start.TunOptions( logger.log("Started tunnel with network settings: \(settings)")
name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: []
)
)
let response = try await client.request(startRequest, type: BurrowResult<AnyResponseData>.self)
self.logger.log("Received start server response: \(String(describing: response))")
} catch { } catch {
self.logger.error("Failed to start tunnel: \(error)") logger.error("Failed to start tunnel: \(error)")
throw error throw error
} }
} }
override func stopTunnel(with reason: NEProviderStopReason) async { override func stopTunnel(with reason: NEProviderStopReason) async {
do { do {
let client = try Client() _ = try await client.tunnelStop(.init())
_ = try await client.single_request("Stop", type: BurrowResult<AnyResponseData>.self) logger.log("Stopped client")
self.logger.log("Stopped client.")
} catch { } catch {
self.logger.error("Failed to stop tunnel: \(error)") logger.error("Failed to stop tunnel: \(error)")
}
}
func loadTunSettings() async throws -> ServerConfig {
guard let client = self.client else {
throw BurrowError.noClient
}
let srvConfig = try await client.single_request("ServerConfig", type: BurrowResult<ServerConfig>.self)
guard let serverconfig = srvConfig.Ok else {
throw BurrowError.resultIsError
}
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)")
return serverconfig
}
private func generateTunSettings(from: ServerConfig) -> NETunnelNetworkSettings? {
// Using a makeshift remote tunnel address
let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
var v4Addresses = [String]()
var v6Addresses = [String]()
for addr in from.address {
if IPv4Address(addr) != nil {
v6Addresses.append(addr)
}
if IPv6Address(addr) != nil {
v4Addresses.append(addr)
}
}
nst.ipv4Settings = NEIPv4Settings(addresses: v4Addresses, subnetMasks: v4Addresses.map { _ in
"255.255.255.0"
})
nst.ipv6Settings = NEIPv6Settings(addresses: v6Addresses, networkPrefixLengths: v6Addresses.map { _ in 64 })
logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)")
return nst
}
func register_events(_ client: Client) {
client.on_event(.ConfigChange) { (cfig: ServerConfig) in
self.logger.info("Config Change Notification: \(String(describing: cfig))")
self.setTunnelNetworkSettings(self.generateTunSettings(from: cfig))
self.logger.info("Updated Tunnel Network Settings.")
} }
} }
} }
extension Burrow_TunnelConfigurationResponse {
fileprivate var settings: NEPacketTunnelNetworkSettings {
let ipv6Addresses = addresses.filter { IPv6Address($0) != nil }
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
settings.mtu = NSNumber(value: mtu)
settings.ipv4Settings = NEIPv4Settings(
addresses: addresses.filter { IPv4Address($0) != nil },
subnetMasks: ["255.255.255.0"]
)
settings.ipv6Settings = NEIPv6Settings(
addresses: ipv6Addresses,
networkPrefixLengths: ipv6Addresses.map { _ in 64 }
)
return settings
}
}

View file

@ -68,9 +68,12 @@ else
CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin"
fi fi
PROTOC=$(readlink -f $(which protoc))
CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH"
# Run cargo without the various environment variables set by Xcode. # Run cargo without the various environment variables set by Xcode.
# Those variables can confuse cargo and the build scripts it runs. # Those variables can confuse cargo and the build scripts it runs.
env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" env -i PATH="$CARGO_PATH" PROTOC="$PROTOC" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}"
mkdir -p "${BUILT_PRODUCTS_DIR}" mkdir -p "${BUILT_PRODUCTS_DIR}"

View file

@ -1,2 +1,2 @@
__attribute__((__swift_name__("spawnInProcess(socketPath:dbPath:)"))) __attribute__((__swift_name__("spawnInProcess(socketPath:databasePath:)")))
extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path); extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path);

Binary file not shown.

View file

@ -1,106 +0,0 @@
import Foundation
import Network
public final class Client {
let connection: NWConnection
private let logger = Logger.logger(for: Client.self)
private var generator = SystemRandomNumberGenerator()
private var continuations: [UInt: UnsafeContinuation<Data, Error>] = [:]
private var eventMap: [NotificationType: [(Data) throws -> Void]] = [:]
private var task: Task<Void, Error>?
public convenience init() throws {
self.init(url: try Constants.socketURL)
}
public 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)
let connection = NWConnection(to: endpoint, using: parameters)
connection.start(queue: .global())
self.connection = connection
self.task = Task { [weak self] in
while true {
let (data, _, _) = try await connection.receiveMessage()
let peek = try JSONDecoder().decode(MessagePeek.self, from: data)
switch peek.type {
case .Response:
let response = try JSONDecoder().decode(ResponsePeek.self, from: data)
self?.logger.info("Received response for \(response.id)")
guard let continuations = self?.continuations else {return}
self?.logger.debug("All keys in continuation table: \(continuations.keys)")
guard let continuation = self?.continuations[response.id] else { return }
self?.logger.debug("Got matching continuation")
continuation.resume(returning: data)
case .Notification:
let peek = try JSONDecoder().decode(NotificationPeek.self, from: data)
guard let handlers = self?.eventMap[peek.method] else { continue }
_ = try handlers.map { try $0(data) }
default:
continue
}
}
}
}
private func send<T: Request, U: Decodable>(_ request: T) async throws -> U {
let data: Data = try await withUnsafeThrowingContinuation { continuation in
continuations[request.id] = continuation
do {
let data = try JSONEncoder().encode(request)
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
}
}
self.logger.debug("Got response data: \(String(describing: data.base64EncodedString()))")
let res = try JSONDecoder().decode(Response<U>.self, from: data)
self.logger.debug("Got response data decoded: \(String(describing: res))")
return res.result
}
public func request<T: Codable, U: Decodable>(_ request: T, type: U.Type = U.self) async throws -> U {
let req = BurrowRequest(
id: generator.next(upperBound: UInt.max),
command: request
)
return try await send(req)
}
public func single_request<U: Decodable>(_ request: String, type: U.Type = U.self) async throws -> U {
let req = BurrowSimpleRequest(
id: generator.next(upperBound: UInt.max),
command: request
)
return try await send(req)
}
public func on_event<T: Codable>(_ event: NotificationType, callable: @escaping (T) throws -> Void) {
let action = { data in
let decoded = try JSONDecoder().decode(Notification<T>.self, from: data)
try callable(decoded.params)
}
if eventMap[event] != nil {
eventMap[event]?.append(action)
} else {
eventMap[event] = [action]
}
}
deinit {
connection.cancel()
}
}

View file

@ -1,139 +0,0 @@
import Foundation
// swiftlint:disable identifier_name raw_value_for_camel_cased_codable_enum
public enum BurrowError: Error {
case addrDoesntExist
case resultIsError
case cantParseResult
case resultIsNone
case noClient
}
public protocol Request: Codable where Params: Codable {
associatedtype Params
var id: UInt { get set }
var method: String { get set }
var params: Params? { get set }
}
public enum MessageType: String, Codable {
case Request
case Response
case Notification
}
public struct MessagePeek: Codable {
public var type: MessageType
public init(type: MessageType) {
self.type = type
}
}
public struct BurrowSimpleRequest: Request {
public var id: UInt
public var method: String
public var params: String?
public init(id: UInt, command: String, params: String? = nil) {
self.id = id
self.method = command
self.params = params
}
}
public struct BurrowRequest<T>: Request where T: Codable {
public var id: UInt
public var method: String
public var params: T?
public init(id: UInt, command: T) {
self.id = id
self.method = "\(T.self)"
self.params = command
}
}
public struct Response<T>: Decodable where T: Decodable {
public var id: UInt
public var result: T
public init(id: UInt, result: T) {
self.id = id
self.result = result
}
}
public struct ResponsePeek: Codable {
public var id: UInt
public init(id: UInt) {
self.id = id
}
}
public enum NotificationType: String, Codable {
case ConfigChange
}
public struct Notification<T>: Codable where T: Codable {
public var method: NotificationType
public var params: T
public init(method: NotificationType, params: T) {
self.method = method
self.params = params
}
}
public struct NotificationPeek: Codable {
public var method: NotificationType
public init(method: NotificationType) {
self.method = method
}
}
public struct AnyResponseData: Codable {
public var type: String
public init(type: String) {
self.type = type
}
}
public struct BurrowResult<T>: Codable where T: Codable {
public var Ok: T?
public var Err: String?
public init(Ok: T, Err: String? = nil) {
self.Ok = Ok
self.Err = Err
}
}
public struct ServerConfig: Codable {
public let address: [String]
public let name: String?
public let mtu: Int32?
public init(address: [String], name: String?, mtu: Int32?) {
self.address = address
self.name = name
self.mtu = mtu
}
}
public struct Start: Codable {
public struct TunOptions: Codable {
public let name: String?
public let no_pi: Bool
public let tun_excl: Bool
public let tun_retrieve: Bool
public let address: [String]
public init(name: String?, no_pi: Bool, tun_excl: Bool, tun_retrieve: Bool, address: [String]) {
self.name = name
self.no_pi = no_pi
self.tun_excl = tun_excl
self.tun_retrieve = tun_retrieve
self.address = address
}
}
public let tun: TunOptions
public init(tun: TunOptions) {
self.tun = tun
}
}
// swiftlint:enable identifier_name raw_value_for_camel_cased_codable_enum

View file

@ -1,32 +0,0 @@
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

@ -1,54 +0,0 @@
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

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 927 B

After

Width:  |  Height:  |  Size: 927 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

@ -2,11 +2,11 @@ import AuthenticationServices
import SwiftUI import SwiftUI
#if !os(macOS) #if !os(macOS)
struct BurrowView: View { public struct BurrowView: View {
@Environment(\.webAuthenticationSession) @Environment(\.webAuthenticationSession)
private var webAuthenticationSession private var webAuthenticationSession
var body: some View { public var body: some View {
NavigationStack { NavigationStack {
VStack { VStack {
HStack { HStack {
@ -35,6 +35,9 @@ struct BurrowView: View {
} }
} }
public init() {
}
private func addHackClubNetwork() { private func addHackClubNetwork() {
Task { Task {
try await authenticateWithSlack() try await authenticateWithSlack()

View file

@ -7,11 +7,11 @@
import SwiftUI import SwiftUI
struct MenuItemToggleView: View { public struct MenuItemToggleView: View {
@Environment(\.tunnel) @Environment(\.tunnel)
var tunnel: Tunnel var tunnel: Tunnel
var body: some View { public var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Burrow") Text("Burrow")
@ -30,10 +30,13 @@ struct MenuItemToggleView: View {
.padding(10) .padding(10)
.frame(minWidth: 300, minHeight: 32, maxHeight: 32) .frame(minWidth: 300, minHeight: 32, maxHeight: 32)
} }
public init() {
}
} }
extension Tunnel { extension Tunnel {
fileprivate var toggleDisabled: Bool { @MainActor fileprivate var toggleDisabled: Bool {
switch status { switch status {
case .disconnected, .permissionRequired, .connected, .disconnecting: case .disconnected, .permissionRequired, .connected, .disconnecting:
false false
@ -42,7 +45,7 @@ extension Tunnel {
} }
} }
var toggleIsOn: Binding<Bool> { @MainActor var toggleIsOn: Binding<Bool> {
Binding { Binding {
switch status { switch status {
case .connecting, .reasserting, .connected: case .connecting, .reasserting, .connected:

View file

@ -2,10 +2,10 @@ import SwiftUI
struct NetworkCarouselView: View { struct NetworkCarouselView: View {
var networks: [any Network] = [ var networks: [any Network] = [
HackClub(id: "1"), HackClub(id: 1),
HackClub(id: "2"), HackClub(id: 2),
WireGuard(id: "4"), WireGuard(id: 4),
HackClub(id: "5"), HackClub(id: 5)
] ]
var body: some View { var body: some View {

View file

@ -1,6 +1,6 @@
import NetworkExtension import NetworkExtension
extension NEVPNManager { extension NEVPNManager: @unchecked @retroactive Sendable {
func remove() async throws { func remove() async throws {
_ = try await withUnsafeThrowingContinuation { continuation in _ = try await withUnsafeThrowingContinuation { continuation in
removeFromPreferences(completionHandler: completion(continuation)) removeFromPreferences(completionHandler: completion(continuation))
@ -14,7 +14,7 @@ extension NEVPNManager {
} }
} }
extension NETunnelProviderManager { extension NETunnelProviderManager: @unchecked @retroactive Sendable {
class var managers: [NETunnelProviderManager] { class var managers: [NETunnelProviderManager] {
get async throws { get async throws {
try await withUnsafeThrowingContinuation { continuation in try await withUnsafeThrowingContinuation { continuation in
@ -34,7 +34,7 @@ private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Err
} }
} }
private func completion<T>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void { private func completion<T: Sendable>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void {
return { value, error in return { value, error in
if let error { if let error {
continuation.resume(throwing: error) continuation.resume(throwing: error)

View file

@ -1,22 +1,23 @@
import BurrowShared import BurrowCore
import NetworkExtension import NetworkExtension
@Observable @Observable
class NetworkExtensionTunnel: Tunnel { public final class NetworkExtensionTunnel: Tunnel {
@MainActor private(set) var status: TunnelStatus = .unknown @MainActor public private(set) var status: TunnelStatus = .unknown
private var error: NEVPNError? @MainActor private var error: NEVPNError?
private let logger = Logger.logger(for: Tunnel.self) private let logger = Logger.logger(for: Tunnel.self)
private let bundleIdentifier: String private let bundleIdentifier: String
private var tasks: [Task<Void, Error>] = [] private let configurationChanged: Task<Void, Error>
private let statusChanged: Task<Void, Error>
// Each manager corresponds to one entry in the Settings app. // Each manager corresponds to one entry in the Settings app.
// Our goal is to maintain a single manager, so we create one if none exist and delete any extra. // Our goal is to maintain a single manager, so we create one if none exist and delete any extra.
private var managers: [NEVPNManager]? { @MainActor private var managers: [NEVPNManager]? {
didSet { Task { await updateStatus() } } didSet { Task { await updateStatus() } }
} }
private var currentStatus: TunnelStatus { @MainActor private var currentStatus: TunnelStatus {
guard let managers = managers else { guard let managers = managers else {
guard let error = error else { guard let error = error else {
return .unknown return .unknown
@ -41,35 +42,40 @@ class NetworkExtensionTunnel: Tunnel {
return manager.connection.tunnelStatus return manager.connection.tunnelStatus
} }
convenience init() { public init(bundleIdentifier: String) {
self.init(Constants.networkExtensionBundleIdentifier)
}
init(_ bundleIdentifier: String) {
self.bundleIdentifier = bundleIdentifier self.bundleIdentifier = bundleIdentifier
let center = NotificationCenter.default let center = NotificationCenter.default
let configurationChanged = Task { [weak self] in let tunnel: OSAllocatedUnfairLock<NetworkExtensionTunnel?> = .init(initialState: .none)
for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { configurationChanged = Task {
await self?.update() for try await _ in center.notifications(named: .NEVPNConfigurationChange) {
try Task.checkCancellation()
await tunnel.withLock { $0 }?.update()
} }
} }
let statusChanged = Task { [weak self] in statusChanged = Task {
for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { for try await _ in center.notifications(named: .NEVPNStatusDidChange) {
await self?.updateStatus() try Task.checkCancellation()
await tunnel.withLock { $0 }?.updateStatus()
} }
} }
tasks = [configurationChanged, statusChanged] tunnel.withLock { $0 = self }
Task { await update() } Task { await update() }
} }
private func update() async { private func update() async {
do { do {
managers = try await NETunnelProviderManager.managers let result = try await NETunnelProviderManager.managers
await MainActor.run {
managers = result
status = currentStatus
}
await self.updateStatus() await self.updateStatus()
} catch let vpnError as NEVPNError { } catch let vpnError as NEVPNError {
error = vpnError await MainActor.run {
error = vpnError
}
} catch { } catch {
logger.error("Failed to update VPN configurations: \(error)") logger.error("Failed to update VPN configurations: \(error)")
} }
@ -82,12 +88,7 @@ class NetworkExtensionTunnel: Tunnel {
} }
func configure() async throws { func configure() async throws {
if managers == nil { let managers = try await NETunnelProviderManager.managers
await update()
}
guard let managers = managers else { return }
if managers.count > 1 { if managers.count > 1 {
try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in
for manager in managers.suffix(from: 1) { for manager in managers.suffix(from: 1) {
@ -110,9 +111,9 @@ class NetworkExtensionTunnel: Tunnel {
try await manager.save() try await manager.save()
} }
func start() { public func start() {
guard let manager = managers?.first else { return }
Task { Task {
guard let manager = try await NETunnelProviderManager.managers.first else { return }
do { do {
if !manager.isEnabled { if !manager.isEnabled {
manager.isEnabled = true manager.isEnabled = true
@ -125,12 +126,14 @@ class NetworkExtensionTunnel: Tunnel {
} }
} }
func stop() { public func stop() {
guard let manager = managers?.first else { return } Task {
manager.connection.stopVPNTunnel() guard let manager = try await NETunnelProviderManager.managers.first else { return }
manager.connection.stopVPNTunnel()
}
} }
func enable() { public func enable() {
Task { Task {
do { do {
try await configure() try await configure()
@ -141,7 +144,8 @@ class NetworkExtensionTunnel: Tunnel {
} }
deinit { deinit {
tasks.forEach { $0.cancel() } configurationChanged.cancel()
statusChanged.cancel()
} }
} }

View file

@ -1,10 +1,14 @@
import BurrowCore
import SwiftUI import SwiftUI
struct HackClub: Network { struct HackClub: Network {
var id: String typealias NetworkType = Burrow_WireGuardNetwork
static let type: Burrow_NetworkType = .hackClub
var id: Int32
var backgroundColor: Color { .init("HackClub") } var backgroundColor: Color { .init("HackClub") }
var label: some View { @MainActor var label: some View {
GeometryReader { reader in GeometryReader { reader in
VStack(alignment: .leading) { VStack(alignment: .leading) {
Image("HackClub") Image("HackClub")

View file

@ -0,0 +1,36 @@
import Atomics
import BurrowCore
import SwiftProtobuf
import SwiftUI
protocol Network {
associatedtype NetworkType: Message
associatedtype Label: View
static var type: Burrow_NetworkType { get }
var id: Int32 { get }
var backgroundColor: Color { get }
@MainActor var label: Label { get }
}
@Observable
@MainActor
final class NetworkViewModel: Sendable {
private(set) var networks: [Burrow_Network] = []
private var task: Task<Void, Error>!
init(socketURL: URL) {
task = Task { [weak self] in
let client = NetworksClient.unix(socketURL: socketURL)
for try await networks in client.networkList(.init()) {
guard let viewModel = self else { continue }
Task { @MainActor in
viewModel.networks = networks.network
}
}
}
}
}

View file

@ -1,10 +1,14 @@
import BurrowCore
import SwiftUI import SwiftUI
struct WireGuard: Network { struct WireGuard: Network {
var id: String typealias NetworkType = Burrow_WireGuardNetwork
static let type: BurrowCore.Burrow_NetworkType = .wireGuard
var id: Int32
var backgroundColor: Color { .init("WireGuard") } var backgroundColor: Color { .init("WireGuard") }
var label: some View { @MainActor var label: some View {
GeometryReader { reader in GeometryReader { reader in
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {

View file

@ -1,5 +1,6 @@
import AuthenticationServices import AuthenticationServices
import Foundation import Foundation
import os
import SwiftUI import SwiftUI
enum OAuth2 { enum OAuth2 {
@ -25,11 +26,16 @@ enum OAuth2 {
var clientID: String var clientID: String
var clientSecret: String var clientSecret: String
fileprivate static var queue: [Int: CheckedContinuation<URL, Swift.Error>] = [:] fileprivate static let queue: OSAllocatedUnfairLock<[Int: CheckedContinuation<URL, Swift.Error>]> = {
.init(initialState: [:])
}()
fileprivate static func handle(url: URL) { fileprivate static func handle(url: URL) {
let continuations = queue let continuations = queue.withLock { continuations in
queue.removeAll() let copy = continuations
continuations.removeAll()
return copy
}
for (_, continuation) in continuations { for (_, continuation) in continuations {
continuation.resume(returning: url) continuation.resume(returning: url)
} }
@ -56,7 +62,7 @@ enum OAuth2 {
var queryItems: [URLQueryItem] = [ var queryItems: [URLQueryItem] = [
.init(name: "client_id", value: clientID), .init(name: "client_id", value: clientID),
.init(name: "response_type", value: responseType.rawValue), .init(name: "response_type", value: responseType.rawValue),
.init(name: "redirect_uri", value: redirectURI.absoluteString), .init(name: "redirect_uri", value: redirectURI.absoluteString)
] ]
if !scopes.isEmpty { if !scopes.isEmpty {
queryItems.append(.init(name: "scope", value: scopes.joined(separator: ","))) queryItems.append(.init(name: "scope", value: scopes.joined(separator: ",")))
@ -206,6 +212,9 @@ enum OAuth2 {
} }
} }
extension WebAuthenticationSession: @unchecked @retroactive Sendable {
}
extension WebAuthenticationSession { extension WebAuthenticationSession {
#if canImport(BrowserEngineKit) #if canImport(BrowserEngineKit)
@available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *)
@ -243,12 +252,12 @@ extension WebAuthenticationSession {
let id = Int.random(in: 0..<Int.max) let id = Int.random(in: 0..<Int.max)
group.addTask { group.addTask {
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
OAuth2.Session.queue[id] = continuation OAuth2.Session.queue.withLock { $0[id] = continuation }
} }
} }
guard let url = try await group.next() else { throw OAuth2.Error.invalidCallbackURL } guard let url = try await group.next() else { throw OAuth2.Error.invalidCallbackURL }
group.cancelAll() group.cancelAll()
OAuth2.Session.queue[id] = nil OAuth2.Session.queue.withLock { $0[id] = nil }
return url return url
} }
} }

61
Apple/UI/Tunnel.swift Normal file
View file

@ -0,0 +1,61 @@
import BurrowConfiguration
import NetworkExtension
import SwiftUI
protocol Tunnel: Sendable {
@MainActor var status: TunnelStatus { get }
func start()
func stop()
func enable()
}
public enum TunnelStatus: Sendable, Equatable, Hashable {
case unknown
case permissionRequired
case disabled
case connecting
case connected(Date)
case disconnecting
case disconnected
case reasserting
case invalid
case configurationReadWriteFailed
}
struct TunnelKey: EnvironmentKey {
static var defaultValue: any Tunnel {
NetworkExtensionTunnel(bundleIdentifier: Constants.networkExtensionBundleIdentifier)
}
}
extension EnvironmentValues {
var tunnel: any Tunnel {
get { self[TunnelKey.self] }
set { self[TunnelKey.self] = newValue }
}
}
#if DEBUG
@Observable
@MainActor
final class PreviewTunnel: Tunnel {
private(set) var status: TunnelStatus = .permissionRequired
nonisolated func start() {
set(.connected(.now))
}
nonisolated func stop() {
set(.disconnected)
}
nonisolated func enable() {
set(.disconnected)
}
nonisolated private func set(_ status: TunnelStatus) {
Task { @MainActor in self.status = status }
}
}
#endif

View file

@ -21,7 +21,7 @@ struct TunnelButton: View {
} }
extension Tunnel { extension Tunnel {
fileprivate var action: TunnelButton.Action? { @MainActor fileprivate var action: TunnelButton.Action? {
switch status { switch status {
case .permissionRequired, .invalid: case .permissionRequired, .invalid:
.enable .enable

View file

@ -10,7 +10,7 @@ struct TunnelStatusView: View {
} }
extension TunnelStatus: CustomStringConvertible { extension TunnelStatus: CustomStringConvertible {
var description: String { public var description: String {
switch self { switch self {
case .unknown: case .unknown:
"Unknown" "Unknown"

3
Apple/UI/UI.xcconfig Normal file
View file

@ -0,0 +1,3 @@
#include "../Configuration/Framework.xcconfig"
ENABLE_PREVIEWS = YES

1363
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@ RUN set -eux && \
curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \
echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \
apt-get update && \ apt-get update && \
apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev && \ apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev protobuf-compiler libprotobuf-dev && \
ln -s clang-$LLVM_VERSION /usr/bin/clang && \ ln -s clang-$LLVM_VERSION /usr/bin/clang && \
ln -s clang /usr/bin/clang++ && \ ln -s clang /usr/bin/clang++ && \
ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \

View file

@ -20,6 +20,12 @@ start:
stop: stop:
@$(cargo_norm) stop @$(cargo_norm) stop
status:
@$(cargo_norm) server-status
tunnel-config:
@$(cargo_norm) tunnel-config
test-dns: test-dns:
@sudo route delete 8.8.8.8 @sudo route delete 8.8.8.8
@sudo route add 8.8.8.8 -interface $(tun) @sudo route add 8.8.8.8 -interface $(tun)

View file

@ -4,7 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux && \ RUN set -eux && \
dnf update -y && \ dnf update -y && \
dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite sqlite-devel dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite sqlite-devel protobuf-compiler protobuf-devel
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"

View file

@ -19,6 +19,7 @@ tokio = { version = "1.37", features = [
"signal", "signal",
"time", "time",
"tracing", "tracing",
"fs",
] } ] }
tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] }
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
@ -56,8 +57,17 @@ reqwest = { version = "0.12", default-features = false, features = [
"json", "json",
"rustls-tls", "rustls-tls",
] } ] }
rusqlite = "0.31.0" rusqlite = { version = "0.31.0", features = ["blob"] }
dotenv = "0.15.0" dotenv = "0.15.0"
tonic = "0.12.0"
prost = "0.13.1"
prost-types = "0.13.1"
tokio-stream = "0.1"
async-stream = "0.2"
tower = "0.4.13"
hyper-util = "0.1.6"
toml = "0.8.15"
rust-ini = "0.21.0"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
caps = "0.5" caps = "0.5"
@ -66,7 +76,7 @@ tracing-journald = "0.3"
[target.'cfg(target_vendor = "apple")'.dependencies] [target.'cfg(target_vendor = "apple")'.dependencies]
nix = { version = "0.27" } nix = { version = "0.27" }
rusqlite = { version = "0.31.0", features = ["bundled"] } rusqlite = { version = "0.31.0", features = ["bundled", "blob"] }
[dev-dependencies] [dev-dependencies]
insta = { version = "1.32", features = ["yaml"] } insta = { version = "1.32", features = ["yaml"] }
@ -83,3 +93,7 @@ pre_uninstall_script = "../package/rpm/pre_uninstall"
[features] [features]
tokio-console = ["dep:console-subscriber"] tokio-console = ["dep:console-subscriber"]
bundled = ["rusqlite/bundled"] bundled = ["rusqlite/bundled"]
[build-dependencies]
tonic-build = "0.12.0"

Some files were not shown because too many files have changed in this diff Show more