Add proxy subscription runtime support
Add daemon RPCs, Apple and GTK import flows, packet proxy runtime support, diagnostics, and BEPs for proxy subscription handling. Redact subscription URL secrets from fetch errors before they reach logs or UI surfaces.
This commit is contained in:
parent
97c569fb35
commit
d1638726ca
46 changed files with 15079 additions and 456 deletions
|
|
@ -15,5 +15,7 @@
|
|||
<array>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ EXCLUDED_SOURCE_FILE_NAMES = MainMenu.xib
|
|||
|
||||
EXCLUDED_SOURCE_FILE_NAMES[sdk=macosx*] =
|
||||
INFOPLIST_KEY_LSUIElement[sdk=macosx*] = YES
|
||||
INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu
|
||||
INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication
|
||||
INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities
|
||||
|
||||
CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements
|
||||
CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = App/App-macOS.entitlements
|
||||
|
||||
SWIFT_INCLUDE_PATHS = $(inherited) $(PROJECT_DIR)/NetworkExtension
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
#if os(macOS)
|
||||
import AppKit
|
||||
import BurrowConfiguration
|
||||
import BurrowCore
|
||||
import BurrowUI
|
||||
import SwiftUI
|
||||
import libburrow
|
||||
|
||||
@main
|
||||
@MainActor
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var windowController: NSWindowController?
|
||||
private let logger = Logger.logger(for: AppDelegate.self)
|
||||
|
||||
private let quitItem: NSMenuItem = {
|
||||
let quitItem = NSMenuItem(
|
||||
title: "Quit Burrow",
|
||||
action: #selector(NSApplication.terminate(_:)),
|
||||
keyEquivalent: "q"
|
||||
)
|
||||
quitItem.target = NSApplication.shared
|
||||
quitItem.keyEquivalentModifierMask = .command
|
||||
return quitItem
|
||||
}()
|
||||
private let quitItem = AppDelegate.makeQuitItem()
|
||||
|
||||
private lazy var openItem: NSMenuItem = {
|
||||
let item = NSMenuItem(
|
||||
|
|
@ -61,9 +55,85 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}()
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
installMainMenu()
|
||||
startDaemon()
|
||||
statusItem.menu = menu
|
||||
}
|
||||
|
||||
private func startDaemon() {
|
||||
do {
|
||||
libburrow.spawnInProcess(
|
||||
socketPath: try Constants.socketURL.path(percentEncoded: false),
|
||||
databasePath: try Constants.databaseURL.path(percentEncoded: false)
|
||||
)
|
||||
} catch {
|
||||
logger.error("Failed to spawn daemon thread: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func installMainMenu() {
|
||||
let mainMenu = NSMenu(title: "Burrow")
|
||||
|
||||
let appItem = NSMenuItem(title: "Burrow", action: nil, keyEquivalent: "")
|
||||
let appMenu = NSMenu(title: "Burrow")
|
||||
|
||||
let aboutItem = NSMenuItem(
|
||||
title: "About Burrow",
|
||||
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
aboutItem.target = NSApplication.shared
|
||||
appMenu.addItem(aboutItem)
|
||||
appMenu.addItem(.separator())
|
||||
appMenu.addItem(Self.makeQuitItem())
|
||||
|
||||
appItem.submenu = appMenu
|
||||
mainMenu.addItem(appItem)
|
||||
|
||||
let editItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
|
||||
editItem.submenu = makeEditMenu()
|
||||
mainMenu.addItem(editItem)
|
||||
|
||||
NSApplication.shared.mainMenu = mainMenu
|
||||
}
|
||||
|
||||
private static func makeQuitItem() -> NSMenuItem {
|
||||
let quitItem = NSMenuItem(
|
||||
title: "Quit Burrow",
|
||||
action: #selector(NSApplication.terminate(_:)),
|
||||
keyEquivalent: "q"
|
||||
)
|
||||
quitItem.target = NSApplication.shared
|
||||
quitItem.keyEquivalentModifierMask = .command
|
||||
return quitItem
|
||||
}
|
||||
|
||||
private func makeEditMenu() -> NSMenu {
|
||||
let editMenu = NSMenu(title: "Edit")
|
||||
|
||||
editMenu.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"))
|
||||
editMenu.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"))
|
||||
editMenu.addItem(.separator())
|
||||
|
||||
editMenu.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
|
||||
editMenu.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
|
||||
editMenu.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
|
||||
|
||||
let pastePlainItem = NSMenuItem(
|
||||
title: "Paste and Match Style",
|
||||
action: #selector(NSTextView.pasteAsPlainText(_:)),
|
||||
keyEquivalent: "V"
|
||||
)
|
||||
pastePlainItem.keyEquivalentModifierMask = [.command, .option, .shift]
|
||||
editMenu.addItem(pastePlainItem)
|
||||
|
||||
editMenu.addItem(NSMenuItem(title: "Delete", action: #selector(NSText.delete(_:)), keyEquivalent: ""))
|
||||
editMenu.addItem(.separator())
|
||||
editMenu.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
|
||||
|
||||
return editMenu
|
||||
}
|
||||
|
||||
@objc
|
||||
private func openWindow() {
|
||||
if let window = windowController?.window {
|
||||
|
|
@ -74,9 +144,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
|
||||
let contentView = BurrowView()
|
||||
let hostingController = NSHostingController(rootView: contentView)
|
||||
if #available(macOS 13.0, *) {
|
||||
hostingController.sizingOptions = []
|
||||
}
|
||||
let window = NSWindow(contentViewController: hostingController)
|
||||
window.title = "Burrow"
|
||||
window.setContentSize(NSSize(width: 820, height: 720))
|
||||
window.contentMinSize = NSSize(width: 640, height: 560)
|
||||
window.styleMask.insert([.titled, .closable, .miniaturizable, .resizable])
|
||||
window.center()
|
||||
|
||||
|
|
@ -87,4 +161,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
enum BurrowApplication {
|
||||
@MainActor
|
||||
private static let delegate = AppDelegate()
|
||||
|
||||
@MainActor
|
||||
static func main() {
|
||||
let application = NSApplication.shared
|
||||
application.delegate = delegate
|
||||
application.setActivationPolicy(.accessory)
|
||||
application.finishLaunching()
|
||||
application.run()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<?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="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23091"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; };
|
||||
D11000012F70000100112233 /* BurrowUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11000042F70000100112233 /* BurrowUITests.swift */; };
|
||||
D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; };
|
||||
D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E22C8DA375008A8CEC /* SwiftProtobuf */; };
|
||||
|
|
@ -19,6 +18,7 @@
|
|||
D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; platformFilters = (macos, ); };
|
||||
D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = D0B1D10F2C436152004B7823 /* AsyncAlgorithms */; };
|
||||
D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; };
|
||||
D0BCC60C2A09A0C200AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; };
|
||||
D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; };
|
||||
D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; };
|
||||
D0D4E53A2C8D996F007F820A /* BurrowCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
|
@ -43,20 +43,14 @@
|
|||
D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; };
|
||||
D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; };
|
||||
D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E02C8DA375008A8CEC /* GRPC */; };
|
||||
D0FA10012D10200100112233 /* burrow.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10032D10200100112233 /* burrow.pb.swift */; };
|
||||
D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10042D10200100112233 /* burrow.grpc.swift */; };
|
||||
D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */ = {isa = PBXBuildFile; productRef = D0F7597D2C8DB30500126CF3 /* CGRPCZlib */; };
|
||||
D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4992C8D921A007F820A /* Client.swift */; };
|
||||
D0FA10012D10200100112233 /* Generated/burrow.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10032D10200100112233 /* Generated/burrow.pb.swift */; };
|
||||
D0FA10022D10200100112233 /* Generated/burrow.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10042D10200100112233 /* Generated/burrow.grpc.swift */; };
|
||||
D11000012F70000100112233 /* BurrowUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11000042F70000100112233 /* BurrowUITests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
D11000022F70000100112233 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D05B9F7129E39EEC008CB1F9;
|
||||
remoteInfo = App;
|
||||
};
|
||||
D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
||||
|
|
@ -106,6 +100,13 @@
|
|||
remoteGlobalIDString = D0D4E5302C8D996F007F820A;
|
||||
remoteInfo = Core;
|
||||
};
|
||||
D11000022F70000100112233 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D05B9F7129E39EEC008CB1F9;
|
||||
remoteInfo = App;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -138,9 +139,6 @@
|
|||
/* Begin PBXFileReference section */
|
||||
D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = "<group>"; };
|
||||
D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D11000032F70000100112233 /* BurrowUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BurrowUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D11000042F70000100112233 /* BurrowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowUITests.swift; sourceTree = "<group>"; };
|
||||
D11000052F70000100112233 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = "<group>"; };
|
||||
D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = "<group>"; };
|
||||
D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = "<group>"; };
|
||||
D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
|
@ -188,18 +186,14 @@
|
|||
D0D4E58E2C8D9D0A007F820A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = "<group>"; };
|
||||
D0D4E58F2C8D9D0A007F820A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
D0D4E5902C8D9D0A007F820A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
|
||||
D0FA10032D10200100112233 /* burrow.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.pb.swift; sourceTree = "<group>"; };
|
||||
D0FA10042D10200100112233 /* burrow.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.grpc.swift; sourceTree = "<group>"; };
|
||||
D0FA10032D10200100112233 /* Generated/burrow.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.pb.swift; sourceTree = "<group>"; };
|
||||
D0FA10042D10200100112233 /* Generated/burrow.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.grpc.swift; sourceTree = "<group>"; };
|
||||
D11000032F70000100112233 /* BurrowUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BurrowUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D11000042F70000100112233 /* BurrowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowUITests.swift; sourceTree = "<group>"; };
|
||||
D11000052F70000100112233 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
D11000062F70000100112233 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D020F65029E4A697002790F6 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -218,6 +212,7 @@
|
|||
D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */,
|
||||
D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */,
|
||||
D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */,
|
||||
D0BCC60C2A09A0C200AD070D /* libburrow.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -242,6 +237,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D11000062F70000100112233 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
|
|
@ -324,14 +326,6 @@
|
|||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D11000072F70000100112233 /* AppUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D11000042F70000100112233 /* BurrowUITests.swift */,
|
||||
);
|
||||
path = AppUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0B98FD729FDDB57004E7149 /* libburrow */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -346,8 +340,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D0D4E4952C8D921A007F820A /* burrow.proto */,
|
||||
D0FA10032D10200100112233 /* burrow.pb.swift */,
|
||||
D0FA10042D10200100112233 /* burrow.grpc.swift */,
|
||||
D0FA10032D10200100112233 /* Generated/burrow.pb.swift */,
|
||||
D0FA10042D10200100112233 /* Generated/burrow.grpc.swift */,
|
||||
);
|
||||
path = Client;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -401,27 +395,17 @@
|
|||
path = Constants;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D11000072F70000100112233 /* AppUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D11000042F70000100112233 /* BurrowUITests.swift */,
|
||||
);
|
||||
path = AppUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
D11000082F70000100112233 /* BurrowUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */;
|
||||
buildPhases = (
|
||||
D110000A2F70000100112233 /* Sources */,
|
||||
D11000062F70000100112233 /* Frameworks */,
|
||||
D11000092F70000100112233 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D110000B2F70000100112233 /* PBXTargetDependency */,
|
||||
);
|
||||
name = BurrowUITests;
|
||||
productName = BurrowUITests;
|
||||
productReference = D11000032F70000100112233 /* BurrowUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
D020F65229E4A697002790F6 /* NetworkExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */;
|
||||
|
|
@ -527,6 +511,24 @@
|
|||
productReference = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
D11000082F70000100112233 /* BurrowUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */;
|
||||
buildPhases = (
|
||||
D110000A2F70000100112233 /* Sources */,
|
||||
D11000062F70000100112233 /* Frameworks */,
|
||||
D11000092F70000100112233 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D110000B2F70000100112233 /* PBXTargetDependency */,
|
||||
);
|
||||
name = BurrowUITests;
|
||||
productName = BurrowUITests;
|
||||
productReference = D11000032F70000100112233 /* BurrowUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
|
|
@ -537,19 +539,54 @@
|
|||
LastSwiftUpdateCheck = 1600;
|
||||
LastUpgradeCheck = 1520;
|
||||
TargetAttributes = {
|
||||
D11000082F70000100112233 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
TestTargetID = D05B9F7129E39EEC008CB1F9;
|
||||
};
|
||||
D020F65229E4A697002790F6 = {
|
||||
CreatedOnToolsVersion = 14.3;
|
||||
DevelopmentTeam = 2KPBQHRJ2D;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.ApplicationGroups.Mac = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
D05B9F7129E39EEC008CB1F9 = {
|
||||
CreatedOnToolsVersion = 14.3;
|
||||
DevelopmentTeam = 2KPBQHRJ2D;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.ApplicationGroups.Mac = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.ApplicationGroups.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.AssociatedDomains = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.NetworkExtensions.iOS = {
|
||||
enabled = 1;
|
||||
};
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
D0D4E5302C8D996F007F820A = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
D11000082F70000100112233 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
TestTargetID = D05B9F7129E39EEC008CB1F9;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = D05B9F6D29E39EEC008CB1F9 /* Build configuration list for PBXProject "Burrow" */;
|
||||
|
|
@ -583,13 +620,6 @@
|
|||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
D11000092F70000100112233 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D05B9F7029E39EEC008CB1F9 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -605,6 +635,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D11000092F70000100112233 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
|
|
@ -653,14 +690,6 @@
|
|||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
D110000A2F70000100112233 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D11000012F70000100112233 /* BurrowUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D020F64F29E4A697002790F6 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -682,8 +711,8 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0FA10012D10200100112233 /* burrow.pb.swift in Sources */,
|
||||
D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */,
|
||||
D0FA10012D10200100112233 /* Generated/burrow.pb.swift in Sources */,
|
||||
D0FA10022D10200100112233 /* Generated/burrow.grpc.swift in Sources */,
|
||||
D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */,
|
||||
D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */,
|
||||
);
|
||||
|
|
@ -716,14 +745,17 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D110000A2F70000100112233 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D11000012F70000100112233 /* BurrowUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
D110000B2F70000100112233 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D05B9F7129E39EEC008CB1F9 /* App */;
|
||||
targetProxy = D11000022F70000100112233 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D020F65C29E4A697002790F6 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D020F65229E4A697002790F6 /* NetworkExtension */;
|
||||
|
|
@ -763,27 +795,19 @@
|
|||
isa = PBXTargetDependency;
|
||||
productRef = D0F759892C8DB34200126CF3 /* GRPC */;
|
||||
};
|
||||
D110000B2F70000100112233 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D05B9F7129E39EEC008CB1F9 /* App */;
|
||||
targetProxy = D11000022F70000100112233 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
D110000C2F70000100112233 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D110000D2F70000100112233 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
D020F65F29E4A697002790F6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -791,6 +815,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
@ -798,6 +823,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F64029E4A1FF002790F6 /* Compiler.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -805,6 +831,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F64029E4A1FF002790F6 /* Compiler.xcconfig */;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
@ -812,6 +839,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -819,6 +850,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
@ -864,18 +899,23 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
D110000C2F70000100112233 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D110000D2F70000100112233 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D11000052F70000100112233 /* UITests.xcconfig */;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D110000C2F70000100112233 /* Debug */,
|
||||
D110000D2F70000100112233 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
@ -930,6 +970,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D110000E2F70000100112233 /* Build configuration list for PBXNativeTarget "BurrowUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D110000C2F70000100112233 /* Debug */,
|
||||
D110000D2F70000100112233 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ public enum Constants {
|
|||
try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
public static var packetTunnelSocketURL: URL {
|
||||
get throws {
|
||||
try groupContainerURL.appending(component: "burrow-packet-tunnel.sock", directoryHint: .notDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
public static var databaseURL: URL {
|
||||
get throws {
|
||||
try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
DEVELOPMENT_TEAM = P6PV2R9443
|
||||
APP_BUNDLE_IDENTIFIER = com.hackclub.burrow
|
||||
DEVELOPMENT_TEAM = 2KPBQHRJ2D
|
||||
APP_BUNDLE_IDENTIFIER = com.tianruichen.burrow
|
||||
|
|
|
|||
|
|
@ -108,6 +108,83 @@ public struct Burrow_TailnetLoginStatusResponse: Sendable {
|
|||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionImportRequest: Sendable {
|
||||
public var url: String = ""
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionNodePreview: Sendable {
|
||||
public var ordinal: Int32 = 0
|
||||
public var name: String = ""
|
||||
public var `protocol`: String = ""
|
||||
public var server: String = ""
|
||||
public var port: Int32 = 0
|
||||
public var warnings: [String] = []
|
||||
public var runtimeSupported: Bool = false
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionRejectedEntry: Sendable {
|
||||
public var ordinal: Int32 = 0
|
||||
public var reason: String = ""
|
||||
public var scheme: String = ""
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionPreviewResponse: Sendable {
|
||||
public var suggestedName: String = ""
|
||||
public var detectedFormat: String = ""
|
||||
public var compatibleCount: Int32 = 0
|
||||
public var rejectedCount: Int32 = 0
|
||||
public var node: [Burrow_ProxySubscriptionNodePreview] = []
|
||||
public var rejected: [Burrow_ProxySubscriptionRejectedEntry] = []
|
||||
public var warnings: [String] = []
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionApplyRequest: Sendable {
|
||||
public var id: Int32 = 0
|
||||
public var url: String = ""
|
||||
public var name: String = ""
|
||||
public var selectedOrdinal: Int32 = 0
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionApplyResponse: Sendable {
|
||||
public var id: Int32 = 0
|
||||
public var importedCount: Int32 = 0
|
||||
public var selectedOrdinal: Int32 = 0
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionSelectRequest: Sendable {
|
||||
public var id: Int32 = 0
|
||||
public var selectedOrdinal: Int32 = 0
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_ProxySubscriptionSelectResponse: Sendable {
|
||||
public var id: Int32 = 0
|
||||
public var selectedOrdinal: Int32 = 0
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public struct Burrow_TunnelPacket: Sendable {
|
||||
public var payload = Data()
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
|
@ -417,6 +494,239 @@ extension Burrow_TunnelPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionImportRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionImportRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "url")
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularStringField(value: &self.url)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.url.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.url, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionNodePreview: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionNodePreview"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "ordinal"),
|
||||
2: .same(proto: "name"),
|
||||
3: .same(proto: "protocol"),
|
||||
4: .same(proto: "server"),
|
||||
5: .same(proto: "port"),
|
||||
6: .same(proto: "warnings"),
|
||||
7: .standard(proto: "runtime_supported"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.ordinal)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self.name)
|
||||
case 3: try decoder.decodeSingularStringField(value: &self.`protocol`)
|
||||
case 4: try decoder.decodeSingularStringField(value: &self.server)
|
||||
case 5: try decoder.decodeSingularInt32Field(value: &self.port)
|
||||
case 6: try decoder.decodeRepeatedStringField(value: &self.warnings)
|
||||
case 7: try decoder.decodeSingularBoolField(value: &self.runtimeSupported)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.ordinal != 0 { try visitor.visitSingularInt32Field(value: self.ordinal, fieldNumber: 1) }
|
||||
if !self.name.isEmpty { try visitor.visitSingularStringField(value: self.name, fieldNumber: 2) }
|
||||
if !self.`protocol`.isEmpty { try visitor.visitSingularStringField(value: self.`protocol`, fieldNumber: 3) }
|
||||
if !self.server.isEmpty { try visitor.visitSingularStringField(value: self.server, fieldNumber: 4) }
|
||||
if self.port != 0 { try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 5) }
|
||||
if !self.warnings.isEmpty { try visitor.visitRepeatedStringField(value: self.warnings, fieldNumber: 6) }
|
||||
if self.runtimeSupported { try visitor.visitSingularBoolField(value: self.runtimeSupported, fieldNumber: 7) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionRejectedEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionRejectedEntry"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "ordinal"),
|
||||
2: .same(proto: "reason"),
|
||||
3: .same(proto: "scheme"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.ordinal)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self.reason)
|
||||
case 3: try decoder.decodeSingularStringField(value: &self.scheme)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.ordinal != 0 { try visitor.visitSingularInt32Field(value: self.ordinal, fieldNumber: 1) }
|
||||
if !self.reason.isEmpty { try visitor.visitSingularStringField(value: self.reason, fieldNumber: 2) }
|
||||
if !self.scheme.isEmpty { try visitor.visitSingularStringField(value: self.scheme, fieldNumber: 3) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionPreviewResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionPreviewResponse"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .standard(proto: "suggested_name"),
|
||||
2: .standard(proto: "detected_format"),
|
||||
3: .standard(proto: "compatible_count"),
|
||||
4: .standard(proto: "rejected_count"),
|
||||
5: .same(proto: "node"),
|
||||
6: .same(proto: "rejected"),
|
||||
7: .same(proto: "warnings"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularStringField(value: &self.suggestedName)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self.detectedFormat)
|
||||
case 3: try decoder.decodeSingularInt32Field(value: &self.compatibleCount)
|
||||
case 4: try decoder.decodeSingularInt32Field(value: &self.rejectedCount)
|
||||
case 5: try decoder.decodeRepeatedMessageField(value: &self.node)
|
||||
case 6: try decoder.decodeRepeatedMessageField(value: &self.rejected)
|
||||
case 7: try decoder.decodeRepeatedStringField(value: &self.warnings)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.suggestedName.isEmpty { try visitor.visitSingularStringField(value: self.suggestedName, fieldNumber: 1) }
|
||||
if !self.detectedFormat.isEmpty { try visitor.visitSingularStringField(value: self.detectedFormat, fieldNumber: 2) }
|
||||
if self.compatibleCount != 0 { try visitor.visitSingularInt32Field(value: self.compatibleCount, fieldNumber: 3) }
|
||||
if self.rejectedCount != 0 { try visitor.visitSingularInt32Field(value: self.rejectedCount, fieldNumber: 4) }
|
||||
if !self.node.isEmpty { try visitor.visitRepeatedMessageField(value: self.node, fieldNumber: 5) }
|
||||
if !self.rejected.isEmpty { try visitor.visitRepeatedMessageField(value: self.rejected, fieldNumber: 6) }
|
||||
if !self.warnings.isEmpty { try visitor.visitRepeatedStringField(value: self.warnings, fieldNumber: 7) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionApplyRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionApplyRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "id"),
|
||||
2: .same(proto: "url"),
|
||||
3: .same(proto: "name"),
|
||||
4: .standard(proto: "selected_ordinal"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.id)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self.url)
|
||||
case 3: try decoder.decodeSingularStringField(value: &self.name)
|
||||
case 4: try decoder.decodeSingularInt32Field(value: &self.selectedOrdinal)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.id != 0 { try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) }
|
||||
if !self.url.isEmpty { try visitor.visitSingularStringField(value: self.url, fieldNumber: 2) }
|
||||
if !self.name.isEmpty { try visitor.visitSingularStringField(value: self.name, fieldNumber: 3) }
|
||||
if self.selectedOrdinal != 0 { try visitor.visitSingularInt32Field(value: self.selectedOrdinal, fieldNumber: 4) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionApplyResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionApplyResponse"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "id"),
|
||||
2: .standard(proto: "imported_count"),
|
||||
3: .standard(proto: "selected_ordinal"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.id)
|
||||
case 2: try decoder.decodeSingularInt32Field(value: &self.importedCount)
|
||||
case 3: try decoder.decodeSingularInt32Field(value: &self.selectedOrdinal)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.id != 0 { try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) }
|
||||
if self.importedCount != 0 { try visitor.visitSingularInt32Field(value: self.importedCount, fieldNumber: 2) }
|
||||
if self.selectedOrdinal != 0 { try visitor.visitSingularInt32Field(value: self.selectedOrdinal, fieldNumber: 3) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionSelectRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionSelectRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "id"),
|
||||
2: .standard(proto: "selected_ordinal"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.id)
|
||||
case 2: try decoder.decodeSingularInt32Field(value: &self.selectedOrdinal)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.id != 0 { try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) }
|
||||
if self.selectedOrdinal != 0 { try visitor.visitSingularInt32Field(value: self.selectedOrdinal, fieldNumber: 2) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
extension Burrow_ProxySubscriptionSelectResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = "burrow.ProxySubscriptionSelectResponse"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "id"),
|
||||
2: .standard(proto: "selected_ordinal"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularInt32Field(value: &self.id)
|
||||
case 2: try decoder.decodeSingularInt32Field(value: &self.selectedOrdinal)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.id != 0 { try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) }
|
||||
if self.selectedOrdinal != 0 { try visitor.visitSingularInt32Field(value: self.selectedOrdinal, fieldNumber: 2) }
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
}
|
||||
|
||||
public struct TailnetClient: Client, GRPCClient {
|
||||
public let channel: GRPCChannel
|
||||
public var defaultCallOptions: CallOptions
|
||||
|
|
@ -487,6 +797,52 @@ public struct TailnetClient: Client, GRPCClient {
|
|||
}
|
||||
}
|
||||
|
||||
public struct ProxySubscriptionClient: Client, GRPCClient {
|
||||
public let channel: GRPCChannel
|
||||
public var defaultCallOptions: CallOptions
|
||||
|
||||
public init(channel: any GRPCChannel) {
|
||||
self.channel = channel
|
||||
self.defaultCallOptions = .init()
|
||||
}
|
||||
|
||||
public func previewImport(
|
||||
_ request: Burrow_ProxySubscriptionImportRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
) async throws -> Burrow_ProxySubscriptionPreviewResponse {
|
||||
try await self.performAsyncUnaryCall(
|
||||
path: "/burrow.ProxySubscriptions/PreviewImport",
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: []
|
||||
)
|
||||
}
|
||||
|
||||
public func applyImport(
|
||||
_ request: Burrow_ProxySubscriptionApplyRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
) async throws -> Burrow_ProxySubscriptionApplyResponse {
|
||||
try await self.performAsyncUnaryCall(
|
||||
path: "/burrow.ProxySubscriptions/ApplyImport",
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: []
|
||||
)
|
||||
}
|
||||
|
||||
public func selectNode(
|
||||
_ request: Burrow_ProxySubscriptionSelectRequest,
|
||||
callOptions: CallOptions? = nil
|
||||
) async throws -> Burrow_ProxySubscriptionSelectResponse {
|
||||
try await self.performAsyncUnaryCall(
|
||||
path: "/burrow.ProxySubscriptions/SelectNode",
|
||||
request: request,
|
||||
callOptions: callOptions ?? self.defaultCallOptions,
|
||||
interceptors: []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct TunnelPacketClient: Client, GRPCClient {
|
||||
public let channel: GRPCChannel
|
||||
public var defaultCallOptions: CallOptions
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
public typealias RawValue = Int
|
||||
case wireGuard // = 0
|
||||
case tailnet // = 1
|
||||
case trojan // = 2
|
||||
case proxySubscription // = 3
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
public init() {
|
||||
|
|
@ -35,6 +37,8 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
switch rawValue {
|
||||
case 0: self = .wireGuard
|
||||
case 1: self = .tailnet
|
||||
case 2: self = .trojan
|
||||
case 3: self = .proxySubscription
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +47,8 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
switch self {
|
||||
case .wireGuard: return 0
|
||||
case .tailnet: return 1
|
||||
case .trojan: return 2
|
||||
case .proxySubscription: return 3
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +57,8 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
public static let allCases: [Burrow_NetworkType] = [
|
||||
.wireGuard,
|
||||
.tailnet,
|
||||
.trojan,
|
||||
.proxySubscription,
|
||||
]
|
||||
|
||||
}
|
||||
|
|
@ -217,6 +225,8 @@ public struct Burrow_TunnelConfigurationResponse: Sendable {
|
|||
|
||||
public var routes: [String] = []
|
||||
|
||||
public var excludedRoutes: [String] = []
|
||||
|
||||
public var dnsServers: [String] = []
|
||||
|
||||
public var searchDomains: [String] = []
|
||||
|
|
@ -236,6 +246,8 @@ extension Burrow_NetworkType: SwiftProtobuf._ProtoNameProviding {
|
|||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
0: .same(proto: "WireGuard"),
|
||||
1: .same(proto: "Tailnet"),
|
||||
2: .same(proto: "Trojan"),
|
||||
3: .same(proto: "ProxySubscription"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -544,6 +556,7 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob
|
|||
4: .standard(proto: "dns_servers"),
|
||||
5: .standard(proto: "search_domains"),
|
||||
6: .standard(proto: "include_default_route"),
|
||||
7: .standard(proto: "excluded_routes"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -558,6 +571,7 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob
|
|||
case 4: try { try decoder.decodeRepeatedStringField(value: &self.dnsServers) }()
|
||||
case 5: try { try decoder.decodeRepeatedStringField(value: &self.searchDomains) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self.includeDefaultRoute) }()
|
||||
case 7: try { try decoder.decodeRepeatedStringField(value: &self.excludedRoutes) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -573,6 +587,9 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob
|
|||
if !self.routes.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.routes, fieldNumber: 3)
|
||||
}
|
||||
if !self.excludedRoutes.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.excludedRoutes, fieldNumber: 7)
|
||||
}
|
||||
if !self.dnsServers.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.dnsServers, fieldNumber: 4)
|
||||
}
|
||||
|
|
@ -589,6 +606,7 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob
|
|||
if lhs.addresses != rhs.addresses {return false}
|
||||
if lhs.mtu != rhs.mtu {return false}
|
||||
if lhs.routes != rhs.routes {return false}
|
||||
if lhs.excludedRoutes != rhs.excludedRoutes {return false}
|
||||
if lhs.dnsServers != rhs.dnsServers {return false}
|
||||
if lhs.searchDomains != rhs.searchDomains {return false}
|
||||
if lhs.includeDefaultRoute != rhs.includeDefaultRoute {return false}
|
||||
|
|
|
|||
|
|
@ -9,3 +9,5 @@ CODE_SIGN_ENTITLEMENTS = NetworkExtension/NetworkExtension-iOS.entitlements
|
|||
CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = NetworkExtension/NetworkExtension-macOS.entitlements
|
||||
|
||||
SWIFT_INCLUDE_PATHS = $(inherited) $(PROJECT_DIR)/NetworkExtension
|
||||
|
||||
OTHER_LDFLAGS = $(inherited) -framework SystemConfiguration
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
|
|||
get throws { try _client.get() }
|
||||
}
|
||||
private let _client: Result<TunnelClient, Swift.Error> = Result {
|
||||
try TunnelClient.unix(socketURL: Constants.socketURL)
|
||||
try TunnelClient.unix(socketURL: Constants.packetTunnelSocketURL)
|
||||
}
|
||||
|
||||
override init() {
|
||||
do {
|
||||
libburrow.spawnInProcess(
|
||||
socketPath: try Constants.socketURL.path(percentEncoded: false),
|
||||
socketPath: try Constants.packetTunnelSocketURL.path(percentEncoded: false),
|
||||
databasePath: try Constants.databaseURL.path(percentEncoded: false)
|
||||
)
|
||||
} catch {
|
||||
|
|
@ -54,6 +54,11 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
|
|||
guard let settings = configuration?.settings else {
|
||||
throw Error.missingTunnelConfiguration
|
||||
}
|
||||
if let configuration {
|
||||
logger.log(
|
||||
"Applying tunnel configuration addresses=\(configuration.addresses.joined(separator: ","), privacy: .public) routes=\(configuration.routes.joined(separator: ","), privacy: .public) excludedRoutes=\(configuration.excludedRoutes.joined(separator: ","), privacy: .public) dns=\(configuration.dnsServers.joined(separator: ","), privacy: .public) includeDefaultRoute=\(configuration.includeDefaultRoute, privacy: .public)"
|
||||
)
|
||||
}
|
||||
try await setTunnelNetworkSettings(settings)
|
||||
try startPacketBridge()
|
||||
logger.log("Started tunnel with network settings: \(settings)")
|
||||
|
|
@ -88,15 +93,22 @@ extension PacketTunnelProvider {
|
|||
private func startPacketBridge() throws {
|
||||
stopPacketBridge()
|
||||
|
||||
let packetClient = TunnelPacketClient.unix(socketURL: try Constants.socketURL)
|
||||
let packetClient = TunnelPacketClient.unix(socketURL: try Constants.packetTunnelSocketURL)
|
||||
let call = packetClient.makeTunnelPacketsCall()
|
||||
self.packetCall = call
|
||||
|
||||
inboundPacketTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
var packetCount = 0
|
||||
var byteCount = 0
|
||||
do {
|
||||
for try await packet in call.responseStream {
|
||||
let payload = packet.payload
|
||||
packetCount += 1
|
||||
byteCount += payload.count
|
||||
if packetCount == 1 || packetCount % 1024 == 0 {
|
||||
self.logger.log("Tunnel packet receive progress packets=\(packetCount, privacy: .public) bytes=\(byteCount, privacy: .public) lastBytes=\(payload.count, privacy: .public)")
|
||||
}
|
||||
self.packetFlow.writePackets(
|
||||
[payload],
|
||||
withProtocols: [Self.protocolNumber(for: payload)]
|
||||
|
|
@ -111,10 +123,17 @@ extension PacketTunnelProvider {
|
|||
outboundPacketTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
defer { call.requestStream.finish() }
|
||||
var packetCount = 0
|
||||
var byteCount = 0
|
||||
do {
|
||||
while !Task.isCancelled {
|
||||
let packets = await self.readPacketsBatch()
|
||||
for (payload, _) in packets {
|
||||
packetCount += 1
|
||||
byteCount += payload.count
|
||||
if packetCount == 1 || packetCount % 1024 == 0 {
|
||||
self.logger.log("Tunnel packet send progress packets=\(packetCount, privacy: .public) bytes=\(byteCount, privacy: .public) lastBytes=\(payload.count, privacy: .public)")
|
||||
}
|
||||
var packet = Burrow_TunnelPacket()
|
||||
packet.payload = payload
|
||||
try await call.requestStream.send(packet)
|
||||
|
|
@ -128,6 +147,7 @@ extension PacketTunnelProvider {
|
|||
}
|
||||
|
||||
private func stopPacketBridge() {
|
||||
logger.log("Stopping tunnel packet bridge")
|
||||
inboundPacketTask?.cancel()
|
||||
inboundPacketTask = nil
|
||||
outboundPacketTask?.cancel()
|
||||
|
|
@ -165,6 +185,9 @@ extension Burrow_TunnelConfigurationResponse {
|
|||
let parsedRoutes = routes.compactMap(ParsedTunnelRoute.init(rawValue:))
|
||||
var ipv4Routes = parsedRoutes.compactMap(\.ipv4Route)
|
||||
var ipv6Routes = parsedRoutes.compactMap(\.ipv6Route)
|
||||
let parsedExcludedRoutes = excludedRoutes.compactMap(ParsedTunnelRoute.init(rawValue:))
|
||||
let excludedIPv4Routes = parsedExcludedRoutes.compactMap(\.ipv4Route)
|
||||
let excludedIPv6Routes = parsedExcludedRoutes.compactMap(\.ipv6Route)
|
||||
if includeDefaultRoute {
|
||||
ipv4Routes.append(.default())
|
||||
ipv6Routes.append(.default())
|
||||
|
|
@ -180,6 +203,9 @@ extension Burrow_TunnelConfigurationResponse {
|
|||
if !ipv4Routes.isEmpty {
|
||||
ipv4Settings.includedRoutes = ipv4Routes
|
||||
}
|
||||
if !excludedIPv4Routes.isEmpty {
|
||||
ipv4Settings.excludedRoutes = excludedIPv4Routes
|
||||
}
|
||||
settings.ipv4Settings = ipv4Settings
|
||||
}
|
||||
if !ipv6Addresses.isEmpty {
|
||||
|
|
@ -190,6 +216,9 @@ extension Burrow_TunnelConfigurationResponse {
|
|||
if !ipv6Routes.isEmpty {
|
||||
ipv6Settings.includedRoutes = ipv6Routes
|
||||
}
|
||||
if !excludedIPv6Routes.isEmpty {
|
||||
ipv6Settings.excludedRoutes = excludedIPv6Routes
|
||||
}
|
||||
settings.ipv6Settings = ipv6Settings
|
||||
}
|
||||
if !dnsServers.isEmpty {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# This is a build script. It is run by Xcode as a build step.
|
||||
# The type of build is described in various environment variables set by Xcode.
|
||||
|
||||
export PATH="${PATH}:${HOME}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/${USER}/bin"
|
||||
export PATH="${PATH}:${HOME}/.cargo/bin:${HOME}/.nix-profile/bin:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/${USER}/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin"
|
||||
|
||||
if ! [[ -x "$(command -v cargo)" ]]; then
|
||||
echo 'error: Unable to find cargo'
|
||||
|
|
@ -63,13 +63,13 @@ else
|
|||
fi
|
||||
|
||||
if [[ -x "$(command -v rustup)" ]]; then
|
||||
CARGO_PATH="$(dirname $(rustup which cargo)):/usr/bin"
|
||||
CARGO_PATH="$(dirname "$(rustup which cargo)"):/usr/bin"
|
||||
else
|
||||
CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin"
|
||||
CARGO_PATH="$(dirname "$(readlink -f "$(command -v cargo)")"):/usr/bin"
|
||||
fi
|
||||
|
||||
PROTOC=$(readlink -f $(which protoc))
|
||||
CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH"
|
||||
PROTOC="$(readlink -f "$(command -v 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import BurrowConfiguration
|
||||
import Foundation
|
||||
import GRPC
|
||||
import SwiftUI
|
||||
#if canImport(AuthenticationServices)
|
||||
import AuthenticationServices
|
||||
|
|
@ -15,6 +16,7 @@ public struct BurrowView: View {
|
|||
@State private var accountStore = NetworkAccountStore()
|
||||
@State private var activeSheet: ConfigurationSheet?
|
||||
@State private var didRunAutomation = false
|
||||
@State private var expandedProxySubscriptionID: Int32?
|
||||
|
||||
public var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -37,6 +39,9 @@ public struct BurrowView: View {
|
|||
Button("Add WireGuard Network") {
|
||||
activeSheet = .wireGuard
|
||||
}
|
||||
Button("Import Proxy Subscription") {
|
||||
activeSheet = .proxySubscription
|
||||
}
|
||||
Button("Save Tor Account") {
|
||||
activeSheet = .tor
|
||||
}
|
||||
|
|
@ -67,8 +72,22 @@ public struct BurrowView: View {
|
|||
Text(connectionError)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(4)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
NetworkCarouselView(
|
||||
networks: networkViewModel.cards,
|
||||
selectedNetworkID: expandedProxySubscriptionID
|
||||
) { network in
|
||||
guard networkViewModel.proxySubscriptions.contains(where: { $0.id == network.id }) else {
|
||||
return
|
||||
}
|
||||
withAnimation(.snappy) {
|
||||
expandedProxySubscriptionID = expandedProxySubscriptionID == network.id
|
||||
? nil
|
||||
: network.id
|
||||
}
|
||||
}
|
||||
NetworkCarouselView(networks: networkViewModel.cards)
|
||||
}
|
||||
|
||||
if showsAccountsSection {
|
||||
|
|
@ -119,6 +138,15 @@ public struct BurrowView: View {
|
|||
accountStore: accountStore
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: proxySubscriptionSheetBinding) {
|
||||
ProxySubscriptionManagerView(
|
||||
networks: networkViewModel.proxySubscriptions,
|
||||
networkViewModel: networkViewModel,
|
||||
selectedNetworkID: $expandedProxySubscriptionID
|
||||
)
|
||||
.frame(width: 820, height: 600)
|
||||
.padding(20)
|
||||
}
|
||||
.onAppear {
|
||||
runAutomationIfNeeded()
|
||||
}
|
||||
|
|
@ -157,15 +185,29 @@ public struct BurrowView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var proxySubscriptionSheetBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { expandedProxySubscriptionID != nil },
|
||||
set: { isPresented in
|
||||
guard !isPresented else { return }
|
||||
expandedProxySubscriptionID = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sectionHeader(title: String, detail: String?) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.title2.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
if let detail, !detail.isEmpty {
|
||||
Text(detail)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,13 +232,14 @@ public struct BurrowView: View {
|
|||
#if os(iOS)
|
||||
!accountStore.accounts.isEmpty
|
||||
#else
|
||||
true
|
||||
!accountStore.accounts.isEmpty || networkViewModel.networks.isEmpty
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
||||
case wireGuard
|
||||
case proxySubscription
|
||||
case tor
|
||||
case tailnet
|
||||
|
||||
|
|
@ -205,6 +248,7 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
|||
var kind: AccountNetworkKind {
|
||||
switch self {
|
||||
case .wireGuard: .wireGuard
|
||||
case .proxySubscription: .proxySubscription
|
||||
case .tor: .tor
|
||||
case .tailnet: .tailnet
|
||||
}
|
||||
|
|
@ -214,6 +258,8 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .wireGuard:
|
||||
"wave.3.right"
|
||||
case .proxySubscription:
|
||||
"link.badge.plus"
|
||||
case .tor:
|
||||
"shield.lefthalf.filled.badge.checkmark"
|
||||
case .tailnet:
|
||||
|
|
@ -225,6 +271,8 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .wireGuard:
|
||||
"WireGuard"
|
||||
case .proxySubscription:
|
||||
"Proxy Subscription"
|
||||
case .tor:
|
||||
"Tor"
|
||||
case .tailnet:
|
||||
|
|
@ -236,6 +284,8 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .wireGuard:
|
||||
"Import a tunnel"
|
||||
case .proxySubscription:
|
||||
"Import Trojan or Shadowsocks"
|
||||
case .tor:
|
||||
"Save an Arti profile"
|
||||
case .tailnet:
|
||||
|
|
@ -247,6 +297,8 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .wireGuard:
|
||||
.blue
|
||||
case .proxySubscription:
|
||||
.teal
|
||||
case .tor, .tailnet:
|
||||
kind.accentColor
|
||||
}
|
||||
|
|
@ -286,6 +338,8 @@ private struct AccountDraft {
|
|||
var accountName = ""
|
||||
var identityName = ""
|
||||
var wireGuardConfig = ""
|
||||
var proxySubscriptionURL = ""
|
||||
var proxySubscriptionName = ""
|
||||
|
||||
var discoveryEmail = ""
|
||||
var authority = ""
|
||||
|
|
@ -304,6 +358,8 @@ private struct AccountDraft {
|
|||
switch sheet {
|
||||
case .wireGuard:
|
||||
break
|
||||
case .proxySubscription:
|
||||
title = "Proxy Subscription"
|
||||
case .tor:
|
||||
title = "Default Tor"
|
||||
accountName = "default"
|
||||
|
|
@ -338,6 +394,11 @@ private struct ConfigurationSheetView: View {
|
|||
@State private var tailnetLoginError: String?
|
||||
@State private var tailnetLoginSessionID: String?
|
||||
@State private var isStartingTailnetLogin = false
|
||||
@State private var proxySubscriptionPreview: ProxySubscriptionPreview?
|
||||
@State private var proxySubscriptionPreviewError: String?
|
||||
@State private var isPreviewingProxySubscription = false
|
||||
@State private var selectedProxySubscriptionOrdinal: Int32?
|
||||
@State private var proxySubscriptionNodeFilter = ""
|
||||
@State private var tailnetPresentedAuthURL: URL?
|
||||
@State private var preserveTailnetLoginSession = false
|
||||
@State private var usesCustomTailnetAuthority = false
|
||||
|
|
@ -374,31 +435,11 @@ private struct ConfigurationSheetView: View {
|
|||
}
|
||||
}
|
||||
|
||||
switch sheet {
|
||||
case .wireGuard:
|
||||
Section("WireGuard Configuration") {
|
||||
TextEditor(text: $draft.wireGuardConfig)
|
||||
.font(.body.monospaced())
|
||||
.frame(minHeight: wireGuardEditorHeight)
|
||||
.contextMenu {
|
||||
wireGuardContextActions
|
||||
}
|
||||
}
|
||||
case .tor:
|
||||
Section("Tor Preferences") {
|
||||
TextField("Virtual Addresses", text: $draft.torAddresses)
|
||||
TextField("DNS Resolvers", text: $draft.torDNS)
|
||||
TextField("MTU", text: $draft.torMTU)
|
||||
TextField("Transparent Listener", text: $draft.torListen)
|
||||
}
|
||||
case .tailnet:
|
||||
tailnetSections
|
||||
}
|
||||
configurationSections
|
||||
|
||||
if let errorMessage {
|
||||
Section {
|
||||
Text(errorMessage)
|
||||
.foregroundStyle(.red)
|
||||
feedbackText(errorMessage, color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -445,7 +486,8 @@ private struct ConfigurationSheetView: View {
|
|||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 520, minHeight: 620)
|
||||
.frame(width: 520)
|
||||
.frame(minHeight: 620)
|
||||
#endif
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if showsBottomActionButton {
|
||||
|
|
@ -473,6 +515,12 @@ private struct ConfigurationSheetView: View {
|
|||
await cancelTailnetLoginIfNeeded()
|
||||
}
|
||||
}
|
||||
.onChange(of: draft.proxySubscriptionURL) { _, _ in
|
||||
proxySubscriptionPreview = nil
|
||||
proxySubscriptionPreviewError = nil
|
||||
selectedProxySubscriptionOrdinal = nil
|
||||
proxySubscriptionNodeFilter = ""
|
||||
}
|
||||
.onDisappear {
|
||||
tailnetLoginPollTask?.cancel()
|
||||
tailnetDiscoveryTask?.cancel()
|
||||
|
|
@ -486,6 +534,71 @@ private struct ConfigurationSheetView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var configurationSections: some View {
|
||||
switch sheet {
|
||||
case .wireGuard:
|
||||
wireGuardConfigurationSection
|
||||
case .proxySubscription:
|
||||
proxySubscriptionSections
|
||||
case .tor:
|
||||
torPreferenceSection
|
||||
case .tailnet:
|
||||
tailnetSections
|
||||
}
|
||||
}
|
||||
|
||||
private var wireGuardConfigurationSection: some View {
|
||||
Section("WireGuard Configuration") {
|
||||
TextEditor(text: $draft.wireGuardConfig)
|
||||
.font(.body.monospaced())
|
||||
.frame(minHeight: wireGuardEditorHeight)
|
||||
.contextMenu {
|
||||
wireGuardContextActions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var proxySubscriptionSections: some View {
|
||||
Section("Subscription") {
|
||||
TextField("Subscription URL", text: $draft.proxySubscriptionURL)
|
||||
.autocorrectionDisabled()
|
||||
TextField("Name", text: $draft.proxySubscriptionName)
|
||||
}
|
||||
|
||||
Section("Preview") {
|
||||
proxySubscriptionPreviewButton
|
||||
|
||||
if let proxySubscriptionPreview {
|
||||
proxySubscriptionPreviewView(proxySubscriptionPreview)
|
||||
} else if let proxySubscriptionPreviewError {
|
||||
feedbackText(proxySubscriptionPreviewError, color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var proxySubscriptionPreviewButton: some View {
|
||||
Button {
|
||||
previewProxySubscription()
|
||||
} label: {
|
||||
Label(
|
||||
isPreviewingProxySubscription ? "Previewing" : "Preview",
|
||||
systemImage: isPreviewingProxySubscription ? "hourglass" : "eye"
|
||||
)
|
||||
}
|
||||
.disabled(isPreviewingProxySubscription || normalizedOptional(draft.proxySubscriptionURL) == nil)
|
||||
}
|
||||
|
||||
private var torPreferenceSection: some View {
|
||||
Section("Tor Preferences") {
|
||||
TextField("Virtual Addresses", text: $draft.torAddresses)
|
||||
TextField("DNS Resolvers", text: $draft.torDNS)
|
||||
TextField("MTU", text: $draft.torMTU)
|
||||
TextField("Transparent Listener", text: $draft.torListen)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var identityFields: some View {
|
||||
TextField("Title", text: $draft.title)
|
||||
|
|
@ -792,6 +905,143 @@ private struct ConfigurationSheetView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private func proxySubscriptionPreviewView(_ preview: ProxySubscriptionPreview) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
labeledValue("Format", preview.detectedFormat)
|
||||
labeledValue("Compatible", "\(preview.compatibleCount)")
|
||||
labeledValue("Runtime Supported", "\(preview.nodes.filter { $0.runtimeSupported }.count)")
|
||||
labeledValue("Rejected", "\(preview.rejectedCount)")
|
||||
|
||||
let selectableNodes = supportedProxySubscriptionNodes(in: preview)
|
||||
if !selectableNodes.isEmpty {
|
||||
TextField("Filter nodes", text: $proxySubscriptionNodeFilter)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
let filteredNodes = filteredProxySubscriptionNodes(selectableNodes)
|
||||
proxySubscriptionNodeList(nodes: filteredNodes, preview: preview)
|
||||
|
||||
if !proxySubscriptionNodeFilter.isEmpty && filteredNodes.isEmpty {
|
||||
feedbackText("No nodes match the current filter.", color: .orange)
|
||||
}
|
||||
} else if !preview.nodes.isEmpty {
|
||||
Text("No previewed nodes are supported by the packet runtime.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
ForEach(preview.warnings, id: \.self) { warning in
|
||||
feedbackText(warning, color: .orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func proxySubscriptionNodeList(
|
||||
nodes: [ProxySubscriptionNodePreview],
|
||||
preview: ProxySubscriptionPreview
|
||||
) -> some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(nodes) { node in
|
||||
proxySubscriptionNodeRow(
|
||||
node: node,
|
||||
isSelected: selectedProxySubscriptionNode(in: preview)?.ordinal == node.ordinal
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.frame(maxHeight: 260)
|
||||
}
|
||||
|
||||
private func proxySubscriptionNodeRow(
|
||||
node: ProxySubscriptionNodePreview,
|
||||
isSelected: Bool
|
||||
) -> some View {
|
||||
Button {
|
||||
selectedProxySubscriptionOrdinal = node.ordinal
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(isSelected ? Color.accentColor : Color.secondary)
|
||||
.imageScale(.small)
|
||||
.frame(width: 16)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(node.name)
|
||||
.font(.footnote.weight(isSelected ? .semibold : .regular))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
Text(node.proxyProtocol.uppercased())
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 2)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(.secondary.opacity(0.12))
|
||||
)
|
||||
}
|
||||
|
||||
Text("\(node.server):\(node.port)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(isSelected ? Color.accentColor.opacity(0.14) : Color.secondary.opacity(0.08))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(isSelected ? Color.accentColor.opacity(0.45) : Color.clear, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func supportedProxySubscriptionNodes(
|
||||
in preview: ProxySubscriptionPreview
|
||||
) -> [ProxySubscriptionNodePreview] {
|
||||
preview.nodes.filter { $0.runtimeSupported }
|
||||
}
|
||||
|
||||
private func filteredProxySubscriptionNodes(
|
||||
_ nodes: [ProxySubscriptionNodePreview]
|
||||
) -> [ProxySubscriptionNodePreview] {
|
||||
let query = proxySubscriptionNodeFilter.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !query.isEmpty else { return nodes }
|
||||
return nodes.filter { node in
|
||||
node.name.localizedCaseInsensitiveContains(query)
|
||||
|| node.server.localizedCaseInsensitiveContains(query)
|
||||
|| node.proxyProtocol.localizedCaseInsensitiveContains(query)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectedProxySubscriptionNode(in preview: ProxySubscriptionPreview) -> ProxySubscriptionNodePreview? {
|
||||
let ordinal = selectedProxySubscriptionOrdinal
|
||||
?? preview.nodes.first(where: { $0.runtimeSupported })?.ordinal
|
||||
return preview.nodes.first { $0.ordinal == ordinal }
|
||||
}
|
||||
|
||||
private func feedbackText(_ message: String, color: Color) -> some View {
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
.foregroundStyle(color)
|
||||
.lineLimit(6)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var bottomActionBar: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -827,6 +1077,12 @@ private struct ConfigurationSheetView: View {
|
|||
}
|
||||
.disabled(draft.wireGuardConfig.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
|
||||
case .proxySubscription:
|
||||
Button("Preview Subscription") {
|
||||
previewProxySubscription()
|
||||
}
|
||||
.disabled(normalizedOptional(draft.proxySubscriptionURL) == nil)
|
||||
|
||||
case .tor:
|
||||
Menu("Presets") {
|
||||
Button("Recommended Tor Defaults") {
|
||||
|
|
@ -883,6 +1139,8 @@ private struct ConfigurationSheetView: View {
|
|||
switch sheet {
|
||||
case .wireGuard:
|
||||
.blue
|
||||
case .proxySubscription:
|
||||
.teal
|
||||
case .tor, .tailnet:
|
||||
sheet.kind.accentColor
|
||||
}
|
||||
|
|
@ -892,6 +1150,8 @@ private struct ConfigurationSheetView: View {
|
|||
switch sheet {
|
||||
case .wireGuard:
|
||||
"Import WireGuard"
|
||||
case .proxySubscription:
|
||||
"Import Proxy Subscription"
|
||||
case .tor:
|
||||
"Configure Tor"
|
||||
case .tailnet:
|
||||
|
|
@ -911,6 +1171,8 @@ private struct ConfigurationSheetView: View {
|
|||
switch sheet {
|
||||
case .wireGuard, .tor:
|
||||
return true
|
||||
case .proxySubscription:
|
||||
return false
|
||||
case .tailnet:
|
||||
return showsAdvancedTailnetSettings
|
||||
}
|
||||
|
|
@ -928,6 +1190,8 @@ private struct ConfigurationSheetView: View {
|
|||
switch sheet {
|
||||
case .wireGuard:
|
||||
return "Add Network"
|
||||
case .proxySubscription:
|
||||
return "Import"
|
||||
case .tor:
|
||||
return "Save Account"
|
||||
case .tailnet:
|
||||
|
|
@ -942,7 +1206,7 @@ private struct ConfigurationSheetView: View {
|
|||
return normalizedOptional(draft.authority) == nil
|
||||
}
|
||||
return false
|
||||
case .wireGuard, .tor:
|
||||
case .wireGuard, .tor, .proxySubscription:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -951,6 +1215,9 @@ private struct ConfigurationSheetView: View {
|
|||
switch sheet {
|
||||
case .wireGuard:
|
||||
return draft.wireGuardConfig.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
case .proxySubscription:
|
||||
return normalizedOptional(draft.proxySubscriptionURL) == nil
|
||||
|| proxySubscriptionPreview?.nodes.contains(where: { $0.runtimeSupported }) != true
|
||||
case .tor:
|
||||
return normalizedOptional(draft.accountName) == nil || normalizedOptional(draft.identityName) == nil
|
||||
case .tailnet:
|
||||
|
|
@ -1025,6 +1292,9 @@ private struct ConfigurationSheetView: View {
|
|||
case .wireGuard:
|
||||
try await submitWireGuard()
|
||||
dismiss()
|
||||
case .proxySubscription:
|
||||
try await submitProxySubscription()
|
||||
dismiss()
|
||||
case .tor:
|
||||
try submitTor()
|
||||
dismiss()
|
||||
|
|
@ -1032,7 +1302,7 @@ private struct ConfigurationSheetView: View {
|
|||
try await submitTailnet()
|
||||
}
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
errorMessage = displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1062,6 +1332,44 @@ private struct ConfigurationSheetView: View {
|
|||
try accountStore.upsert(record, secret: nil)
|
||||
}
|
||||
|
||||
private func submitProxySubscription() async throws {
|
||||
let url = normalized(draft.proxySubscriptionURL, fallback: "")
|
||||
let name = normalized(draft.proxySubscriptionName, fallback: "Proxy Subscription")
|
||||
let selectedOrdinal = selectedProxySubscriptionOrdinal
|
||||
?? proxySubscriptionPreview?.nodes.first(where: { $0.runtimeSupported })?.ordinal
|
||||
_ = try await networkViewModel.importProxySubscription(
|
||||
url: url,
|
||||
name: name,
|
||||
selectedOrdinal: selectedOrdinal
|
||||
)
|
||||
}
|
||||
|
||||
private func previewProxySubscription() {
|
||||
guard let url = normalizedOptional(draft.proxySubscriptionURL) else {
|
||||
proxySubscriptionPreviewError = "Enter a subscription URL before previewing."
|
||||
return
|
||||
}
|
||||
isPreviewingProxySubscription = true
|
||||
proxySubscriptionPreviewError = nil
|
||||
Task { @MainActor in
|
||||
defer { isPreviewingProxySubscription = false }
|
||||
do {
|
||||
let preview = try await networkViewModel.previewProxySubscription(url: url)
|
||||
proxySubscriptionPreview = preview
|
||||
selectedProxySubscriptionOrdinal = preview.nodes.first(where: { $0.runtimeSupported })?.ordinal
|
||||
proxySubscriptionNodeFilter = ""
|
||||
if draft.proxySubscriptionName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
draft.proxySubscriptionName = preview.suggestedName
|
||||
}
|
||||
} catch {
|
||||
proxySubscriptionPreview = nil
|
||||
selectedProxySubscriptionOrdinal = nil
|
||||
proxySubscriptionNodeFilter = ""
|
||||
proxySubscriptionPreviewError = displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func submitTor() throws {
|
||||
let title = titleOrFallback("Tor \(normalized(draft.identityName, fallback: "apple"))")
|
||||
let note = [
|
||||
|
|
@ -1529,6 +1837,8 @@ private struct ConfigurationSheetView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.body.monospaced())
|
||||
.lineLimit(3)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1543,9 +1853,11 @@ private struct AccountRowView: View {
|
|||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(account.title)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
Text(account.kind.title)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(account.kind.accentColor)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(account.kind.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
if hasSecret {
|
||||
|
|
@ -1578,6 +1890,8 @@ private struct AccountRowView: View {
|
|||
Text(note)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
|
@ -1596,11 +1910,378 @@ private struct AccountRowView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.body.monospaced())
|
||||
.lineLimit(3)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProxySubscriptionManagerView: View {
|
||||
@Environment(\.tunnel)
|
||||
private var tunnel: any Tunnel
|
||||
|
||||
let networks: [ProxySubscriptionNetwork]
|
||||
let networkViewModel: NetworkViewModel
|
||||
@Binding var selectedNetworkID: Int32?
|
||||
|
||||
@State private var nodeFilter = ""
|
||||
@State private var operationID: String?
|
||||
@State private var feedback: String?
|
||||
@State private var errorMessage: String?
|
||||
@State private var pendingDelete: ProxySubscriptionNetwork?
|
||||
|
||||
var body: some View {
|
||||
if let network = selectedNetwork {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Proxy Subscription")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(network.name)
|
||||
.font(.title3.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
if let selectedNode = network.selectedNode {
|
||||
Text("\(selectedNode.proxyProtocol.uppercased()) \(selectedNode.server):\(selectedNode.port)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
refresh(network)
|
||||
} label: {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.disabled(isBusy)
|
||||
|
||||
Button(role: .destructive) {
|
||||
pendingDelete = network
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
.disabled(isBusy)
|
||||
}
|
||||
.burrowGlassControl()
|
||||
.controlSize(.small)
|
||||
|
||||
Button {
|
||||
withAnimation(.snappy) {
|
||||
selectedNetworkID = nil
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close proxy subscription picker")
|
||||
}
|
||||
|
||||
if networks.count > 1 {
|
||||
Picker("Subscription", selection: selectedNetworkIDBinding) {
|
||||
ForEach(networks) { network in
|
||||
Text(network.name).tag(network.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Label(network.detectedFormat, systemImage: "doc.text")
|
||||
Label("\(network.runtimeSupportedNodes.count) usable of \(network.nodes.count) nodes", systemImage: "circle.grid.2x1")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextField("Filter nodes", text: $nodeFilter)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Text("Nodes")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Spacer()
|
||||
Text("\(filteredNodes(for: network).count) shown")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: nodeColumns, alignment: .leading, spacing: 10) {
|
||||
ForEach(filteredNodes(for: network)) { node in
|
||||
nodeTile(node, in: network)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.frame(maxHeight: 410)
|
||||
.scrollIndicators(.visible)
|
||||
|
||||
if let feedback {
|
||||
Text(feedback)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
.lineLimit(3)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onAppear {
|
||||
ensureSelectedNetwork()
|
||||
}
|
||||
.onChange(of: networks) { _, _ in
|
||||
ensureSelectedNetwork()
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Remove proxy subscription?",
|
||||
isPresented: Binding(
|
||||
get: { pendingDelete != nil },
|
||||
set: { if !$0 { pendingDelete = nil } }
|
||||
),
|
||||
presenting: pendingDelete
|
||||
) { network in
|
||||
Button("Remove \(network.name)", role: .destructive) {
|
||||
delete(network)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedNetwork: ProxySubscriptionNetwork? {
|
||||
guard let selectedNetworkID else {
|
||||
return nil
|
||||
}
|
||||
return networks.first(where: { $0.id == selectedNetworkID })
|
||||
}
|
||||
|
||||
private var selectedNetworkIDBinding: Binding<Int32> {
|
||||
Binding(
|
||||
get: { selectedNetwork?.id ?? networks.first?.id ?? 0 },
|
||||
set: { selectedNetworkID = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
private var isBusy: Bool {
|
||||
operationID != nil
|
||||
}
|
||||
|
||||
private var nodeColumns: [GridItem] {
|
||||
[
|
||||
GridItem(
|
||||
.adaptive(minimum: 245, maximum: 320),
|
||||
spacing: 10,
|
||||
alignment: .top
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func ensureSelectedNetwork() {
|
||||
guard let selectedNetworkID else {
|
||||
return
|
||||
}
|
||||
if !networks.contains(where: { $0.id == selectedNetworkID }) {
|
||||
self.selectedNetworkID = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func filteredNodes(for network: ProxySubscriptionNetwork) -> [ProxySubscriptionNetworkNode] {
|
||||
let query = nodeFilter.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard !query.isEmpty else {
|
||||
return network.nodes
|
||||
}
|
||||
return network.nodes.filter { node in
|
||||
node.name.lowercased().contains(query)
|
||||
|| node.server.lowercased().contains(query)
|
||||
|| node.proxyProtocol.lowercased().contains(query)
|
||||
}
|
||||
}
|
||||
|
||||
private func nodeTile(
|
||||
_ node: ProxySubscriptionNetworkNode,
|
||||
in network: ProxySubscriptionNetwork
|
||||
) -> some View {
|
||||
let isSelected = network.selectedNode?.ordinal == node.ordinal
|
||||
return Button {
|
||||
select(node, in: network)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(isSelected ? Color.accentColor : Color.secondary)
|
||||
.frame(width: 18)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(node.name)
|
||||
.font(.footnote.weight(isSelected ? .semibold : .regular))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(node.proxyProtocol.uppercased())
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule().fill(.secondary.opacity(0.12)))
|
||||
if !node.runtimeSupported {
|
||||
Text("UNSUPPORTED")
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text("\(node.server):\(node.port)")
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
if operationID == operationIdentifier(networkID: network.id, ordinal: node.ordinal) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 94, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(isSelected ? Color.accentColor.opacity(0.10) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(isSelected ? Color.accentColor.opacity(0.65) : Color.secondary.opacity(0.16), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isBusy || !node.runtimeSupported)
|
||||
}
|
||||
|
||||
private func select(
|
||||
_ node: ProxySubscriptionNetworkNode,
|
||||
in network: ProxySubscriptionNetwork
|
||||
) {
|
||||
let operationIdentifier = operationIdentifier(networkID: network.id, ordinal: node.ordinal)
|
||||
operationID = operationIdentifier
|
||||
feedback = nil
|
||||
errorMessage = nil
|
||||
Task { @MainActor in
|
||||
defer { operationID = nil }
|
||||
do {
|
||||
_ = try await networkViewModel.updateProxySubscriptionSelection(
|
||||
network: network,
|
||||
selectedOrdinal: node.ordinal
|
||||
)
|
||||
if isTunnelRunning {
|
||||
_ = try await networkViewModel.updateActiveProxySubscriptionSelection(
|
||||
network: network,
|
||||
selectedOrdinal: node.ordinal
|
||||
)
|
||||
feedback = "Selected \(node.name) for the active tunnel"
|
||||
} else {
|
||||
feedback = "Selected \(node.name)"
|
||||
}
|
||||
} catch {
|
||||
errorMessage = displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh(_ network: ProxySubscriptionNetwork) {
|
||||
operationID = "refresh-\(network.id)"
|
||||
feedback = nil
|
||||
errorMessage = nil
|
||||
Task { @MainActor in
|
||||
defer { operationID = nil }
|
||||
do {
|
||||
_ = try await networkViewModel.refreshProxySubscription(network: network)
|
||||
if isTunnelRunning {
|
||||
_ = try await networkViewModel.refreshActiveProxySubscription(network: network)
|
||||
feedback = "Refreshed \(network.name) for the active tunnel"
|
||||
} else {
|
||||
feedback = "Refreshed \(network.name)"
|
||||
}
|
||||
} catch {
|
||||
errorMessage = displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func delete(_ network: ProxySubscriptionNetwork) {
|
||||
operationID = "delete-\(network.id)"
|
||||
feedback = nil
|
||||
errorMessage = nil
|
||||
Task { @MainActor in
|
||||
defer { operationID = nil }
|
||||
do {
|
||||
try await networkViewModel.deleteNetwork(id: network.id)
|
||||
pendingDelete = nil
|
||||
selectedNetworkID = nil
|
||||
feedback = nil
|
||||
} catch {
|
||||
errorMessage = displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func operationIdentifier(networkID: Int32, ordinal: Int32) -> String {
|
||||
"select-\(networkID)-\(ordinal)"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var isTunnelRunning: Bool {
|
||||
switch tunnel.status {
|
||||
case .connected, .reasserting:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private func labeledValue(_ label: String, _ value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.caption.monospaced())
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
@ViewBuilder
|
||||
func burrowGlassControl() -> some View {
|
||||
if #available(macOS 26.0, iOS 26.0, *) {
|
||||
self.buttonStyle(.glass)
|
||||
} else {
|
||||
self.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func burrowLoginField() -> some View {
|
||||
#if os(iOS)
|
||||
|
|
@ -1674,6 +2355,13 @@ private final class TailnetBrowserAuthenticator {
|
|||
}
|
||||
#endif
|
||||
|
||||
private func displayError(_ error: Error) -> String {
|
||||
if let status = error as? GRPCStatus {
|
||||
return status.message ?? status.description
|
||||
}
|
||||
return error.localizedDescription
|
||||
}
|
||||
|
||||
private struct BurrowAutomationConfig {
|
||||
enum Action: String {
|
||||
case tailnetLogin = "tailnet-login"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@ import SwiftUI
|
|||
|
||||
struct NetworkCarouselView: View {
|
||||
var networks: [NetworkCardModel]
|
||||
var selectedNetworkID: Int32?
|
||||
var onSelect: ((NetworkCardModel) -> Void)?
|
||||
|
||||
init(
|
||||
networks: [NetworkCardModel],
|
||||
selectedNetworkID: Int32? = nil,
|
||||
onSelect: ((NetworkCardModel) -> Void)? = nil
|
||||
) {
|
||||
self.networks = networks
|
||||
self.selectedNetworkID = selectedNetworkID
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
|
|
@ -29,10 +41,19 @@ struct NetworkCarouselView: View {
|
|||
.frame(maxWidth: .infinity, minHeight: 175)
|
||||
#endif
|
||||
} else {
|
||||
#if os(macOS)
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
ForEach(networks) { network in
|
||||
networkCard(network)
|
||||
.frame(maxWidth: 560)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#else
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack {
|
||||
ForEach(networks) { network in
|
||||
NetworkView(network: network)
|
||||
networkCard(network)
|
||||
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
|
||||
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
|
||||
content
|
||||
|
|
@ -47,9 +68,33 @@ struct NetworkCarouselView: View {
|
|||
.defaultScrollAnchor(.center)
|
||||
.scrollTargetBehavior(.viewAligned)
|
||||
.containerRelativeFrame(.horizontal)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func networkCard(_ network: NetworkCardModel) -> some View {
|
||||
if let onSelect {
|
||||
Button {
|
||||
onSelect(network)
|
||||
} label: {
|
||||
NetworkView(network: network)
|
||||
.overlay(selectionOverlay(for: network))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
NetworkView(network: network)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectionOverlay(for network: NetworkCardModel) -> some View {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(
|
||||
selectedNetworkID == network.id ? Color.accentColor : Color.clear,
|
||||
lineWidth: 3
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -98,23 +98,33 @@ public final class NetworkExtensionTunnel: Tunnel {
|
|||
}
|
||||
}
|
||||
|
||||
guard managers.isEmpty else { return }
|
||||
|
||||
let manager = NETunnelProviderManager()
|
||||
manager.localizedDescription = "Burrow"
|
||||
let manager = managers.first ?? NETunnelProviderManager()
|
||||
var needsSave = managers.isEmpty
|
||||
if manager.localizedDescription != "Burrow" {
|
||||
manager.localizedDescription = "Burrow"
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
let proto = NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = bundleIdentifier
|
||||
proto.serverAddress = "burrow.rs"
|
||||
proto.proxySettings = nil
|
||||
|
||||
manager.protocolConfiguration = proto
|
||||
try await manager.save()
|
||||
if !manager.protocolConfiguration.isBurrowEquivalent(to: proto) {
|
||||
manager.protocolConfiguration = proto
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
if needsSave {
|
||||
try await manager.save()
|
||||
}
|
||||
}
|
||||
|
||||
public func start() {
|
||||
Task {
|
||||
guard let manager = try await NETunnelProviderManager.managers.first else { return }
|
||||
do {
|
||||
try await configure()
|
||||
guard let manager = try await NETunnelProviderManager.managers.first else { return }
|
||||
if !manager.isEnabled {
|
||||
manager.isEnabled = true
|
||||
try await manager.save()
|
||||
|
|
@ -149,6 +159,17 @@ public final class NetworkExtensionTunnel: Tunnel {
|
|||
}
|
||||
}
|
||||
|
||||
private extension Optional where Wrapped == NEVPNProtocol {
|
||||
func isBurrowEquivalent(to expected: NETunnelProviderProtocol) -> Bool {
|
||||
guard let current = self as? NETunnelProviderProtocol else {
|
||||
return false
|
||||
}
|
||||
return current.providerBundleIdentifier == expected.providerBundleIdentifier
|
||||
&& current.serverAddress == expected.serverAddress
|
||||
&& current.proxySettings == nil
|
||||
}
|
||||
}
|
||||
|
||||
extension NEVPNConnection {
|
||||
fileprivate var tunnelStatus: TunnelStatus {
|
||||
switch status {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,218 @@ struct TailnetLoginStatus: Sendable {
|
|||
var health: [String]
|
||||
}
|
||||
|
||||
struct ProxySubscriptionNodePreview: Sendable, Identifiable {
|
||||
var id: Int32 { ordinal }
|
||||
var ordinal: Int32
|
||||
var name: String
|
||||
var proxyProtocol: String
|
||||
var server: String
|
||||
var port: Int32
|
||||
var warnings: [String]
|
||||
var runtimeSupported: Bool
|
||||
}
|
||||
|
||||
struct ProxySubscriptionPreview: Sendable {
|
||||
var suggestedName: String
|
||||
var detectedFormat: String
|
||||
var compatibleCount: Int32
|
||||
var rejectedCount: Int32
|
||||
var nodes: [ProxySubscriptionNodePreview]
|
||||
var warnings: [String]
|
||||
}
|
||||
|
||||
struct ProxySubscriptionNetwork: Sendable, Identifiable, Hashable {
|
||||
var id: Int32
|
||||
var name: String
|
||||
var sourceURL: String
|
||||
var detectedFormat: String
|
||||
var selectedOrdinal: Int32?
|
||||
var selectedName: String?
|
||||
var nodes: [ProxySubscriptionNetworkNode]
|
||||
var warnings: [String]
|
||||
|
||||
var selectedNode: ProxySubscriptionNetworkNode? {
|
||||
selectedName
|
||||
.flatMap { name in nodes.first { $0.name == name && $0.runtimeSupported } }
|
||||
?? selectedOrdinal
|
||||
.flatMap { ordinal in nodes.first { $0.ordinal == ordinal && $0.runtimeSupported } }
|
||||
?? nodes.first { $0.runtimeSupported }
|
||||
?? nodes.first
|
||||
}
|
||||
|
||||
var runtimeSupportedNodes: [ProxySubscriptionNetworkNode] {
|
||||
nodes.filter(\.runtimeSupported)
|
||||
}
|
||||
|
||||
init?(network: Burrow_Network) {
|
||||
guard network.type == .proxySubscription,
|
||||
let payload = try? JSONDecoder().decode(StoredProxySubscriptionPayload.self, from: network.payload)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
id = network.id
|
||||
name = payload.name
|
||||
sourceURL = payload.source.url
|
||||
detectedFormat = payload.detectedFormat
|
||||
selectedOrdinal = payload.selectedOrdinal.map(Int32.init)
|
||||
selectedName = payload.selectedName
|
||||
nodes = payload.nodes.map(ProxySubscriptionNetworkNode.init)
|
||||
warnings = payload.warnings
|
||||
}
|
||||
}
|
||||
|
||||
struct ProxySubscriptionNetworkNode: Sendable, Identifiable, Hashable {
|
||||
var id: Int32 { ordinal }
|
||||
var ordinal: Int32
|
||||
var name: String
|
||||
var proxyProtocol: String
|
||||
var server: String
|
||||
var port: Int32
|
||||
var warnings: [String]
|
||||
var runtimeSupported: Bool
|
||||
|
||||
fileprivate init(stored: StoredProxySubscriptionNode) {
|
||||
ordinal = Int32(stored.ordinal)
|
||||
name = stored.name
|
||||
proxyProtocol = stored.proxyProtocol
|
||||
server = stored.server
|
||||
port = Int32(stored.port)
|
||||
warnings = stored.warnings
|
||||
runtimeSupported = stored.config.runtimeSupported
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredProxySubscriptionPayload: Decodable {
|
||||
var name: String
|
||||
var source: StoredProxySubscriptionSource
|
||||
var detectedFormat: String
|
||||
var selectedOrdinal: Int?
|
||||
var selectedName: String?
|
||||
var nodes: [StoredProxySubscriptionNode]
|
||||
var warnings: [String]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case source
|
||||
case detectedFormat = "detected_format"
|
||||
case selectedOrdinal = "selected_ordinal"
|
||||
case selectedName = "selected_name"
|
||||
case nodes
|
||||
case warnings
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredProxySubscriptionSource: Decodable {
|
||||
var url: String
|
||||
}
|
||||
|
||||
private struct StoredProxySubscriptionNode: Decodable {
|
||||
var ordinal: Int
|
||||
var name: String
|
||||
var proxyProtocol: String
|
||||
var server: String
|
||||
var port: Int
|
||||
var warnings: [String]
|
||||
var config: StoredProxySubscriptionNodeConfig
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case ordinal
|
||||
case name
|
||||
case proxyProtocol = "protocol"
|
||||
case server
|
||||
case port
|
||||
case warnings
|
||||
case config
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredProxySubscriptionNodeConfig: Decodable {
|
||||
var type: String
|
||||
var network: StoredProxySubscriptionNetworkTransport?
|
||||
var cipher: String?
|
||||
var plugin: StoredJSONValue?
|
||||
var udpOverTCP: Bool?
|
||||
|
||||
var runtimeSupported: Bool {
|
||||
switch type {
|
||||
case "trojan":
|
||||
return network?.isTrojanTCP ?? true
|
||||
case "shadowsocks":
|
||||
return plugin == nil
|
||||
&& udpOverTCP != true
|
||||
&& Self.supportedShadowsocksCiphers.contains(cipher?.lowercased() ?? "")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case network
|
||||
case cipher
|
||||
case plugin
|
||||
case udpOverTCP = "udp_over_tcp"
|
||||
}
|
||||
|
||||
private static let supportedShadowsocksCiphers: Set<String> = [
|
||||
"aes-128-gcm",
|
||||
"aead_aes_128_gcm",
|
||||
"aes-256-gcm",
|
||||
"aead_aes_256_gcm",
|
||||
"chacha20-ietf-poly1305",
|
||||
"chacha20-poly1305",
|
||||
"aead_chacha20_poly1305",
|
||||
]
|
||||
}
|
||||
|
||||
private enum StoredProxySubscriptionNetworkTransport: Decodable, Hashable {
|
||||
case named(String)
|
||||
case keyed(String)
|
||||
|
||||
var isTrojanTCP: Bool {
|
||||
switch self {
|
||||
case .named(let value), .keyed(let value):
|
||||
return value == "tcp"
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Swift.Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let value = try? container.decode(String.self) {
|
||||
self = .named(value)
|
||||
return
|
||||
}
|
||||
let object = try container.decode([String: StoredJSONValue].self)
|
||||
self = .keyed(object.keys.first ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private enum StoredJSONValue: Decodable, Hashable {
|
||||
case string(String)
|
||||
case number(Double)
|
||||
case bool(Bool)
|
||||
case object([String: StoredJSONValue])
|
||||
case array([StoredJSONValue])
|
||||
case null
|
||||
|
||||
init(from decoder: Swift.Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if container.decodeNil() {
|
||||
self = .null
|
||||
} else if let value = try? container.decode(Bool.self) {
|
||||
self = .bool(value)
|
||||
} else if let value = try? container.decode(Double.self) {
|
||||
self = .number(value)
|
||||
} else if let value = try? container.decode(String.self) {
|
||||
self = .string(value)
|
||||
} else if let value = try? container.decode([StoredJSONValue].self) {
|
||||
self = .array(value)
|
||||
} else {
|
||||
self = .object(try container.decode([String: StoredJSONValue].self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TailnetDiscoveryClient {
|
||||
static func discover(email: String, socketURL: URL) async throws -> TailnetDiscoveryResponse {
|
||||
var request = Burrow_TailnetDiscoverRequest()
|
||||
|
|
@ -139,6 +351,76 @@ enum TailnetLoginClient {
|
|||
}
|
||||
}
|
||||
|
||||
enum ProxySubscriptionImportClient {
|
||||
static func preview(url: String, socketURL: URL) async throws -> ProxySubscriptionPreview {
|
||||
var request = Burrow_ProxySubscriptionImportRequest()
|
||||
request.url = url
|
||||
|
||||
let response = try await ProxySubscriptionClient.unix(socketURL: socketURL)
|
||||
.previewImport(request)
|
||||
return ProxySubscriptionPreview(
|
||||
suggestedName: response.suggestedName,
|
||||
detectedFormat: response.detectedFormat,
|
||||
compatibleCount: response.compatibleCount,
|
||||
rejectedCount: response.rejectedCount,
|
||||
nodes: response.node.map { node in
|
||||
ProxySubscriptionNodePreview(
|
||||
ordinal: node.ordinal,
|
||||
name: node.name,
|
||||
proxyProtocol: node.`protocol`,
|
||||
server: node.server,
|
||||
port: node.port,
|
||||
warnings: node.warnings,
|
||||
runtimeSupported: node.runtimeSupported
|
||||
)
|
||||
},
|
||||
warnings: response.warnings
|
||||
)
|
||||
}
|
||||
|
||||
static func apply(
|
||||
id: Int32,
|
||||
url: String,
|
||||
name: String,
|
||||
selectedOrdinal: Int32?,
|
||||
socketURL: URL
|
||||
) async throws -> Int32 {
|
||||
var request = Burrow_ProxySubscriptionApplyRequest()
|
||||
request.id = id
|
||||
request.url = url
|
||||
request.name = name
|
||||
request.selectedOrdinal = selectedOrdinal ?? -1
|
||||
let response = try await ProxySubscriptionClient.unix(socketURL: socketURL)
|
||||
.applyImport(request)
|
||||
return response.id
|
||||
}
|
||||
|
||||
static func selectNode(
|
||||
id: Int32,
|
||||
selectedOrdinal: Int32,
|
||||
socketURL: URL
|
||||
) async throws -> Int32 {
|
||||
var request = Burrow_ProxySubscriptionSelectRequest()
|
||||
request.id = id
|
||||
request.selectedOrdinal = selectedOrdinal
|
||||
let response = try await ProxySubscriptionClient.unix(socketURL: socketURL)
|
||||
.selectNode(request)
|
||||
return response.selectedOrdinal
|
||||
}
|
||||
}
|
||||
|
||||
private struct DaemonUnavailableError: LocalizedError {
|
||||
var socketPath: String
|
||||
|
||||
var errorDescription: String? {
|
||||
"Burrow daemon is not reachable yet. Enable the tunnel or start the daemon, then try again."
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
"The daemon socket does not exist at \(socketPath)."
|
||||
}
|
||||
}
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class NetworkViewModel: Sendable {
|
||||
|
|
@ -161,6 +443,16 @@ final class NetworkViewModel: Sendable {
|
|||
networks.map(Self.makeCard(for:))
|
||||
}
|
||||
|
||||
var nonProxyCards: [NetworkCardModel] {
|
||||
networks
|
||||
.filter { $0.type != .proxySubscription }
|
||||
.map(Self.makeCard(for:))
|
||||
}
|
||||
|
||||
var proxySubscriptions: [ProxySubscriptionNetwork] {
|
||||
networks.compactMap(ProxySubscriptionNetwork.init)
|
||||
}
|
||||
|
||||
var nextNetworkID: Int32 {
|
||||
(networks.map(\.id).max() ?? 0) + 1
|
||||
}
|
||||
|
|
@ -173,13 +465,83 @@ final class NetworkViewModel: Sendable {
|
|||
try await addNetwork(type: .tailnet, payload: payload.encoded())
|
||||
}
|
||||
|
||||
func previewProxySubscription(url: String) async throws -> ProxySubscriptionPreview {
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await ProxySubscriptionImportClient.preview(url: url, socketURL: socketURL)
|
||||
}
|
||||
|
||||
func importProxySubscription(url: String, name: String, selectedOrdinal: Int32?) async throws -> Int32 {
|
||||
let socketURL = try daemonSocketURL()
|
||||
let networkID = nextNetworkID
|
||||
return try await ProxySubscriptionImportClient.apply(
|
||||
id: networkID,
|
||||
url: url,
|
||||
name: name,
|
||||
selectedOrdinal: selectedOrdinal,
|
||||
socketURL: socketURL
|
||||
)
|
||||
}
|
||||
|
||||
func updateProxySubscriptionSelection(
|
||||
network: ProxySubscriptionNetwork,
|
||||
selectedOrdinal: Int32
|
||||
) async throws -> Int32 {
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await ProxySubscriptionImportClient.selectNode(
|
||||
id: network.id,
|
||||
selectedOrdinal: selectedOrdinal,
|
||||
socketURL: socketURL
|
||||
)
|
||||
}
|
||||
|
||||
func updateActiveProxySubscriptionSelection(
|
||||
network: ProxySubscriptionNetwork,
|
||||
selectedOrdinal: Int32
|
||||
) async throws -> Int32 {
|
||||
let socketURL = try Constants.packetTunnelSocketURL
|
||||
return try await ProxySubscriptionImportClient.selectNode(
|
||||
id: network.id,
|
||||
selectedOrdinal: selectedOrdinal,
|
||||
socketURL: socketURL
|
||||
)
|
||||
}
|
||||
|
||||
func refreshProxySubscription(network: ProxySubscriptionNetwork) async throws -> Int32 {
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await ProxySubscriptionImportClient.apply(
|
||||
id: network.id,
|
||||
url: network.sourceURL,
|
||||
name: network.name,
|
||||
selectedOrdinal: network.selectedNode?.ordinal,
|
||||
socketURL: socketURL
|
||||
)
|
||||
}
|
||||
|
||||
func refreshActiveProxySubscription(network: ProxySubscriptionNetwork) async throws -> Int32 {
|
||||
let socketURL = try Constants.packetTunnelSocketURL
|
||||
return try await ProxySubscriptionImportClient.apply(
|
||||
id: network.id,
|
||||
url: network.sourceURL,
|
||||
name: network.name,
|
||||
selectedOrdinal: network.selectedNode?.ordinal,
|
||||
socketURL: socketURL
|
||||
)
|
||||
}
|
||||
|
||||
func deleteNetwork(id: Int32) async throws {
|
||||
let socketURL = try daemonSocketURL()
|
||||
var request = Burrow_NetworkDeleteRequest()
|
||||
request.id = id
|
||||
_ = try await NetworksClient.unix(socketURL: socketURL).networkDelete(request)
|
||||
}
|
||||
|
||||
func discoverTailnet(email: String) async throws -> TailnetDiscoveryResponse {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await TailnetDiscoveryClient.discover(email: email, socketURL: socketURL)
|
||||
}
|
||||
|
||||
func probeTailnetAuthority(_ authority: String) async throws -> TailnetAuthorityProbeStatus {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await TailnetAuthorityProbeClient.probe(authority: authority, socketURL: socketURL)
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +551,7 @@ final class NetworkViewModel: Sendable {
|
|||
hostname: String?,
|
||||
authority: String
|
||||
) async throws -> TailnetLoginStatus {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await TailnetLoginClient.start(
|
||||
accountName: accountName,
|
||||
identityName: identityName,
|
||||
|
|
@ -200,17 +562,17 @@ final class NetworkViewModel: Sendable {
|
|||
}
|
||||
|
||||
func tailnetLoginStatus(sessionID: String) async throws -> TailnetLoginStatus {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
return try await TailnetLoginClient.status(sessionID: sessionID, socketURL: socketURL)
|
||||
}
|
||||
|
||||
func cancelTailnetLogin(sessionID: String) async throws {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
try await TailnetLoginClient.cancel(sessionID: sessionID, socketURL: socketURL)
|
||||
}
|
||||
|
||||
private func addNetwork(type: Burrow_NetworkType, payload: Data) async throws -> Int32 {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try daemonSocketURL()
|
||||
let networkID = nextNetworkID
|
||||
let request = Burrow_Network.with {
|
||||
$0.id = networkID
|
||||
|
|
@ -223,12 +585,25 @@ final class NetworkViewModel: Sendable {
|
|||
return networkID
|
||||
}
|
||||
|
||||
private func daemonSocketURL() throws -> URL {
|
||||
try Self.daemonSocketURL(from: socketURLResult)
|
||||
}
|
||||
|
||||
private static func daemonSocketURL(from result: Result<URL, Error>) throws -> URL {
|
||||
let socketURL = try result.get()
|
||||
let socketPath = socketURL.path(percentEncoded: false)
|
||||
guard FileManager.default.fileExists(atPath: socketPath) else {
|
||||
throw DaemonUnavailableError(socketPath: socketPath)
|
||||
}
|
||||
return socketURL
|
||||
}
|
||||
|
||||
private func startStreaming() {
|
||||
task?.cancel()
|
||||
let socketURLResult = self.socketURLResult
|
||||
task = Task { [weak self] in
|
||||
do {
|
||||
let socketURL = try socketURLResult.get()
|
||||
let socketURL = try Self.daemonSocketURL(from: socketURLResult)
|
||||
let client = NetworksClient.unix(socketURL: socketURL)
|
||||
for try await response in client.networkList(.init()) {
|
||||
guard !Task.isCancelled else { return }
|
||||
|
|
@ -254,6 +629,14 @@ final class NetworkViewModel: Sendable {
|
|||
WireGuardCard(network: network).card
|
||||
case .tailnet:
|
||||
TailnetCard(network: network).card
|
||||
case .trojan:
|
||||
unsupportedCard(
|
||||
id: network.id,
|
||||
title: "Legacy Trojan Proxy",
|
||||
detail: "Unsupported SOCKS-era profile. Import a proxy subscription instead."
|
||||
)
|
||||
case .proxySubscription:
|
||||
proxySubscriptionCard(network: network)
|
||||
case .UNRECOGNIZED(let rawValue):
|
||||
unsupportedCard(
|
||||
id: network.id,
|
||||
|
|
@ -269,6 +652,76 @@ final class NetworkViewModel: Sendable {
|
|||
}
|
||||
}
|
||||
|
||||
private static func proxySubscriptionCard(network: Burrow_Network) -> NetworkCardModel {
|
||||
guard let subscription = ProxySubscriptionNetwork(network: network) else {
|
||||
return unsupportedCard(
|
||||
id: network.id,
|
||||
title: "Proxy Subscription",
|
||||
detail: "Imported proxy subscription."
|
||||
)
|
||||
}
|
||||
return NetworkCardModel(
|
||||
id: subscription.id,
|
||||
backgroundColor: .teal,
|
||||
label: AnyView(
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Proxy Subscription")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
Text(subscription.name)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
if let selectedNode = subscription.selectedNode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Selected node")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.white.opacity(0.72))
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text(selectedNode.name)
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text(selectedNode.proxyProtocol.uppercased())
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundStyle(.white.opacity(0.78))
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(.white.opacity(0.18))
|
||||
)
|
||||
}
|
||||
|
||||
Text("\(selectedNode.server):\(selectedNode.port)")
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.white.opacity(0.78))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
Text("\(subscription.runtimeSupportedNodes.count) usable of \(subscription.nodes.count) nodes · \(subscription.detectedFormat)")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.white.opacity(0.78))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static func unsupportedCard(id: Int32, title: String, detail: String) -> NetworkCardModel {
|
||||
NetworkCardModel(
|
||||
id: id,
|
||||
|
|
@ -278,9 +731,13 @@ final class NetworkViewModel: Sendable {
|
|||
Text(title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
Text(detail)
|
||||
.font(.body)
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
.lineLimit(4)
|
||||
.truncationMode(.tail)
|
||||
Spacer()
|
||||
Text("Network #\(id)")
|
||||
.font(.footnote.monospaced())
|
||||
|
|
@ -358,6 +815,7 @@ enum TailnetProvider: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
|
||||
enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
||||
case wireGuard
|
||||
case proxySubscription
|
||||
case tor
|
||||
case tailnet
|
||||
|
||||
|
|
@ -366,6 +824,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
var title: String {
|
||||
switch self {
|
||||
case .wireGuard: "WireGuard"
|
||||
case .proxySubscription: "Proxy Subscription"
|
||||
case .tor: "Tor"
|
||||
case .tailnet: "Tailnet"
|
||||
}
|
||||
|
|
@ -374,6 +833,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
var subtitle: String {
|
||||
switch self {
|
||||
case .wireGuard: "Import a tunnel and optional account metadata."
|
||||
case .proxySubscription: "Import Trojan and Shadowsocks subscription nodes."
|
||||
case .tor: "Store Arti account and identity preferences."
|
||||
case .tailnet: "Save Tailnet authority, identity defaults, and login material."
|
||||
}
|
||||
|
|
@ -382,6 +842,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
var accentColor: Color {
|
||||
switch self {
|
||||
case .wireGuard: .init("WireGuard")
|
||||
case .proxySubscription: .teal
|
||||
case .tor: .orange
|
||||
case .tailnet: .mint
|
||||
}
|
||||
|
|
@ -390,6 +851,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
var actionTitle: String {
|
||||
switch self {
|
||||
case .wireGuard: "Add Network"
|
||||
case .proxySubscription: "Import"
|
||||
case .tor: "Save Account"
|
||||
case .tailnet: "Save Account"
|
||||
}
|
||||
|
|
@ -397,7 +859,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable {
|
|||
|
||||
var availabilityNote: String? {
|
||||
switch self {
|
||||
case .wireGuard:
|
||||
case .wireGuard, .proxySubscription:
|
||||
nil
|
||||
case .tor:
|
||||
"Tor account preferences are stored on Apple now. The managed Tor runtime is not wired on Apple in this branch yet."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue