Compare commits

..

No commits in common. "main" and "builds/2" have entirely different histories.

177 changed files with 1660 additions and 4941 deletions

View file

@ -35,6 +35,7 @@ runs:
-authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-onlyUsePackageVersionsFromResolvedFile \
-scheme '${{ inputs.scheme }}' \
-destination '${{ inputs.destination }}' \
-archivePath '${{ inputs.archive-path }}' \

View file

@ -27,9 +27,7 @@ runs:
Apple/DerivedData
key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }}
${{ runner.os }}-${{ inputs.scheme }}-
${{ runner.os }}-
- name: Build
shell: bash
working-directory: Apple

View file

@ -1,30 +0,0 @@
name: Download Provisioning Profiles
inputs:
app-store-key:
description: App Store key in PEM PKCS#8 format
required: true
app-store-key-id:
description: App Store key ID
required: true
app-store-key-issuer-id:
description: App Store key issuer ID
required: true
runs:
using: composite
steps:
- shell: bash
env:
FASTLANE_OPT_OUT_USAGE: 'YES'
run: |
APP_STORE_KEY=$(echo "${{ inputs.app-store-key }}" | jq -sR .)
cat << EOF > api-key.json
{
"key_id": "${{ inputs.app-store-key-id }}",
"issuer_id": "${{ inputs.app-store-key-issuer-id }}",
"key": $APP_STORE_KEY
}
EOF
fastlane sigh download_all --api_key_path api-key.json
rm -rf api-key.json

View file

@ -12,8 +12,11 @@ inputs:
archive-path:
description: Xcode archive path
required: true
export-options:
description: The export options in JSON format
destination:
description: The Xcode export destination. This can either be "export" or "upload"
required: true
method:
description: The Xcode export method. This can be one of app-store, validation, ad-hoc, package, enterprise, development, developer-id, or mac-application.
required: true
export-path:
description: The path to export the archive to
@ -26,7 +29,8 @@ runs:
run: |
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
echo '${{ inputs.export-options }}' | plutil -convert xml1 -o ExportOptions.plist -
echo '{"destination":"${{ inputs.destination }}","method":"${{ inputs.method }}"}' \
| plutil -convert xml1 -o ExportOptions.plist -
xcodebuild \
-exportArchive \

View file

@ -9,6 +9,16 @@ inputs:
app-store-key-issuer-id:
description: App Store key issuer ID
required: true
archive-path:
description: Xcode archive path
required: true
export-path:
description: The path to export the archive to
required: true
outputs:
notarized-app:
description: The compressed and notarized app
value: ${{ steps.notarize.outputs.notarized-app }}
runs:
using: composite
steps:
@ -18,8 +28,31 @@ runs:
run: |
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
ditto -c -k --keepParent Release/Burrow.app Upload.zip
xcrun notarytool submit --wait --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip
xcrun stapler staple Release/Burrow.app
echo '{"destination":"upload","method":"developer-id"}' \
| plutil -convert xml1 -o ExportOptions.plist -
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 Release
xcodebuild \
-exportArchive \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \
-authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-archivePath '${{ inputs.archive-path }}' \
-exportOptionsPlist ExportOptions.plist
until xcodebuild \
-exportNotarizedApp \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \
-authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-archivePath '${{ inputs.archive-path }}' \
-exportPath ${{ inputs.export-path }}
do
echo "Failed to export app, trying again in 10s..."
sleep 10
done
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist

View file

@ -6,9 +6,6 @@ on:
pull_request:
branches:
- "*"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
appimage:
name: Build AppImage

View file

@ -24,7 +24,7 @@ jobs:
rust-targets:
- aarch64-apple-ios
- scheme: App
destination: platform=iOS Simulator,OS=18.0,name=iPhone 15 Pro
destination: platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro
platform: iOS Simulator
sdk-name: iphonesimulator
rust-targets:
@ -38,8 +38,7 @@ jobs:
- x86_64-apple-darwin
- aarch64-apple-darwin
env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v3
@ -55,9 +54,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Build
id: build
uses: ./.github/actions/build-for-testing

View file

@ -6,9 +6,6 @@ on:
pull_request:
branches:
- "*"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Docker Image

View file

@ -5,7 +5,7 @@ jobs:
name: Build RPM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Install RPM
run: cargo install cargo-generate-rpm

View file

@ -21,21 +21,15 @@ jobs:
- x86_64-unknown-linux-gnu
targets:
- aarch64-unknown-linux-gnu
- os: macos-13
platform: macOS (Intel)
xcode: /Applications/Xcode_15.2.app
- os: macos-12
platform: macOS
test-targets:
- x86_64-apple-darwin
targets:
- x86_64-apple-ios
- os: macos-14
platform: macOS
xcode: /Applications/Xcode_16.0.app
test-targets:
- aarch64-apple-darwin
targets:
- aarch64-apple-ios
- aarch64-apple-ios-sim
- x86_64-apple-ios
- os: windows-2022
platform: Windows
test-targets:
@ -44,11 +38,10 @@ jobs:
- aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
env:
DEVELOPER_DIR: ${{ matrix.xcode }}/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
CARGO_INCREMENTAL: 0
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
RUST_BACKTRACE: short
PROTOC_VERSION: 3.25.1
steps:
- name: Checkout
uses: actions/checkout@v3
@ -61,14 +54,10 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y ${{ join(matrix.packages, ' ') }}
- name: Configure LLVM
- name: Install Windows Deps
if: matrix.os == 'windows-2022'
shell: bash
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
uses: dtolnay/rust-toolchain@stable
with:

View file

@ -13,4 +13,4 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Lint
run: swiftlint lint --strict --reporter github-actions-logging
run: swiftlint lint --reporter github-actions-logging

View file

@ -1,4 +1,4 @@
name: Release (Linux)
name: Release (AppImage)
on:
release:
types:
@ -13,17 +13,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build AppImage
- name: Build
run: |
docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile
docker create --name temp appimage-builder
docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage .
docker rm temp
- name: Attach Artifacts
- name: Upload to GitHub
uses: SierraSoftworks/gh-releases@v1.0.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
release_tag: ${{ github.ref_name }}
overwrite: "true"
overwrite: 'true'
files: |
Burrow-x86_64.AppImage

View file

@ -13,16 +13,17 @@ jobs:
fail-fast: false
matrix:
include:
- platform: iOS
- destination: generic/platform=iOS
platform: iOS
rust-targets:
- aarch64-apple-ios
- platform: macOS
- destination: generic/platform=macOS
platform: macOS
rust-targets:
- x86_64-apple-darwin
- aarch64-apple-darwin
env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v4
@ -33,56 +34,41 @@ jobs:
with:
certificate: ${{ secrets.DEVELOPER_CERT }}
password: ${{ secrets.DEVELOPER_CERT_PASSWORD }}
- name: Download Provisioning Profiles
uses: ./.github/actions/download-profiles
with:
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_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
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Configure Version
id: version
shell: bash
run: echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT
run: Tools/version.sh
- name: Archive
uses: ./.github/actions/archive
with:
scheme: App
destination: generic/platform=${{ matrix.platform }}
destination: ${{ matrix.destination }}
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive
- name: Export
uses: ./.github/actions/export
with:
method: ${{ matrix.platform == 'macOS' && 'developer-id' || 'ad-hoc' }}
destination: export
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive
export-options: |
{"teamID":"P6PV2R9443","destination":"export","method":"developer-id","provisioningProfiles":{"com.hackclub.burrow":"Burrow Developer ID","com.hackclub.burrow.network":"Burrow Network Developer ID"},"signingCertificate":"Developer ID Application","signingStyle":"manual"}
export-path: Release
- name: Notarize
- name: Notarize (macOS)
if: ${{ matrix.platform == 'macOS' }}
uses: ./.github/actions/notarize
with:
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive
- name: Export IPA (iOS)
if: ${{ matrix.platform == 'iOS' }}
uses: ./.github/actions/export
with:
method: ad-hoc
destination: export
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive
export-path: Release
- name: Compress (iOS)
if: ${{ matrix.platform == 'iOS' }}
shell: bash
@ -97,23 +83,33 @@ jobs:
aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar
aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar
rm -rf Apple/Release
- name: Upload to GitHub
- name: Upload to GitHub (iOS)
if: ${{ matrix.platform == 'iOS' }}
uses: SierraSoftworks/gh-releases@v1.0.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
release_tag: ${{ github.ref_name }}
overwrite: 'true'
files: |
${{ matrix.platform == 'macOS' && 'Burrow.aap.aar' || 'Burrow.ipa' }}
Burrow.ipa
Burrow-${{ matrix.platform }}.xcarchive.aar
- name: Upload to GitHub (macOS)
if: ${{ matrix.platform == 'macOS' }}
uses: SierraSoftworks/gh-releases@v1.0.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
release_tag: ${{ github.ref_name }}
overwrite: 'true'
files: |
Burrow.aap.aar
Burrow-${{ matrix.platform }}.xcarchive.aar
- name: Upload to App Store Connect
if: ${{ matrix.platform == 'iOS' }}
uses: ./.github/actions/export
with:
method: app-store
destination: upload
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive
export-options: |
{"method": "app-store", "destination": "upload"}
export-path: Release

View file

@ -9,8 +9,6 @@ jobs:
create:
name: Create Release If Needed
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@v4

9
.gitignore vendored
View file

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

View file

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

40
.vscode/settings.json vendored
View file

@ -1,26 +1,18 @@
{
"files.autoSave": "onFocusChange",
"files.defaultLanguage": "rust",
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"editor.suggest.preview": true,
"editor.acceptSuggestionOnEnter": "on",
"rust-analyzer.restartServerOnConfigChange": true,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.linkedProjects": [
"./burrow/Cargo.toml"
],
"[yaml]": {
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.autoIndent": "advanced",
"diffEditor.ignoreTrimWhitespace": false,
"editor.formatOnSave": false
}
"files.autoSave": "onFocusChange",
"files.defaultLanguage": "rust",
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"editor.suggest.preview": true,
"editor.acceptSuggestionOnEnter": "on",
"rust-analyzer.restartServerOnConfigChange": true,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.rustfmt.extraArgs": [
"+nightly"
],
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
},
"rust-analyzer.inlayHints.typeHints.enable": false
}

View file

@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:burrow.rs?mode=developer</string>
<string>webcredentials:burrow.rs?mode=developer</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>

View file

@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:burrow.rs?mode=developer</string>
<string>webcredentials:burrow.rs?mode=developer</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>

View file

@ -1,10 +1,9 @@
#if os(macOS)
import AppKit
import BurrowUI
import SwiftUI
@main
@MainActor
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
private let quitItem: NSMenuItem = {
let quitItem = NSMenuItem(

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

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

View file

@ -0,0 +1,26 @@
import SwiftUI
struct BurrowView: View {
var body: some View {
NavigationStack {
VStack {
NetworkCarouselView()
Spacer()
TunnelStatusView()
TunnelButton()
.padding(.bottom)
}
.padding()
.navigationTitle("Networks")
}
}
}
#if DEBUG
struct NetworkView_Previews: PreviewProvider {
static var previews: some View {
BurrowView()
.environment(\.tunnel, PreviewTunnel())
}
}
#endif

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,88 @@
import SwiftUI
struct NetworkView<Content: View>: View {
var color: Color
var content: () -> Content
private var gradient: LinearGradient {
LinearGradient(
colors: [
color.opacity(0.8),
color
],
startPoint: .init(x: 0.2, y: 0),
endPoint: .init(x: 0.8, y: 1)
)
}
var body: some View {
content()
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(gradient)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
)
.shadow(color: .black.opacity(0.1), radius: 3.0, x: 0, y: 2)
}
}
struct AddNetworkView: View {
var body: some View {
Text("Add Network")
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(style: .init(lineWidth: 2, dash: [6]))
)
}
}
extension NetworkView where Content == AnyView {
init(network: any Network) {
color = network.backgroundColor
content = { AnyView(network.label) }
}
}
struct NetworkCarouselView: View {
var networks: [any Network] = [
HackClub(id: "1"),
HackClub(id: "2"),
WireGuard(id: "4"),
HackClub(id: "5"),
]
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(networks, id: \.id) { network in
NetworkView(network: network)
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
content
.scaleEffect(1.0 - abs(phase.value) * 0.1)
}
}
AddNetworkView()
}
.scrollTargetLayout()
}
.scrollClipDisabled()
.scrollIndicators(.hidden)
.defaultScrollAnchor(.center)
.scrollTargetBehavior(.viewAligned)
.containerRelativeFrame(.horizontal)
}
}
#if DEBUG
struct NetworkCarouselView_Previews: PreviewProvider {
static var previews: some View {
NetworkCarouselView()
}
}
#endif

View file

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

View file

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

View file

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

50
Apple/App/Tunnel.swift Normal file
View file

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

View file

@ -4,24 +4,21 @@ struct TunnelButton: View {
@Environment(\.tunnel)
var tunnel: any Tunnel
private var action: Action? { tunnel.action }
var body: some View {
Button {
if let action {
if let action = tunnel.action {
Button {
tunnel.perform(action)
} label: {
Text(action.description)
}
} label: {
Text(action.description)
.padding(.horizontal)
.buttonStyle(.floating)
}
.disabled(action.isDisabled)
.padding(.horizontal)
.buttonStyle(.floating)
}
}
extension Tunnel {
@MainActor fileprivate var action: TunnelButton.Action? {
fileprivate var action: TunnelButton.Action? {
switch status {
case .permissionRequired, .invalid:
.enable
@ -43,21 +40,12 @@ extension TunnelButton {
}
}
extension TunnelButton.Action? {
extension TunnelButton.Action {
var description: LocalizedStringKey {
switch self {
case .enable: "Enable"
case .start: "Start"
case .stop: "Stop"
case .none: "Start"
}
}
var isDisabled: Bool {
if case .none = self {
true
} else {
false
}
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,123 +1,86 @@
{
"originHash" : "fa512b990383b7e309c5854a5279817052294a8191a6d3c55c49cfb38e88c0c3",
"pins" : [
{
"identity" : "grpc-swift",
"identity" : "collectionconcurrencykit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift.git",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : {
"revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1",
"version" : "1.23.0"
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "0.2.0"
}
},
{
"identity" : "swift-async-algorithms",
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20",
"version" : "1.0.1"
"revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c",
"version" : "1.8.1"
}
},
{
"identity" : "swift-atomics",
"identity" : "sourcekitten",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"location" : "https://github.com/jpsim/SourceKitten.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
"version" : "0.34.1"
}
},
{
"identity" : "swift-collections",
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.1.3"
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
}
},
{
"identity" : "swift-http-types",
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd",
"version" : "1.3.0"
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
},
{
"identity" : "swift-log",
"identity" : "swiftlint",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"location" : "https://github.com/realm/SwiftLint.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
"branch" : "main",
"revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f"
}
},
{
"identity" : "swift-nio",
"identity" : "swiftytexttable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state" : {
"revision" : "9746cf80e29edfef2a39924a66731249223f42a3",
"version" : "2.72.0"
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version" : "0.9.0"
}
},
{
"identity" : "swift-nio-extras",
"identity" : "swxmlhash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"location" : "https://github.com/drmohundro/SWXMLHash.git",
"state" : {
"revision" : "d1ead62745cc3269e482f1c51f27608057174379",
"version" : "1.24.0"
"revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f",
"version" : "7.0.2"
}
},
{
"identity" : "swift-nio-http2",
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"location" : "https://github.com/jpsim/Yams.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"
"revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.6"
}
}
],
"version" : 3
"version" : 2
}

View file

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

View file

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

View file

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

View file

@ -1,17 +1,16 @@
#include "Identity.xcconfig"
#include "Debug.xcconfig"
#include "Version.xcconfig"
SDKROOT = auto
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_VERSION = 6.0
IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
SUPPORTS_MACCATALYST = NO
ALWAYS_SEARCH_USER_PATHS = NO
PRODUCT_NAME = $(TARGET_NAME:c99extidentifier)
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(PRODUCT_NAME)
MARKETING_VERSION = 0.1
@ -20,25 +19,43 @@ SKIP_INSTALL = YES
CODE_SIGN_IDENTITY = Apple Development
GENERATE_INFOPLIST_FILE = YES
INFOPLIST_FILE = Configuration/Info.plist
GENERATE_INFOPLIST_FILE = YES
INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club
INFOPLIST_KEY_CFBundleDisplayName = Burrow
ENABLE_APP_SANDBOX[sdk=macosx*] = YES
ENABLE_BITCODE = NO
ALWAYS_SEARCH_USER_PATHS = NO
ENABLE_APP_SANDBOX[sdk=macosx*] = YES
ENABLE_HARDENED_RUNTIME[sdk=macosx*] = YES
COMBINE_HIDPI_IMAGES = YES
EAGER_LINKING = YES
COPY_PHASE_STRIP = NO
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[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER)
NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network
// https://github.com/grpc/grpc-swift/issues/683#issuecomment-1130118953
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
SWIFT_VERSION = 5.0
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,38 +0,0 @@
@_implementationOnly import CConstants
import OSLog
public enum Constants {
enum Error: Swift.Error {
case invalidAppGroupIdentifier
}
public static let bundleIdentifier = AppBundleIdentifier
public static let appGroupIdentifier = AppGroupIdentifier
public static let networkExtensionBundleIdentifier = NetworkExtensionBundleIdentifier
public static var socketURL: URL {
get throws {
try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory)
}
}
public static var databaseURL: URL {
get throws {
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,26 +0,0 @@
// 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,6 +1,4 @@
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
MERGED_BINARY_TYPE = manual
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macos*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,61 @@
import Foundation
// swiftlint:disable identifier_name
enum BurrowError: Error {
case addrDoesntExist
case resultIsError
case cantParseResult
case resultIsNone
}
protocol Request: Codable where Command: Codable {
associatedtype Command
var id: UInt { get set }
var command: Command { get set }
}
struct BurrowSingleCommand: Request {
var id: UInt
var command: String
}
struct BurrowRequest<T>: Request where T: Codable {
var id: UInt
var command: T
}
struct BurrowStartRequest: Codable {
struct TunOptions: Codable {
let name: String?
let no_pi: Bool
let tun_excl: Bool
let tun_retrieve: Bool
let address: [String]
}
struct StartOptions: Codable {
let tun: TunOptions
}
let Start: StartOptions
}
struct Response<T>: Decodable where T: Decodable {
var id: UInt
var result: T
}
struct BurrowResult<T>: 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

View file

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

View file

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

View file

@ -1,74 +1,84 @@
import AsyncAlgorithms
import BurrowConfiguration
import BurrowCore
import BurrowShared
import libburrow
import NetworkExtension
import os
class PacketTunnelProvider: NEPacketTunnelProvider {
enum Error: Swift.Error {
case missingTunnelConfiguration
}
private let logger = Logger.logger(for: PacketTunnelProvider.self)
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() {
do {
libburrow.spawnInProcess(
socketPath: try Constants.socketURL.path(percentEncoded: false),
databasePath: try Constants.databaseURL.path(percentEncoded: false)
)
libburrow.spawnInProcess(socketPath: try Constants.socketURL.path)
} catch {
logger.error("Failed to spawn networking thread: \(error)")
logger.error("Failed to spawn: \(error)")
}
}
override func startTunnel(options: [String: NSObject]? = nil) async throws {
do {
let configuration = try await Array(client.tunnelConfiguration(.init()).prefix(1)).first
guard let settings = configuration?.settings else {
throw Error.missingTunnelConfiguration
let client = try Client()
let command = BurrowRequest(id: 0, command: "ServerConfig")
let data = try await client.request(command, type: Response<BurrowResult<ServerConfigData>>.self)
let encoded = try JSONEncoder().encode(data.result)
self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))")
guard let serverconfig = data.result.Ok else {
throw BurrowError.resultIsError
}
try await setTunnelNetworkSettings(settings)
_ = try await client.tunnelStart(.init())
logger.log("Started tunnel with network settings: \(settings)")
guard let tunNs = generateTunSettings(from: serverconfig) else {
throw BurrowError.addrDoesntExist
}
try await self.setTunnelNetworkSettings(tunNs)
self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)")
let startRequest = BurrowRequest(
id: .random(in: (.min)..<(.max)),
command: BurrowStartRequest(
Start: BurrowStartRequest.StartOptions(
tun: BurrowStartRequest.TunOptions(
name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: []
)
)
)
)
let response = try await client.request(startRequest, type: Response<BurrowResult<String>>.self)
self.logger.log("Received start server response: \(String(describing: response.result))")
} catch {
logger.error("Failed to start tunnel: \(error)")
self.logger.error("Failed to start tunnel: \(error)")
throw error
}
}
override func stopTunnel(with reason: NEProviderStopReason) async {
do {
_ = try await client.tunnelStop(.init())
logger.log("Stopped client")
let client = try Client()
let command = BurrowRequest(id: 0, command: "Stop")
let data = try await client.request(command, type: Response<BurrowResult<String>>.self)
self.logger.log("Stopped client.")
} catch {
logger.error("Failed to stop tunnel: \(error)")
self.logger.error("Failed to stop tunnel: \(error)")
}
}
}
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
private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? {
let cfig = from.ServerConfig
let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
var v4Addresses = [String]()
var v6Addresses = [String]()
for addr in cfig.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
}
}

View file

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

View file

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

View file

@ -0,0 +1,23 @@
@_implementationOnly import Constants
public enum Constants {
enum Error: Swift.Error {
case invalidAppGroupIdentifier
}
public static let bundleIdentifier = AppBundleIdentifier
public static let appGroupIdentifier = AppGroupIdentifier
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)
}()
}

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