diff --git a/Apple/App/App-macOS.entitlements b/Apple/App/App-macOS.entitlements index 595fe08..53fcbb7 100644 --- a/Apple/App/App-macOS.entitlements +++ b/Apple/App/App-macOS.entitlements @@ -15,7 +15,5 @@ $(APP_GROUP_IDENTIFIER) - com.apple.security.network.client - diff --git a/Apple/App/App.xcconfig b/Apple/App/App.xcconfig index bfdd25d..4e42ddc 100644 --- a/Apple/App/App.xcconfig +++ b/Apple/App/App.xcconfig @@ -15,10 +15,9 @@ 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 diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index a322943..c3cb4cb 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -1,17 +1,23 @@ #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 = AppDelegate.makeQuitItem() + 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 lazy var openItem: NSMenuItem = { let item = NSMenuItem( @@ -55,85 +61,9 @@ 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 { @@ -144,13 +74,9 @@ 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() @@ -161,19 +87,4 @@ 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 diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib index 5c7bc1e..50ba431 100644 --- a/Apple/App/MainMenu.xib +++ b/Apple/App/MainMenu.xib @@ -1,8 +1,7 @@ - + - - + diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 6c64803..83d32e0 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* 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 */; }; @@ -18,7 +19,6 @@ 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,14 +43,20 @@ 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 */; @@ -100,13 +106,6 @@ remoteGlobalIDString = D0D4E5302C8D996F007F820A; remoteInfo = Core; }; - D11000022F70000100112233 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D05B9F7129E39EEC008CB1F9; - remoteInfo = App; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -139,6 +138,9 @@ /* Begin PBXFileReference section */ D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 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 = ""; }; + D11000052F70000100112233 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -186,14 +188,18 @@ D0D4E58E2C8D9D0A007F820A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; D0D4E58F2C8D9D0A007F820A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D0D4E5902C8D9D0A007F820A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - D0FA10032D10200100112233 /* Generated/burrow.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.pb.swift; sourceTree = ""; }; - D0FA10042D10200100112233 /* Generated/burrow.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.grpc.swift; sourceTree = ""; }; - 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 = ""; }; - D11000052F70000100112233 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = ""; }; + D0FA10032D10200100112233 /* burrow.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.pb.swift; sourceTree = ""; }; + D0FA10042D10200100112233 /* burrow.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.grpc.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D11000062F70000100112233 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D020F65029E4A697002790F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -212,7 +218,6 @@ D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */, D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */, D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */, - D0BCC60C2A09A0C200AD070D /* libburrow.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -237,13 +242,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D11000062F70000100112233 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -326,6 +324,14 @@ path = App; sourceTree = ""; }; + D11000072F70000100112233 /* AppUITests */ = { + isa = PBXGroup; + children = ( + D11000042F70000100112233 /* BurrowUITests.swift */, + ); + path = AppUITests; + sourceTree = ""; + }; D0B98FD729FDDB57004E7149 /* libburrow */ = { isa = PBXGroup; children = ( @@ -340,8 +346,8 @@ isa = PBXGroup; children = ( D0D4E4952C8D921A007F820A /* burrow.proto */, - D0FA10032D10200100112233 /* Generated/burrow.pb.swift */, - D0FA10042D10200100112233 /* Generated/burrow.grpc.swift */, + D0FA10032D10200100112233 /* burrow.pb.swift */, + D0FA10042D10200100112233 /* burrow.grpc.swift */, ); path = Client; sourceTree = ""; @@ -395,17 +401,27 @@ path = Constants; sourceTree = ""; }; - D11000072F70000100112233 /* AppUITests */ = { - isa = PBXGroup; - children = ( - D11000042F70000100112233 /* BurrowUITests.swift */, - ); - path = AppUITests; - sourceTree = ""; - }; /* 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" */; @@ -511,24 +527,6 @@ 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 */ @@ -539,54 +537,19 @@ LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1520; TargetAttributes = { - 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; }; + D020F65229E4A697002790F6 = { + CreatedOnToolsVersion = 14.3; + }; + D05B9F7129E39EEC008CB1F9 = { + CreatedOnToolsVersion = 14.3; + }; + D0D4E5302C8D996F007F820A = { + CreatedOnToolsVersion = 16.0; + }; }; }; buildConfigurationList = D05B9F6D29E39EEC008CB1F9 /* Build configuration list for PBXProject "Burrow" */; @@ -620,6 +583,13 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D11000092F70000100112233 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D05B9F7029E39EEC008CB1F9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -635,13 +605,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D11000092F70000100112233 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -690,6 +653,14 @@ /* 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; @@ -711,8 +682,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0FA10012D10200100112233 /* Generated/burrow.pb.swift in Sources */, - D0FA10022D10200100112233 /* Generated/burrow.grpc.swift in Sources */, + D0FA10012D10200100112233 /* burrow.pb.swift in Sources */, + D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */, D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */, D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */, ); @@ -745,17 +716,14 @@ ); 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 */; @@ -795,19 +763,27 @@ 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; }; @@ -815,7 +791,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */; buildSettings = { - DEVELOPMENT_TEAM = 2KPBQHRJ2D; }; name = Release; }; @@ -823,7 +798,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D020F64029E4A1FF002790F6 /* Compiler.xcconfig */; buildSettings = { - DEVELOPMENT_TEAM = 2KPBQHRJ2D; }; name = Debug; }; @@ -831,7 +805,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D020F64029E4A1FF002790F6 /* Compiler.xcconfig */; buildSettings = { - DEVELOPMENT_TEAM = 2KPBQHRJ2D; }; name = Release; }; @@ -839,10 +812,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */; buildSettings = { - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 2KPBQHRJ2D; - PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Debug; }; @@ -850,10 +819,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = D020F64929E4A34B002790F6 /* App.xcconfig */; buildSettings = { - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 2KPBQHRJ2D; - PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Release; }; @@ -899,23 +864,18 @@ }; 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 = ( @@ -970,15 +930,6 @@ 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 */ diff --git a/Apple/Configuration/Constants/Constants.swift b/Apple/Configuration/Constants/Constants.swift index 648ded2..95d8c78 100644 --- a/Apple/Configuration/Constants/Constants.swift +++ b/Apple/Configuration/Constants/Constants.swift @@ -16,13 +16,6 @@ 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) diff --git a/Apple/Configuration/Identity.xcconfig b/Apple/Configuration/Identity.xcconfig index 7318401..e9ca78d 100644 --- a/Apple/Configuration/Identity.xcconfig +++ b/Apple/Configuration/Identity.xcconfig @@ -1,2 +1,2 @@ -DEVELOPMENT_TEAM = 2KPBQHRJ2D -APP_BUNDLE_IDENTIFIER = com.tianruichen.burrow +DEVELOPMENT_TEAM = P6PV2R9443 +APP_BUNDLE_IDENTIFIER = com.hackclub.burrow diff --git a/Apple/Core/Client.swift b/Apple/Core/Client.swift index 559238f..7d4cfc7 100644 --- a/Apple/Core/Client.swift +++ b/Apple/Core/Client.swift @@ -108,83 +108,6 @@ 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() @@ -494,239 +417,6 @@ 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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 @@ -797,52 +487,6 @@ 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 diff --git a/Apple/Core/Client/Generated/burrow.pb.swift b/Apple/Core/Client/Generated/burrow.pb.swift index d235078..fccd769 100644 --- a/Apple/Core/Client/Generated/burrow.pb.swift +++ b/Apple/Core/Client/Generated/burrow.pb.swift @@ -25,7 +25,6 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case wireGuard // = 0 case tailnet // = 1 - case proxySubscription // = 3 case UNRECOGNIZED(Int) public init() { @@ -36,7 +35,6 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable { switch rawValue { case 0: self = .wireGuard case 1: self = .tailnet - case 3: self = .proxySubscription default: self = .UNRECOGNIZED(rawValue) } } @@ -45,7 +43,6 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable { switch self { case .wireGuard: return 0 case .tailnet: return 1 - case .proxySubscription: return 3 case .UNRECOGNIZED(let i): return i } } @@ -54,7 +51,6 @@ public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable { public static let allCases: [Burrow_NetworkType] = [ .wireGuard, .tailnet, - .proxySubscription, ] } @@ -221,8 +217,6 @@ public struct Burrow_TunnelConfigurationResponse: Sendable { public var routes: [String] = [] - public var excludedRoutes: [String] = [] - public var dnsServers: [String] = [] public var searchDomains: [String] = [] @@ -242,7 +236,6 @@ extension Burrow_NetworkType: SwiftProtobuf._ProtoNameProviding { public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 0: .same(proto: "WireGuard"), 1: .same(proto: "Tailnet"), - 3: .same(proto: "ProxySubscription"), ] } @@ -551,7 +544,6 @@ 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(decoder: inout D) throws { @@ -566,7 +558,6 @@ 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 } } @@ -582,9 +573,6 @@ 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) } @@ -601,7 +589,6 @@ 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} diff --git a/Apple/NetworkExtension/NetworkExtension.xcconfig b/Apple/NetworkExtension/NetworkExtension.xcconfig index a6a2635..35c7198 100644 --- a/Apple/NetworkExtension/NetworkExtension.xcconfig +++ b/Apple/NetworkExtension/NetworkExtension.xcconfig @@ -9,5 +9,3 @@ 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 diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 12f0cd0..3f3d8b4 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -28,13 +28,13 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { get throws { try _client.get() } } private let _client: Result = Result { - try TunnelClient.unix(socketURL: Constants.packetTunnelSocketURL) + try TunnelClient.unix(socketURL: Constants.socketURL) } override init() { do { libburrow.spawnInProcess( - socketPath: try Constants.packetTunnelSocketURL.path(percentEncoded: false), + socketPath: try Constants.socketURL.path(percentEncoded: false), databasePath: try Constants.databaseURL.path(percentEncoded: false) ) } catch { @@ -54,11 +54,6 @@ 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)") @@ -93,22 +88,15 @@ extension PacketTunnelProvider { private func startPacketBridge() throws { stopPacketBridge() - let packetClient = TunnelPacketClient.unix(socketURL: try Constants.packetTunnelSocketURL) + let packetClient = TunnelPacketClient.unix(socketURL: try Constants.socketURL) 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)] @@ -123,17 +111,10 @@ 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) @@ -147,7 +128,6 @@ extension PacketTunnelProvider { } private func stopPacketBridge() { - logger.log("Stopping tunnel packet bridge") inboundPacketTask?.cancel() inboundPacketTask = nil outboundPacketTask?.cancel() @@ -185,9 +165,6 @@ 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()) @@ -203,9 +180,6 @@ extension Burrow_TunnelConfigurationResponse { if !ipv4Routes.isEmpty { ipv4Settings.includedRoutes = ipv4Routes } - if !excludedIPv4Routes.isEmpty { - ipv4Settings.excludedRoutes = excludedIPv4Routes - } settings.ipv4Settings = ipv4Settings } if !ipv6Addresses.isEmpty { @@ -216,9 +190,6 @@ extension Burrow_TunnelConfigurationResponse { if !ipv6Routes.isEmpty { ipv6Settings.includedRoutes = ipv6Routes } - if !excludedIPv6Routes.isEmpty { - ipv6Settings.excludedRoutes = excludedIPv6Routes - } settings.ipv6Settings = ipv6Settings } if !dnsServers.isEmpty { diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index 79a45d6..5db2a2b 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -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:${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" +export PATH="${PATH}:${HOME}/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/${USER}/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 "$(command -v cargo)")"):/usr/bin" + CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi -PROTOC="$(readlink -f "$(command -v protoc)")" -CARGO_PATH="$(dirname "$PROTOC"):$CARGO_PATH" +PROTOC=$(readlink -f $(which protoc)) +CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH" # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. diff --git a/Apple/UI/BurrowView.swift b/Apple/UI/BurrowView.swift index bc9e361..e15d3f7 100644 --- a/Apple/UI/BurrowView.swift +++ b/Apple/UI/BurrowView.swift @@ -1,6 +1,5 @@ import BurrowConfiguration import Foundation -import GRPC import SwiftUI #if canImport(AuthenticationServices) import AuthenticationServices @@ -16,7 +15,6 @@ 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 { @@ -39,9 +37,6 @@ public struct BurrowView: View { Button("Add WireGuard Network") { activeSheet = .wireGuard } - Button("Import Proxy Subscription") { - activeSheet = .proxySubscription - } Button("Save Tor Account") { activeSheet = .tor } @@ -72,22 +67,8 @@ 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 { @@ -138,15 +119,6 @@ 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() } @@ -185,29 +157,15 @@ public struct BurrowView: View { } } - private var proxySubscriptionSheetBinding: Binding { - 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) } } } @@ -232,14 +190,13 @@ public struct BurrowView: View { #if os(iOS) !accountStore.accounts.isEmpty #else - !accountStore.accounts.isEmpty || networkViewModel.networks.isEmpty + true #endif } } private enum ConfigurationSheet: String, CaseIterable, Identifiable { case wireGuard - case proxySubscription case tor case tailnet @@ -248,7 +205,6 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable { var kind: AccountNetworkKind { switch self { case .wireGuard: .wireGuard - case .proxySubscription: .proxySubscription case .tor: .tor case .tailnet: .tailnet } @@ -258,8 +214,6 @@ 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: @@ -271,8 +225,6 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable { switch self { case .wireGuard: "WireGuard" - case .proxySubscription: - "Proxy Subscription" case .tor: "Tor" case .tailnet: @@ -284,8 +236,6 @@ 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: @@ -297,8 +247,6 @@ private enum ConfigurationSheet: String, CaseIterable, Identifiable { switch self { case .wireGuard: .blue - case .proxySubscription: - .teal case .tor, .tailnet: kind.accentColor } @@ -338,8 +286,6 @@ private struct AccountDraft { var accountName = "" var identityName = "" var wireGuardConfig = "" - var proxySubscriptionURL = "" - var proxySubscriptionName = "" var discoveryEmail = "" var authority = "" @@ -358,8 +304,6 @@ private struct AccountDraft { switch sheet { case .wireGuard: break - case .proxySubscription: - title = "Proxy Subscription" case .tor: title = "Default Tor" accountName = "default" @@ -394,11 +338,6 @@ 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 @@ -435,11 +374,31 @@ private struct ConfigurationSheetView: View { } } - configurationSections + 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 + } if let errorMessage { Section { - feedbackText(errorMessage, color: .red) + Text(errorMessage) + .foregroundStyle(.red) } } } @@ -486,8 +445,7 @@ private struct ConfigurationSheetView: View { } } #if os(macOS) - .frame(width: 520) - .frame(minHeight: 620) + .frame(minWidth: 520, minHeight: 620) #endif .safeAreaInset(edge: .bottom) { if showsBottomActionButton { @@ -515,12 +473,6 @@ private struct ConfigurationSheetView: View { await cancelTailnetLoginIfNeeded() } } - .onChange(of: draft.proxySubscriptionURL) { _, _ in - proxySubscriptionPreview = nil - proxySubscriptionPreviewError = nil - selectedProxySubscriptionOrdinal = nil - proxySubscriptionNodeFilter = "" - } .onDisappear { tailnetLoginPollTask?.cancel() tailnetDiscoveryTask?.cancel() @@ -534,71 +486,6 @@ 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) @@ -905,143 +792,6 @@ 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) { @@ -1077,12 +827,6 @@ 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") { @@ -1139,8 +883,6 @@ private struct ConfigurationSheetView: View { switch sheet { case .wireGuard: .blue - case .proxySubscription: - .teal case .tor, .tailnet: sheet.kind.accentColor } @@ -1150,8 +892,6 @@ private struct ConfigurationSheetView: View { switch sheet { case .wireGuard: "Import WireGuard" - case .proxySubscription: - "Import Proxy Subscription" case .tor: "Configure Tor" case .tailnet: @@ -1171,8 +911,6 @@ private struct ConfigurationSheetView: View { switch sheet { case .wireGuard, .tor: return true - case .proxySubscription: - return false case .tailnet: return showsAdvancedTailnetSettings } @@ -1190,8 +928,6 @@ private struct ConfigurationSheetView: View { switch sheet { case .wireGuard: return "Add Network" - case .proxySubscription: - return "Import" case .tor: return "Save Account" case .tailnet: @@ -1206,7 +942,7 @@ private struct ConfigurationSheetView: View { return normalizedOptional(draft.authority) == nil } return false - case .wireGuard, .tor, .proxySubscription: + case .wireGuard, .tor: return true } } @@ -1215,9 +951,6 @@ 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: @@ -1292,9 +1025,6 @@ private struct ConfigurationSheetView: View { case .wireGuard: try await submitWireGuard() dismiss() - case .proxySubscription: - try await submitProxySubscription() - dismiss() case .tor: try submitTor() dismiss() @@ -1302,7 +1032,7 @@ private struct ConfigurationSheetView: View { try await submitTailnet() } } catch { - errorMessage = displayError(error) + errorMessage = error.localizedDescription } } } @@ -1332,44 +1062,6 @@ 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 = [ @@ -1837,8 +1529,6 @@ private struct ConfigurationSheetView: View { .foregroundStyle(.secondary) Text(value) .font(.body.monospaced()) - .lineLimit(3) - .truncationMode(.middle) } } } @@ -1853,11 +1543,9 @@ 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 { @@ -1890,8 +1578,6 @@ private struct AccountRowView: View { Text(note) .font(.footnote) .foregroundStyle(.secondary) - .lineLimit(3) - .truncationMode(.middle) } } .padding() @@ -1910,378 +1596,11 @@ 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 { - 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) @@ -2355,13 +1674,6 @@ 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" diff --git a/Apple/UI/NetworkCarouselView.swift b/Apple/UI/NetworkCarouselView.swift index d109672..e7368db 100644 --- a/Apple/UI/NetworkCarouselView.swift +++ b/Apple/UI/NetworkCarouselView.swift @@ -2,18 +2,6 @@ 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 { @@ -41,19 +29,10 @@ 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 - networkCard(network) + NetworkView(network: network) .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) .scrollTransition(.interactive, axis: .horizontal) { content, phase in content @@ -68,33 +47,9 @@ 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 diff --git a/Apple/UI/NetworkExtensionTunnel.swift b/Apple/UI/NetworkExtensionTunnel.swift index 151e683..23559f3 100644 --- a/Apple/UI/NetworkExtensionTunnel.swift +++ b/Apple/UI/NetworkExtensionTunnel.swift @@ -98,33 +98,23 @@ public final class NetworkExtensionTunnel: Tunnel { } } - let manager = managers.first ?? NETunnelProviderManager() - var needsSave = managers.isEmpty - if manager.localizedDescription != "Burrow" { - manager.localizedDescription = "Burrow" - needsSave = true - } + guard managers.isEmpty else { return } + + let manager = NETunnelProviderManager() + manager.localizedDescription = "Burrow" let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = bundleIdentifier proto.serverAddress = "burrow.rs" - proto.proxySettings = nil - if !manager.protocolConfiguration.isBurrowEquivalent(to: proto) { - manager.protocolConfiguration = proto - needsSave = true - } - - if needsSave { - try await manager.save() - } + manager.protocolConfiguration = proto + 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() @@ -159,17 +149,6 @@ 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 { diff --git a/Apple/UI/Networks/Network.swift b/Apple/UI/Networks/Network.swift index 4b68430..35bd0e1 100644 --- a/Apple/UI/Networks/Network.swift +++ b/Apple/UI/Networks/Network.swift @@ -53,456 +53,6 @@ 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 smux: StoredJSONValue? - var udp: Bool? - var udpOverTCP: Bool? - var clientFingerprint: String? - - var runtimeSupported: Bool { - switch type { - case "trojan": - return network?.isTrojanTCP ?? true - case "shadowsocks": - guard Self.supportedShadowsocksSmux(smux) else { - return false - } - return Self.supportedShadowsocksPlugin(plugin, clientFingerprint: clientFingerprint) - && Self.supportedShadowsocksCiphers.contains(cipher?.lowercased() ?? "") - default: - return false - } - } - - enum CodingKeys: String, CodingKey { - case type - case network - case cipher - case plugin - case smux - case udp - case udpOverTCP = "udp_over_tcp" - case clientFingerprint = "client_fingerprint" - } - - private static let supportedShadowsocksCiphers: Set = [ - "none", - "plain", - "table", - "rc4-md5", - "rc4", - "aes-128-ctr", - "aes-192-ctr", - "aes-256-ctr", - "aes-128-cfb", - "aes-128-cfb1", - "aes-128-cfb8", - "aes-128-cfb128", - "aes-192-cfb", - "aes-192-cfb1", - "aes-192-cfb8", - "aes-192-cfb128", - "aes-256-cfb", - "aes-256-cfb1", - "aes-256-cfb8", - "aes-256-cfb128", - "aes-128-ofb", - "aes-192-ofb", - "aes-256-ofb", - "camellia-128-ctr", - "camellia-192-ctr", - "camellia-256-ctr", - "camellia-128-cfb", - "camellia-128-cfb1", - "camellia-128-cfb8", - "camellia-128-cfb128", - "camellia-192-cfb", - "camellia-192-cfb1", - "camellia-192-cfb8", - "camellia-192-cfb128", - "camellia-256-cfb", - "camellia-256-cfb1", - "camellia-256-cfb8", - "camellia-256-cfb128", - "camellia-128-ofb", - "camellia-192-ofb", - "camellia-256-ofb", - "chacha20", - "chacha20-ietf", - "xchacha20", - "aes-128-gcm", - "aead_aes_128_gcm", - "aes-192-gcm", - "aes-256-gcm", - "aead_aes_256_gcm", - "aes-128-ccm", - "aes-192-ccm", - "aes-256-ccm", - "aes-128-gcm-siv", - "aes-256-gcm-siv", - "chacha8-ietf-poly1305", - "chacha20-ietf-poly1305", - "chacha20-poly1305", - "aead_chacha20_poly1305", - "rabbit128-poly1305", - "xchacha8-ietf-poly1305", - "xchacha20-ietf-poly1305", - "sm4-gcm", - "sm4-ccm", - "aegis-128l", - "aegis-256", - "aez-384", - "deoxys-ii-256-128", - "ascon128", - "ascon128a", - "lea-128-gcm", - "lea-192-gcm", - "lea-256-gcm", - "2022-blake3-aes-128-gcm", - "2022-blake3-aes-256-gcm", - "2022-blake3-aes-128-ccm", - "2022-blake3-aes-256-ccm", - "2022-blake3-chacha20-poly1305", - "2022-blake3-chacha8-poly1305", - ] - - private static func supportedShadowsocksPlugin( - _ plugin: StoredJSONValue?, - clientFingerprint: String? - ) -> Bool { - guard let plugin else { - return true - } - guard case .object(let object) = plugin, - case .string(let name)? = object["name"] - else { - return false - } - let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines) - let opts: [String: StoredJSONValue] - if case .object(let decodedOpts)? = object["opts"] { - opts = decodedOpts - } else { - opts = [:] - } - if normalizedName == "restls", - let clientFingerprint, - Self.shadowsocksClientFingerprintIsActive(clientFingerprint) - { - return false - } - if normalizedName == "v2ray-plugin" || normalizedName == "gost-plugin" { - guard let mode = opts["mode"]?.stringValue ?? opts["obfs"]?.stringValue else { - return false - } - guard mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "websocket" else { - return false - } - return true - } - if normalizedName == "shadow-tls" { - let version = opts["version"]?.stringValue ?? "2" - let normalizedVersion = version.trimmingCharacters(in: .whitespacesAndNewlines) - switch normalizedVersion { - case "1": - return true - case "2", "3": - if let clientFingerprint, - Self.shadowsocksClientFingerprintIsActive(clientFingerprint) - { - return false - } - return true - default: - return false - } - } - if normalizedName == "kcptun" { - guard let smuxVersion = opts["smuxver"]?.stringValue else { - return true - } - guard let version = Double(smuxVersion.trimmingCharacters(in: .whitespacesAndNewlines)) else { - return false - } - return version >= 0 - } - if normalizedName == "restls" { - let versionHint = opts["version-hint"]?.stringValue ?? "" - switch versionHint.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "tls12", "tls13": - return true - default: - return false - } - } - guard normalizedName == "obfs" else { - return true - } - guard let mode = opts["mode"]?.stringValue ?? opts["obfs"]?.stringValue else { - return false - } - switch mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "http", "tls": - return true - default: - return false - } - } - - private static func shadowsocksClientFingerprintIsActive(_ value: String) -> Bool { - let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - return [ - "chrome", - "firefox", - "safari", - "ios", - "android", - "edge", - "360", - "qq", - "random", - "chrome120", - "firefox120", - "safari16", - "chrome_psk", - "chrome_psk_shuffle", - "chrome_padding_psk_shuffle", - "chrome_pq", - "chrome_pq_psk", - "randomized", - ].contains(normalized) - } - - private static func supportedShadowsocksSmux(_ smux: StoredJSONValue?) -> Bool { - guard case .object(let object)? = smux else { - return true - } - guard object["enabled"]?.boolValue ?? false else { - return true - } - let protocolName = object["protocol"]?.stringValue? - .trimmingCharacters(in: .whitespacesAndNewlines) - .lowercased() ?? "h2mux" - guard protocolName == "h2mux" || protocolName == "smux" || protocolName == "yamux" else { - return false - } - return true - } -} - -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 - - var stringValue: String? { - switch self { - case .string(let value): - return value - case .number(let value): - if value.rounded(.towardZero) == value { - return String(Int(value)) - } - return String(value) - case .bool(let value): - return String(value) - default: - return nil - } - } - - var boolValue: Bool? { - switch self { - case .bool(let value): - return value - case .string(let value): - switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "1", "true", "yes", "y": - return true - case "0", "false", "no", "n": - return false - default: - return nil - } - case .number(let value): - return value != 0 - default: - return nil - } - } - - 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() @@ -589,76 +139,6 @@ 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 { @@ -681,16 +161,6 @@ 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 } @@ -703,83 +173,13 @@ 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 daemonSocketURL() + let socketURL = try socketURLResult.get() return try await TailnetDiscoveryClient.discover(email: email, socketURL: socketURL) } func probeTailnetAuthority(_ authority: String) async throws -> TailnetAuthorityProbeStatus { - let socketURL = try daemonSocketURL() + let socketURL = try socketURLResult.get() return try await TailnetAuthorityProbeClient.probe(authority: authority, socketURL: socketURL) } @@ -789,7 +189,7 @@ final class NetworkViewModel: Sendable { hostname: String?, authority: String ) async throws -> TailnetLoginStatus { - let socketURL = try daemonSocketURL() + let socketURL = try socketURLResult.get() return try await TailnetLoginClient.start( accountName: accountName, identityName: identityName, @@ -800,17 +200,17 @@ final class NetworkViewModel: Sendable { } func tailnetLoginStatus(sessionID: String) async throws -> TailnetLoginStatus { - let socketURL = try daemonSocketURL() + let socketURL = try socketURLResult.get() return try await TailnetLoginClient.status(sessionID: sessionID, socketURL: socketURL) } func cancelTailnetLogin(sessionID: String) async throws { - let socketURL = try daemonSocketURL() + let socketURL = try socketURLResult.get() try await TailnetLoginClient.cancel(sessionID: sessionID, socketURL: socketURL) } private func addNetwork(type: Burrow_NetworkType, payload: Data) async throws -> Int32 { - let socketURL = try daemonSocketURL() + let socketURL = try socketURLResult.get() let networkID = nextNetworkID let request = Burrow_Network.with { $0.id = networkID @@ -823,25 +223,12 @@ final class NetworkViewModel: Sendable { return networkID } - private func daemonSocketURL() throws -> URL { - try Self.daemonSocketURL(from: socketURLResult) - } - - private static func daemonSocketURL(from result: Result) 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 Self.daemonSocketURL(from: socketURLResult) + let socketURL = try socketURLResult.get() let client = NetworksClient.unix(socketURL: socketURL) for try await response in client.networkList(.init()) { guard !Task.isCancelled else { return } @@ -867,8 +254,6 @@ final class NetworkViewModel: Sendable { WireGuardCard(network: network).card case .tailnet: TailnetCard(network: network).card - case .proxySubscription: - proxySubscriptionCard(network: network) case .UNRECOGNIZED(let rawValue): unsupportedCard( id: network.id, @@ -884,76 +269,6 @@ 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, @@ -963,13 +278,9 @@ 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()) @@ -1047,7 +358,6 @@ enum TailnetProvider: String, CaseIterable, Codable, Identifiable, Sendable { enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable { case wireGuard - case proxySubscription case tor case tailnet @@ -1056,7 +366,6 @@ 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" } @@ -1065,7 +374,6 @@ 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." } @@ -1074,7 +382,6 @@ 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 } @@ -1083,7 +390,6 @@ 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" } @@ -1091,7 +397,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable { var availabilityNote: String? { switch self { - case .wireGuard, .proxySubscription: + case .wireGuard: nil case .tor: "Tor account preferences are stored on Apple now. The managed Tor runtime is not wired on Apple in this branch yet." diff --git a/Cargo.lock b/Cargo.lock index 7a24f95..2950701 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -14,30 +23,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.6", + "crypto-common", "generic-array", ] -[[package]] -name = "aead" -version = "0.6.0-rc.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" -dependencies = [ - "crypto-common 0.2.2", - "inout 0.2.2", -] - -[[package]] -name = "aegis" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85687c501b265f8538af0b3245221b56ff5c40acd341ba06f050466516fcf2cd" -dependencies = [ - "cc", - "softaes", -] - [[package]] name = "aes" version = "0.8.4" @@ -45,75 +34,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", + "cipher", + "cpufeatures", "zeroize", ] -[[package]] -name = "aes" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" -dependencies = [ - "cipher 0.5.2", - "cpubits", - "cpufeatures 0.3.0", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aes-gcm-siv" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" -dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr", - "polyval", - "subtle", - "zeroize", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -257,16 +182,10 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.17", + "cpufeatures", "password-hash 0.5.0", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" @@ -491,16 +410,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "asynk-strim" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "atomic" version = "0.5.3" @@ -528,28 +437,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.6.20" @@ -565,7 +452,7 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.32", "itoa", - "matchit 0.7.3", + "matchit", "memchr", "mime", "percent-encoding", @@ -594,7 +481,7 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "itoa", - "matchit 0.7.3", + "matchit", "memchr", "mime", "percent-encoding", @@ -650,6 +537,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -701,7 +603,7 @@ dependencies = [ "quote", "regex", "rustc-hash 1.1.0", - "shlex 1.3.0", + "shlex", "syn 1.0.109", "which", ] @@ -724,29 +626,11 @@ dependencies = [ "quote", "regex", "rustc-hash 1.1.0", - "shlex 1.3.0", + "shlex", "syn 2.0.106", "which", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex 1.3.0", - "syn 2.0.106", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -755,9 +639,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitvec" @@ -780,20 +664,6 @@ dependencies = [ "digest", ] -[[package]] -name = "blake3" -version = "1.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq 0.4.2", - "cpufeatures 0.3.0", -] - [[package]] name = "blanket" version = "0.3.0" @@ -814,35 +684,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "blowfish" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" -dependencies = [ - "byteorder", - "cipher 0.5.2", -] - -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "bytes", - "cfg_aliases", -] - [[package]] name = "bstr" version = "1.12.1" @@ -864,10 +705,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" name = "burrow" version = "0.1.0" dependencies = [ - "aead 0.5.2", - "aegis", - "aes 0.8.4", - "aes-gcm", + "aead", "anyhow", "argon2", "arti-client", @@ -876,70 +714,45 @@ dependencies = [ "axum 0.7.9", "base64 0.21.7", "blake2", - "blake3", "bytes", "caps", - "ccm", - "chacha20", "chacha20poly1305", "clap", "console", "console-subscriber", - "deoxys", "dotenv", "fehler", "futures", - "h2 0.4.12", "hickory-proto", "hmac", - "http 1.3.1", "hyper-util", "insta", "ip_network", "ip_network_table", "ipnetwork", - "lea", "libc", "libsystemd", "log", - "md-5", - "native-tls", "netstack-smoltcp", "nix 0.27.1", "once_cell", - "parking_lot 0.12.4", - "poly1305", + "parking_lot", "prost 0.13.5", "prost-types 0.13.5", - "rabbit", - "rama-net", - "rama-tls-boring", - "rama-ua", "rand 0.8.5", "rand_core 0.6.4", "reqwest 0.12.23", "ring", "rusqlite", "rust-ini", - "rust_tokio_kcp", - "rustls", - "rustls-pemfile 2.2.0", "schemars 0.8.22", "serde", "serde_json", - "serde_yaml", - "sha2", - "shadowsocks", - "smux_rust", "subtle", "tempfile", "tokio", - "tokio-native-tls", - "tokio-rustls", - "tokio-snappy", "tokio-stream", "tokio-util", - "tokio_smux", "toml 0.8.23", "tonic 0.12.3", "tonic-build", @@ -951,11 +764,7 @@ dependencies = [ "tracing-oslog", "tracing-subscriber", "tun", - "webpki-roots 0.22.6", - "webpki-roots 0.26.11", "x25519-dalek", - "yamux", - "zears", ] [[package]] @@ -964,12 +773,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" -[[package]] -name = "byte_string" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" - [[package]] name = "bytemuck" version = "1.25.0" @@ -1008,16 +811,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "camellia" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" -dependencies = [ - "byteorder", - "cipher 0.4.4", -] - [[package]] name = "caps" version = "0.5.5" @@ -1040,37 +833,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "cast5" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed80521f830bbce4d3a4857d6f2b91a8339bf0b65711fe189ab6f8d7a2f629e" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "cc" -version = "1.2.63" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", -] - -[[package]] -name = "ccm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" -dependencies = [ - "aead 0.5.2", - "cipher 0.4.4", - "ctr", - "subtle", + "shlex", ] [[package]] @@ -1101,8 +873,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", + "cipher", + "cpufeatures", ] [[package]] @@ -1111,9 +883,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead 0.5.2", + "aead", "chacha20", - "cipher 0.4.4", + "cipher", "poly1305", "zeroize", ] @@ -1157,37 +929,17 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", - "inout 0.1.4", + "crypto-common", + "inout", "zeroize", ] -[[package]] -name = "cipher" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" -dependencies = [ - "block-buffer 0.12.0", - "crypto-common 0.2.2", - "inout 0.2.2", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -1239,15 +991,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "coarsetime" version = "0.1.37" @@ -1369,39 +1112,12 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - [[package]] name = "convert_case" version = "0.7.1" @@ -1436,12 +1152,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1451,15 +1161,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1592,43 +1293,13 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -1638,7 +1309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1761,19 +1432,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.11", -] - [[package]] name = "data-encoding" version = "2.9.0" @@ -1821,17 +1479,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "deoxys" -version = "0.2.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cb5efc49a3af2efb079e9c9d1cfad0201ea65c2a6f1a4eb1ede501593d2ddb8" -dependencies = [ - "aead 0.6.0-rc.10", - "aes 0.9.1", - "subtle", -] - [[package]] name = "der" version = "0.7.10" @@ -1948,24 +1595,15 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "des" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", - "crypto-common 0.1.6", + "crypto-common", "subtle", ] @@ -2031,38 +1669,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dynosaur" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12303417f378f29ba12cb12fc78a9df0d8e16ccb1ad94abf04d48d96bdda532" -dependencies = [ - "dynosaur_derive", -] - -[[package]] -name = "dynosaur_derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0713d5c1d52e774c5cd7bb8b043d7c0fc4f921abfb678556140bfbe6ab2364" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -2155,12 +1767,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -2282,9 +1888,6 @@ name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -dependencies = [ - "getrandom 0.2.16", -] [[package]] name = "fehler" @@ -2348,9 +1951,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "fixedbitset" @@ -2374,18 +1977,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" -[[package]] -name = "flume" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" -dependencies = [ - "fastrand", - "futures-core", - "futures-sink", - "spin 0.9.8", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2410,28 +2001,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "foreign-types-shared", ] [[package]] @@ -2440,12 +2010,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2470,12 +2034,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fslock" version = "0.2.1" @@ -2581,21 +2139,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2660,14 +2203,10 @@ dependencies = [ ] [[package]] -name = "ghash" -version = "0.5.1" +name = "gimli" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -2755,9 +2294,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" @@ -2941,12 +2477,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" @@ -2975,15 +2505,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hybrid-array" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" -dependencies = [ - "typenum", -] - [[package]] name = "hyper" version = "0.14.32" @@ -3045,7 +2566,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots", ] [[package]] @@ -3282,7 +2803,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "inotify-sys", "libc", ] @@ -3305,15 +2826,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "hybrid-array", -] - [[package]] name = "insta" version = "1.43.2" @@ -3326,15 +2838,6 @@ dependencies = [ "similar", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "inventory" version = "0.3.24" @@ -3344,6 +2847,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + [[package]] name = "ip_network" version = "0.4.1" @@ -3452,41 +2966,15 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kcp" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e387a924f42063d380be14857714a2f0c177e44dbeab8895244e5370d8a8e68" -dependencies = [ - "bytes", - "log", - "thiserror 1.0.69", -] - [[package]] name = "keccak" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures", ] -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "kqueue" version = "1.1.1" @@ -3513,7 +3001,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] [[package]] @@ -3522,16 +3010,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lea" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f03a660d3662be37724af25ce5152a4df89c4ffc3649823e6d1b415c19608a" -dependencies = [ - "cfg-if", - "cipher 0.3.0", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -3596,7 +3074,7 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "libc", "plain", "redox_syscall 0.7.3", @@ -3665,43 +3143,12 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" -dependencies = [ - "hashbrown 0.12.3", -] - [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lru_time_cache" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" - [[package]] name = "managed" version = "0.8.0" @@ -3723,28 +3170,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "matchit" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - [[package]] name = "memchr" version = "2.7.5" @@ -3819,16 +3244,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3856,23 +3271,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot 0.12.4", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "multimap" version = "0.10.1" @@ -3906,21 +3304,12 @@ dependencies = [ "futures", "rand 0.8.5", "smoltcp", - "spin 0.9.8", + "spin", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" version = "0.26.4" @@ -3940,7 +3329,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "cfg-if", "libc", "memoffset 0.9.1", @@ -3952,19 +3341,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", "memoffset 0.9.1", ] -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - [[package]] name = "nom" version = "7.1.3" @@ -3996,7 +3379,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "inotify", "kqueue", "libc", @@ -4013,7 +3396,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", ] [[package]] @@ -4124,7 +3507,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", ] [[package]] @@ -4137,6 +3520,15 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -4180,9 +3572,9 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "once_cell", "openssl-macros", @@ -4306,17 +3698,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.4" @@ -4324,21 +3705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -4394,16 +3761,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "peeking_take_while" version = "0.1.2" @@ -4577,19 +3934,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -4609,7 +3954,7 @@ dependencies = [ "atomic 0.5.3", "crossbeam-queue", "futures", - "parking_lot 0.12.4", + "parking_lot", "pin-project", "static_assertions", "thiserror 1.0.69", @@ -4793,21 +4138,6 @@ dependencies = [ "prost 0.13.5", ] -[[package]] -name = "psl" -version = "2.1.212" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f6415ab1b08394487005d41ae7ee69e69903efa1ace538f3b274db605bf8ec" -dependencies = [ - "psl-types", -] - -[[package]] -name = "psl-types" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" - [[package]] name = "pwd-grp" version = "1.0.2" @@ -4896,280 +4226,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rabbit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7204c97584ac097d9749f4a52f2f96910d988f12303fe3e3b021628f88923f1" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "radium" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "rama-boring" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8c66935959385543d370f6f1485461d520028d4ac9ae852131ec1c9226869b" -dependencies = [ - "bitflags 2.11.1", - "foreign-types 0.5.0", - "libc", - "openssl-macros", - "rama-boring-sys", -] - -[[package]] -name = "rama-boring-sys" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50c9107d147c768182b5d6494670c0a54eadc99ee3eb24784a5b5504480eed8" -dependencies = [ - "bindgen 0.72.1", - "cmake", - "fs_extra", - "fslock", -] - -[[package]] -name = "rama-boring-tokio" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7bccdd6efce0302b0fdd0e8b8ff8fba6202841bdfeeac357be115b9f42dd6b" -dependencies = [ - "rama-boring", - "rama-boring-sys", - "tokio", -] - -[[package]] -name = "rama-core" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b93751ab27c9d151e84c1100057eab3f2a6a1378bc31b62abd416ecb1847658" -dependencies = [ - "ahash 0.8.12", - "asynk-strim", - "bytes", - "futures", - "parking_lot 0.12.4", - "pin-project-lite", - "rama-error", - "rama-macros", - "rama-utils", - "serde", - "serde_json", - "tokio", - "tokio-graceful", - "tokio-util", - "tracing", -] - -[[package]] -name = "rama-error" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c452aba1beb7e29b873ff32f304536164cffcc596e786921aea64e858ff8f40" - -[[package]] -name = "rama-http" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453d60af031e23af2d48995e41b17023f6150044738680508b63671f8d7417dd" -dependencies = [ - "ahash 0.8.12", - "base64 0.22.1", - "bitflags 2.11.1", - "chrono", - "const_format", - "csv", - "http 1.3.1", - "http-range-header", - "httpdate", - "iri-string", - "matchit 0.9.2", - "parking_lot 0.12.4", - "percent-encoding", - "pin-project-lite", - "radix_trie", - "rama-core", - "rama-error", - "rama-http-headers", - "rama-http-types", - "rama-net", - "rama-utils", - "rand 0.9.2", - "serde", - "serde_html_form", - "serde_json", - "tokio", - "uuid", -] - -[[package]] -name = "rama-http-headers" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d74fe0cd9bd4440827dc6dc0f504cf66065396532e798891dee2c1b740b2285" -dependencies = [ - "ahash 0.8.12", - "base64 0.22.1", - "chrono", - "const_format", - "httpdate", - "rama-core", - "rama-error", - "rama-http-types", - "rama-macros", - "rama-net", - "rama-utils", - "rand 0.9.2", - "serde", - "sha1", -] - -[[package]] -name = "rama-http-types" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dae655a72da5f2b97cfacb67960d8b28c5025e62707b4c8c5f0c5c9843a444" -dependencies = [ - "ahash 0.8.12", - "bytes", - "const_format", - "fnv", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "itoa", - "memchr", - "mime", - "mime_guess", - "nom 8.0.0", - "pin-project-lite", - "rama-core", - "rama-error", - "rama-macros", - "rama-utils", - "rand 0.9.2", - "serde", - "serde_json", - "sync_wrapper 1.0.2", - "tokio", -] - -[[package]] -name = "rama-macros" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea18a110bcf21e35c5f194168e6914ccea45ffdd0fea51bc4b169fbeafef6428" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "rama-net" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28ee9e1e5d39264414b71f5c33e7fbb66b382c3fac456fe0daad39cf5509933" -dependencies = [ - "ahash 0.8.12", - "const_format", - "flume", - "hex", - "ipnet", - "itertools 0.14.0", - "md5", - "nom 8.0.0", - "parking_lot 0.12.4", - "pin-project-lite", - "psl", - "radix_trie", - "rama-core", - "rama-http-types", - "rama-macros", - "rama-utils", - "serde", - "sha2", - "socket2 0.6.3", - "tokio", -] - -[[package]] -name = "rama-tls-boring" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def3d5d06d3ca3a2d2e4376cf93de0555cd9c7960f085bf77be9562f5c9ace8f" -dependencies = [ - "ahash 0.8.12", - "flume", - "itertools 0.14.0", - "moka", - "parking_lot 0.12.4", - "pin-project-lite", - "rama-boring", - "rama-boring-tokio", - "rama-core", - "rama-net", - "rama-utils", - "schannel", - "tokio", -] - -[[package]] -name = "rama-ua" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7abde8e7b428c80c5948885c1ee0492852a21d91844c8414a0de4a8a1d62262" -dependencies = [ - "ahash 0.8.12", - "itertools 0.14.0", - "rama-core", - "rama-http", - "rama-net", - "rama-utils", - "rand 0.9.2", - "serde", - "serde_html_form", - "serde_json", -] - -[[package]] -name = "rama-utils" -version = "0.3.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf28b18ba4a57f8334d7992d3f8020194ea359b246ae6f8f98b8df524c7a14ef" -dependencies = [ - "const_format", - "parking_lot 0.12.4", - "pin-project-lite", - "rama-macros", - "regex", - "serde", - "smallvec", - "smol_str", - "tokio", - "wildcard", -] - [[package]] name = "rand" version = "0.8.5" @@ -5269,22 +4331,13 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", ] [[package]] @@ -5293,7 +4346,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", ] [[package]] @@ -5307,19 +4360,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "reed-solomon-erasure" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" -dependencies = [ - "libm", - "lru", - "parking_lot 0.11.2", - "smallvec", - "spin 0.9.8", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -5342,9 +4382,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -5354,9 +4394,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -5393,7 +4433,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -5444,7 +4484,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots", ] [[package]] @@ -5480,19 +4520,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ring-compat" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" -dependencies = [ - "aead 0.5.2", - "ed25519", - "generic-array", - "pkcs8", - "ring", -] - [[package]] name = "rsa" version = "0.9.10" @@ -5530,7 +4557,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5551,35 +4578,10 @@ dependencies = [ ] [[package]] -name = "rust_tokio_kcp" -version = "0.2.5" +name = "rustc-demangle" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa76f74fbbd4a3c15e3d688e8125e64583c4cd73f75f96ab2ba5b683708b1e" -dependencies = [ - "aes 0.9.1", - "aes-gcm", - "blowfish", - "byte_string", - "byteorder", - "bytes", - "cast5", - "cipher 0.5.2", - "crc32fast", - "des", - "futures-util", - "hybrid-array", - "kcp", - "log", - "pbkdf2 0.12.2", - "rand 0.8.5", - "reed-solomon-erasure", - "salsa20", - "sha1", - "sm4 0.6.0", - "spin 0.9.8", - "tokio", - "twofish", -] +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -5617,7 +4619,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5630,7 +4632,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.11.0", @@ -5643,8 +4645,6 @@ version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ - "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -5662,15 +4662,6 @@ dependencies = [ "base64 0.21.7", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -5687,7 +4678,6 @@ version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5718,16 +4708,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "salsa20" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" -dependencies = [ - "cfg-if", - "cipher 0.5.2", -] - [[package]] name = "same-file" version = "1.0.6" @@ -5809,29 +4789,12 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sealed" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "sec1" version = "0.7.3" @@ -5852,7 +4815,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -5875,16 +4838,6 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[package]] -name = "sendfd" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b183bfd5b1bc64ab0c1ef3ee06b008a9ef1b68a7d3a99ba566fbfe7a7c6d745b" -dependencies = [ - "libc", - "tokio", -] - [[package]] name = "serde" version = "1.0.228" @@ -5936,19 +4889,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "serde_html_form" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94" -dependencies = [ - "form_urlencoded", - "indexmap 2.11.4", - "itoa", - "ryu", - "serde_core", -] - [[package]] name = "serde_ignored" version = "0.1.14" @@ -6044,19 +4984,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.11.4", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha-1" version = "0.10.1" @@ -6064,7 +4991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -6075,7 +5002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -6086,7 +5013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -6100,70 +5027,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "shadowsocks" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "482831bf9d55acf3c98e211b6c852c3dfdf1d1b0d23fdf1d887c5a4b2acad4e4" -dependencies = [ - "aes 0.8.4", - "base64 0.22.1", - "blake3", - "byte_string", - "bytes", - "cfg-if", - "dynosaur", - "futures", - "libc", - "log", - "lru_time_cache", - "percent-encoding", - "pin-project", - "rand 0.9.2", - "sealed", - "sendfd", - "serde", - "serde_json", - "serde_urlencoded", - "shadowsocks-crypto", - "socket2 0.6.3", - "spin 0.10.0", - "thiserror 2.0.16", - "tokio", - "tokio-tfo", - "trait-variant", - "url", - "windows-sys 0.61.0", -] - -[[package]] -name = "shadowsocks-crypto" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d038a3d17586f1c1ab3c1c3b9e4d5ef8fba98fb3890ad740c8487038b2e2ca5" -dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "aes-gcm", - "aes-gcm-siv", - "blake3", - "bytes", - "camellia", - "ccm", - "cfg-if", - "chacha20", - "chacha20poly1305", - "ctr", - "ghash", - "hkdf", - "md-5", - "rand 0.9.2", - "ring-compat", - "sha1", - "sm4 0.5.1", - "subtle", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -6190,12 +5053,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -6256,42 +5113,11 @@ dependencies = [ "void", ] -[[package]] -name = "sm4" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7abf5135ffd68fb4b438e1fb246923b80d25eda386d8b798bb4ad3ed00f75f" -dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "sm4" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2d621883cdeeaee6759d39f4bc8cd314b29db72b6ac3d6714b31422512ee11" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] [[package]] name = "smoltcp" @@ -6308,23 +5134,6 @@ dependencies = [ "managed", ] -[[package]] -name = "smux_rust" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da4231ce17872ec83fffdc189d64a121e1df79e2c0e08ee6c2d9f12f13f9994" -dependencies = [ - "bytes", - "futures", - "tokio", -] - -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - [[package]] name = "socket2" version = "0.5.10" @@ -6345,12 +5154,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "softaes" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea" - [[package]] name = "spin" version = "0.9.8" @@ -6360,15 +5163,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.7.3" @@ -6397,7 +5191,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" dependencies = [ - "cipher 0.4.4", + "cipher", "ssh-encoding", ] @@ -6583,12 +5377,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tap" version = "1.0.1" @@ -6735,33 +5523,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ + "backtrace", "bytes", + "io-uring", "libc", "mio", - "parking_lot 0.12.4", "pin-project-lite", "signal-hook-registry", + "slab", "socket2 0.6.3", "tokio-macros", "tracing", - "windows-sys 0.61.0", -] - -[[package]] -name = "tokio-graceful" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45740b38b48641855471cd402922e89156bdfbd97b69b45eeff170369cc18c7d" -dependencies = [ - "loom", - "pin-project-lite", - "slab", - "tokio", - "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -6776,9 +5553,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -6805,19 +5582,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-snappy" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb0ab9ee987e7134c0cdfe038b737102433aa8d7e04ad99cf1ec258907af976" -dependencies = [ - "bytes", - "futures", - "pin-project", - "snap", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -6829,23 +5593,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tfo" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ad2c3b3bb958ad992354a7ebc468fc0f7cdc9af4997bf4d3fd3cb28bad36dc" -dependencies = [ - "cfg-if", - "futures", - "libc", - "log", - "once_cell", - "pin-project", - "socket2 0.6.3", - "tokio", - "windows-sys 0.60.2", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -6860,19 +5607,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio_smux" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5826746b94900340cfda9e18a29a75986992df653d5941b5e359ca3b667117" -dependencies = [ - "byteorder", - "dashmap", - "log", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "toml" version = "0.8.23" @@ -7097,7 +5831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ab0c79bc92a957e85959cf397a2d8f9c8294c35fa4f65247a9393b20ac95551" dependencies = [ "amplify", - "bitflags 2.11.1", + "bitflags 2.9.4", "bytes", "caret", "derive-deftly", @@ -7596,7 +6330,7 @@ version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c6989a1c6d06ffd6835e2917edaae4aeef544f8e5fdd68b54cc365f2af523de" dependencies = [ - "aes 0.8.4", + "aes", "base64ct", "ctr", "curve25519-dalek", @@ -7695,7 +6429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41be8f47f521fc95206d2ba5facac8fb1a5b5b82169bd41ebeecdf46d1e77246" dependencies = [ "async-trait", - "bitflags 2.11.1", + "bitflags 2.9.4", "derive_more", "futures", "humantime", @@ -7724,7 +6458,7 @@ checksum = "ea8bce73d2c78bd78a2a927336ca639cf6bd5d8ad092ebcd0b3fdeaa47dcc77e" dependencies = [ "amplify", "base64ct", - "cipher 0.4.4", + "cipher", "derive-deftly", "derive_builder_fork_arti", "derive_more", @@ -7799,7 +6533,7 @@ dependencies = [ "bytes", "caret", "cfg-if", - "cipher 0.4.4", + "cipher", "coarsetime", "criterion-cycles-per-byte", "derive-deftly", @@ -8031,7 +6765,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "bytes", "futures-util", "http 1.3.1", @@ -8131,7 +6865,7 @@ dependencies = [ "cfg-if", "fnv", "once_cell", - "parking_lot 0.12.4", + "parking_lot", "tracing-core", "tracing-subscriber", ] @@ -8175,17 +6909,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -8219,15 +6942,6 @@ dependencies = [ "zip", ] -[[package]] -name = "twofish" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0218ef702a91e18ba84dd516edf8df33a9f9fb4431d6ada13e6f9502c3bcb6fc" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "typed-index-collections" version = "3.5.0" @@ -8240,9 +6954,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uncased" @@ -8253,12 +6967,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.19" @@ -8295,16 +7003,10 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.6", + "crypto-common", "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -8347,7 +7049,6 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", "js-sys", "serde", "wasm-bindgen", @@ -8532,7 +7233,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.9.4", "hashbrown 0.15.5", "indexmap 2.11.4", "semver", @@ -8564,34 +7265,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.3", -] - [[package]] name = "webpki-roots" version = "1.0.3" @@ -8619,15 +7292,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" -[[package]] -name = "wildcard" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b0540e91e49de3817c314da0dd3bc518093ceacc6ea5327cb0e1eb073e5189" -dependencies = [ - "thiserror 2.0.16", -] - [[package]] name = "winapi" version = "0.3.9" @@ -9136,7 +7800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.9.4", "indexmap 2.11.4", "log", "serde", @@ -9199,22 +7863,6 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" -[[package]] -name = "yamux" -version = "0.13.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" -dependencies = [ - "futures", - "log", - "nohash-hasher", - "parking_lot 0.12.4", - "pin-project", - "rand 0.9.2", - "static_assertions", - "web-time", -] - [[package]] name = "yoke" version = "0.8.0" @@ -9239,18 +7887,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zears" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e54700681e12629b566fc71a9ee538961b451ebf65b7509c2e12f114dd8e5aa" -dependencies = [ - "aes 0.8.4", - "blake2", - "constant_time_eq 0.4.2", - "cpufeatures 0.2.17", -] - [[package]] name = "zerocopy" version = "0.8.27" @@ -9351,15 +7987,15 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes 0.8.4", + "aes", "byteorder", "bzip2", - "constant_time_eq 0.1.5", + "constant_time_eq", "crc32fast", "crossbeam-utils", "flate2", "hmac", - "pbkdf2 0.11.0", + "pbkdf2", "sha1", "time", "zstd 0.11.2+zstd.1.5.2", diff --git a/Scripts/burrow-doctor b/Scripts/burrow-doctor deleted file mode 100755 index a364e59..0000000 --- a/Scripts/burrow-doctor +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: Scripts/burrow-doctor [--last 10m] - -Print a single stdout diagnostics report for the CURRENT macOS networking state. - -Run this while Burrow is the active VPN but routing/websites are broken, then -redirect stdout to a file before switching to a working VPN: - - Scripts/burrow-doctor --last 10m > burrow-broken.txt - -The report focuses on live VPN/proxy/DNS/route state, Burrow NetworkExtension -state, socket ownership, the selected Burrow node, and recent Burrow logs. -It intentionally does not redact local output, because broken proxy state often -depends on exact subscription and node configuration. -EOF -} - -last="10m" -while [[ $# -gt 0 ]]; do - case "$1" in - --last) - [[ $# -ge 2 ]] || { echo "--last requires a value" >&2; exit 2; } - last="$2" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "unknown argument: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -group_id="${BURROW_APP_GROUP_ID:-2KPBQHRJ2D.com.tianruichen.burrow}" -group_dir="${BURROW_APP_GROUP_DIR:-$HOME/Library/Group Containers/$group_id}" -db_path="${BURROW_DB_PATH:-$group_dir/burrow.db}" -runtime_log="${BURROW_RUNTIME_LOG:-$group_dir/burrow-runtime.log}" -bundle_id="${BURROW_BUNDLE_ID:-com.tianruichen.burrow}" -extension_process="${BURROW_EXTENSION_PROCESS:-BurrowNetworkExtension}" - -section() { - printf '\n## %s\n\n' "$1" -} - -cmd() { - printf '$' - printf ' %q' "$@" - printf '\n' -} - -run() { - local title="$1" - shift - section "$title" - cmd "$@" - printf '\n' - "$@" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?" -} - -run_zsh() { - local title="$1" - local script="$2" - section "$title" - printf '$ %s\n\n' "$script" - /bin/zsh -lc "$script" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?" -} - -run_python() { - if command -v uv >/dev/null 2>&1; then - uv run python "$@" - elif command -v python3 >/dev/null 2>&1; then - python3 "$@" - else - return 127 - fi -} - -capture() { - "$@" 2>&1 || true -} - -nc_list="$(capture scutil --nc list)" -proxy_state="$(capture scutil --proxy)" -dns_state="$(capture scutil --dns)" -inet_routes="$(capture netstat -rn -f inet)" -proxy_listeners="$(capture /bin/zsh -lc "lsof -nP -iTCP:6152 -iTCP:6153 -iTCP:7890 -iTCP:7891 -iTCP:7897 -iTCP:1080 -iTCP:1087 -sTCP:LISTEN")" - -cat < dict | None: - nodes = payload.get("nodes") or [] - selected_name = payload.get("selected_name") - selected_ordinal = payload.get("selected_ordinal") - for node in nodes: - if selected_name is not None and node.get("name") == selected_name: - return node - if selected_ordinal is not None and node.get("ordinal") == selected_ordinal: - return node - return nodes[0] if nodes else None - - -conn = sqlite3.connect(db_path) -conn.row_factory = sqlite3.Row -row = conn.execute( - "select payload from network where type = 'ProxySubscription' order by idx limit 1" -).fetchone() -if row is None: - print("No ProxySubscription network in database.") - raise SystemExit - -payload_blob = row["payload"] -payload_text = payload_blob.decode("utf-8", errors="replace") if isinstance(payload_blob, bytes) else payload_blob -payload = json.loads(payload_text) -node = choose_node(payload) -if not node: - print("ProxySubscription has no nodes.") - raise SystemExit - -server = node.get("server") -port = int(node.get("port")) -print(f"selected_proxy_server={server}:{port}") - -addresses = [] -try: - for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(server, port, type=socket.SOCK_STREAM): - host, resolved_port = sockaddr[:2] - key = (host, resolved_port) - if key not in addresses: - addresses.append(key) -except Exception as exc: - print(f"resolve_error={type(exc).__name__}: {exc}") - raise SystemExit - -print("resolved_addresses=" + ",".join(f"{host}:{resolved_port}" for host, resolved_port in addresses)) - -for host, _ in addresses: - print(f"\n$ route -n get {host}") - try: - result = subprocess.run( - ["route", "-n", "get", host], - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - timeout=5, - ) - print(result.stdout.rstrip()) - print(f"[exit={result.returncode}]") - except Exception as exc: - print(f"route_probe_error={type(exc).__name__}: {exc}") - -for host, resolved_port in addresses: - started = time.monotonic() - try: - with socket.create_connection((host, resolved_port), timeout=5): - elapsed_ms = (time.monotonic() - started) * 1000 - print(f"tcp_connect_ok address={host}:{resolved_port} elapsed_ms={elapsed_ms:.1f}") - except Exception as exc: - elapsed_ms = (time.monotonic() - started) * 1000 - print(f"tcp_connect_error address={host}:{resolved_port} elapsed_ms={elapsed_ms:.1f} error={type(exc).__name__}: {exc}") -PY -else - echo "DB not found: $db_path" -fi - -run_zsh "Route Probes" "for host in 1.1.1.1 8.8.8.8 google.com; do echo; echo \"\$ route -n get \$host\"; route -n get \"\$host\" 2>&1 || true; done" - -run_zsh "DNS Probes" "if command -v dig >/dev/null 2>&1; then dig +time=3 +tries=1 google.com A; echo; dig +time=3 +tries=1 @1.1.1.1 google.com A; else dscacheutil -q host -a name google.com; fi" - -run_zsh "Scoped Physical DNS Probes" "if command -v dig >/dev/null 2>&1; then servers=\$(scutil --dns | awk 'BEGIN { scoped=0; current=\"\" } /^DNS configuration \\(for scoped queries\\)/ { scoped=1; next } scoped && /^resolver #/ { current=\"\"; next } scoped && /nameserver\\[[0-9]+\\]/ { current=\$3 } scoped && /if_index/ && \$0 !~ /utun/ && current != \"\" { print current; current=\"\" }'); if [[ -z \"\$servers\" ]]; then echo 'No scoped non-utun DNS servers found.'; else for server in \$servers; do echo; echo \"\$ dig +time=3 +tries=1 @\$server google.com A\"; dig +time=3 +tries=1 @\"\$server\" google.com A; done; fi; else echo 'dig not found'; fi" - -run_zsh "TCP Probes" "for target in '1.1.1.1 53' '1.1.1.1 443' 'google.com 443'; do set -- \$target; echo; echo \"\$ nc -vz -w 5 \$1 \$2\"; nc -vz -w 5 \"\$1\" \"\$2\" 2>&1 || true; done" - -run_zsh "HTTP Probes" "for url in 'http://www.gstatic.com/generate_204' 'https://www.google.com/generate_204' 'https://www.cloudflare.com/cdn-cgi/trace'; do echo; echo \"\$ curl -4 -I --connect-timeout 5 --max-time 10 \$url\"; curl -4 -I --connect-timeout 5 --max-time 10 \"\$url\" 2>&1 || true; done" - -run_zsh "Burrow Controlled Proxy Self-Test" "if [[ -x '$repo_root/Scripts/burrow-proxy-selftest' ]]; then '$repo_root/Scripts/burrow-proxy-selftest'; else echo 'Scripts/burrow-proxy-selftest not found or not executable'; fi" - -run_zsh "Burrow Direct Provider Proxy Ladder" "if [[ -x '$repo_root/Scripts/burrow-proxy-ladder' ]]; then '$repo_root/Scripts/burrow-proxy-ladder' --db '$db_path' --resolve-server doh-all --target google.com:80 --http-host google.com --skip-udp --timeout 8; else echo 'Scripts/burrow-proxy-ladder not found or not executable'; fi" - -run_zsh "Burrow Daemon Packet Proxy Probe" "if [[ -x '$repo_root/target/debug/burrow' ]]; then BURROW_SOCKET_PATH=\"\${BURROW_SOCKET_PATH:-burrow.sock}\" '$repo_root/target/debug/burrow' proxy-tcp-probe --dns-name google.com --remote 1.1.1.1:80 --timeout-ms 12000; else echo 'target/debug/burrow not found; run cargo build -p burrow first'; fi" - -run_zsh "Burrow Daemon HTTPS Packet Proxy Probe" "if [[ -x '$repo_root/target/debug/burrow' ]]; then BURROW_SOCKET_PATH=\"\${BURROW_SOCKET_PATH:-burrow.sock}\" '$repo_root/target/debug/burrow' proxy-https-probe --dns-name news.ycombinator.com --remote 1.1.1.1:443 --timeout-ms 20000; else echo 'target/debug/burrow not found; run cargo build -p burrow first'; fi" - -section "Proxy Subscription Endpoint Status" -if [[ -f "$db_path" ]]; then - run_python - "$db_path" <<'PY' -from __future__ import annotations - -import json -import sqlite3 -import sys -import urllib.error -import urllib.request - -db_path = sys.argv[1] - -conn = sqlite3.connect(db_path) -conn.row_factory = sqlite3.Row -row = conn.execute( - "select payload from network where type = 'ProxySubscription' order by idx limit 1" -).fetchone() -if row is None: - print("No ProxySubscription network in database.") - raise SystemExit - -payload_blob = row["payload"] -payload_text = payload_blob.decode("utf-8", errors="replace") if isinstance(payload_blob, bytes) else payload_blob -payload = json.loads(payload_text) -url = (payload.get("source") or {}).get("url") -if not url: - print("ProxySubscription has no source URL.") - raise SystemExit - -print("source_url_redacted=yes") -request = urllib.request.Request(url, headers={"User-Agent": "mihomo/1.18.3"}) -try: - with urllib.request.urlopen(request, timeout=20) as response: - body = response.read(1024 * 1024) - print(f"status={response.status}") - for key, value in response.headers.items(): - lower = key.lower() - if ( - lower in {"subscription-userinfo", "profile-update-interval", "profile-title", "content-type"} - or "expire" in lower - or "subscription" in lower - ): - print(f"header_{key}={value}") - print(f"body_bytes={len(body)}") -except urllib.error.HTTPError as exc: - print(f"status={exc.code}") - print(f"reason={exc.reason}") - for key, value in exc.headers.items(): - lower = key.lower() - if ( - lower in {"subscription-userinfo", "profile-update-interval", "profile-title", "content-type"} - or "expire" in lower - or "subscription" in lower - ): - print(f"header_{key}={value}") - body = exc.read(1000).decode("utf-8", errors="replace").strip() - if body: - print(f"body_prefix={body[:200]}") -except Exception as exc: - print(f"fetch_error={type(exc).__name__}: {exc}") -PY -else - echo "DB not found: $db_path" -fi - -section "Burrow Runtime Log File" -if [[ -f "$runtime_log" ]]; then - cmd tail -400 "$runtime_log" - printf '\n' - tail -400 "$runtime_log" 2>&1 || printf '\n[burrow-doctor] command exited with status %s\n' "$?" -else - echo "Runtime log not found: $runtime_log" -fi - -section "Recent Burrow NetworkExtension Logs" -cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"nesessionmanager\" AND eventMessage CONTAINS[c] \"Burrow\"" -printf '\n' -{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"nesessionmanager\" AND eventMessage CONTAINS[c] \"Burrow\"" 2>&1 \ - || printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; } - -section "Recent Burrow Extension Logs" -cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"$extension_process\" OR eventMessage CONTAINS[c] \"$bundle_id.network\"" -printf '\n' -{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "process == \"$extension_process\" OR eventMessage CONTAINS[c] \"$bundle_id.network\"" 2>&1 \ - || printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; } - -section "Recent Burrow Errors" -cmd /usr/bin/log show --last "$last" --style compact --info --debug --predicate "((process CONTAINS[c] \"Burrow\") OR (eventMessage CONTAINS[c] \"$bundle_id\") OR (eventMessage CONTAINS[c] \"Trojan\") OR (eventMessage CONTAINS[c] \"proxy\")) AND (messageType == error OR messageType == fault OR eventMessage CONTAINS[c] \"failed\" OR eventMessage CONTAINS[c] \"error\" OR eventMessage CONTAINS[c] \"panic\")" -printf '\n' -{ /usr/bin/log show --last "$last" --style compact --info --debug --predicate "((process CONTAINS[c] \"Burrow\") OR (eventMessage CONTAINS[c] \"$bundle_id\") OR (eventMessage CONTAINS[c] \"Trojan\") OR (eventMessage CONTAINS[c] \"proxy\")) AND (messageType == error OR messageType == fault OR eventMessage CONTAINS[c] \"failed\" OR eventMessage CONTAINS[c] \"error\" OR eventMessage CONTAINS[c] \"panic\")" 2>&1 \ - || printf '\n[burrow-doctor] log command exited with status %s\n' "$?"; } diff --git a/Scripts/burrow-proxy-ladder b/Scripts/burrow-proxy-ladder deleted file mode 100755 index 9c1da3d..0000000 --- a/Scripts/burrow-proxy-ladder +++ /dev/null @@ -1,576 +0,0 @@ -#!/usr/bin/env python3 -"""Probe a Burrow proxy subscription node without enabling system proxying. - -This script intentionally bypasses the Burrow daemon, Network Extension, -routes, DNS settings, and browsers. It loads the selected proxy node from the -Burrow SQLite database and speaks the provider protocol directly. -""" - -from __future__ import annotations - -import argparse -import hashlib -import ipaddress -import json -import os -import socket -import sqlite3 -import ssl -import struct -import sys -import time -import urllib.parse -import urllib.request -from dataclasses import dataclass -from pathlib import Path -from typing import Any - - -DEFAULT_APP_GROUP = "2KPBQHRJ2D.com.tianruichen.burrow" -DEFAULT_DB = ( - Path.home() - / "Library" - / "Group Containers" - / DEFAULT_APP_GROUP - / "burrow.db" -) -DEFAULT_TARGET = "1.1.1.1:80" -DEFAULT_HTTP_HOST = "one.one.one.one" -DEFAULT_DNS_SERVER = "1.1.1.1:53" -DEFAULT_DNS_NAME = "google.com" -DEFAULT_TIMEOUT = 5.0 -DOH_PROVIDERS = { - "doh-google": "https://dns.google/resolve", - "doh-cloudflare": "https://cloudflare-dns.com/dns-query", -} - -TROJAN_CMD_CONNECT = 0x01 -TROJAN_CMD_UDP_ASSOCIATE = 0x03 - - -@dataclass(frozen=True) -class Endpoint: - host: str - port: int - - -@dataclass(frozen=True) -class ProbeResult: - ok: bool - detail: str - - -def parse_endpoint(value: str, default_port: int | None = None) -> Endpoint: - text = value.strip() - if not text: - raise ValueError("endpoint must not be empty") - - if text.startswith("["): - end = text.find("]") - if end == -1: - raise ValueError(f"invalid bracketed IPv6 endpoint: {value}") - host = text[1:end] - rest = text[end + 1 :] - if rest.startswith(":"): - port = int(rest[1:]) - elif default_port is not None: - port = default_port - else: - raise ValueError(f"endpoint is missing a port: {value}") - return Endpoint(host, port) - - if text.count(":") == 1: - host, port_text = text.rsplit(":", 1) - return Endpoint(host, int(port_text)) - - try: - ipaddress.ip_address(text) - except ValueError: - if ":" in text: - if default_port is None: - raise ValueError(f"IPv6 endpoint is missing a port: {value}") - return Endpoint(text, default_port) - if default_port is None: - raise ValueError(f"endpoint is missing a port: {value}") - return Endpoint(text, default_port) - - if default_port is None: - raise ValueError(f"endpoint is missing a port: {value}") - return Endpoint(text, default_port) - - -def encode_socks_addr(endpoint: Endpoint) -> bytes: - try: - ip = ipaddress.ip_address(endpoint.host) - except ValueError: - host = endpoint.host.encode("idna") - if len(host) > 255: - raise ValueError(f"SOCKS domain is too long: {endpoint.host}") - return bytes([3, len(host)]) + host + struct.pack("!H", endpoint.port) - - if ip.version == 4: - return bytes([1]) + ip.packed + struct.pack("!H", endpoint.port) - return bytes([4]) + ip.packed + struct.pack("!H", endpoint.port) - - -def read_exact(sock: ssl.SSLSocket, length: int) -> bytes: - chunks: list[bytes] = [] - remaining = length - while remaining: - chunk = sock.recv(remaining) - if not chunk: - raise EOFError(f"expected {remaining} more bytes") - chunks.append(chunk) - remaining -= len(chunk) - return b"".join(chunks) - - -def read_socks_addr(sock: ssl.SSLSocket) -> Endpoint: - atyp = read_exact(sock, 1)[0] - if atyp == 1: - host = str(ipaddress.IPv4Address(read_exact(sock, 4))) - elif atyp == 4: - host = str(ipaddress.IPv6Address(read_exact(sock, 16))) - elif atyp == 3: - length = read_exact(sock, 1)[0] - host = read_exact(sock, length).decode("idna") - else: - raise ValueError(f"unsupported SOCKS address type in response: {atyp}") - port = struct.unpack("!H", read_exact(sock, 2))[0] - return Endpoint(host, port) - - -def trojan_password_hash(password: str) -> bytes: - return hashlib.sha224(password.encode()).hexdigest().encode() - - -def trojan_header(password: str, command: int, target: Endpoint) -> bytes: - return ( - trojan_password_hash(password) - + b"\r\n" - + bytes([command]) - + encode_socks_addr(target) - + b"\r\n" - ) - - -def trojan_udp_frame(target: Endpoint, payload: bytes) -> bytes: - return encode_socks_addr(target) + struct.pack("!H", len(payload)) + b"\r\n" + payload - - -def build_dns_query(name: str) -> tuple[int, bytes]: - query_id = int(time.time() * 1000) & 0xFFFF - labels = [label for label in name.rstrip(".").split(".") if label] - qname = b"".join(bytes([len(label)]) + label.encode("ascii") for label in labels) + b"\x00" - header = struct.pack("!HHHHHH", query_id, 0x0100, 1, 0, 0, 0) - question = qname + struct.pack("!HH", 1, 1) - return query_id, header + question - - -def parse_dns_response(query_id: int, payload: bytes) -> str: - if len(payload) < 12: - return f"truncated DNS response: {len(payload)} bytes" - response_id, flags, questions, answers, authority, additional = struct.unpack( - "!HHHHHH", payload[:12] - ) - rcode = flags & 0x000F - qr = bool(flags & 0x8000) - id_note = "matching id" if response_id == query_id else f"id mismatch {response_id:#06x}" - return ( - f"{len(payload)} bytes, qr={qr}, rcode={rcode}, answers={answers}, " - f"authority={authority}, additional={additional}, questions={questions}, {id_note}" - ) - - -def load_payload(db_path: Path, network_id: int | None) -> tuple[int, dict[str, Any]]: - if not db_path.exists(): - raise FileNotFoundError(f"Burrow database not found: {db_path}") - conn = sqlite3.connect(str(db_path)) - conn.row_factory = sqlite3.Row - try: - if network_id is None: - row = conn.execute( - "select id,payload from network where type = 'ProxySubscription' order by idx limit 1" - ).fetchone() - else: - row = conn.execute( - "select id,payload from network where id = ? and type = 'ProxySubscription'", - (network_id,), - ).fetchone() - finally: - conn.close() - if row is None: - selector = "first ProxySubscription" if network_id is None else f"ProxySubscription id={network_id}" - raise LookupError(f"could not find {selector} in {db_path}") - payload_blob = row["payload"] - if isinstance(payload_blob, bytes): - payload_text = payload_blob.decode("utf-8", errors="replace") - else: - payload_text = payload_blob or "{}" - return int(row["id"]), json.loads(payload_text) - - -def choose_node( - payload: dict[str, Any], - node_name: str | None = None, - node_ordinal: int | None = None, -) -> dict[str, Any]: - nodes = payload.get("nodes") or [] - if node_name is not None: - for node in nodes: - if node.get("name") == node_name: - return node - raise LookupError(f"ProxySubscription payload has no node named {node_name!r}") - if node_ordinal is not None: - for node in nodes: - if node.get("ordinal") == node_ordinal: - return node - raise LookupError(f"ProxySubscription payload has no node with ordinal {node_ordinal}") - - selected_name = payload.get("selected_name") - selected_ordinal = payload.get("selected_ordinal") - for node in nodes: - if selected_name is not None and node.get("name") == selected_name: - return node - if selected_ordinal is not None and node.get("ordinal") == selected_ordinal: - return node - if nodes: - return nodes[0] - raise LookupError("ProxySubscription payload has no compatible nodes") - - -def resolved_stream_addresses(endpoint: Endpoint) -> list[tuple[int, tuple[Any, ...]]]: - addresses: list[tuple[int, tuple[Any, ...]]] = [] - seen: set[tuple[Any, ...]] = set() - for family, _, _, _, sockaddr in socket.getaddrinfo( - endpoint.host, endpoint.port, type=socket.SOCK_STREAM - ): - key = (family, sockaddr) - if key not in seen: - seen.add(key) - addresses.append((family, sockaddr)) - return addresses - - -def resolve_doh(host: str, port: int, provider: str, timeout: float) -> list[Endpoint]: - base = DOH_PROVIDERS[provider] - url = base + "?" + urllib.parse.urlencode({"name": host, "type": "A"}) - request = urllib.request.Request( - url, - headers={"Accept": "application/dns-json", "User-Agent": "burrow-proxy-ladder"}, - ) - with urllib.request.urlopen(request, timeout=timeout) as response: - body = json.load(response) - endpoints: list[Endpoint] = [] - for answer in body.get("Answer") or []: - if answer.get("type") != 1: - continue - address = str(answer.get("data") or "") - try: - ipaddress.IPv4Address(address) - except ValueError: - continue - endpoint = Endpoint(address, port) - if endpoint not in endpoints: - endpoints.append(endpoint) - return endpoints - - -def resolve_doh_all(host: str, port: int, timeout: float) -> list[Endpoint]: - endpoints: list[Endpoint] = [] - errors: list[str] = [] - for provider in DOH_PROVIDERS: - try: - provider_endpoints = resolve_doh(host, port, provider, timeout) - except BaseException as exc: - errors.append(f"{provider}={type(exc).__name__}: {exc}") - continue - print( - f"{provider}_answers=" - + ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in provider_endpoints) - ) - for endpoint in provider_endpoints: - if endpoint not in endpoints: - endpoints.append(endpoint) - if errors: - print("doh_warnings=" + "; ".join(errors)) - return endpoints - - -def is_fake_ip(host: str) -> bool: - try: - ip = ipaddress.ip_address(host) - except ValueError: - return False - return ip in ipaddress.ip_network("198.18.0.0/15") - - -def server_hostname(server: str, sni: str | None) -> str | None: - value = sni or server - if not value: - return None - return value - - -def tls_context(config: dict[str, Any]) -> ssl.SSLContext: - skip_verify = bool(config.get("skip_cert_verify")) - if skip_verify: - context = ssl._create_unverified_context() - else: - context = ssl.create_default_context() - - alpn_present = bool(config.get("alpn_present")) - alpn = config.get("alpn") or [] - if not alpn_present: - alpn = ["h2", "http/1.1"] - if alpn: - context.set_alpn_protocols([str(value) for value in alpn]) - return context - - -def connect_tls( - connect_endpoint: Endpoint, - tls_server_name: str, - node: dict[str, Any], - timeout: float, -) -> ssl.SSLSocket: - config = node["config"] - context = tls_context(config) - sni = server_hostname(tls_server_name, config.get("sni")) - last_error: BaseException | None = None - for _family, sockaddr in resolved_stream_addresses(connect_endpoint): - started = time.monotonic() - raw_sock: socket.socket | None = None - try: - raw_sock = socket.create_connection(sockaddr, timeout=timeout) - raw_sock.settimeout(timeout) - tls_sock = context.wrap_socket(raw_sock, server_hostname=sni) - tls_sock.settimeout(timeout) - elapsed_ms = (time.monotonic() - started) * 1000 - peer = sockaddr[0] if isinstance(sockaddr, tuple) else str(sockaddr) - print( - f" tls_connect_ok address={peer}:{connect_endpoint.port} " - f"elapsed_ms={elapsed_ms:.1f} alpn={tls_sock.selected_alpn_protocol()!r}" - ) - return tls_sock - except BaseException as exc: - last_error = exc - elapsed_ms = (time.monotonic() - started) * 1000 - peer = sockaddr[0] if isinstance(sockaddr, tuple) else str(sockaddr) - print( - f" tls_connect_error address={peer}:{connect_endpoint.port} " - f"elapsed_ms={elapsed_ms:.1f} error={type(exc).__name__}: {exc}" - ) - if raw_sock is not None: - raw_sock.close() - assert last_error is not None - raise last_error - - -def probe_trojan_tcp( - connect_endpoint: Endpoint, - tls_server_name: str, - node: dict[str, Any], - target: Endpoint, - http_host: str, - timeout: float, -) -> ProbeResult: - request = ( - f"GET / HTTP/1.1\r\n" - f"Host: {http_host}\r\n" - f"Connection: close\r\n" - f"User-Agent: burrow-proxy-ladder\r\n\r\n" - ).encode() - try: - with connect_tls(connect_endpoint, tls_server_name, node, timeout) as sock: - sock.sendall(trojan_header(node["config"]["password"], TROJAN_CMD_CONNECT, target)) - sock.sendall(request) - data = sock.recv(2048) - except BaseException as exc: - return ProbeResult(False, f"{type(exc).__name__}: {exc}") - if not data: - return ProbeResult(False, "proxy returned EOF before any HTTP response bytes") - first_line = data.splitlines()[0].decode("utf-8", errors="replace") - return ProbeResult(True, f"received {len(data)} bytes; first_line={first_line!r}") - - -def probe_trojan_udp_dns( - connect_endpoint: Endpoint, - tls_server_name: str, - node: dict[str, Any], - dns_server: Endpoint, - dns_name: str, - timeout: float, -) -> ProbeResult: - if node["config"].get("udp") is False: - return ProbeResult(False, "selected Trojan node has udp=false") - query_id, query = build_dns_query(dns_name) - try: - with connect_tls(connect_endpoint, tls_server_name, node, timeout) as sock: - sock.sendall( - trojan_header(node["config"]["password"], TROJAN_CMD_UDP_ASSOCIATE, dns_server) - ) - sock.sendall(trojan_udp_frame(dns_server, query)) - source = read_socks_addr(sock) - payload_len = struct.unpack("!H", read_exact(sock, 2))[0] - delimiter = read_exact(sock, 2) - if delimiter != b"\r\n": - return ProbeResult(False, f"invalid Trojan UDP delimiter: {delimiter!r}") - payload = read_exact(sock, payload_len) - except BaseException as exc: - return ProbeResult(False, f"{type(exc).__name__}: {exc}") - dns_summary = parse_dns_response(query_id, payload) - return ProbeResult( - True, - f"received UDP frame from {source.host}:{source.port}; DNS response {dns_summary}", - ) - - -def print_node_summary(network_id: int, payload: dict[str, Any], node: dict[str, Any]) -> None: - config = node.get("config") or {} - print(f"network_id={network_id}") - print(f"subscription={payload.get('name')!r}") - print(f"selected_node={node.get('name')!r}") - print(f"protocol={node.get('protocol')!r}") - print(f"server={node.get('server')}:{node.get('port')}") - print(f"sni={config.get('sni')!r}") - print(f"skip_cert_verify={bool(config.get('skip_cert_verify'))}") - print(f"alpn_present={bool(config.get('alpn_present'))} alpn={config.get('alpn') or []!r}") - print(f"udp={config.get('udp', True)!r}") - - -def main() -> int: - parser = argparse.ArgumentParser( - description=( - "Directly probe the selected Burrow proxy subscription node without " - "using the daemon, Network Extension, system proxy, routes, or DNS settings." - ) - ) - parser.add_argument("--db", type=Path, default=Path(os.environ.get("BURROW_DB", DEFAULT_DB))) - parser.add_argument("--network-id", type=int) - parser.add_argument("--node-name", help="Probe this subscription node instead of the selected node.") - parser.add_argument("--node-ordinal", type=int, help="Probe this subscription ordinal instead of the selected node.") - parser.add_argument( - "--server-address", - help=( - "Override the proxy connect address, as host or host:port. " - "This bypasses system DNS for the proxy hostname while preserving SNI." - ), - ) - parser.add_argument( - "--resolve-server", - choices=["system", "doh-google", "doh-cloudflare", "doh-all"], - default="system", - help="How to resolve the proxy server when --server-address is not provided.", - ) - parser.add_argument( - "--all-server-addresses", - action="store_true", - help="Probe every resolved proxy server address and pass if any address works.", - ) - parser.add_argument("--target", default=DEFAULT_TARGET, help="TCP HTTP target, host:port") - parser.add_argument("--http-host", default=DEFAULT_HTTP_HOST) - parser.add_argument("--dns-server", default=DEFAULT_DNS_SERVER, help="UDP DNS target, host:port") - parser.add_argument("--dns-name", default=DEFAULT_DNS_NAME) - parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT) - parser.add_argument("--skip-udp", action="store_true") - parser.add_argument("--require-udp", action="store_true") - args = parser.parse_args() - - print("Burrow proxy ladder: direct provider/protocol probe") - print("bypasses=daemon,NetworkExtension,system_proxy,routes,system_dns,browser") - print(f"db={args.db}") - - try: - if args.node_name is not None and args.node_ordinal is not None: - raise ValueError("--node-name and --node-ordinal are mutually exclusive") - network_id, payload = load_payload(args.db, args.network_id) - node = choose_node(payload, args.node_name, args.node_ordinal) - print_node_summary(network_id, payload, node) - if node.get("protocol") != "trojan": - print(f"unsupported protocol for this direct probe: {node.get('protocol')!r}") - return 2 - server = Endpoint(str(node["server"]), int(node["port"])) - if args.server_address: - connect_endpoints = [parse_endpoint(args.server_address, default_port=server.port)] - elif args.resolve_server == "system": - system_addresses = resolved_stream_addresses(server) - connect_endpoints = [ - Endpoint(str(sockaddr[0]), int(sockaddr[1])) for _family, sockaddr in system_addresses - ] - if not args.all_server_addresses and connect_endpoints: - connect_endpoints = connect_endpoints[:1] - if not connect_endpoints: - connect_endpoints = [server] - elif args.resolve_server == "doh-all": - connect_endpoints = resolve_doh_all(server.host, server.port, args.timeout) - if not args.all_server_addresses and connect_endpoints: - connect_endpoints = connect_endpoints[:1] - else: - connect_endpoints = resolve_doh(server.host, server.port, args.resolve_server, args.timeout) - print( - f"{args.resolve_server}_answers=" - + ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in connect_endpoints) - ) - if not args.all_server_addresses and connect_endpoints: - connect_endpoints = connect_endpoints[:1] - if not connect_endpoints: - raise LookupError("proxy server resolution returned no usable A records") - target = parse_endpoint(args.target) - dns_server = parse_endpoint(args.dns_server) - except BaseException as exc: - print(f"setup_error={type(exc).__name__}: {exc}", file=sys.stderr) - return 2 - - print( - "connect_endpoints=" - + ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in connect_endpoints) - ) - if any(endpoint != server for endpoint in connect_endpoints): - print(f"tls_server_name_source={server.host!r} sni_preserved=True") - if any(is_fake_ip(endpoint.host) for endpoint in connect_endpoints): - print( - "warning=proxy hostname resolved into 198.18.0.0/15; " - "rerun with --resolve-server doh-all or --server-address REAL_IP to avoid fake-IP DNS" - ) - - any_tcp_ok = False - any_required_udp_ok = False - for idx, connect_endpoint in enumerate(connect_endpoints, start=1): - if len(connect_endpoints) > 1: - print(f"\n=== proxy server candidate {idx}/{len(connect_endpoints)} ===") - print(f"connect_endpoint={connect_endpoint.host}:{connect_endpoint.port}") - - print("\n[1/2] Trojan TCP CONNECT probe") - print(f"target={target.host}:{target.port} http_host={args.http_host!r}") - tcp_result = probe_trojan_tcp( - connect_endpoint, server.host, node, target, args.http_host, args.timeout - ) - print(("ok: " if tcp_result.ok else "fail: ") + tcp_result.detail) - any_tcp_ok = any_tcp_ok or tcp_result.ok - - udp_result = ProbeResult(True, "skipped") - if not args.skip_udp: - print("\n[2/2] Trojan UDP DNS probe") - print(f"dns_server={dns_server.host}:{dns_server.port} dns_name={args.dns_name!r}") - udp_result = probe_trojan_udp_dns( - connect_endpoint, server.host, node, dns_server, args.dns_name, args.timeout - ) - print(("ok: " if udp_result.ok else "fail: ") + udp_result.detail) - else: - print("\n[2/2] Trojan UDP DNS probe") - print("skipped by --skip-udp") - any_required_udp_ok = any_required_udp_ok or udp_result.ok - - if tcp_result.ok and (not args.require_udp or udp_result.ok): - break - - if not any_tcp_ok: - return 1 - if args.require_udp and not any_required_udp_ok: - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/Scripts/burrow-proxy-selftest b/Scripts/burrow-proxy-selftest deleted file mode 100755 index fd3daaa..0000000 --- a/Scripts/burrow-proxy-selftest +++ /dev/null @@ -1,1417 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: Scripts/burrow-proxy-selftest - -Run a controlled Burrow proxy packet-runtime test without changing system proxy -settings or using the user's Burrow database. The script starts local Trojan and -Shadowsocks servers, starts a Burrow daemon against a temporary database/socket, -imports a temporary ProxySubscription pointed at those servers, then runs the -packet-level proxy TCP, UDP, UDP-over-TCP, and plugin probes through daemon -packet streaming. -EOF -} - -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -burrow_bin="${BURROW_BIN:-$repo_root/target/debug/burrow}" - -if [[ -z "${BURROW_BIN:-}" ]]; then - (cd "$repo_root" && cargo build -p burrow) -elif [[ ! -x "$burrow_bin" ]]; then - echo "BURROW_BIN is not executable: $burrow_bin" >&2 - exit 2 -fi - -if ! command -v openssl >/dev/null 2>&1; then - echo "openssl is required for the local TLS server certificate" >&2 - exit 2 -fi - -if ! command -v uv >/dev/null 2>&1; then - echo "uv is required for the local Python self-test harness" >&2 - exit 2 -fi - -tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-proxy-selftest.XXXXXX")" -daemon_pid="" -server_pid="" -ss_server_pid="" -status=0 - -cleanup() { - if [[ -n "$ss_server_pid" ]]; then - kill "$ss_server_pid" >/dev/null 2>&1 || true - fi - if [[ -n "$server_pid" ]]; then - kill "$server_pid" >/dev/null 2>&1 || true - fi - if [[ -n "$daemon_pid" ]]; then - kill "$daemon_pid" >/dev/null 2>&1 || true - fi - if [[ "${BURROW_SELFTEST_KEEP:-}" == "1" || "$status" -ne 0 ]]; then - echo "preserving self-test artifacts: $tmpdir" >&2 - else - rm -rf "$tmpdir" - fi -} -trap 'status=$?; cleanup' EXIT - -cert="$tmpdir/cert.pem" -key="$tmpdir/key.pem" -socket="$tmpdir/burrow.sock" -port_file="$tmpdir/server.port" -ss_port_file="$tmpdir/shadowsocks-server.port" -server_result="$tmpdir/server-result.json" -ss_server_result="$tmpdir/shadowsocks-server-result.json" -payload="$tmpdir/proxy-payload.json" -daemon_log="$tmpdir/daemon.log" -server_log="$tmpdir/server.log" -ss_server_log="$tmpdir/shadowsocks-server.log" - -openssl req \ - -x509 \ - -newkey rsa:2048 \ - -nodes \ - -subj /CN=localhost \ - -keyout "$key" \ - -out "$cert" \ - -days 1 >/dev/null 2>&1 - -uv run python - "$port_file" "$server_result" "$cert" "$key" >"$server_log" 2>&1 <<'PY' & -from __future__ import annotations - -import hashlib -import json -import socket -import ssl -import struct -import sys - -port_file, result_file, cert_file, key_file = sys.argv[1:] -password = "secret" - - -def read_exact(sock: ssl.SSLSocket, length: int) -> bytes: - chunks: list[bytes] = [] - remaining = length - while remaining: - chunk = sock.recv(remaining) - if not chunk: - raise EOFError(f"expected {remaining} more bytes") - chunks.append(chunk) - remaining -= len(chunk) - return b"".join(chunks) - - -def read_socks_addr(sock: ssl.SSLSocket) -> dict[str, object]: - atyp = read_exact(sock, 1)[0] - if atyp == 1: - host = socket.inet_ntop(socket.AF_INET, read_exact(sock, 4)) - elif atyp == 4: - host = socket.inet_ntop(socket.AF_INET6, read_exact(sock, 16)) - elif atyp == 3: - length = read_exact(sock, 1)[0] - host = read_exact(sock, length).decode("idna") - else: - raise ValueError(f"unsupported SOCKS address type {atyp}") - port = struct.unpack("!H", read_exact(sock, 2))[0] - return {"atyp": atyp, "host": host, "port": port} - - -def read_http_headers(sock: ssl.SSLSocket) -> bytes: - data = bytearray() - while b"\r\n\r\n" not in data: - chunk = sock.recv(4096) - if not chunk: - break - data.extend(chunk) - if len(data) > 65535: - raise ValueError("HTTP request headers exceeded 64 KiB") - return bytes(data) - - -ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) -ctx.load_cert_chain(cert_file, key_file) -ctx.set_alpn_protocols(["h2", "http/1.1"]) - -listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -listener.bind(("127.0.0.1", 0)) -listener.listen(1) -port = listener.getsockname()[1] -with open(port_file, "w", encoding="utf-8") as f: - f.write(str(port)) - -raw, peer = listener.accept() -raw.settimeout(10) -result: dict[str, object] = {"peer": str(peer)} -try: - with ctx.wrap_socket(raw, server_side=True) as tls: - tls.settimeout(10) - result["alpn"] = tls.selected_alpn_protocol() - got_hash = read_exact(tls, 56) - expected_hash = hashlib.sha224(password.encode()).hexdigest().encode() - result["password_hash_ok"] = got_hash == expected_hash - if not result["password_hash_ok"]: - raise ValueError("unexpected Trojan password hash") - if read_exact(tls, 2) != b"\r\n": - raise ValueError("missing Trojan password delimiter") - command = read_exact(tls, 1)[0] - result["command"] = command - target = read_socks_addr(tls) - result["target"] = target - if read_exact(tls, 2) != b"\r\n": - raise ValueError("missing Trojan header delimiter") - request = read_http_headers(tls) - result["request_first_line"] = request.splitlines()[0].decode("utf-8", "replace") - body = json.dumps( - { - "ok": True, - "target": target, - "request_first_line": result["request_first_line"], - }, - sort_keys=True, - ).encode() - tls.sendall( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) -finally: - with open(result_file, "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2, sort_keys=True) - listener.close() -PY -server_pid=$! - -for _ in {1..100}; do - if [[ -s "$port_file" ]]; then - break - fi - if ! kill -0 "$server_pid" >/dev/null 2>&1; then - echo "local Trojan server exited before listening" >&2 - cat "$server_log" >&2 || true - exit 1 - fi - sleep 0.05 -done - -if [[ ! -s "$port_file" ]]; then - echo "timed out waiting for local Trojan server" >&2 - cat "$server_log" >&2 || true - exit 1 -fi -server_port="$(cat "$port_file")" - -uv run python - "$ss_port_file" "$ss_server_result" >"$ss_server_log" 2>&1 <<'PY' & -from __future__ import annotations - -import json -import base64 -import socket -import struct -import sys - -port_file, result_file = sys.argv[1:] - - -def read_exact(sock: socket.socket, length: int) -> bytes: - chunks: list[bytes] = [] - remaining = length - while remaining: - chunk = sock.recv(remaining) - if not chunk: - raise EOFError(f"expected {remaining} more bytes") - chunks.append(chunk) - remaining -= len(chunk) - return b"".join(chunks) - - -def read_socks_addr(sock: socket.socket) -> dict[str, object]: - atyp = read_exact(sock, 1)[0] - if atyp == 1: - host = socket.inet_ntop(socket.AF_INET, read_exact(sock, 4)) - elif atyp == 4: - host = socket.inet_ntop(socket.AF_INET6, read_exact(sock, 16)) - elif atyp == 3: - length = read_exact(sock, 1)[0] - host = read_exact(sock, length).decode("idna") - else: - raise ValueError(f"unsupported SOCKS address type {atyp}") - port = struct.unpack("!H", read_exact(sock, 2))[0] - return {"atyp": atyp, "host": host, "port": port} - - -def parse_socks_addr_packet(data: bytes) -> tuple[dict[str, object], int]: - offset = 0 - atyp = data[offset] - offset += 1 - if atyp == 1: - host = socket.inet_ntop(socket.AF_INET, data[offset : offset + 4]) - offset += 4 - elif atyp == 4: - host = socket.inet_ntop(socket.AF_INET6, data[offset : offset + 16]) - offset += 16 - elif atyp == 3: - length = data[offset] - offset += 1 - host = data[offset : offset + length].decode("idna") - offset += length - else: - raise ValueError(f"unsupported SOCKS address type {atyp}") - port = struct.unpack("!H", data[offset : offset + 2])[0] - offset += 2 - return {"atyp": atyp, "host": host, "port": port}, offset - - -def read_uot_addr(sock: socket.socket) -> tuple[dict[str, object], bytes]: - raw = bytearray() - atyp = read_exact(sock, 1)[0] - raw.append(atyp) - if atyp == 0: - addr = read_exact(sock, 4) - raw.extend(addr) - host = socket.inet_ntop(socket.AF_INET, addr) - elif atyp == 1: - addr = read_exact(sock, 16) - raw.extend(addr) - host = socket.inet_ntop(socket.AF_INET6, addr) - elif atyp == 2: - length = read_exact(sock, 1)[0] - raw.append(length) - addr = read_exact(sock, length) - raw.extend(addr) - host = addr.decode("idna") - else: - raise ValueError(f"unsupported UDP-over-TCP address type {atyp}") - port_bytes = read_exact(sock, 2) - raw.extend(port_bytes) - port = struct.unpack("!H", port_bytes)[0] - return {"atyp": atyp, "host": host, "port": port}, bytes(raw) - - -def read_uot_frame(sock: socket.socket) -> tuple[dict[str, object], bytes, bytes]: - target, target_bytes = read_uot_addr(sock) - length_bytes = read_exact(sock, 2) - payload_len = struct.unpack("!H", length_bytes)[0] - payload = read_exact(sock, payload_len) - return target, target_bytes + length_bytes + payload, payload - - -def read_uot_request(sock: socket.socket) -> dict[str, object]: - version = read_exact(sock, 1)[0] - target = read_socks_addr(sock) - return {"version": version, "target": target} - - -def handle_uot_connection( - listener: socket.socket, - result: dict[str, object], - prefix: str, - expect_request: bool, -) -> None: - uot_raw, uot_peer = listener.accept() - uot_raw.settimeout(10) - try: - uot_magic = read_socks_addr(uot_raw) - if expect_request: - result[f"{prefix}_request"] = read_uot_request(uot_raw) - uot_target, uot_frame, uot_payload = read_uot_frame(uot_raw) - result[f"{prefix}_peer"] = str(uot_peer) - result[f"{prefix}_magic_target"] = uot_magic - result[f"{prefix}_target"] = uot_target - result[f"{prefix}_payload"] = uot_payload.decode("utf-8", "replace") - uot_raw.sendall(uot_frame) - finally: - uot_raw.close() - - -def read_http_headers(sock: socket.socket) -> bytes: - return read_http_headers_with_prefix(sock, b"") - - -def read_http_headers_with_prefix(sock: socket.socket, prefix: bytes) -> bytes: - data = bytearray(prefix) - while b"\r\n\r\n" not in data: - chunk = sock.recv(4096) - if not chunk: - break - data.extend(chunk) - if len(data) > 65535: - raise ValueError("HTTP request headers exceeded 64 KiB") - return bytes(data) - - -def read_obfs_http_headers(sock: socket.socket) -> tuple[bytes, bytes]: - data = bytearray() - while b"\r\n\r\n" not in data: - chunk = sock.recv(4096) - if not chunk: - break - data.extend(chunk) - if len(data) > 65535: - raise ValueError("simple-obfs HTTP headers exceeded 64 KiB") - header_end = bytes(data).find(b"\r\n\r\n") - if header_end < 0: - raise ValueError("simple-obfs HTTP headers were incomplete") - header_end += 4 - return bytes(data[:header_end]), bytes(data[header_end:]) - - -def read_simple_obfs_tls_payload(sock: socket.socket) -> tuple[bytes, str | None]: - header = read_exact(sock, 5) - if header[0] != 22: - raise ValueError(f"expected TLS handshake record, got type {header[0]}") - record_len = struct.unpack("!H", header[3:5])[0] - record = read_exact(sock, record_len) - if len(record) < 4 or record[0] != 1: - raise ValueError("expected TLS ClientHello handshake") - body_len = int.from_bytes(record[1:4], "big") - body = record[4 : 4 + body_len] - offset = 0 - offset += 2 # version - offset += 32 # random - session_id_len = body[offset] - offset += 1 + session_id_len - cipher_len = struct.unpack("!H", body[offset : offset + 2])[0] - offset += 2 + cipher_len - compression_len = body[offset] - offset += 1 + compression_len - extension_len = struct.unpack("!H", body[offset : offset + 2])[0] - offset += 2 - end = offset + extension_len - ticket_payload: bytes | None = None - server_name: str | None = None - while offset + 4 <= end: - extension_type = struct.unpack("!H", body[offset : offset + 2])[0] - length = struct.unpack("!H", body[offset + 2 : offset + 4])[0] - offset += 4 - extension = body[offset : offset + length] - offset += length - if extension_type == 0x0023: - ticket_payload = extension - elif extension_type == 0x0000 and len(extension) >= 5: - name_len = struct.unpack("!H", extension[3:5])[0] - if len(extension) >= 5 + name_len: - server_name = extension[5 : 5 + name_len].decode("idna") - if ticket_payload is None: - raise ValueError("simple-obfs TLS ClientHello did not include payload extension") - return ticket_payload, server_name - - -def read_simple_obfs_tls_application_payload(sock: socket.socket) -> bytes: - header = read_exact(sock, 5) - if header[:3] != b"\x17\x03\x03": - raise ValueError(f"expected TLS application-data record, got {header.hex()}") - record_len = struct.unpack("!H", header[3:5])[0] - return read_exact(sock, record_len) - - -def read_simple_obfs_tls_plaintext_headers(sock: socket.socket, prefix: bytes) -> bytes: - data = bytearray(prefix) - while b"\r\n\r\n" not in data: - data.extend(read_simple_obfs_tls_application_payload(sock)) - if len(data) > 65535: - raise ValueError("simple-obfs TLS plaintext headers exceeded 64 KiB") - return bytes(data) - - -def read_client_websocket_frame(sock: socket.socket) -> bytes: - header = read_exact(sock, 2) - if header[0] & 0x0F != 2: - raise ValueError(f"expected binary websocket frame, got opcode {header[0] & 0x0F}") - if header[1] & 0x80 == 0: - raise ValueError("client websocket frame was not masked") - length = header[1] & 0x7F - if length == 126: - length = struct.unpack("!H", read_exact(sock, 2))[0] - elif length == 127: - length = struct.unpack("!Q", read_exact(sock, 8))[0] - mask = read_exact(sock, 4) - payload = bytearray(read_exact(sock, length)) - for index, byte in enumerate(payload): - payload[index] = byte ^ mask[index % 4] - return bytes(payload) - - -def server_websocket_frame(payload: bytes) -> bytes: - if len(payload) < 126: - return b"\x82" + bytes([len(payload)]) + payload - if len(payload) <= 65535: - return b"\x82\x7e" + struct.pack("!H", len(payload)) + payload - return b"\x82\x7f" + struct.pack("!Q", len(payload)) + payload - - -def read_websocket_plaintext_headers(sock: socket.socket, prefix: bytes) -> bytes: - data = bytearray(prefix) - while b"\r\n\r\n" not in data: - data.extend(read_client_websocket_frame(sock)) - if len(data) > 65535: - raise ValueError("websocket plaintext headers exceeded 64 KiB") - return bytes(data) - - -def http_header_value(headers: bytes, name: bytes) -> str | None: - name = name.lower() + b":" - for line in headers.splitlines(): - if line.lower().startswith(name): - return line.decode("utf-8", "replace").split(":", 1)[1].strip() - return None - - -def decode_websocket_early_data(value: str | None) -> bytes: - if not value: - raise ValueError("missing websocket early-data protocol header") - padding = "=" * (-len(value) % 4) - return base64.urlsafe_b64decode((value + padding).encode()) - - -def parse_v2ray_mux_client_payload(payload: bytes) -> bytes: - open_len = struct.unpack("!H", payload[:2])[0] - open_frame = payload[2 : 2 + open_len] - if len(open_frame) != open_len or open_frame[:4] != b"\x00\x00\x01\x00": - raise ValueError("v2ray mux open frame was invalid") - offset = 2 + open_len - data_header = payload[offset : offset + 8] - if len(data_header) != 8 or data_header[:6] != b"\x00\x04\x00\x00\x02\x01": - raise ValueError("v2ray mux data frame header was invalid") - data_len = struct.unpack("!H", data_header[6:8])[0] - data_start = offset + 8 - data = payload[data_start : data_start + data_len] - if len(data) != data_len: - raise ValueError("v2ray mux data frame payload was incomplete") - return data - - -def v2ray_mux_server_data_frame(payload: bytes) -> bytes: - if len(payload) > 65535: - raise ValueError("v2ray mux payload is too large") - return b"\x00\x04\x00\x00\x02\x01" + struct.pack("!H", len(payload)) + payload - - -def parse_v2ray_mux_data_payload(payload: bytes) -> bytes: - if len(payload) < 8 or payload[:6] != b"\x00\x04\x00\x00\x02\x01": - raise ValueError("v2ray mux data frame header was invalid") - data_len = struct.unpack("!H", payload[6:8])[0] - data = payload[8 : 8 + data_len] - if len(data) != data_len: - raise ValueError("v2ray mux data frame payload was incomplete") - return data - - -def read_v2ray_mux_plaintext_headers(sock: socket.socket, prefix: bytes) -> bytes: - data = bytearray(prefix) - while b"\r\n\r\n" not in data: - data.extend(parse_v2ray_mux_data_payload(read_client_websocket_frame(sock))) - if len(data) > 65535: - raise ValueError("v2ray mux plaintext headers exceeded 64 KiB") - return bytes(data) - - -listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -listener.bind(("127.0.0.1", 0)) -listener.listen(16) -port = listener.getsockname()[1] -udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -udp.bind(("127.0.0.1", port)) -udp.settimeout(10) -with open(port_file, "w", encoding="utf-8") as f: - f.write(str(port)) - -raw, peer = listener.accept() -raw.settimeout(10) -result: dict[str, object] = {"tcp_peer": str(peer)} -try: - target = read_socks_addr(raw) - result["tcp_target"] = target - request = read_http_headers(raw) - result["tcp_request_first_line"] = request.splitlines()[0].decode("utf-8", "replace") - body = json.dumps( - { - "ok": True, - "target": target, - "request_first_line": result["tcp_request_first_line"], - }, - sort_keys=True, - ).encode() - raw.sendall( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - datagram, udp_peer = udp.recvfrom(65535) - udp_target, used = parse_socks_addr_packet(datagram) - udp_payload = datagram[used:] - result["udp_peer"] = str(udp_peer) - result["udp_target"] = udp_target - result["udp_payload"] = udp_payload.decode("utf-8", "replace") - udp.sendto(datagram[:used] + udp_payload, udp_peer) - handle_uot_connection(listener, result, "uot", False) - handle_uot_connection(listener, result, "uot_v2", True) - obfs_raw, obfs_peer = listener.accept() - obfs_raw.settimeout(10) - try: - obfs_headers, obfs_payload = read_obfs_http_headers(obfs_raw) - obfs_target, used = parse_socks_addr_packet(obfs_payload) - result["obfs_http_peer"] = str(obfs_peer) - result["obfs_http_first_line"] = obfs_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - result["obfs_http_host"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in obfs_headers.splitlines() - if line.lower().startswith(b"host:") - ), - None, - ) - result["obfs_http_content_length"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in obfs_headers.splitlines() - if line.lower().startswith(b"content-length:") - ), - None, - ) - result["obfs_http_target"] = obfs_target - obfs_request = read_http_headers_with_prefix(obfs_raw, obfs_payload[used:]) - result["obfs_http_request_first_line"] = obfs_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - body = json.dumps( - { - "ok": True, - "target": obfs_target, - "request_first_line": result["obfs_http_request_first_line"], - }, - sort_keys=True, - ).encode() - obfs_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - finally: - obfs_raw.close() - obfs_tls_raw, obfs_tls_peer = listener.accept() - obfs_tls_raw.settimeout(10) - try: - obfs_tls_payload, obfs_tls_server_name = read_simple_obfs_tls_payload(obfs_tls_raw) - obfs_tls_target, used = parse_socks_addr_packet(obfs_tls_payload) - obfs_tls_request = read_simple_obfs_tls_plaintext_headers( - obfs_tls_raw, - obfs_tls_payload[used:], - ) - result["obfs_tls_peer"] = str(obfs_tls_peer) - result["obfs_tls_server_name"] = obfs_tls_server_name - result["obfs_tls_target"] = obfs_tls_target - result["obfs_tls_request_first_line"] = obfs_tls_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - body = json.dumps( - { - "ok": True, - "target": obfs_tls_target, - "request_first_line": result["obfs_tls_request_first_line"], - }, - sort_keys=True, - ).encode() - response = ( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - obfs_tls_raw.sendall(bytes(105) + len(response).to_bytes(2, "big") + response) - finally: - obfs_tls_raw.close() - v2ray_raw, v2ray_peer = listener.accept() - v2ray_raw.settimeout(10) - try: - v2ray_headers = read_http_headers(v2ray_raw) - result["v2ray_http_upgrade_peer"] = str(v2ray_peer) - result["v2ray_http_upgrade_first_line"] = v2ray_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - result["v2ray_http_upgrade_host"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in v2ray_headers.splitlines() - if line.lower().startswith(b"host:") - ), - None, - ) - result["v2ray_http_upgrade_has_websocket_key"] = any( - line.lower().startswith(b"sec-websocket-key:") - for line in v2ray_headers.splitlines() - ) - v2ray_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - ) - v2ray_target = read_socks_addr(v2ray_raw) - v2ray_request = read_http_headers(v2ray_raw) - result["v2ray_http_upgrade_target"] = v2ray_target - result["v2ray_http_upgrade_request_first_line"] = ( - v2ray_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - body = json.dumps( - { - "ok": True, - "target": v2ray_target, - "request_first_line": result["v2ray_http_upgrade_request_first_line"], - }, - sort_keys=True, - ).encode() - v2ray_raw.sendall( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - finally: - v2ray_raw.close() - v2ray_ws_raw, v2ray_ws_peer = listener.accept() - v2ray_ws_raw.settimeout(10) - try: - v2ray_ws_headers = read_http_headers(v2ray_ws_raw) - result["v2ray_websocket_peer"] = str(v2ray_ws_peer) - result["v2ray_websocket_first_line"] = ( - v2ray_ws_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - result["v2ray_websocket_host"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in v2ray_ws_headers.splitlines() - if line.lower().startswith(b"host:") - ), - None, - ) - result["v2ray_websocket_has_websocket_key"] = any( - line.lower().startswith(b"sec-websocket-key:") - for line in v2ray_ws_headers.splitlines() - ) - v2ray_ws_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - ) - v2ray_ws_payload = read_client_websocket_frame(v2ray_ws_raw) - v2ray_ws_target, used = parse_socks_addr_packet(v2ray_ws_payload) - v2ray_ws_request = read_websocket_plaintext_headers( - v2ray_ws_raw, - v2ray_ws_payload[used:], - ) - result["v2ray_websocket_target"] = v2ray_ws_target - result["v2ray_websocket_request_first_line"] = ( - v2ray_ws_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - body = json.dumps( - { - "ok": True, - "target": v2ray_ws_target, - "request_first_line": result["v2ray_websocket_request_first_line"], - }, - sort_keys=True, - ).encode() - response = ( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - v2ray_ws_raw.sendall(server_websocket_frame(response)) - finally: - v2ray_ws_raw.close() - v2ray_mux_raw, v2ray_mux_peer = listener.accept() - v2ray_mux_raw.settimeout(10) - try: - v2ray_mux_headers = read_http_headers(v2ray_mux_raw) - result["v2ray_mux_peer"] = str(v2ray_mux_peer) - result["v2ray_mux_first_line"] = ( - v2ray_mux_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - result["v2ray_mux_host"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in v2ray_mux_headers.splitlines() - if line.lower().startswith(b"host:") - ), - None, - ) - result["v2ray_mux_has_websocket_key"] = any( - line.lower().startswith(b"sec-websocket-key:") - for line in v2ray_mux_headers.splitlines() - ) - v2ray_mux_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - ) - v2ray_mux_payload = parse_v2ray_mux_client_payload( - read_client_websocket_frame(v2ray_mux_raw), - ) - v2ray_mux_target, used = parse_socks_addr_packet(v2ray_mux_payload) - v2ray_mux_request = read_v2ray_mux_plaintext_headers( - v2ray_mux_raw, - v2ray_mux_payload[used:], - ) - result["v2ray_mux_target"] = v2ray_mux_target - result["v2ray_mux_request_first_line"] = ( - v2ray_mux_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - body = json.dumps( - { - "ok": True, - "target": v2ray_mux_target, - "request_first_line": result["v2ray_mux_request_first_line"], - }, - sort_keys=True, - ).encode() - response = ( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - v2ray_mux_raw.sendall( - server_websocket_frame(v2ray_mux_server_data_frame(response)), - ) - finally: - v2ray_mux_raw.close() - gost_ws_raw, gost_ws_peer = listener.accept() - gost_ws_raw.settimeout(10) - try: - gost_ws_headers = read_http_headers(gost_ws_raw) - result["gost_websocket_peer"] = str(gost_ws_peer) - result["gost_websocket_first_line"] = ( - gost_ws_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - result["gost_websocket_host"] = next( - ( - line.decode("utf-8", "replace").split(":", 1)[1].strip() - for line in gost_ws_headers.splitlines() - if line.lower().startswith(b"host:") - ), - None, - ) - result["gost_websocket_has_websocket_key"] = any( - line.lower().startswith(b"sec-websocket-key:") - for line in gost_ws_headers.splitlines() - ) - gost_ws_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - ) - gost_ws_payload = read_client_websocket_frame(gost_ws_raw) - gost_ws_target, used = parse_socks_addr_packet(gost_ws_payload) - gost_ws_request = read_websocket_plaintext_headers( - gost_ws_raw, - gost_ws_payload[used:], - ) - result["gost_websocket_target"] = gost_ws_target - result["gost_websocket_request_first_line"] = ( - gost_ws_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - body = json.dumps( - { - "ok": True, - "target": gost_ws_target, - "request_first_line": result["gost_websocket_request_first_line"], - }, - sort_keys=True, - ).encode() - response = ( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - gost_ws_raw.sendall(server_websocket_frame(response)) - finally: - gost_ws_raw.close() - v2ray_ed_raw, v2ray_ed_peer = listener.accept() - v2ray_ed_raw.settimeout(10) - try: - v2ray_ed_headers = read_http_headers(v2ray_ed_raw) - result["v2ray_early_data_peer"] = str(v2ray_ed_peer) - result["v2ray_early_data_first_line"] = ( - v2ray_ed_headers.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - result["v2ray_early_data_host"] = http_header_value(v2ray_ed_headers, b"host") - result["v2ray_early_data_has_websocket_key"] = ( - http_header_value(v2ray_ed_headers, b"sec-websocket-key") is not None - ) - early_data = decode_websocket_early_data( - http_header_value(v2ray_ed_headers, b"sec-websocket-protocol"), - ) - result["v2ray_early_data_len"] = len(early_data) - v2ray_ed_raw.sendall( - b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n\r\n" - ) - v2ray_ed_payload = early_data + read_client_websocket_frame(v2ray_ed_raw) - v2ray_ed_target, used = parse_socks_addr_packet(v2ray_ed_payload) - v2ray_ed_request = read_websocket_plaintext_headers( - v2ray_ed_raw, - v2ray_ed_payload[used:], - ) - result["v2ray_early_data_target"] = v2ray_ed_target - result["v2ray_early_data_request_first_line"] = ( - v2ray_ed_request.splitlines()[0].decode( - "utf-8", - "replace", - ) - ) - body = json.dumps( - { - "ok": True, - "target": v2ray_ed_target, - "request_first_line": result["v2ray_early_data_request_first_line"], - }, - sort_keys=True, - ).encode() - response = ( - b"HTTP/1.1 200 OK\r\n" - + f"Content-Length: {len(body)}\r\n".encode() - + b"Connection: close\r\n" - + b"Content-Type: application/json\r\n\r\n" - + body - ) - v2ray_ed_raw.sendall(server_websocket_frame(response)) - finally: - v2ray_ed_raw.close() -finally: - raw.close() - udp.close() - with open(result_file, "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2, sort_keys=True) - listener.close() -PY -ss_server_pid=$! - -for _ in {1..100}; do - if [[ -s "$ss_port_file" ]]; then - break - fi - if ! kill -0 "$ss_server_pid" >/dev/null 2>&1; then - echo "local Shadowsocks server exited before listening" >&2 - cat "$ss_server_log" >&2 || true - exit 1 - fi - sleep 0.05 -done - -if [[ ! -s "$ss_port_file" ]]; then - echo "timed out waiting for local Shadowsocks server" >&2 - cat "$ss_server_log" >&2 || true - exit 1 -fi -ss_server_port="$(cat "$ss_port_file")" - -cat >"$payload" <"$daemon_log" 2>&1 -) & -daemon_pid=$! - -for _ in {1..100}; do - if [[ -S "$socket" ]]; then - break - fi - if ! kill -0 "$daemon_pid" >/dev/null 2>&1; then - echo "Burrow daemon exited before creating socket" >&2 - cat "$daemon_log" >&2 || true - exit 1 - fi - sleep 0.05 -done - -if [[ ! -S "$socket" ]]; then - echo "timed out waiting for Burrow daemon socket" >&2 - cat "$daemon_log" >&2 || true - exit 1 -fi - -( - cd "$tmpdir" - BURROW_SOCKET_PATH="$socket" "$burrow_bin" network-add 1 3 "$payload" - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 -) - -wait "$server_pid" -server_pid="" - -echo -echo "Local Trojan server observed:" -cat "$server_result" -echo - -( - cd "$tmpdir" - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 2 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-udp-echo \ - --message burrow-shadowsocks-udp-selftest \ - --timeout-ms 8000 \ - 9.9.9.9:5353 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 3 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-udp-echo \ - --message burrow-shadowsocks-uot-selftest \ - --timeout-ms 8000 \ - 9.9.9.9:5354 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 4 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-udp-echo \ - --message burrow-shadowsocks-uot-v2-selftest \ - --timeout-ms 8000 \ - 9.9.9.9:5355 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 5 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 6 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 7 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 8 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 9 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 10 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-subscription-select 1 11 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 -) - -wait "$ss_server_pid" -ss_server_pid="" - -echo -echo "Local Shadowsocks server observed:" -cat "$ss_server_result" -echo - -uv run python - "$server_result" "$ss_server_result" <<'PY' -from __future__ import annotations - -import json -import sys - -trojan_path, shadowsocks_path = sys.argv[1:] - - -def load(path: str) -> dict[str, object]: - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -def expect(value: object, expected: object, label: str) -> None: - if value != expected: - raise AssertionError(f"{label}: expected {expected!r}, got {value!r}") - - -def expect_startswith(value: object, prefix: str, label: str) -> None: - if not isinstance(value, str) or not value.startswith(prefix): - raise AssertionError(f"{label}: expected prefix {prefix!r}, got {value!r}") - - -def expect_target(result: dict[str, object], key: str, host: str, port: int) -> None: - target = result.get(key) - if not isinstance(target, dict): - raise AssertionError(f"{key}: missing target object") - expect(target.get("host"), host, f"{key}.host") - expect(target.get("port"), port, f"{key}.port") - - -trojan = load(trojan_path) -expect(trojan.get("password_hash_ok"), True, "trojan password hash") -expect(trojan.get("command"), 1, "trojan tcp command") -expect(trojan.get("request_first_line"), "GET / HTTP/1.1", "trojan request") -expect_target(trojan, "target", "selftest.invalid", 80) - -ss = load(shadowsocks_path) -for key in [ - "tcp_target", - "obfs_http_target", - "obfs_tls_target", - "v2ray_http_upgrade_target", - "v2ray_websocket_target", - "v2ray_mux_target", - "gost_websocket_target", - "v2ray_early_data_target", -]: - expect_target(ss, key, "selftest.invalid", 80) - -expect(ss.get("udp_payload"), "burrow-shadowsocks-udp-selftest", "native UDP payload") -expect_target(ss, "udp_target", "9.9.9.9", 5353) -expect(ss.get("uot_payload"), "burrow-shadowsocks-uot-selftest", "legacy UOT payload") -expect_target(ss, "uot_magic_target", "sp.udp-over-tcp.arpa", 0) -expect_target(ss, "uot_target", "9.9.9.9", 5354) -expect(ss.get("uot_v2_payload"), "burrow-shadowsocks-uot-v2-selftest", "v2 UOT payload") -expect_target(ss, "uot_v2_magic_target", "sp.v2.udp-over-tcp.arpa", 0) -expect_target(ss, "uot_v2_target", "9.9.9.9", 5355) -expect_startswith(ss.get("obfs_http_host"), "front.example:", "simple-obfs HTTP host") -expect(ss.get("obfs_tls_server_name"), "front.example", "simple-obfs TLS SNI") -expect(ss.get("v2ray_http_upgrade_has_websocket_key"), False, "v2ray raw upgrade key") -expect(ss.get("v2ray_websocket_has_websocket_key"), True, "v2ray websocket key") -expect(ss.get("v2ray_mux_has_websocket_key"), True, "v2ray mux websocket key") -expect(ss.get("gost_websocket_has_websocket_key"), True, "gost websocket key") -expect(ss.get("v2ray_early_data_first_line"), "GET /ed?trace=1 HTTP/1.1", "early-data path") -expect(ss.get("v2ray_early_data_len"), 8, "early-data length") - -print("Proxy self-test assertions passed") -PY diff --git a/Scripts/burrow-shadowsocks-singmux-interop b/Scripts/burrow-shadowsocks-singmux-interop deleted file mode 100755 index e02374b..0000000 --- a/Scripts/burrow-shadowsocks-singmux-interop +++ /dev/null @@ -1,504 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: Scripts/burrow-shadowsocks-singmux-interop - -Run controlled Burrow Shadowsocks top-level sing-mux interop tests against the -same github.com/metacubex/sing-mux Go module used by Mihomo. By default this -checks smux, yamux, and h2mux with and without Mihomo's padded sing-mux -preface/framing. The test does not change system proxy settings or use the -user's Burrow database. -EOF -} - -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -burrow_bin="${BURROW_BIN:-$repo_root/target/debug/burrow}" -sing_mux_dir="${SING_MUX_DIR:-/Users/jettchen/go/pkg/mod/github.com/metacubex/sing-mux@v0.3.9}" -sing_dir="${SING_DIR:-/Users/jettchen/go/pkg/mod/github.com/metacubex/sing@v0.5.7}" - -if [[ -z "${BURROW_SINGMUX_INTEROP_SINGLE:-}" ]]; then - for protocol in ${BURROW_SINGMUX_PROTOCOLS:-smux yamux h2mux}; do - for padding in ${BURROW_SINGMUX_PADDING_MODES:-false true}; do - echo "=== Mihomo sing-mux interop: $protocol padding=$padding ===" - BURROW_SINGMUX_INTEROP_SINGLE=1 \ - BURROW_SINGMUX_PROTOCOL="$protocol" \ - BURROW_SINGMUX_PADDING="$padding" \ - "$0" - done - done - exit 0 -fi - -protocol="${BURROW_SINGMUX_PROTOCOL:-smux}" -case "$protocol" in - smux | yamux | h2mux) ;; - *) - echo "unsupported sing-mux protocol: $protocol" >&2 - exit 2 - ;; -esac - -padding="${BURROW_SINGMUX_PADDING:-false}" -case "$padding" in - false | true) ;; - *) - echo "unsupported sing-mux padding mode: $padding" >&2 - exit 2 - ;; -esac - -node_udp="${BURROW_SINGMUX_NODE_UDP:-false}" -case "$node_udp" in - false | true) ;; - *) - echo "unsupported Shadowsocks node udp flag: $node_udp" >&2 - exit 2 - ;; -esac - -if [[ -z "${BURROW_BIN:-}" ]]; then - (cd "$repo_root" && cargo build -p burrow) -elif [[ ! -x "$burrow_bin" ]]; then - echo "BURROW_BIN is not executable: $burrow_bin" >&2 - exit 2 -fi - -if ! command -v go >/dev/null 2>&1; then - echo "go is required for the Mihomo sing-mux interop peer" >&2 - exit 2 -fi - -if ! command -v uv >/dev/null 2>&1; then - echo "uv is required for result assertions" >&2 - exit 2 -fi - -if [[ ! -d "$sing_mux_dir" ]]; then - echo "sing-mux module directory not found: $sing_mux_dir" >&2 - exit 2 -fi - -if [[ ! -d "$sing_dir" ]]; then - echo "sing module directory not found: $sing_dir" >&2 - exit 2 -fi - -tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-singmux-interop.XXXXXX")" -daemon_pid="" -server_pid="" -status=0 - -cleanup() { - if [[ -n "$server_pid" ]]; then - kill "$server_pid" >/dev/null 2>&1 || true - fi - if [[ -n "$daemon_pid" ]]; then - kill "$daemon_pid" >/dev/null 2>&1 || true - fi - if [[ "${BURROW_SELFTEST_KEEP:-}" == "1" || "$status" -ne 0 ]]; then - echo "preserving sing-mux interop artifacts: $tmpdir" >&2 - else - rm -rf "$tmpdir" - fi -} -trap 'status=$?; cleanup' EXIT - -socket="$tmpdir/burrow.sock" -port_file="$tmpdir/singmux.port" -result_file="$tmpdir/singmux-result.json" -payload="$tmpdir/proxy-payload.json" -server_log="$tmpdir/singmux-server.log" -daemon_log="$tmpdir/daemon.log" -go_dir="$tmpdir/go-peer" -mkdir -p "$go_dir" - -cat >"$go_dir/go.mod" < $sing_dir -replace github.com/metacubex/sing-mux => $sing_mux_dir -EOF - -cat >"$go_dir/main.go" <<'EOF' -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net" - "os" - "strings" - "sync" - "time" - - mux "github.com/metacubex/sing-mux" - "github.com/metacubex/sing/common/buf" - "github.com/metacubex/sing/common/bufio" - "github.com/metacubex/sing/common/logger" - M "github.com/metacubex/sing/common/metadata" - N "github.com/metacubex/sing/common/network" -) - -type noopLogger struct{} - -func (noopLogger) Trace(args ...any) {} -func (noopLogger) Debug(args ...any) {} -func (noopLogger) Info(args ...any) {} -func (noopLogger) Warn(args ...any) {} -func (noopLogger) Error(args ...any) {} -func (noopLogger) Fatal(args ...any) {} -func (noopLogger) Panic(args ...any) {} -func (noopLogger) TraceContext(context.Context, ...any) {} -func (noopLogger) DebugContext(context.Context, ...any) {} -func (noopLogger) InfoContext(context.Context, ...any) {} -func (noopLogger) WarnContext(context.Context, ...any) {} -func (noopLogger) ErrorContext(context.Context, ...any) {} -func (noopLogger) FatalContext(context.Context, ...any) {} -func (noopLogger) PanicContext(context.Context, ...any) {} - -var _ logger.ContextLogger = noopLogger{} - -type observed struct { - MuxTarget map[string]any `json:"mux_target,omitempty"` - TCPDestination map[string]any `json:"tcp_destination,omitempty"` - TCPFirstLine string `json:"tcp_first_line,omitempty"` - UDPDestination map[string]any `json:"udp_destination,omitempty"` - UDPPayload string `json:"udp_payload,omitempty"` - Protocol string `json:"protocol,omitempty"` - Padding bool `json:"padding,omitempty"` - PacketReplyCount int `json:"packet_reply_count,omitempty"` -} - -type handler struct { - mu sync.Mutex - result observed - done chan struct{} - doneOnce sync.Once -} - -func (h *handler) markDoneIfReady() { - if h.result.TCPFirstLine != "" && h.result.UDPPayload != "" { - h.doneOnce.Do(func() { close(h.done) }) - } -} - -func (h *handler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - defer conn.Close() - request, err := readHTTPHeaders(conn) - if err != nil { - return err - } - firstLine := strings.SplitN(string(request), "\r\n", 2)[0] - body := []byte("burrow sing-mux tcp ok\n") - response := fmt.Sprintf( - "HTTP/1.1 200 OK\r\nContent-Length: %d\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n%s", - len(body), - body, - ) - if _, err := conn.Write([]byte(response)); err != nil { - return err - } - h.mu.Lock() - h.result.TCPDestination = socksaddrJSON(metadata.Destination) - h.result.TCPFirstLine = firstLine - h.markDoneIfReady() - h.mu.Unlock() - return nil -} - -func (h *handler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { - defer conn.Close() - packet := buf.NewPacket() - defer packet.Release() - destination, err := conn.ReadPacket(packet) - if err != nil { - return err - } - payload := append([]byte(nil), packet.Bytes()...) - if _, err := bufio.WritePacketBuffer(conn, buf.As(payload), destination); err != nil { - return err - } - h.mu.Lock() - h.result.UDPDestination = socksaddrJSON(metadata.Destination) - h.result.UDPPayload = string(payload) - h.result.PacketReplyCount++ - h.markDoneIfReady() - h.mu.Unlock() - return nil -} - -func main() { - portFile := os.Args[1] - resultFile := os.Args[2] - protocol := os.Args[3] - padding := os.Args[4] == "true" - - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - panic(err) - } - defer listener.Close() - port := listener.Addr().(*net.TCPAddr).Port - if err := os.WriteFile(portFile, []byte(fmt.Sprint(port)), 0o644); err != nil { - panic(err) - } - - h := &handler{done: make(chan struct{})} - h.result.Protocol = protocol - h.result.Padding = padding - service, err := mux.NewService(mux.ServiceOptions{ - NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { return ctx }, - Logger: noopLogger{}, - Handler: h, - }) - if err != nil { - panic(err) - } - - raw, err := listener.Accept() - if err != nil { - panic(err) - } - raw.SetDeadline(time.Now().Add(20 * time.Second)) - target, err := readSocksAddr(raw) - if err != nil { - panic(err) - } - h.mu.Lock() - h.result.MuxTarget = target - h.mu.Unlock() - - go func() { - if err := service.NewConnection(context.Background(), raw, M.Metadata{}); err != nil { - fmt.Fprintln(os.Stderr, err) - } - }() - - select { - case <-h.done: - case <-time.After(20 * time.Second): - panic("timed out waiting for sing-mux TCP and UDP probes") - } - - h.mu.Lock() - output, err := json.MarshalIndent(h.result, "", " ") - h.mu.Unlock() - if err != nil { - panic(err) - } - if err := os.WriteFile(resultFile, append(output, '\n'), 0o644); err != nil { - panic(err) - } -} - -func readHTTPHeaders(reader io.Reader) ([]byte, error) { - data := make([]byte, 0, 512) - tmp := make([]byte, 1) - for !strings.Contains(string(data), "\r\n\r\n") { - if _, err := io.ReadFull(reader, tmp); err != nil { - return nil, err - } - data = append(data, tmp[0]) - if len(data) > 65535 { - return nil, fmt.Errorf("HTTP headers exceeded 64 KiB") - } - } - return data, nil -} - -func readSocksAddr(reader io.Reader) (map[string]any, error) { - destination, err := M.SocksaddrSerializer.ReadAddrPort(reader) - if err != nil { - return nil, err - } - return socksaddrJSON(destination), nil -} - -func socksaddrJSON(destination M.Socksaddr) map[string]any { - if destination.IsFqdn() { - return map[string]any{ - "host": destination.Fqdn, - "port": destination.Port, - } - } - return map[string]any{ - "host": destination.Addr.String(), - "port": destination.Port, - } -} - -func (h *handler) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata M.Metadata) error { - return fmt.Errorf("unexpected packet handler path") -} -EOF - -( - cd "$go_dir" - go mod tidy -) - -( - cd "$go_dir" - go run . "$port_file" "$result_file" "$protocol" "$padding" -) >"$server_log" 2>&1 & -server_pid=$! - -for _ in {1..600}; do - if [[ -s "$port_file" ]]; then - break - fi - if ! kill -0 "$server_pid" >/dev/null 2>&1; then - echo "sing-mux peer exited before listening" >&2 - cat "$server_log" >&2 || true - exit 1 - fi - sleep 0.05 -done - -if [[ ! -s "$port_file" ]]; then - echo "timed out waiting for sing-mux peer" >&2 - cat "$server_log" >&2 || true - exit 1 -fi -server_port="$(cat "$port_file")" - -cat >"$payload" <"$daemon_log" 2>&1 -) & -daemon_pid=$! - -for _ in {1..100}; do - if [[ -S "$socket" ]]; then - break - fi - if ! kill -0 "$daemon_pid" >/dev/null 2>&1; then - echo "Burrow daemon exited before creating socket" >&2 - cat "$daemon_log" >&2 || true - exit 1 - fi - sleep 0.05 -done - -if [[ ! -S "$socket" ]]; then - echo "timed out waiting for Burrow daemon socket" >&2 - cat "$daemon_log" >&2 || true - exit 1 -fi - -( - cd "$tmpdir" - BURROW_SOCKET_PATH="$socket" "$burrow_bin" network-add 1 3 "$payload" - BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \ - --dns-name selftest.invalid \ - --remote 1.1.1.1:80 \ - --timeout-ms 8000 - BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-udp-echo \ - --message burrow-singmux-udp-selftest \ - --timeout-ms 8000 \ - 9.9.9.9:5353 -) - -wait "$server_pid" -server_pid="" - -echo -echo "Mihomo sing-mux peer observed:" -cat "$result_file" -echo - -uv run python - "$result_file" "$protocol" "$padding" <<'PY' -from __future__ import annotations - -import json -import sys - -with open(sys.argv[1], "r", encoding="utf-8") as f: - result = json.load(f) -expected_protocol = sys.argv[2] -expected_padding = sys.argv[3] == "true" - - -def expect(value: object, expected: object, label: str) -> None: - if value != expected: - raise AssertionError(f"{label}: expected {expected!r}, got {value!r}") - - -def expect_target(key: str, host: str, port: int) -> None: - target = result.get(key) - if not isinstance(target, dict): - raise AssertionError(f"{key}: missing target object") - expect(target.get("host"), host, f"{key}.host") - expect(target.get("port"), port, f"{key}.port") - - -expect(result.get("protocol"), expected_protocol, "sing-mux protocol") -expect(result.get("padding", False), expected_padding, "sing-mux padding") -expect_target("mux_target", "sp.mux.sing-box.arpa", 444) -expect_target("tcp_destination", "selftest.invalid", 80) -expect(result.get("tcp_first_line"), "GET / HTTP/1.1", "tcp first line") -expect_target("udp_destination", "9.9.9.9", 5353) -expect(result.get("udp_payload"), "burrow-singmux-udp-selftest", "udp payload") -expect(result.get("packet_reply_count"), 1, "packet reply count") -padding_label = "padded" if expected_padding else "unpadded" -print(f"Mihomo sing-mux {expected_protocol} {padding_label} interop assertions passed") -PY diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock index f64b325..a1a9ebd 100644 --- a/burrow-gtk/Cargo.lock +++ b/burrow-gtk/Cargo.lock @@ -36,7 +36,18 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", - "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", ] [[package]] @@ -48,76 +59,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloca" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" -dependencies = [ - "cc", -] - -[[package]] -name = "amplify" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f7fb4ac7c881e54a8e7015e399b6112a2a5bc958b6c89ac510840ff20273b31" -dependencies = [ - "amplify_derive", - "amplify_num", - "ascii", - "getrandom 0.2.12", - "getrandom 0.3.3", - "wasm-bindgen", -] - -[[package]] -name = "amplify_derive" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a6309e6b8d89b36b9f959b7a8fa093583b94922a0f6438a24fb08936de4d428" -dependencies = [ - "amplify_syn", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "amplify_num" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afed304556696656d2d71495e1e5f2c4b524a3fb6eb0f2f3778ffc482a40b8a8" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "amplify_syn" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.11" @@ -134,9 +75,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.14" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" @@ -172,123 +113,6 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash 0.5.0", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "arti-client" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89842cae6e3bda0fd128a5c66eb3392ed412065dc698c77d9fcc4b77e4159f2" -dependencies = [ - "async-trait", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "educe", - "fs-mistrust", - "futures", - "hostname-validator", - "humantime", - "humantime-serde", - "libc", - "once_cell", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "thiserror 2.0.16", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-chanmgr", - "tor-circmgr", - "tor-config", - "tor-config-path", - "tor-dircommon", - "tor-dirmgr", - "tor-error", - "tor-guardmgr", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - -[[package]] -name = "asn1-rs" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 2.0.16", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "assert_matches" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" - [[package]] name = "async-channel" version = "2.1.1" @@ -296,40 +120,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 4.0.3", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-compression" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" -dependencies = [ - "flate2", - "futures-core", - "futures-io", - "memchr", - "pin-project-lite", - "xz2", - "zstd 0.13.0", - "zstd-safe 7.0.0", -] - -[[package]] -name = "async-native-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" -dependencies = [ - "futures-util", - "native-tls", - "thiserror 1.0.56", - "url", -] - [[package]] name = "async-stream" version = "0.2.1" @@ -384,49 +180,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async_executors" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a982d2f86de6137cc05c9db9a915a19886c97911f9790d04f174cede74be01a5" -dependencies = [ - "blanket", - "futures-core", - "futures-task", - "futures-util", - "pin-project", - "rustc_version", - "tokio", -] - -[[package]] -name = "asynchronous-codec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" -dependencies = [ - "bytes", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -439,29 +192,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "aws-lc-rs" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" -dependencies = [ - "bindgen 0.69.5", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.7.5" @@ -532,12 +262,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.21.7" @@ -552,19 +276,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "serde", - "unty", -] +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" @@ -611,29 +325,6 @@ dependencies = [ "which", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.11.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -642,21 +333,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blake2" @@ -667,17 +346,6 @@ dependencies = [ "digest", ] -[[package]] -name = "blanket" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "block" version = "0.1.6" @@ -693,26 +361,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata 0.4.14", - "serde", -] - [[package]] name = "bumpalo" version = "3.14.0" @@ -725,14 +373,11 @@ version = "0.1.0" dependencies = [ "aead", "anyhow", - "argon2", - "arti-client", "async-channel", "async-stream 0.2.1", "axum", "base64 0.21.7", "blake2", - "bytes", "caps", "chacha20poly1305", "clap", @@ -740,17 +385,12 @@ dependencies = [ "dotenv", "fehler", "futures", - "hickory-proto", "hmac", "hyper-util", "ip_network", "ip_network_table", - "ipnetwork", - "libc", "libsystemd", "log", - "md-5", - "netstack-smoltcp", "nix 0.27.1", "once_cell", "parking_lot", @@ -762,21 +402,14 @@ dependencies = [ "ring", "rusqlite", "rust-ini", - "rustls", - "schemars 0.8.16", + "schemars", "serde", "serde_json", - "serde_yaml", - "sha2", - "subtle", "tokio", - "tokio-rustls", "tokio-stream", - "tokio-util", - "toml 0.8.23", + "toml", "tonic", "tonic-build", - "tor-rtcompat", "tower", "tracing", "tracing-journald", @@ -784,7 +417,6 @@ dependencies = [ "tracing-oslog", "tracing-subscriber", "tun", - "webpki-roots 0.26.11", "x25519-dalek", ] @@ -797,23 +429,9 @@ dependencies = [ "gettext-rs", "glib-build-tools", "relm4", - "serde", - "serde_json", "tokio", ] -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - [[package]] name = "byteorder" version = "1.5.0" @@ -882,28 +500,14 @@ dependencies = [ "thiserror 1.0.56", ] -[[package]] -name = "caret" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beae2cb9f60bc3f21effaaf9c64e51f6627edd54eedc9199ba07f519ef2a2101" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" -version = "1.2.62" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "find-msvc-tools", "jobserver", "libc", - "shlex", ] [[package]] @@ -961,45 +565,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1024,9 +589,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -1034,23 +599,23 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.106", @@ -1058,29 +623,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "coarsetime" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" -dependencies = [ - "libc", - "wasix", - "wasm-bindgen", -] +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" @@ -1110,12 +655,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-random" version = "0.1.18" @@ -1142,24 +681,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1194,85 +715,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" -dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-cycles-per-byte" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5396de42a52e9e5d8f67ef0702dae30451f310a9ba1c3094dcf228f0be0e54bc" -dependencies = [ - "cfg-if", - "criterion", -] - -[[package]] -name = "criterion-plot" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" -dependencies = [ - "cast", - "itertools 0.13.0", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -1285,18 +727,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -1308,15 +738,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "curve25519-dalek" version = "4.1.1" @@ -1326,7 +747,6 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", "fiat-crypto", "platforms", "rustc_version", @@ -1345,271 +765,13 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "defmt" -version = "0.3.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" -dependencies = [ - "defmt 1.1.0", -] - -[[package]] -name = "defmt" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" -dependencies = [ - "defmt-parser", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.16", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "cookie-factory", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", -] - [[package]] name = "deranged" -version = "0.5.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde_core", -] - -[[package]] -name = "derive-deftly" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284db66a66f03c3dafbe17360d959eb76b83f77cfe191677e2a7899c0da291f3" -dependencies = [ - "derive-deftly-macros", - "heck 0.5.0", -] - -[[package]] -name = "derive-deftly-macros" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caef6056a5788d05d173cdc3c562ac28ae093828f851f69378b74e4e3d578e41" -dependencies = [ - "heck 0.5.0", - "indexmap 2.11.4", - "itertools 0.14.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "sha3", - "strum", - "syn 2.0.106", - "void", -] - -[[package]] -name = "derive_builder_core_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c1b715c79be6328caa9a5e1a387a196ea503740f0722ec3dd8f67a9e72314d" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eae24d595f4d0ecc90a9a5a6d11c2bd8dafe2375ec4a1ec63250e5ade7d228" -dependencies = [ - "derive_builder_macro_fork_arti", -] - -[[package]] -name = "derive_builder_macro_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69887769a2489cd946bf782eb2b1bb2cb7bc88551440c94a765d4f040c08ebf3" -dependencies = [ - "derive_builder_core_fork_arti", - "syn 1.0.109", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.106", - "unicode-xid", ] [[package]] @@ -1619,52 +781,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "dlv-list" version = "0.5.2" @@ -1680,75 +800,11 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" -version = "1.0.20" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "merlin", - "rand_core 0.6.4", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" @@ -1756,25 +812,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1790,64 +827,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "enumset" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1864,15 +843,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "etherparse" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d8a704b617484e9d867a0423cd45f7577f008c4068e2e33378f8d3860a6d73" -dependencies = [ - "arrayvec", -] - [[package]] name = "event-listener" version = "4.0.3" @@ -1884,24 +854,13 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.3", + "event-listener", "pin-project-lite", ] @@ -1919,9 +878,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fehler" @@ -1943,16 +902,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.2.5" @@ -1969,35 +918,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic 0.6.1", - "serde", - "toml 0.8.23", - "uncased", - "version_check", -] - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -2014,12 +934,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fluid-let" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" - [[package]] name = "flume" version = "0.10.14" @@ -2030,7 +944,7 @@ dependencies = [ "futures-sink", "nanorand", "pin-project", - "spin 0.9.8", + "spin", ] [[package]] @@ -2039,18 +953,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "foreign-types" version = "0.3.2" @@ -2068,9 +970,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2081,43 +983,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" -[[package]] -name = "fs-mistrust" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5ac9f88fd18733e0f9ce1f4a95c40eb1d4f83131bf1472e81d1f128fefb7c2" -dependencies = [ - "derive_builder_fork_arti", - "dirs", - "libc", - "pwd-grp", - "serde", - "thiserror 2.0.16", - "walkdir", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fslock" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.30" @@ -2135,9 +1000,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -2145,9 +1010,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" @@ -2162,15 +1027,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -2179,21 +1044,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2203,6 +1068,7 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] @@ -2274,7 +1140,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -2299,36 +1164,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "gettext-rs" version = "0.7.0" @@ -2424,7 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ "anyhow", - "heck 0.4.1", + "heck", "proc-macro-crate", "proc-macro-error", "proc-macro2", @@ -2448,12 +1288,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "glob-match" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" - [[package]] name = "gobject-sys" version = "0.17.10" @@ -2488,17 +1322,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "gsk4" version = "0.6.3" @@ -2625,26 +1448,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2656,42 +1459,23 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash 0.1.5", + "ahash", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" -version = "0.11.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", + "hashbrown 0.14.3", ] [[package]] @@ -2700,52 +1484,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "thiserror 2.0.16", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -2764,12 +1508,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hostname-validator" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" - [[package]] name = "http" version = "0.2.11" @@ -2838,22 +1576,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - [[package]] name = "hyper" version = "0.14.28" @@ -2871,7 +1593,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2957,149 +1679,20 @@ dependencies = [ "hyper 1.6.0", "libc", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" -version = "1.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -3110,7 +1703,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -3120,29 +1712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "inotify" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" -dependencies = [ - "bitflags 2.11.1", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", + "hashbrown 0.16.0", ] [[package]] @@ -3154,22 +1724,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - [[package]] name = "io-uring" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.4.2", "cfg-if", "libc", ] @@ -3202,33 +1763,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "ipnetwork" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" -dependencies = [ - "serde", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -3246,63 +1780,28 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ - "getrandom 0.3.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ - "cfg-if", - "futures-util", "once_cell", "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags 2.11.1", - "libc", -] - [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] [[package]] name = "lazycell" @@ -3310,12 +1809,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libadwaita" version = "0.4.4" @@ -3375,26 +1868,11 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "libc", -] - [[package]] name = "libsqlite3-sys" -version = "0.36.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" dependencies = [ "cc", "pkg-config", @@ -3425,12 +1903,6 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - [[package]] name = "locale_config" version = "0.3.0" @@ -3466,17 +1938,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -3486,12 +1947,6 @@ dependencies = [ "libc", ] -[[package]] -name = "managed" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" - [[package]] name = "matchers" version = "0.1.0" @@ -3507,31 +1962,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.7.1" @@ -3550,18 +1986,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "merlin" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" -dependencies = [ - "byteorder", - "keccak", - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "miette" version = "5.10.0" @@ -3613,7 +2037,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3651,22 +2074,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "netstack-smoltcp" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398691cef792b89eb5d29e6ea6b3999def706b908d355e29815ba8101cf5c4c8" -dependencies = [ - "etherparse", - "futures", - "rand 0.8.5", - "smoltcp", - "spin 0.9.8", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "nix" version = "0.26.4" @@ -3686,7 +2093,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.4.2", "cfg-if", "libc", "memoffset 0.9.0", @@ -3702,47 +2109,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonany" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b8866ec53810a9a4b3d434a29801e78c707430a9ae11c2db4b8b62bb9675a0" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.11.1", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3753,90 +2119,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "objc" version = "0.2.7" @@ -3857,25 +2139,6 @@ dependencies = [ "objc_id", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - [[package]] name = "objc_id" version = "0.1.1" @@ -3896,28 +2159,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.4" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "oneshot-fused-workaround" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17b52d0e4a06a4c7eb8d2943c0015fa628cf4ccc409429cebc0f5bed6d33a82" -dependencies = [ - "futures", -] - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -3931,7 +2175,7 @@ version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -3969,21 +2213,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.7.3" @@ -3994,69 +2223,12 @@ dependencies = [ "hashbrown 0.14.3", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" -dependencies = [ - "memchr", -] - [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p521" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" -dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", - "rand_core 0.6.4", - "sha2", -] - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "pango" version = "0.17.10" @@ -4123,23 +2295,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.11.0" @@ -4148,7 +2303,7 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", - "password-hash 0.4.2", + "password-hash", "sha2", ] @@ -4158,20 +2313,11 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -4183,49 +2329,6 @@ dependencies = [ "indexmap 2.11.4", ] -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.4" @@ -4258,27 +2361,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.29" @@ -4291,34 +2373,6 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -4330,36 +2384,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic 0.5.3", - "crossbeam-queue", - "futures", - "parking_lot", - "pin-project", - "static_assertions", - "thiserror 1.0.56", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -4374,34 +2398,14 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", "syn 2.0.106", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "priority-queue" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" -dependencies = [ - "equivalent", - "indexmap 2.11.4", - "serde", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -4436,28 +2440,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "proc-macro2" version = "1.0.101" @@ -4483,8 +2465,8 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", + "heck", + "itertools", "log", "multimap", "once_cell", @@ -4504,7 +2486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", "syn 2.0.106", @@ -4519,18 +2501,6 @@ dependencies = [ "prost", ] -[[package]] -name = "pwd-grp" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2023f41b5fcb7c30eb5300a5733edfaa9e0e0d502d51b586f65633fd39e40c" -dependencies = [ - "derive-deftly", - "libc", - "paste", - "thiserror 2.0.16", -] - [[package]] name = "quinn" version = "0.11.9" @@ -4544,7 +2514,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.16", "tokio", "tracing", @@ -4581,16 +2551,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -4601,18 +2571,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -4672,46 +2630,6 @@ dependencies = [ "getrandom 0.3.3", ] -[[package]] -name = "rand_jitter" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16df48f071248e67b8fc5e866d9448d45c08ad8b672baaaf796e2f15e606ff0" -dependencies = [ - "libc", - "rand_core 0.9.3", - "winapi", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rdrand" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -4721,47 +2639,16 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.12", - "libredox", - "thiserror 2.0.16", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "regex" -version = "1.12.3" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.14", - "regex-syntax 0.8.10", + "regex-automata 0.4.4", + "regex-syntax 0.8.2", ] [[package]] @@ -4775,13 +2662,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.10", + "regex-syntax 0.8.2", ] [[package]] @@ -4792,9 +2679,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relm4" @@ -4905,25 +2792,6 @@ dependencies = [ "winreg 0.52.0", ] -[[package]] -name = "retry-error" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c322ea521636c5a3f13685a6266055b2dda7e54e2be35214d7c2a5d0672a5db" -dependencies = [ - "humantime", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - [[package]] name = "ring" version = "0.17.7" @@ -4933,56 +2801,23 @@ dependencies = [ "cc", "getrandom 0.2.12", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.48.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rsqlite-vfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.16", -] - [[package]] name = "rusqlite" -version = "0.38.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.4.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", - "sqlite-wasm-rs", - "time", ] [[package]] @@ -5022,22 +2857,13 @@ dependencies = [ "semver", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - [[package]] name = "rustix" version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -5050,8 +2876,6 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -5081,11 +2905,10 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5103,43 +2926,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" -[[package]] -name = "safelog" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a907e0d82c61b1b06a2030c968eb313dcf432686b77801a26bc4b206f96573" -dependencies = [ - "derive_more", - "educe", - "either", - "fluid-let", - "thiserror 2.0.16", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "sanitize-filename" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" -dependencies = [ - "regex", -] - -[[package]] -name = "saturating-time" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63583a1dd0647d1484228529ab4ecaa874048d2956f117362aa5f5826456230" - [[package]] name = "schannel" version = "0.1.23" @@ -5161,30 +2947,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - [[package]] name = "schemars_derive" version = "0.8.16" @@ -5203,20 +2965,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - [[package]] name = "security-framework" version = "2.9.2" @@ -5248,38 +2996,28 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ "serde_core", "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -5297,27 +3035,15 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "serde_ignored" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", - "memchr", + "ryu", "serde", - "serde_core", - "zmij", ] [[package]] @@ -5340,15 +3066,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5361,51 +3078,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.4", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.11.4", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha-1" version = "0.10.1" @@ -5439,16 +3111,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha3" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest", - "keccak", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -5458,17 +3120,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shellexpand" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" -dependencies = [ - "bstr", - "dirs", - "os_str_bytes", -] - [[package]] name = "shlex" version = "1.3.0" @@ -5484,22 +3135,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - [[package]] name = "slab" version = "0.4.9" @@ -5509,29 +3144,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "serde", - "version_check", -] - -[[package]] -name = "slotmap-careful" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed92816c1fbb29891a525b92d5fa95757c9dee47044f76c8e06ceb1e052a8d64" -dependencies = [ - "paste", - "serde", - "slotmap", - "thiserror 2.0.16", - "void", -] - [[package]] name = "smallvec" version = "1.13.1" @@ -5539,18 +3151,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] -name = "smoltcp" -version = "0.12.0" +name = "socket2" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ - "bitflags 1.3.2", - "byteorder", - "cfg-if", - "defmt 0.3.100", - "heapless", - "log", - "managed", + "libc", + "winapi", ] [[package]] @@ -5563,22 +3170,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -5588,70 +3179,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlite-wasm-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] -name = "ssh-cipher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" -dependencies = [ - "cipher", - "ssh-encoding", -] - -[[package]] -name = "ssh-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" -dependencies = [ - "base64ct", - "pem-rfc7468", - "sha2", -] - -[[package]] -name = "ssh-key" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" -dependencies = [ - "num-bigint-dig", - "p256", - "p384", - "p521", - "rand_core 0.6.4", - "rsa", - "sec1", - "sha2", - "signature", - "ssh-cipher", - "ssh-encoding", - "subtle", - "zeroize", -] - [[package]] name = "ssri" version = "9.2.0" @@ -5668,56 +3195,17 @@ dependencies = [ "xxhash-rust", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "subtle" -version = "2.6.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -5747,31 +3235,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sysinfo" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows 0.61.3", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -5800,18 +3263,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ "cfg-expr", - "heck 0.4.1", + "heck", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.13" @@ -5889,35 +3346,21 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", - "itoa", - "js-sys", - "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", - "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tiny-keccak" @@ -5928,27 +3371,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -6028,16 +3450,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -6047,26 +3469,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_edit 0.22.27", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.11.4", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.13", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -6076,15 +3483,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.19.15" @@ -6092,7 +3490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.11.4", - "toml_datetime 0.6.11", + "toml_datetime", "winnow 0.5.34", ] @@ -6104,33 +3502,18 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.11.4", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_write", "winnow 0.7.13", ] -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tonic" version = "0.12.3" @@ -6175,959 +3558,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "tor-async-utils" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895c61a46909134501c6815eceeb66c9c80fc494ee89429821b0f05ccf34b4f5" -dependencies = [ - "derive-deftly", - "educe", - "futures", - "oneshot-fused-workaround", - "pin-project", - "postage", - "thiserror 2.0.16", - "void", -] - -[[package]] -name = "tor-basic-utils" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac6e4d7e131b7d69bc85558383cd4ac61e46b4dd0d4ed51632f28fac98cac0c" -dependencies = [ - "derive_more", - "hex", - "itertools 0.14.0", - "libc", - "paste", - "rand 0.9.2", - "rand_chacha 0.9.0", - "serde", - "slab", - "smallvec", - "thiserror 2.0.16", - "weak-table", -] - -[[package]] -name = "tor-bytes" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64454947258e49f238a5f06a06250a0c54598a1c7409898b5c79505e6a99e7af" -dependencies = [ - "bytes", - "derive-deftly", - "digest", - "educe", - "getrandom 0.4.2", - "safelog", - "thiserror 2.0.16", - "tor-error", - "tor-llcrypto", - "zeroize", -] - -[[package]] -name = "tor-cell" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab0c79bc92a957e85959cf397a2d8f9c8294c35fa4f65247a9393b20ac95551" -dependencies = [ - "amplify", - "bitflags 2.11.1", - "bytes", - "caret", - "derive-deftly", - "derive_more", - "educe", - "itertools 0.14.0", - "paste", - "rand 0.9.2", - "smallvec", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-bytes", - "tor-cert", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-protover", - "tor-units", - "void", -] - -[[package]] -name = "tor-cert" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debc911738298ee801fce4577c36a50c55295b0bb9c5519461b83cc486a1f86e" -dependencies = [ - "caret", - "derive_builder_fork_arti", - "derive_more", - "digest", - "thiserror 2.0.16", - "tor-bytes", - "tor-checkable", - "tor-error", - "tor-llcrypto", -] - -[[package]] -name = "tor-chanmgr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af5b7c2f1e16d1304b5185fcbc91ca5c8df991c21be00702f925f055573eea1" -dependencies = [ - "async-trait", - "caret", - "cfg-if", - "derive-deftly", - "derive_more", - "educe", - "futures", - "oneshot-fused-workaround", - "percent-encoding", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "serde_with", - "thiserror 2.0.16", - "tor-async-utils", - "tor-basic-utils", - "tor-cell", - "tor-config", - "tor-error", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-netdir", - "tor-proto", - "tor-rtcompat", - "tor-socksproto", - "tor-units", - "tracing", - "url", - "void", -] - -[[package]] -name = "tor-checkable" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b13a5b50bb55037f2e81b25dde42f420d57c75154216b8ef989006cea3ebee" -dependencies = [ - "humantime", - "signature", - "thiserror 2.0.16", - "tor-llcrypto", -] - -[[package]] -name = "tor-circmgr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b878f3f7c6be0c7f130d90b347ada2e7c46519dfbdde8273eae2e5d1792caa87" -dependencies = [ - "amplify", - "async-trait", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "downcast-rs", - "dyn-clone", - "educe", - "futures", - "humantime-serde", - "itertools 0.14.0", - "once_cell", - "oneshot-fused-workaround", - "pin-project", - "rand 0.9.2", - "retry-error", - "safelog", - "serde", - "thiserror 2.0.16", - "tor-async-utils", - "tor-basic-utils", - "tor-cell", - "tor-chanmgr", - "tor-config", - "tor-dircommon", - "tor-error", - "tor-guardmgr", - "tor-linkspec", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-relay-selection", - "tor-rtcompat", - "tor-units", - "tracing", - "void", - "weak-table", -] - -[[package]] -name = "tor-config" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc74a00ab15bb986e3747c6969e40a58a63065d6f99077e7ee2f4657bb8b03" -dependencies = [ - "amplify", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "educe", - "either", - "figment", - "fs-mistrust", - "futures", - "humantime-serde", - "itertools 0.14.0", - "notify", - "paste", - "postage", - "regex", - "serde", - "serde-value", - "serde_ignored", - "strum", - "thiserror 2.0.16", - "toml 0.9.12+spec-1.1.0", - "tor-basic-utils", - "tor-error", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "tor-config-path" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3005ab7b9a26a7271e5adf3dfb4ae18c09a943e32aeccc4f6d1c53a60de74b8d" -dependencies = [ - "directories", - "serde", - "shellexpand", - "thiserror 2.0.16", - "tor-error", - "tor-general-addr", -] - -[[package]] -name = "tor-consdiff" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bfa2b7b71c72830f61c48da4bb3e13191e0c0e1404b9c5168c795e4f5feb4a8" -dependencies = [ - "digest", - "hex", - "thiserror 2.0.16", - "tor-llcrypto", -] - -[[package]] -name = "tor-dirclient" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccd6fac844ac77c33ccdfcb56bf23ff40ebbb821ea708be79a481ec30e8c39c" -dependencies = [ - "async-compression", - "base64ct", - "derive_more", - "futures", - "hex", - "http 1.3.1", - "httparse", - "httpdate", - "itertools 0.14.0", - "memchr", - "thiserror 2.0.16", - "tor-circmgr", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-netdoc", - "tor-proto", - "tor-rtcompat", - "tracing", -] - -[[package]] -name = "tor-dircommon" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cf39a3c30321d145a4d60753ae7ef5bb58a66a00ac9e2bfc30bd823faf2a4" -dependencies = [ - "base64ct", - "derive-deftly", - "getset", - "humantime", - "humantime-serde", - "serde", - "tor-basic-utils", - "tor-checkable", - "tor-config", - "tor-linkspec", - "tor-llcrypto", - "tor-netdoc", - "tracing", -] - -[[package]] -name = "tor-dirmgr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52919aa9dbb82a354c5b904bef82e91beb702b9f8ad14e6eac4237d6128bf67" -dependencies = [ - "async-trait", - "base64ct", - "derive_builder_fork_arti", - "derive_more", - "digest", - "educe", - "event-listener 5.4.1", - "fs-mistrust", - "fslock", - "futures", - "hex", - "humantime", - "humantime-serde", - "itertools 0.14.0", - "memmap2", - "oneshot-fused-workaround", - "paste", - "postage", - "rand 0.9.2", - "rusqlite", - "safelog", - "scopeguard", - "serde", - "serde_json", - "signature", - "static_assertions", - "strum", - "thiserror 2.0.16", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-checkable", - "tor-circmgr", - "tor-config", - "tor-consdiff", - "tor-dirclient", - "tor-dircommon", - "tor-error", - "tor-guardmgr", - "tor-llcrypto", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-rtcompat", - "tracing", -] - -[[package]] -name = "tor-error" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595b005e6f571ac3890a34a00f361200aab781fd0218f2c528c86fc7af088df5" -dependencies = [ - "derive_more", - "futures", - "paste", - "retry-error", - "static_assertions", - "strum", - "thiserror 2.0.16", - "tracing", - "void", -] - -[[package]] -name = "tor-general-addr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727b8c8bc01c1587486055edab5c2cd0d5c960f5bb3fac796fc9911872b8b397" -dependencies = [ - "derive_more", - "thiserror 2.0.16", - "void", -] - -[[package]] -name = "tor-guardmgr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d337f465a477c0fb3b2faafa4654d70ff9df3590e57d22707591dddb4e4450c1" -dependencies = [ - "amplify", - "base64ct", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "dyn-clone", - "educe", - "futures", - "humantime", - "humantime-serde", - "itertools 0.14.0", - "num_enum", - "oneshot-fused-workaround", - "pin-project", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "strum", - "thiserror 2.0.16", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-dircommon", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-relay-selection", - "tor-rtcompat", - "tor-units", - "tracing", -] - -[[package]] -name = "tor-hscrypto" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3693cd43f05cd01ac0aaa060dae5c5e53c4364f89e0d769e33cd629a2fd3118" -dependencies = [ - "data-encoding", - "derive-deftly", - "derive_more", - "digest", - "hex", - "humantime", - "itertools 0.14.0", - "paste", - "rand 0.9.2", - "safelog", - "serde", - "signature", - "subtle", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-bytes", - "tor-error", - "tor-key-forge", - "tor-llcrypto", - "tor-units", - "void", -] - -[[package]] -name = "tor-key-forge" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ade9065ae49cfe2ab020ca9ca9f2b3c5c9b5fc0d8980fa681d8b3a0668e042f" -dependencies = [ - "derive-deftly", - "derive_more", - "downcast-rs", - "paste", - "rand 0.9.2", - "rsa", - "signature", - "ssh-key", - "thiserror 2.0.16", - "tor-bytes", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-llcrypto", -] - -[[package]] -name = "tor-keymgr" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243c3163d376c4723cd67271fcd6e5d6b498a6865c6b98299640e1be01c38826" -dependencies = [ - "amplify", - "arrayvec", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "downcast-rs", - "dyn-clone", - "fs-mistrust", - "glob-match", - "humantime", - "inventory", - "itertools 0.14.0", - "rand 0.9.2", - "safelog", - "serde", - "signature", - "ssh-key", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-config-path", - "tor-error", - "tor-hscrypto", - "tor-key-forge", - "tor-llcrypto", - "tor-persist", - "tracing", - "visibility", - "walkdir", - "zeroize", -] - -[[package]] -name = "tor-linkspec" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f1ea8786900d6fbe4c9f775d341b1ba01bbd1f750d89bd63be78b6b01e1836" -dependencies = [ - "base64ct", - "by_address", - "caret", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "hex", - "itertools 0.14.0", - "safelog", - "serde", - "serde_with", - "strum", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-llcrypto", - "tor-memquota", - "tor-protover", -] - -[[package]] -name = "tor-llcrypto" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6989a1c6d06ffd6835e2917edaae4aeef544f8e5fdd68b54cc365f2af523de" -dependencies = [ - "aes", - "base64ct", - "ctr", - "curve25519-dalek", - "der-parser", - "derive-deftly", - "derive_more", - "digest", - "ed25519-dalek", - "educe", - "getrandom 0.4.2", - "hex", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_core 0.6.4", - "rand_core 0.9.3", - "rand_jitter", - "rdrand", - "rsa", - "safelog", - "serde", - "sha1", - "sha2", - "sha3", - "signature", - "subtle", - "thiserror 2.0.16", - "tor-error", - "tor-memquota-cost", - "visibility", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "tor-log-ratelim" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1cd642180923d12e3fab5996b4aa2189718da7f465df6eb196ce2b9c70e293" -dependencies = [ - "futures", - "humantime", - "thiserror 2.0.16", - "tor-error", - "tor-rtcompat", - "tracing", - "weak-table", -] - -[[package]] -name = "tor-memquota" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599daea60fd3272eb72a795d1c593b45bbe15343cbc702340a81db124c06eed5" -dependencies = [ - "cfg-if", - "derive-deftly", - "derive_more", - "dyn-clone", - "educe", - "futures", - "itertools 0.14.0", - "paste", - "pin-project", - "serde", - "slotmap-careful", - "static_assertions", - "sysinfo", - "thiserror 2.0.16", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", - "tor-log-ratelim", - "tor-memquota-cost", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "tor-memquota-cost" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd92b07c0fc24e6d8166a5ff45e5b8654e68d89743c46d01889a16ab74c0b578" -dependencies = [ - "derive-deftly", - "itertools 0.14.0", - "paste", - "void", -] - -[[package]] -name = "tor-netdir" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41be8f47f521fc95206d2ba5facac8fb1a5b5b82169bd41ebeecdf46d1e77246" -dependencies = [ - "async-trait", - "bitflags 2.11.1", - "derive_more", - "futures", - "humantime", - "itertools 0.14.0", - "num_enum", - "rand 0.9.2", - "serde", - "strum", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-netdoc", - "tor-protover", - "tor-units", - "tracing", - "typed-index-collections", -] - -[[package]] -name = "tor-netdoc" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8bce73d2c78bd78a2a927336ca639cf6bd5d8ad092ebcd0b3fdeaa47dcc77e" -dependencies = [ - "amplify", - "base64ct", - "cipher", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "digest", - "educe", - "enumset", - "hex", - "humantime", - "itertools 0.14.0", - "memchr", - "paste", - "phf", - "saturating-time", - "serde", - "serde_with", - "signature", - "smallvec", - "strum", - "subtle", - "thiserror 2.0.16", - "time", - "tinystr", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-llcrypto", - "tor-protover", - "void", - "zeroize", -] - -[[package]] -name = "tor-persist" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507ab4b6a3d59ed0df5804eeed66dcacde75e3be13d3694216cdfdb666bce625" -dependencies = [ - "derive-deftly", - "derive_more", - "filetime", - "fs-mistrust", - "fslock", - "futures", - "itertools 0.14.0", - "oneshot-fused-workaround", - "paste", - "sanitize-filename", - "serde", - "serde_json", - "thiserror 2.0.16", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-error", - "tracing", - "void", -] - -[[package]] -name = "tor-proto" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc552d535d36539d5782bb02028590bc472d219e49da51a96810725e80ff56" -dependencies = [ - "amplify", - "async-trait", - "asynchronous-codec", - "bitvec", - "bytes", - "caret", - "cfg-if", - "cipher", - "coarsetime", - "criterion-cycles-per-byte", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more", - "digest", - "educe", - "enum_dispatch", - "futures", - "futures-util", - "hkdf", - "hmac", - "itertools 0.14.0", - "nonany", - "oneshot-fused-workaround", - "pin-project", - "postage", - "rand 0.9.2", - "rand_core 0.9.3", - "safelog", - "slotmap-careful", - "smallvec", - "static_assertions", - "subtle", - "sync_wrapper", - "thiserror 2.0.16", - "tokio", - "tokio-util", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-cert", - "tor-checkable", - "tor-config", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-log-ratelim", - "tor-memquota", - "tor-protover", - "tor-relay-crypto", - "tor-rtcompat", - "tor-rtmock", - "tor-units", - "tracing", - "typenum", - "visibility", - "void", - "zeroize", -] - -[[package]] -name = "tor-protover" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed88527d070c4b7ea4e55a36d2d56d0500e30ca66298b5264f047f7f2f89cfa" -dependencies = [ - "caret", - "paste", - "serde_with", - "thiserror 2.0.16", - "tor-basic-utils", - "tor-bytes", -] - -[[package]] -name = "tor-relay-crypto" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e57e9f71b22ae1df63dbccc8e428cb07feec0abd654735109fa563c10bbb90" -dependencies = [ - "derive-deftly", - "derive_more", - "humantime", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-key-forge", - "tor-keymgr", - "tor-llcrypto", - "tor-persist", -] - -[[package]] -name = "tor-relay-selection" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a372072ac9dea7d17e49693cc3f3ae77b3abf8125630516c9f2d622239b1920a" -dependencies = [ - "rand 0.9.2", - "serde", - "tor-basic-utils", - "tor-linkspec", - "tor-netdir", - "tor-netdoc", -] - -[[package]] -name = "tor-rtcompat" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14428b930e59003e801c0c32697c0aeb9b0495ad33ecbe8c6753bdb596233270" -dependencies = [ - "async-native-tls", - "async-trait", - "async_executors", - "asynchronous-codec", - "cfg-if", - "coarsetime", - "derive_more", - "dyn-clone", - "educe", - "futures", - "hex", - "libc", - "native-tls", - "paste", - "pin-project", - "socket2 0.6.3", - "thiserror 2.0.16", - "tokio", - "tokio-util", - "tor-error", - "tor-general-addr", - "tracing", - "void", - "zeroize", -] - -[[package]] -name = "tor-rtmock" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2da91a432cdaee8a93e0bb21b02f3e9c7667832ccbb4b54e00d9c1214638e70" -dependencies = [ - "amplify", - "assert_matches", - "async-trait", - "derive-deftly", - "derive_more", - "educe", - "futures", - "humantime", - "itertools 0.14.0", - "oneshot-fused-workaround", - "pin-project", - "priority-queue", - "slotmap-careful", - "strum", - "thiserror 2.0.16", - "tor-error", - "tor-general-addr", - "tor-rtcompat", - "tracing", - "tracing-test", - "void", -] - -[[package]] -name = "tor-socksproto" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adbc9115a2f506d9bb86ae4446f0ca70eb523dc2f5e900a33582e7c39decc23a" -dependencies = [ - "amplify", - "caret", - "derive-deftly", - "educe", - "safelog", - "subtle", - "thiserror 2.0.16", - "tor-bytes", - "tor-error", -] - -[[package]] -name = "tor-units" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da90e93b4b4aa4ec356ecbe9e19aced36fdd655e94ca459d1915120d873363f0" -dependencies = [ - "derive-deftly", - "derive_more", - "serde", - "thiserror 2.0.16", - "tor-memquota", -] - [[package]] name = "tower" version = "0.4.13" @@ -7162,9 +3592,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.44" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -7174,9 +3604,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -7185,9 +3615,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7259,27 +3689,6 @@ dependencies = [ "tracing-log 0.2.0", ] -[[package]] -name = "tracing-test" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracing-test-macro", -] - -[[package]] -name = "tracing-test-macro" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" -dependencies = [ - "quote", - "syn 2.0.106", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -7301,7 +3710,7 @@ dependencies = [ "log", "nix 0.26.4", "reqwest 0.11.23", - "schemars 0.8.16", + "schemars", "serde", "socket2 0.5.10", "ssri", @@ -7309,20 +3718,10 @@ dependencies = [ "tokio", "tracing", "widestring", - "windows 0.48.0", + "windows", "zip", ] -[[package]] -name = "typed-index-collections" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" -dependencies = [ - "bincode", - "serde", -] - [[package]] name = "typenum" version = "1.17.0" @@ -7330,13 +3729,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "uncased" -version = "0.9.10" +name = "unicode-bidi" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -7345,10 +3741,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "unicode-segmentation" -version = "1.13.2" +name = "unicode-normalization" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-width" @@ -7356,12 +3755,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" version = "0.5.1" @@ -7372,42 +3765,23 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - [[package]] name = "url" -version = "2.5.8" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.1" @@ -7447,33 +3821,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "visibility" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -7504,32 +3851,14 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" -dependencies = [ - "wasi 0.11.0+wasi-snapshot-preview1", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", @@ -7538,6 +3867,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + [[package]] name = "wasm-bindgen-futures" version = "0.4.40" @@ -7552,9 +3895,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7562,66 +3905,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ - "bumpalo", "proc-macro2", "quote", "syn 2.0.106", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.11.4", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.11.4", - "semver", -] - -[[package]] -name = "weak-table" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" - [[package]] name = "web-sys" version = "0.3.67" @@ -7694,15 +3997,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -7718,145 +4012,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -7875,24 +4030,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -7923,32 +4060,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7961,12 +4072,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7979,12 +4084,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7997,18 +4096,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8021,12 +4108,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8039,12 +4120,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8057,12 +4132,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8075,12 +4144,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.34" @@ -8099,12 +4162,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - [[package]] name = "winreg" version = "0.50.0" @@ -8131,109 +4188,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.11.4", - "prettyplease", - "syn 2.0.106", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.106", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.11.4", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.11.4", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x25519-dalek" version = "2.0.0" @@ -8252,38 +4206,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.27" @@ -8304,27 +4226,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - [[package]] name = "zeroize" version = "1.7.0" @@ -8345,40 +4246,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "zip" version = "0.6.6" @@ -8396,31 +4263,16 @@ dependencies = [ "pbkdf2", "sha1", "time", - "zstd 0.11.2+zstd.1.5.2", + "zstd", ] -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe 5.0.2+zstd.1.5.2", -] - -[[package]] -name = "zstd" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" -dependencies = [ - "zstd-safe 7.0.0", + "zstd-safe", ] [[package]] @@ -8433,15 +4285,6 @@ dependencies = [ "zstd-sys", ] -[[package]] -name = "zstd-safe" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" -dependencies = [ - "zstd-sys", -] - [[package]] name = "zstd-sys" version = "2.0.9+zstd.1.5.5" diff --git a/burrow-gtk/src/components/home_screen.rs b/burrow-gtk/src/components/home_screen.rs index 655cad3..0bfdda2 100644 --- a/burrow-gtk/src/components/home_screen.rs +++ b/burrow-gtk/src/components/home_screen.rs @@ -1,6 +1,6 @@ use super::*; use crate::account_store::{self, AccountKind, AccountRecord}; -use std::{cell::RefCell, rc::Rc, time::Duration}; +use std::time::Duration; pub struct HomeScreen { daemon_banner: adw::Banner, @@ -23,7 +23,6 @@ pub enum HomeScreenMsg { OpenWireGuard, OpenTor, OpenTailnet, - OpenProxySubscription, AddWireGuard { title: String, account: String, @@ -53,14 +52,6 @@ pub enum HomeScreenMsg { hostname: Option, tailnet: Option, }, - PreviewProxySubscription { - url: String, - }, - AddProxySubscription { - url: String, - name: String, - selected_ordinal: Option, - }, } #[relm4::component(pub, async)] @@ -243,7 +234,7 @@ impl AsyncComponent for HomeScreen { configure_add_popover(&widgets.add_button, &sender); let refresh_sender = sender.input_sender().clone(); - gtk::glib::MainContext::default().spawn_local(async move { + relm4::spawn(async move { loop { tokio::time::sleep(Duration::from_secs(5)).await; refresh_sender.emit(HomeScreenMsg::Refresh); @@ -281,7 +272,6 @@ impl AsyncComponent for HomeScreen { HomeScreenMsg::OpenWireGuard => open_wireguard_window(root, &sender), HomeScreenMsg::OpenTor => open_tor_window(root, &sender), HomeScreenMsg::OpenTailnet => open_tailnet_window(root, &sender), - HomeScreenMsg::OpenProxySubscription => open_proxy_subscription_window(root, &sender), HomeScreenMsg::AddWireGuard { title, account, @@ -314,13 +304,6 @@ impl AsyncComponent for HomeScreen { self.add_tailnet(authority, account, identity, hostname, tailnet) .await; } - HomeScreenMsg::PreviewProxySubscription { url } => { - self.preview_proxy_subscription(url).await; - } - HomeScreenMsg::AddProxySubscription { url, name, selected_ordinal } => { - self.add_proxy_subscription(url, name, selected_ordinal) - .await; - } } } } @@ -684,85 +667,6 @@ impl HomeScreen { } } - async fn preview_proxy_subscription(&mut self, url: String) { - let Ok(url) = daemon_api::require_value(&url, "Subscription URL") else { - self.network_status - .set_label("Enter a subscription URL before previewing."); - return; - }; - - self.network_status - .set_label("Previewing proxy subscription..."); - match daemon_api::preview_proxy_subscription(url).await { - Ok(preview) => { - let first_nodes = preview - .nodes - .iter() - .take(3) - .map(|node| { - let warning = if node.warnings.is_empty() { - String::new() - } else { - format!(" - {}", node.warnings.join(", ")) - }; - format!( - "{}: {} {}:{}{}", - node.protocol, node.name, node.server, node.port, warning - ) - }) - .collect::>() - .join("\n"); - let warnings = if preview.warnings.is_empty() { - String::new() - } else { - format!("\n{}", preview.warnings.join("\n")) - }; - self.network_status.set_label(&format!( - "Found {} compatible nodes and {} rejected entries in {}. Suggested name: {}.{}{}", - preview.compatible_count, - preview.rejected_count, - preview.detected_format, - preview.suggested_name, - warnings, - if first_nodes.is_empty() { - String::new() - } else { - format!("\n{first_nodes}") - } - )); - } - Err(error) => self - .network_status - .set_label(&format!("Proxy subscription preview failed: {error}")), - } - } - - async fn add_proxy_subscription( - &mut self, - url: String, - name: String, - selected_ordinal: Option, - ) { - let Ok(url) = daemon_api::require_value(&url, "Subscription URL") else { - self.network_status - .set_label("Enter a subscription URL before importing."); - return; - }; - - self.network_status - .set_label("Importing proxy subscription..."); - match daemon_api::add_proxy_subscription(url, name, selected_ordinal).await { - Ok(id) => { - self.network_status - .set_label(&format!("Imported proxy subscription network #{id}.")); - self.refresh().await; - } - Err(error) => self - .network_status - .set_label(&format!("Unable to import proxy subscription: {error}")), - } - } - fn apply_login_status(&mut self, status: &daemon_api::TailnetLoginStatus) { self.tailnet_session_id = Some(status.session_id.clone()); self.tailnet_running = status.running; @@ -831,10 +735,6 @@ fn configure_add_popover(button: >k::MenuButton, sender: &AsyncComponentSender for (label, msg) in [ ("Add WireGuard Network", HomeScreenMsg::OpenWireGuard), - ( - "Import Proxy Subscription", - HomeScreenMsg::OpenProxySubscription, - ), ("Save Tor Account", HomeScreenMsg::OpenTor), ("Add Tailnet Account", HomeScreenMsg::OpenTailnet), ] { @@ -855,7 +755,6 @@ fn msg_from_template(msg: &HomeScreenMsg) -> HomeScreenMsg { HomeScreenMsg::OpenWireGuard => HomeScreenMsg::OpenWireGuard, HomeScreenMsg::OpenTor => HomeScreenMsg::OpenTor, HomeScreenMsg::OpenTailnet => HomeScreenMsg::OpenTailnet, - HomeScreenMsg::OpenProxySubscription => HomeScreenMsg::OpenProxySubscription, _ => unreachable!(), } } @@ -1192,169 +1091,6 @@ fn open_tailnet_window(root: >k::ScrolledWindow, sender: &AsyncComponentSender window.present(); } -fn open_proxy_subscription_window( - root: >k::ScrolledWindow, - sender: &AsyncComponentSender, -) { - let window = sheet_window(root, "Proxy Subscription", 560, 520); - let content = sheet_content( - &window, - "Import Proxy Subscription", - "Import Trojan and Shadowsocks nodes through the Burrow daemon.", - ); - - let url = gtk::Entry::new(); - url.set_placeholder_text(Some("Subscription URL")); - let name = gtk::Entry::new(); - name.set_placeholder_text(Some("Name")); - let node_list = gtk::ListBox::new(); - node_list.set_selection_mode(gtk::SelectionMode::Single); - node_list.set_sensitive(false); - node_list.add_css_class("boxed-list"); - let node_scroll = gtk::ScrolledWindow::builder() - .min_content_height(220) - .vexpand(true) - .child(&node_list) - .build(); - let preview_status = gtk::Label::new(Some("Preview the subscription to choose a node.")); - preview_status.add_css_class("dim-label"); - preview_status.set_xalign(0.0); - preview_status.set_wrap(true); - let selected_ordinal = Rc::new(RefCell::new(None::)); - let node_ordinals = Rc::new(RefCell::new(Vec::::new())); - - content.append(§ion_label("Subscription")); - content.append(&url); - content.append(&name); - content.append(§ion_label("Node")); - content.append(&node_scroll); - content.append(&preview_status); - - let actions = gtk::Box::new(gtk::Orientation::Horizontal, 8); - let preview = gtk::Button::with_label("Preview"); - let import = gtk::Button::with_label("Import"); - import.add_css_class("suggested-action"); - actions.append(&preview); - actions.append(&import); - content.append(&actions); - - let url_for_preview = url.clone(); - let name_for_preview = name.clone(); - let list_for_preview = node_list.clone(); - let status_for_preview = preview_status.clone(); - let selected_for_preview = selected_ordinal.clone(); - let ordinals_for_preview = node_ordinals.clone(); - preview.connect_clicked(move |_| { - let url = url_for_preview.text().to_string(); - let name = name_for_preview.clone(); - let list = list_for_preview.clone(); - let status = status_for_preview.clone(); - let selected = selected_for_preview.clone(); - let ordinals = ordinals_for_preview.clone(); - status.set_label("Previewing proxy subscription..."); - while let Some(child) = list.first_child() { - list.remove(&child); - } - list.set_sensitive(false); - *selected.borrow_mut() = None; - ordinals.borrow_mut().clear(); - gtk::glib::MainContext::default().spawn_local(async move { - match daemon_api::preview_proxy_subscription(url).await { - Ok(preview) => { - if name.text().trim().is_empty() { - name.set_text(&preview.suggested_name); - } - for node in preview.nodes.iter().filter(|node| node.runtime_supported) { - ordinals.borrow_mut().push(node.ordinal); - let row = gtk::ListBoxRow::new(); - row.set_selectable(true); - row.set_activatable(true); - - let row_content = gtk::Box::new(gtk::Orientation::Vertical, 2); - row_content.set_margin_top(8); - row_content.set_margin_bottom(8); - row_content.set_margin_start(10); - row_content.set_margin_end(10); - - let title = gtk::Label::new(Some(&format!( - "{} {}", - node.protocol.to_uppercase(), - node.name - ))); - title.set_xalign(0.0); - title.set_ellipsize(gtk::pango::EllipsizeMode::End); - let endpoint = gtk::Label::new(Some(&format!("{}:{}", node.server, node.port))); - endpoint.add_css_class("dim-label"); - endpoint.set_xalign(0.0); - endpoint.set_ellipsize(gtk::pango::EllipsizeMode::Middle); - - row_content.append(&title); - row_content.append(&endpoint); - row.set_child(Some(&row_content)); - list.append(&row); - } - if let Some(first) = preview.nodes.iter().find(|node| node.runtime_supported) { - if let Some(row) = list.row_at_index(0) { - list.select_row(Some(&row)); - } - list.set_sensitive(true); - *selected.borrow_mut() = Some(first.ordinal); - } - let unsupported = preview - .nodes - .iter() - .filter(|node| !node.runtime_supported) - .count(); - let supported = preview.nodes.len().saturating_sub(unsupported); - let warnings = if preview.warnings.is_empty() { - String::new() - } else { - format!(" {}", preview.warnings.join(" ")) - }; - status.set_label(&format!( - "Found {} runtime-supported nodes, {} unsupported nodes, and {} rejected entries in {}.{}", - supported, - unsupported, - preview.rejected_count, - preview.detected_format, - warnings - )); - } - Err(error) => { - status.set_label(&format!("Proxy subscription preview failed: {error}")); - } - } - }); - }); - - let selected_for_list = selected_ordinal.clone(); - let ordinals_for_list = node_ordinals.clone(); - node_list.connect_row_selected(move |_, row| { - *selected_for_list.borrow_mut() = row.and_then(|row| { - let index = row.index(); - if index < 0 { - return None; - } - ordinals_for_list.borrow().get(index as usize).copied() - }); - }); - - let input = sender.input_sender().clone(); - let window_for_click = window.clone(); - let selected_for_import = selected_ordinal.clone(); - import.connect_clicked(move |_| { - input.emit(HomeScreenMsg::AddProxySubscription { - url: url.text().to_string(), - name: name.text().to_string(), - selected_ordinal: *selected_for_import.borrow(), - }); - window_for_click.close(); - }); - - window.set_child(Some(&content)); - window.present(); -} - fn sheet_window(root: >k::ScrolledWindow, title: &str, width: i32, height: i32) -> gtk::Window { let window = gtk::Window::builder() .title(title) diff --git a/burrow-gtk/src/daemon_api.rs b/burrow-gtk/src/daemon_api.rs index 1a163bf..4ff8bf5 100644 --- a/burrow-gtk/src/daemon_api.rs +++ b/burrow-gtk/src/daemon_api.rs @@ -4,9 +4,7 @@ use burrow::{ grpc_defs::{ Empty, Network, NetworkType, State, TailnetDiscoverRequest, TailnetLoginCancelRequest, TailnetLoginStartRequest, TailnetLoginStatusRequest, TailnetProbeRequest, - ProxySubscriptionApplyRequest, ProxySubscriptionImportRequest, }, - proxy_subscription::ProxySubscriptionPayload, BurrowClient, }; use std::{path::PathBuf, sync::OnceLock}; @@ -56,27 +54,6 @@ pub struct TailnetLoginStatus { pub health: Vec, } -#[derive(Debug, Clone)] -pub struct ProxySubscriptionPreview { - pub suggested_name: String, - pub detected_format: String, - pub compatible_count: i32, - pub rejected_count: i32, - pub nodes: Vec, - pub warnings: Vec, -} - -#[derive(Debug, Clone)] -pub struct ProxyNodePreview { - pub ordinal: i32, - pub name: String, - pub protocol: String, - pub server: String, - pub port: i32, - pub warnings: Vec, - pub runtime_supported: bool, -} - pub fn default_tailnet_authority() -> &'static str { MANAGED_TAILSCALE_AUTHORITY } @@ -239,63 +216,6 @@ pub async fn add_tailnet( add_network(NetworkType::Tailnet, payload).await } -pub async fn preview_proxy_subscription(url: String) -> Result { - let mut client = BurrowClient::from_uds().await?; - let response = timeout( - Duration::from_secs(20), - client - .proxy_subscription_client - .preview_import(ProxySubscriptionImportRequest { url }), - ) - .await - .context("timed out previewing proxy subscription")?? - .into_inner(); - - Ok(ProxySubscriptionPreview { - suggested_name: response.suggested_name, - detected_format: response.detected_format, - compatible_count: response.compatible_count, - rejected_count: response.rejected_count, - nodes: response - .node - .into_iter() - .map(|node| ProxyNodePreview { - ordinal: node.ordinal, - name: node.name, - protocol: node.protocol, - server: node.server, - port: node.port, - warnings: node.warnings, - runtime_supported: node.runtime_supported, - }) - .collect(), - warnings: response.warnings, - }) -} - -pub async fn add_proxy_subscription( - url: String, - name: String, - selected_ordinal: Option, -) -> Result { - let id = next_network_id().await?; - let mut client = BurrowClient::from_uds().await?; - timeout( - Duration::from_secs(25), - client - .proxy_subscription_client - .apply_import(ProxySubscriptionApplyRequest { - id, - url, - name, - selected_ordinal: selected_ordinal.unwrap_or(-1), - }), - ) - .await - .context("timed out importing proxy subscription")??; - Ok(id) -} - pub async fn discover_tailnet(email: String) -> Result { let mut client = BurrowClient::from_uds().await?; let response = timeout( @@ -408,7 +328,6 @@ fn summarize_network(network: &Network) -> NetworkSummary { match network.r#type() { NetworkType::WireGuard => summarize_wireguard(network), NetworkType::Tailnet => summarize_tailnet(network), - NetworkType::ProxySubscription => summarize_proxy_subscription(network), } } @@ -453,21 +372,6 @@ fn summarize_tailnet(network: &Network) -> NetworkSummary { } } -fn summarize_proxy_subscription(network: &Network) -> NetworkSummary { - match serde_json::from_slice::(&network.payload) { - Ok(payload) => NetworkSummary { - id: network.id, - title: payload.name.clone(), - detail: burrow::proxy_subscription::summarize_payload(&payload), - }, - Err(error) => NetworkSummary { - id: network.id, - title: "Proxy Subscription".to_owned(), - detail: format!("Unable to read proxy subscription payload: {error}"), - }, - } -} - fn decode_tailnet_status( response: burrow::grpc_defs::TailnetLoginStatusResponse, ) -> TailnetLoginStatus { diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index a7d5cea..22f3d25 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -31,24 +31,12 @@ tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1.0" -serde_yaml = "0.9" blake2 = "0.10" -blake3 = "1" -chacha20 = "0.9" chacha20poly1305 = "0.10" rand = "0.8" bytes = "1" rand_core = "0.6" aead = "0.5" -aegis = { version = "0.9.9", default-features = false, features = ["pure-rust"] } -aes = "0.8" -aes-gcm = "0.10" -ccm = "0.5" -deoxys = { version = "0.2.0-rc.3", default-features = false } -lea = "0.5.4" -poly1305 = "0.8" -rabbit = "0.5" -zears = "0.2.1" x25519-dalek = { version = "2.0", features = [ "reusable_secrets", "static_secrets", @@ -56,7 +44,6 @@ x25519-dalek = { version = "2.0", features = [ ring = "0.17" parking_lot = "0.12" hmac = "0.12" -md-5 = "0.10" base64 = "0.21" fehler = "1.0" ip_network_table = "0.2" @@ -70,7 +57,6 @@ arti-client = "0.40.0" hickory-proto = "0.25.2" netstack-smoltcp = "0.2.1" tokio-util = { version = "0.7.18", features = ["compat"] } -tokio_smux = "0.2" tor-rtcompat = "0.40.0" console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" @@ -92,48 +78,6 @@ hyper-util = "0.1.6" toml = "0.8.15" rust-ini = "0.21.0" subtle = "2.6" -rustls = { version = "0.23", default-features = false, features = [ - "aws_lc_rs", - "logging", - "std", - "tls12", -] } -rustls-pemfile = "2" -sha2 = "0.10" -tokio-rustls = "0.26" -rama-tls-boring = { version = "0.3.0-alpha.4", default-features = false, optional = true } -rama-ua = { version = "0.3.0-alpha.4", default-features = false, optional = true, features = [ - "embed-profiles", - "tls", -] } -rama-net = { version = "0.3.0-alpha.4", default-features = false, optional = true, features = ["tls"] } -webpki-roots = "0.26" -webpki-roots-legacy = { package = "webpki-roots", version = "0.22" } -rust_tokio_kcp = { version = "0.2.5", default-features = false, features = [ - "aes", - "tea", - "xtea", - "simple_xor", - "blowfish", - "cast5", - "triple_des", - "twofish", - "salsa20", - "sm4", - "aes-gcm", -] } -smux_rust = "0.2.1" -tokio-snappy = "0.2.0" -shadowsocks = { version = "1.24.0", default-features = false, features = [ - "aead-cipher", - "aead-cipher-extra", - "aead-cipher-2022", - "aead-cipher-2022-extra", - "stream-cipher", -] } -yamux = "0.13.10" -h2 = "0.4.12" -http = "1.3.1" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" @@ -143,11 +87,8 @@ nix = { version = "0.27", features = ["fs", "socket", "uio"] } tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] -libc = "0.2" -native-tls = { version = "0.2", features = ["alpn"] } nix = { version = "0.27" } rusqlite = { version = "0.38.0", features = ["bundled", "blob"] } -tokio-native-tls = "0.3" [target.'cfg(target_os = "macos")'.dependencies] tracing-oslog = { git = "https://github.com/Stormshield-robinc/tracing-oslog" } @@ -168,11 +109,6 @@ pre_uninstall_script = "../package/rpm/pre_uninstall" [features] tokio-console = ["dep:console-subscriber"] bundled = ["rusqlite/bundled"] -boring-browser-fingerprints = [ - "dep:rama-tls-boring", - "dep:rama-ua", - "dep:rama-net", -] [build-dependencies] diff --git a/burrow/src/auth/server/mod.rs b/burrow/src/auth/server/mod.rs index d14e9a3..fdffce3 100644 --- a/burrow/src/auth/server/mod.rs +++ b/burrow/src/auth/server/mod.rs @@ -147,10 +147,7 @@ pub fn build_router(config: AuthServerConfig) -> Router { .route("/v1/control/map", post(control_map)) .route("/v1/tailnet/discover", get(tailnet_discover)) .route("/v1/tailscale/login/start", post(tailscale_login_start)) - .route( - "/v1/tailscale/login/:session_id", - get(tailscale_login_status), - ) + .route("/v1/tailscale/login/:session_id", get(tailscale_login_status)) .with_state(AppState { config, tailscale: tailscale::TailscaleBridgeManager::default(), @@ -250,12 +247,7 @@ async fn tailscale_login_status( .await .map_err(internal_error)? .map(Json) - .ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - "unknown tailscale login session".to_owned(), - ) - }) + .ok_or_else(|| (StatusCode::NOT_FOUND, "unknown tailscale login session".to_owned())) } async fn healthz() -> impl IntoResponse { @@ -427,7 +419,10 @@ mod tests { async fn tailnet_discover_requires_email() -> Result<()> { let app = build_router(AuthServerConfig::default()); let response = app - .oneshot(Request::get("/v1/tailnet/discover?email=").body(Body::empty())?) + .oneshot( + Request::get("/v1/tailnet/discover?email=") + .body(Body::empty())?, + ) .await?; assert_eq!(response.status(), StatusCode::BAD_REQUEST); Ok(()) diff --git a/burrow/src/auth/server/tailscale.rs b/burrow/src/auth/server/tailscale.rs index 85bb1f2..d08c807 100644 --- a/burrow/src/auth/server/tailscale.rs +++ b/burrow/src/auth/server/tailscale.rs @@ -291,10 +291,7 @@ impl TailscaleHelperProcess { } async fn shutdown_with_client(&self, client: &Client) -> Result<()> { - let _ = client - .post(format!("{}/shutdown", self.listen_url)) - .send() - .await; + let _ = client.post(format!("{}/shutdown", self.listen_url)).send().await; for _ in 0..10 { let mut child = self.child.lock().await; @@ -405,9 +402,7 @@ fn helper_command(request: &TailscaleLoginStartRequest, state_dir: &Path) -> Res } pub(crate) fn packet_socket_path(request: &TailscaleLoginStartRequest) -> PathBuf { - state_root() - .join(session_dir_name(request)) - .join("packet.sock") + state_root().join(session_dir_name(request)).join("packet.sock") } pub(crate) fn state_root() -> PathBuf { @@ -509,10 +504,7 @@ mod tests { control_url: None, packet_socket: None, }; - assert_eq!( - session_dir_name(&request), - "default-apple-tailscale-managed" - ); + assert_eq!(session_dir_name(&request), "default-apple-tailscale-managed"); assert_eq!(default_hostname(&request), "burrow-apple"); let custom_request = TailscaleLoginStartRequest { diff --git a/burrow/src/control/discovery.rs b/burrow/src/control/discovery.rs index b86aedb..d044a62 100644 --- a/burrow/src/control/discovery.rs +++ b/burrow/src/control/discovery.rs @@ -92,13 +92,8 @@ pub async fn probe_tailnet_authority(authority: &str) -> Result Result Ok(None), - status => Err(anyhow!( - "tailnet well-known lookup failed with HTTP {status}" - )), + status => Err(anyhow!("tailnet well-known lookup failed with HTTP {status}")), } } -async fn discover_webfinger( - client: &Client, - email: &str, - base_url: &Url, -) -> Result> { +async fn discover_webfinger(client: &Client, email: &str, base_url: &Url) -> Result> { let mut url = base_url .join(WEBFINGER_PATH) .context("failed to build webfinger URL")?; @@ -233,9 +222,7 @@ async fn discover_webfinger( .filter(|href| !href.trim().is_empty())) } StatusCode::NOT_FOUND => Ok(None), - status => Err(anyhow!( - "tailnet webfinger lookup failed with HTTP {status}" - )), + status => Err(anyhow!("tailnet webfinger lookup failed with HTTP {status}")), } } @@ -287,9 +274,7 @@ mod tests { #[test] fn detects_managed_tailscale_authority() { assert!(is_managed_tailscale_authority("controlplane.tailscale.com")); - assert!(is_managed_tailscale_authority( - "https://controlplane.tailscale.com/" - )); + assert!(is_managed_tailscale_authority("https://controlplane.tailscale.com/")); assert!(!is_managed_tailscale_authority("https://ts.burrow.net")); } diff --git a/burrow/src/daemon/apple.rs b/burrow/src/daemon/apple.rs index 45a2047..f369ea9 100644 --- a/burrow/src/daemon/apple.rs +++ b/burrow/src/daemon/apple.rs @@ -1,5 +1,3 @@ -#[cfg(unix)] -use std::os::unix::net::UnixStream; use std::{ ffi::{c_char, CStr}, path::PathBuf, @@ -36,17 +34,12 @@ pub unsafe extern "C" fn spawn_in_process(path: *const c_char, db_path: *const c } pub fn spawn_in_process_with_paths(path_buf: Option, db_path_buf: Option) { - configure_in_process_logging(db_path_buf.as_ref()); crate::tracing::initialize(); let _guard = BURROW_SPAWN_LOCK.lock().unwrap(); if BURROW_READY.get().is_some() { return; } - if socket_is_reachable(path_buf.as_ref()) { - let _ = BURROW_READY.set(()); - return; - } let notify = Arc::new(Notify::new()); let handle = BURROW_HANDLE.get_or_init(|| { @@ -81,28 +74,3 @@ pub fn spawn_in_process_with_paths(path_buf: Option, db_path_buf: Optio handle.block_on(async move { receiver.notified().await }); let _ = BURROW_READY.set(()); } - -fn configure_in_process_logging(db_path_buf: Option<&PathBuf>) { - if std::env::var_os("BURROW_LOG_FILE").is_none() { - if let Some(log_dir) = db_path_buf.and_then(|path| path.parent()) { - std::env::set_var("BURROW_LOG_FILE", log_dir.join("burrow-runtime.log")); - } - } - - if std::env::var_os("BURROW_LOG").is_none() && std::env::var_os("RUST_LOG").is_none() { - std::env::set_var( - "BURROW_LOG", - "burrow=debug,h2=warn,hyper=warn,tower=warn,tonic=warn", - ); - } -} - -#[cfg(unix)] -fn socket_is_reachable(path: Option<&PathBuf>) -> bool { - path.is_some_and(|path| UnixStream::connect(path).is_ok()) -} - -#[cfg(not(unix))] -fn socket_is_reachable(_path: Option<&PathBuf>) -> bool { - false -} diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index b1d5084..9b2e138 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -8,20 +8,16 @@ use rusqlite::Connection; use tokio::sync::{mpsc, watch, RwLock}; use tokio_stream::wrappers::ReceiverStream; use tonic::{Request, Response, Status as RspStatus}; -use tracing::{debug, error, info, warn}; +use tracing::{debug, info, warn}; use tun::tokio::TunInterface; use super::{ rpc::grpc_defs::{ - networks_server::Networks, proxy_subscriptions_server::ProxySubscriptions, - tailnet_control_server::TailnetControl, tunnel_server::Tunnel, Empty, Network, - NetworkDeleteRequest, NetworkListResponse, NetworkReorderRequest, NetworkType, - ProxySubscriptionApplyRequest, ProxySubscriptionApplyResponse, - ProxySubscriptionImportRequest, ProxySubscriptionNodePreview, - ProxySubscriptionPreviewResponse, ProxySubscriptionRejectedEntry, - ProxySubscriptionSelectRequest, ProxySubscriptionSelectResponse, State as RPCTunnelState, - TailnetDiscoverRequest, TailnetDiscoverResponse, TailnetProbeRequest, TailnetProbeResponse, - TunnelConfigurationResponse, TunnelPacket, TunnelStatusResponse, + networks_server::Networks, tailnet_control_server::TailnetControl, tunnel_server::Tunnel, + Empty, Network, NetworkDeleteRequest, NetworkListResponse, NetworkReorderRequest, + State as RPCTunnelState, TailnetDiscoverRequest, TailnetDiscoverResponse, + TailnetProbeRequest, TailnetProbeResponse, TunnelConfigurationResponse, TunnelPacket, + TunnelStatusResponse, }, runtime::{tailnet_helper_request, ActiveTunnel, ResolvedTunnel}, }; @@ -32,11 +28,7 @@ use crate::{ }, control::discovery, daemon::rpc::ServerConfig, - database::{ - add_network, delete_network, get_connection, list_networks, proxy_subscription_payload, - reorder_network, select_proxy_subscription_node, upsert_network, - }, - proxy_subscription, + database::{add_network, delete_network, get_connection, list_networks, reorder_network}, }; #[derive(Debug, Clone)] @@ -97,7 +89,9 @@ impl DaemonRPCServer { async fn current_tunnel_configuration(&self) -> Result { let config = { let active = self.active_tunnel.read().await; - active.as_ref().map(|tunnel| tunnel.server_config().clone()) + active + .as_ref() + .map(|tunnel| tunnel.server_config().clone()) }; let config = match config { Some(config) => config, @@ -110,22 +104,6 @@ impl DaemonRPCServer { Ok(configuration_rsp(config)) } - async fn update_active_proxy_subscription_runtime( - &self, - id: i32, - payload: &proxy_subscription::ProxySubscriptionPayload, - ) -> Result<(), RspStatus> { - let mut active = self.active_tunnel.write().await; - let Some(active) = active.as_mut() else { - return Ok(()); - }; - active - .apply_proxy_subscription_payload(id, payload) - .await - .map_err(proc_err)?; - Ok(()) - } - async fn stop_active_tunnel(&self) -> Result { let current = { self.active_tunnel.write().await.take() }; let Some(current) = current else { @@ -240,7 +218,9 @@ impl Tunnel for DaemonRPCServer { return Err(RspStatus::failed_precondition("no active tunnel")); }; active.packet_stream().ok_or_else(|| { - RspStatus::failed_precondition("active tunnel does not support packet streaming") + RspStatus::failed_precondition( + "active tunnel does not support packet streaming", + ) })? }; @@ -393,98 +373,6 @@ impl Networks for DaemonRPCServer { } } -#[tonic::async_trait] -impl ProxySubscriptions for DaemonRPCServer { - async fn preview_import( - &self, - request: Request, - ) -> Result, RspStatus> { - let request = request.into_inner(); - let body = proxy_subscription::fetch_subscription(&request.url) - .await - .map_err(proc_err)?; - let preview = proxy_subscription::parse_subscription_body(&body, Some(&request.url)); - Ok(Response::new(proxy_subscription_preview_rsp(preview))) - } - - async fn apply_import( - &self, - request: Request, - ) -> Result, RspStatus> { - let request = request.into_inner(); - let body = proxy_subscription::fetch_subscription(&request.url) - .await - .map_err(proc_err)?; - let preview = proxy_subscription::parse_subscription_body(&body, Some(&request.url)); - if preview.nodes.is_empty() { - return Err(RspStatus::invalid_argument( - "subscription contains no compatible Trojan or Shadowsocks nodes", - )); - } - let conn = self.get_connection()?; - let previous_payload = proxy_subscription_payload(&conn, request.id).map_err(proc_err)?; - let selected_ordinal = if let Some(previous_payload) = previous_payload.as_ref() { - let previous_name = selected_proxy_node_name(previous_payload); - supported_selected_name(&preview.nodes, previous_name.as_deref()) - .or_else(|| first_supported_ordinal(&preview.nodes)) - } else { - supported_selected_ordinal(&preview.nodes, request.selected_ordinal) - .or_else(|| first_supported_ordinal(&preview.nodes)) - }; - let payload = proxy_subscription::build_payload( - preview, - &request.url, - (!request.name.trim().is_empty()).then_some(request.name), - selected_ordinal, - ); - crate::proxy_runtime::proxy_runtime_config(&payload) - .map_err(|err| RspStatus::invalid_argument(err.to_string()))?; - let imported_count = payload.nodes.len() as i32; - let selected_ordinal = payload - .selected_ordinal - .or_else(|| payload.nodes.first().map(|node| node.ordinal)) - .unwrap_or_default() as i32; - let network = Network { - id: request.id, - r#type: NetworkType::ProxySubscription.into(), - payload: serde_json::to_vec_pretty(&payload).map_err(proc_err)?, - }; - upsert_network(&conn, &network).map_err(proc_err)?; - self.update_active_proxy_subscription_runtime(request.id, &payload) - .await?; - self.notify_network_update().await?; - #[cfg(not(target_vendor = "apple"))] - self.reconcile_runtime().await?; - Ok(Response::new(ProxySubscriptionApplyResponse { - id: request.id, - imported_count, - selected_ordinal, - })) - } - - async fn select_node( - &self, - request: Request, - ) -> Result, RspStatus> { - let request = request.into_inner(); - let conn = self.get_connection()?; - let selected_ordinal = - select_proxy_subscription_node(&conn, request.id, request.selected_ordinal) - .map_err(proc_err)?; - if let Some(payload) = proxy_subscription_payload(&conn, request.id).map_err(proc_err)? { - self.update_active_proxy_subscription_runtime(request.id, &payload) - .await?; - } - self.notify_network_update().await?; - #[cfg(not(target_vendor = "apple"))] - self.reconcile_runtime().await?; - Ok(Response::new(ProxySubscriptionSelectResponse { - id: request.id, - selected_ordinal, - })) - } -} - #[tonic::async_trait] impl TailnetControl for DaemonRPCServer { async fn discover( @@ -612,9 +500,7 @@ impl TailnetControl for DaemonRPCServer { } fn proc_err(err: impl ToString) -> RspStatus { - let message = err.to_string(); - error!(error = %message, "daemon RPC failed"); - RspStatus::internal(message) + RspStatus::internal(err.to_string()) } fn configuration_rsp(config: ServerConfig) -> TunnelConfigurationResponse { @@ -622,7 +508,6 @@ fn configuration_rsp(config: ServerConfig) -> TunnelConfigurationResponse { addresses: config.address, mtu: config.mtu.unwrap_or(1000), routes: config.routes, - excluded_routes: config.excluded_routes, dns_servers: config.dns_servers, search_domains: config.search_domains, include_default_route: config.include_default_route, @@ -636,107 +521,6 @@ fn status_rsp(state: RunState) -> TunnelStatusResponse { } } -fn proxy_subscription_preview_rsp( - preview: proxy_subscription::ProxySubscriptionPreview, -) -> ProxySubscriptionPreviewResponse { - ProxySubscriptionPreviewResponse { - suggested_name: preview.suggested_name, - detected_format: preview.detected_format.as_str().to_owned(), - compatible_count: preview.nodes.len() as i32, - rejected_count: preview.rejected.len() as i32, - node: preview - .nodes - .into_iter() - .map(|node| { - let runtime_error = crate::proxy_runtime::runtime_support_error(&node); - let mut warnings = node.warnings; - if let Some(error) = runtime_error.as_deref() { - warnings.push(error.to_owned()); - } - ProxySubscriptionNodePreview { - ordinal: node.ordinal as i32, - name: node.name, - protocol: node.protocol.as_str().to_owned(), - server: node.server, - port: node.port.into(), - warnings, - runtime_supported: runtime_error.is_none(), - } - }) - .collect(), - rejected: preview - .rejected - .into_iter() - .map(|entry| ProxySubscriptionRejectedEntry { - ordinal: entry.ordinal as i32, - reason: entry.reason, - scheme: entry.scheme.unwrap_or_default(), - }) - .collect(), - warnings: preview.warnings, - } -} - -fn supported_selected_ordinal( - nodes: &[proxy_subscription::ProxyNode], - selected_ordinal: i32, -) -> Option { - if selected_ordinal < 0 { - return None; - } - let selected_ordinal = selected_ordinal as usize; - nodes - .iter() - .find(|node| { - node.ordinal == selected_ordinal - && crate::proxy_runtime::runtime_support_error(node).is_none() - }) - .map(|node| node.ordinal) -} - -fn supported_selected_name( - nodes: &[proxy_subscription::ProxyNode], - selected_name: Option<&str>, -) -> Option { - let selected_name = selected_name?; - nodes - .iter() - .find(|node| { - node.name == selected_name - && crate::proxy_runtime::runtime_support_error(node).is_none() - }) - .map(|node| node.ordinal) -} - -fn first_supported_ordinal(nodes: &[proxy_subscription::ProxyNode]) -> Option { - nodes - .iter() - .find(|node| crate::proxy_runtime::runtime_support_error(node).is_none()) - .map(|node| node.ordinal) -} - -fn selected_proxy_node_name( - payload: &proxy_subscription::ProxySubscriptionPayload, -) -> Option { - payload - .selected_name - .clone() - .or_else(|| { - payload.selected_ordinal.and_then(|ordinal| { - payload - .nodes - .iter() - .find(|node| node.ordinal == ordinal) - .map(|node| node.name.clone()) - }) - }) - .or_else(|| { - crate::proxy_runtime::selected_node(payload) - .ok() - .map(|node| node.name.clone()) - }) -} - fn tailnet_login_rsp( session_id: String, status: TailscaleLoginStatus, @@ -754,54 +538,3 @@ fn tailnet_login_rsp( health: status.health, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn supported_selected_name_survives_subscription_reorder() { - let preview = proxy_subscription::parse_subscription_body( - r#" -proxies: - - name: fallback - type: ss - server: fallback.example.net - port: 8388 - cipher: aes-128-gcm - password: secret - - name: selected - type: ss - server: selected.example.net - port: 8388 - cipher: aes-128-gcm - password: secret -"#, - Some("https://example.com/sub"), - ); - - assert_eq!( - supported_selected_name(&preview.nodes, Some("selected")), - Some(1) - ); - } - - #[test] - fn selected_proxy_node_name_reads_legacy_ordinal_payloads() { - let preview = proxy_subscription::parse_subscription_body( - "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388#SS%20Node\n", - Some("https://example.com/sub"), - ); - let payload = proxy_subscription::build_payload( - preview, - "https://example.com/sub", - Some("Proxy Subscription".to_owned()), - Some(0), - ); - - assert_eq!( - selected_proxy_node_name(&payload).as_deref(), - Some("SS Node") - ); - } -} diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index ccb9b86..724e3bb 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -1,4 +1,4 @@ -use std::{io, os::unix::net::UnixStream as StdUnixStream, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; pub mod apple; mod instance; @@ -17,8 +17,8 @@ use tracing::info; use crate::{ daemon::rpc::grpc_defs::{ - networks_server::NetworksServer, proxy_subscriptions_server::ProxySubscriptionsServer, - tailnet_control_server::TailnetControlServer, tunnel_server::TunnelServer, + networks_server::NetworksServer, tailnet_control_server::TailnetControlServer, + tunnel_server::TunnelServer, }, database::get_connection, }; @@ -33,23 +33,16 @@ pub async fn daemon_main( let spp = socket_path.clone(); let tmp = get_socket_path(); let sock_path = spp.unwrap_or(Path::new(tmp.as_str())); - let uds = match bind_daemon_socket(sock_path)? { - Some(uds) => uds, - None => { - if let Some(n) = notify_ready { - n.notify_one(); - } - info!(path = %sock_path.display(), "daemon socket already served by another process"); - return Ok(()); - } - }; + if sock_path.exists() { + std::fs::remove_file(sock_path)?; + } + let uds = UnixListener::bind(sock_path)?; let serve_job = tokio::spawn(async move { let uds_stream = UnixListenerStream::new(uds); let tailnet_server = burrow_server.clone(); let _srv = Server::builder() .add_service(TunnelServer::new(burrow_server.clone())) - .add_service(NetworksServer::new(burrow_server.clone())) - .add_service(ProxySubscriptionsServer::new(burrow_server.clone())) + .add_service(NetworksServer::new(burrow_server)) .add_service(TailnetControlServer::new(tailnet_server)) .serve_with_incoming(uds_stream) .await?; @@ -67,24 +60,6 @@ pub async fn daemon_main( .map_err(|e| e.into()) } -fn bind_daemon_socket(sock_path: &Path) -> Result> { - if let Some(parent) = sock_path.parent() { - std::fs::create_dir_all(parent)?; - } - - match UnixListener::bind(sock_path) { - Ok(listener) => Ok(Some(listener)), - Err(error) if error.kind() == io::ErrorKind::AddrInUse => { - if StdUnixStream::connect(sock_path).is_ok() { - return Ok(None); - } - std::fs::remove_file(sock_path)?; - Ok(Some(UnixListener::bind(sock_path)?)) - } - Err(error) => Err(error.into()), - } -} - #[cfg(test)] mod tests { use std::{ diff --git a/burrow/src/daemon/rpc/client.rs b/burrow/src/daemon/rpc/client.rs index 1f3cfa2..aa84c64 100644 --- a/burrow/src/daemon/rpc/client.rs +++ b/burrow/src/daemon/rpc/client.rs @@ -6,14 +6,13 @@ use tonic::transport::{Endpoint, Uri}; use tower::service_fn; use super::grpc_defs::{ - networks_client::NetworksClient, proxy_subscriptions_client::ProxySubscriptionsClient, - tailnet_control_client::TailnetControlClient, tunnel_client::TunnelClient, + networks_client::NetworksClient, tailnet_control_client::TailnetControlClient, + tunnel_client::TunnelClient, }; use crate::daemon::get_socket_path; pub struct BurrowClient { pub networks_client: NetworksClient, - pub proxy_subscription_client: ProxySubscriptionsClient, pub tailnet_client: TailnetControlClient, pub tunnel_client: TunnelClient, } @@ -36,12 +35,10 @@ impl BurrowClient { })) .await?; let nw_client = NetworksClient::new(channel.clone()); - let proxy_subscription_client = ProxySubscriptionsClient::new(channel.clone()); let tailnet_client = TailnetControlClient::new(channel.clone()); let tun_client = TunnelClient::new(channel.clone()); Ok(BurrowClient { networks_client: nw_client, - proxy_subscription_client, tailnet_client, tunnel_client: tun_client, }) diff --git a/burrow/src/daemon/rpc/response.rs b/burrow/src/daemon/rpc/response.rs index d72fcd3..6d03581 100644 --- a/burrow/src/daemon/rpc/response.rs +++ b/burrow/src/daemon/rpc/response.rs @@ -71,8 +71,6 @@ pub struct ServerConfig { #[serde(default)] pub routes: Vec, #[serde(default)] - pub excluded_routes: Vec, - #[serde(default)] pub dns_servers: Vec, #[serde(default)] pub search_domains: Vec, @@ -93,7 +91,6 @@ impl TryFrom<&Config> for ServerConfig { .iter() .flat_map(|peer| peer.allowed_ips.iter().cloned()) .collect(), - excluded_routes: Vec::new(), dns_servers: config.interface.dns.clone(), search_domains: Vec::new(), include_default_route: false, @@ -108,7 +105,6 @@ impl Default for ServerConfig { Self { address: vec!["10.13.13.2".to_string()], // Dummy remote address routes: Vec::new(), - excluded_routes: Vec::new(), dns_servers: Vec::new(), search_domains: Vec::new(), include_default_route: false, diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap index 6bc3ce1..68b4195 100644 --- a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/rpc/response.rs expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" --- -{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"routes":[],"excluded_routes":[],"dns_servers":[],"search_domains":[],"include_default_route":false,"name":null,"mtu":null}},"id":0} +{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"routes":[],"dns_servers":[],"search_domains":[],"include_default_route":false,"name":null,"mtu":null}},"id":0} diff --git a/burrow/src/daemon/runtime.rs b/burrow/src/daemon/runtime.rs index ae5fa99..31821a2 100644 --- a/burrow/src/daemon/runtime.rs +++ b/burrow/src/daemon/runtime.rs @@ -20,8 +20,6 @@ use crate::{ TailscaleLoginStartRequest, TailscaleLoginStatus, }, control::{discovery, TailnetConfig}, - proxy_runtime, - proxy_subscription::ProxySubscriptionPayload, wireguard::{Config, Interface as WireGuardInterface}, }; @@ -48,10 +46,6 @@ pub enum ResolvedTunnel { identity: RuntimeIdentity, config: Config, }, - ProxySubscription { - identity: RuntimeIdentity, - payload: ProxySubscriptionPayload, - }, } impl ResolvedTunnel { @@ -79,12 +73,6 @@ impl ResolvedTunnel { let config = Config::from_content_fmt(&payload, "ini")?; Ok(Self::WireGuard { identity, config }) } - NetworkType::ProxySubscription => { - let payload: ProxySubscriptionPayload = serde_json::from_slice(&network.payload) - .context("proxy subscription payload must be valid JSON")?; - crate::proxy_subscription::validate_payload(&payload)?; - Ok(Self::ProxySubscription { identity, payload }) - } } } @@ -92,8 +80,7 @@ impl ResolvedTunnel { match self { Self::Passthrough { identity } | Self::Tailnet { identity, .. } - | Self::WireGuard { identity, .. } - | Self::ProxySubscription { identity, .. } => identity, + | Self::WireGuard { identity, .. } => identity, } } @@ -102,7 +89,6 @@ impl ResolvedTunnel { Self::Passthrough { .. } => Ok(ServerConfig { address: Vec::new(), routes: Vec::new(), - excluded_routes: Vec::new(), dns_servers: Vec::new(), search_domains: Vec::new(), include_default_route: false, @@ -112,7 +98,6 @@ impl ResolvedTunnel { Self::Tailnet { .. } => Ok(ServerConfig { address: Vec::new(), routes: tailnet_routes(), - excluded_routes: Vec::new(), dns_servers: tailnet_dns_servers(), search_domains: Vec::new(), include_default_route: false, @@ -120,7 +105,6 @@ impl ResolvedTunnel { mtu: Some(1280), }), Self::WireGuard { config, .. } => ServerConfig::try_from(config), - Self::ProxySubscription { payload, .. } => proxy_subscription_server_config(payload), } } @@ -135,7 +119,6 @@ impl ResolvedTunnel { server_config: ServerConfig { address: Vec::new(), routes: Vec::new(), - excluded_routes: Vec::new(), dns_servers: Vec::new(), search_domains: Vec::new(), include_default_route: false, @@ -154,9 +137,10 @@ impl ResolvedTunnel { }; let status = wait_for_tailnet_ready(helper.as_ref()).await?; let server_config = tailnet_server_config(&status); - let packet_socket = helper.packet_socket().map(PathBuf::from).ok_or_else(|| { - anyhow::anyhow!("tailnet helper did not report a packet socket") - })?; + let packet_socket = helper + .packet_socket() + .map(PathBuf::from) + .ok_or_else(|| anyhow::anyhow!("tailnet helper did not report a packet socket"))?; let packet_bridge = connect_tailnet_packet_bridge(packet_socket).await?; #[cfg(target_vendor = "apple")] let tun_task = None; @@ -198,61 +182,10 @@ impl ResolvedTunnel { } } } - Self::ProxySubscription { identity, payload } => { - let runtime_config = proxy_runtime::proxy_runtime_config(&payload)?; - let server_config = proxy_subscription_server_config_for_selected( - &payload, - &runtime_config.selected_name, - )?; - let packet_bridge = - proxy_runtime::start_proxy_packet_bridge(runtime_config.outbound); - #[cfg(target_vendor = "apple")] - let tun_task = None; - #[cfg(not(target_vendor = "apple"))] - let tun_task = { - let tun = TunOptions::new().open()?; - tun_interface.write().await.replace(tun); - Some(tokio::spawn(proxy_runtime::run_proxy_tun_bridge( - tun_interface.clone(), - packet_bridge.outbound_sender(), - packet_bridge.subscribe(), - ))) - }; - Ok(ActiveTunnel::ProxySubscription { - identity, - server_config, - packet_bridge, - tun_task, - }) - } } } } -fn proxy_subscription_server_config(payload: &ProxySubscriptionPayload) -> Result { - let runtime_config = proxy_runtime::proxy_runtime_config(payload)?; - proxy_subscription_server_config_for_selected(payload, &runtime_config.selected_name) -} - -fn proxy_subscription_server_config_for_selected( - payload: &ProxySubscriptionPayload, - selected_name: &str, -) -> Result { - let dns_servers = proxy_runtime::proxy_dns_servers(); - let mut excluded_routes = proxy_runtime::proxy_excluded_routes(payload)?; - excluded_routes.extend(proxy_runtime::proxy_dns_excluded_routes(&dns_servers)); - Ok(ServerConfig { - address: proxy_runtime::proxy_server_addresses(), - routes: Vec::new(), - excluded_routes, - dns_servers, - search_domains: Vec::new(), - include_default_route: true, - name: Some(format!("{} ({})", payload.name, selected_name)), - mtu: Some(1500), - }) -} - pub enum ActiveTunnel { Passthrough { identity: RuntimeIdentity, @@ -272,12 +205,6 @@ pub enum ActiveTunnel { interface: Arc>, task: JoinHandle>, }, - ProxySubscription { - identity: RuntimeIdentity, - server_config: ServerConfig, - packet_bridge: proxy_runtime::ProxyPacketBridge, - tun_task: Option>>, - }, } impl ActiveTunnel { @@ -285,8 +212,7 @@ impl ActiveTunnel { match self { Self::Passthrough { identity, .. } | Self::Tailnet { identity, .. } - | Self::WireGuard { identity, .. } - | Self::ProxySubscription { identity, .. } => identity, + | Self::WireGuard { identity, .. } => identity, } } @@ -294,54 +220,22 @@ impl ActiveTunnel { match self { Self::Passthrough { server_config, .. } | Self::Tailnet { server_config, .. } - | Self::WireGuard { server_config, .. } - | Self::ProxySubscription { server_config, .. } => server_config, + | Self::WireGuard { server_config, .. } => server_config, } } - pub fn packet_stream(&self) -> Option<(mpsc::Sender>, broadcast::Receiver>)> { + pub fn packet_stream( + &self, + ) -> Option<(mpsc::Sender>, broadcast::Receiver>)> { match self { - Self::Tailnet { packet_bridge, .. } => { - Some((packet_bridge.outbound_sender(), packet_bridge.subscribe())) - } - Self::ProxySubscription { packet_bridge, .. } => { - Some((packet_bridge.outbound_sender(), packet_bridge.subscribe())) - } + Self::Tailnet { packet_bridge, .. } => Some(( + packet_bridge.outbound_sender(), + packet_bridge.subscribe(), + )), _ => None, } } - pub async fn apply_proxy_subscription_payload( - &mut self, - network_id: i32, - payload: &ProxySubscriptionPayload, - ) -> Result { - let Self::ProxySubscription { - identity, - server_config, - packet_bridge, - .. - } = self - else { - return Ok(false); - }; - - let RuntimeIdentity::Network { id, network_type, .. } = identity else { - return Ok(false); - }; - if *id != network_id || *network_type != NetworkType::ProxySubscription { - return Ok(false); - } - - let runtime_config = proxy_runtime::proxy_runtime_config(payload)?; - *server_config = - proxy_subscription_server_config_for_selected(payload, &runtime_config.selected_name)?; - packet_bridge - .set_proxy_outbound(runtime_config.outbound) - .await; - Ok(true) - } - pub async fn shutdown(self, tun_interface: &Arc>>) -> Result<()> { match self { Self::Passthrough { .. } => Ok(()), @@ -374,28 +268,17 @@ impl ActiveTunnel { } Ok(()) } - Self::WireGuard { interface, task, .. } => { + Self::WireGuard { + interface, + task, + .. + } => { interface.read().await.remove_tun().await; let task_result = task.await; tun_interface.write().await.take(); task_result??; Ok(()) } - Self::ProxySubscription { packet_bridge, tun_task, .. } => { - if let Some(tun_task) = tun_task { - tun_task.abort(); - match tun_task.await { - Ok(Ok(())) => {} - Ok(Err(err)) => return Err(err), - Err(err) if err.is_cancelled() => {} - Err(err) => return Err(err.into()), - } - } - packet_bridge.abort(); - packet_bridge.join().await?; - tun_interface.write().await.take(); - Ok(()) - } } } } @@ -513,7 +396,6 @@ fn tailnet_server_config(status: &TailscaleLoginStatus) -> ServerConfig { .map(|ip| tailnet_cidr(ip)) .collect(), routes: tailnet_routes(), - excluded_routes: Vec::new(), dns_servers: tailnet_dns_servers(), search_domains, include_default_route: false, @@ -723,10 +605,7 @@ mod tests { fn tailnet_server_config_uses_host_prefixes() { let status = TailscaleLoginStatus { running: true, - tailscale_ips: vec![ - "100.101.102.103".to_owned(), - "fd7a:115c:a1e0::123".to_owned(), - ], + tailscale_ips: vec!["100.101.102.103".to_owned(), "fd7a:115c:a1e0::123".to_owned()], ..Default::default() }; let config = tailnet_server_config(&status); diff --git a/burrow/src/database.rs b/burrow/src/database.rs index 4b2350a..fe9a3c7 100644 --- a/burrow/src/database.rs +++ b/burrow/src/database.rs @@ -1,6 +1,6 @@ use std::path::Path; -use anyhow::{anyhow, Result}; +use anyhow::Result; use rusqlite::{params, Connection}; use crate::{ @@ -8,7 +8,6 @@ use crate::{ daemon::rpc::grpc_defs::{ Network as RPCNetwork, NetworkDeleteRequest, NetworkReorderRequest, NetworkType, }, - proxy_subscription::ProxySubscriptionPayload, wireguard::config::{Config, Interface, Peer}, }; @@ -139,18 +138,6 @@ pub fn add_network(conn: &Connection, network: &RPCNetwork) -> Result<()> { Ok(()) } -pub fn upsert_network(conn: &Connection, network: &RPCNetwork) -> Result<()> { - validate_network_payload(network)?; - let updated = conn.execute( - "UPDATE network SET type = ?, payload = ? WHERE id = ?", - params![network.r#type().as_str_name(), &network.payload, network.id], - )?; - if updated == 0 { - add_network(conn, network)?; - } - Ok(()) -} - pub fn list_networks(conn: &Connection) -> Result> { let mut stmt = conn.prepare("SELECT id, type, payload FROM network ORDER BY idx, id")?; let networks: Vec = stmt @@ -196,70 +183,6 @@ pub fn delete_network(conn: &Connection, req: NetworkDeleteRequest) -> Result<() renumber_networks(conn, &ordered_ids) } -pub fn proxy_subscription_payload( - conn: &Connection, - id: i32, -) -> Result> { - let row = conn.query_row( - "SELECT type, payload FROM network WHERE id = ?", - params![id], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec>(1)?)), - ); - let (network_type, payload) = match row { - Ok(row) => row, - Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), - Err(error) => return Err(error.into()), - }; - let network_type = NetworkType::from_str_name(network_type.as_str()) - .ok_or_else(|| anyhow!("stored network type {network_type} is not recognized"))?; - if network_type != NetworkType::ProxySubscription { - return Ok(None); - } - Ok(Some(serde_json::from_slice(&payload)?)) -} - -pub fn select_proxy_subscription_node( - conn: &Connection, - id: i32, - selected_ordinal: i32, -) -> Result { - if selected_ordinal < 0 { - return Err(anyhow!("selected proxy node ordinal must be non-negative")); - } - - let (network_type, payload): (String, Vec) = conn.query_row( - "SELECT type, payload FROM network WHERE id = ?", - params![id], - |row| Ok((row.get(0)?, row.get(1)?)), - )?; - let network_type = NetworkType::from_str_name(network_type.as_str()) - .ok_or_else(|| anyhow!("stored network type {network_type} is not recognized"))?; - if network_type != NetworkType::ProxySubscription { - return Err(anyhow!("network {id} is not a proxy subscription")); - } - - let mut payload: ProxySubscriptionPayload = serde_json::from_slice(&payload)?; - let selected_ordinal = selected_ordinal as usize; - let node = payload - .nodes - .iter() - .find(|node| node.ordinal == selected_ordinal) - .ok_or_else(|| anyhow!("selected proxy node ordinal {selected_ordinal} was not found"))?; - if let Some(error) = crate::proxy_runtime::runtime_support_error(node) { - return Err(anyhow!(error)); - } - - let selected_name = node.name.clone(); - payload.selected_ordinal = Some(selected_ordinal); - payload.selected_name = Some(selected_name); - crate::proxy_subscription::validate_payload(&payload)?; - conn.execute( - "UPDATE network SET payload = ? WHERE id = ?", - params![serde_json::to_vec_pretty(&payload)?, id], - )?; - Ok(selected_ordinal as i32) -} - fn parse_lst(s: &str) -> Vec { if s.is_empty() { return vec![]; @@ -283,10 +206,6 @@ fn validate_network_payload(network: &RPCNetwork) -> Result<()> { NetworkType::Tailnet => { TailnetConfig::from_slice(&network.payload)?; } - NetworkType::ProxySubscription => { - let payload: ProxySubscriptionPayload = serde_json::from_slice(&network.payload)?; - crate::proxy_subscription::validate_payload(&payload)?; - } } Ok(()) } @@ -359,79 +278,6 @@ Endpoint = wg.burrow.rs:51820 .to_vec() } - fn sample_proxy_subscription_payload() -> Vec { - br#"{ - "name":"example proxy subscription", - "source":{"url":"https://example.com/sub"}, - "detected_format":"uri-list", - "selected_ordinal":0, - "nodes":[{ - "ordinal":0, - "name":"example trojan", - "protocol":"trojan", - "server":"example.com", - "port":443, - "warnings":[], - "config":{ - "type":"trojan", - "password":"secret", - "sni":"front.example", - "alpn":[], - "skip_cert_verify":false, - "network":"tcp", - "client_fingerprint":"chrome", - "fingerprint":null - } - }], - "warnings":[] -}"# - .to_vec() - } - - fn sample_proxy_subscription_payload_with_two_nodes() -> Vec { - br#"{ - "name":"example proxy subscription", - "source":{"url":"https://example.com/sub"}, - "detected_format":"uri-list", - "selected_ordinal":0, - "nodes":[{ - "ordinal":0, - "name":"example trojan", - "protocol":"trojan", - "server":"example.com", - "port":443, - "warnings":[], - "config":{ - "type":"trojan", - "password":"secret", - "sni":"front.example", - "alpn":[], - "skip_cert_verify":false, - "network":"tcp", - "client_fingerprint":"chrome", - "fingerprint":null - } - },{ - "ordinal":1, - "name":"example shadowsocks", - "protocol":"shadowsocks", - "server":"ss.example.com", - "port":8388, - "warnings":[], - "config":{ - "type":"shadowsocks", - "cipher":"aes-256-gcm", - "password":"secret", - "udp":true, - "udp_over_tcp":false, - "plugin":null - } - }], - "warnings":[] -}"# - .to_vec() - } - #[test] fn test_db() { let conn = Connection::open_in_memory().unwrap(); @@ -471,16 +317,6 @@ Endpoint = wg.burrow.rs:51820 &conn, &RPCNetwork { id: 3, - r#type: NetworkType::ProxySubscription.into(), - payload: sample_proxy_subscription_payload(), - }, - ) - .unwrap(); - - add_network( - &conn, - &RPCNetwork { - id: 4, r#type: NetworkType::WireGuard.into(), payload: sample_wireguard_payload_with_address("10.42.0.2/32", 1380), }, @@ -490,7 +326,7 @@ Endpoint = wg.burrow.rs:51820 assert!(add_network( &conn, &RPCNetwork { - id: 5, + id: 4, r#type: NetworkType::WireGuard.into(), payload: b"not-a-config".to_vec(), }, @@ -500,29 +336,19 @@ Endpoint = wg.burrow.rs:51820 assert!(add_network( &conn, &RPCNetwork { - id: 6, + id: 5, r#type: NetworkType::Tailnet.into(), payload: b"not-a-tailnet-config".to_vec(), }, ) .is_err()); - assert!(add_network( - &conn, - &RPCNetwork { - id: 7, - r#type: NetworkType::ProxySubscription.into(), - payload: b"not-a-trojan-config".to_vec(), - }, - ) - .is_err()); - let ids: Vec = list_networks(&conn) .unwrap() .into_iter() .map(|n| n.id) .collect(); - assert_eq!(ids, vec![1, 2, 3, 4]); + assert_eq!(ids, vec![1, 2, 3]); } #[test] @@ -563,87 +389,6 @@ Endpoint = wg.burrow.rs:51820 assert_eq!(ids, vec![3, 2]); } - #[test] - fn upsert_network_preserves_priority() { - let conn = Connection::open_in_memory().unwrap(); - initialize_tables(&conn).unwrap(); - - add_network( - &conn, - &RPCNetwork { - id: 1, - r#type: NetworkType::WireGuard.into(), - payload: sample_wireguard_payload_with_address("10.42.0.2/32", 1380), - }, - ) - .unwrap(); - add_network( - &conn, - &RPCNetwork { - id: 2, - r#type: NetworkType::WireGuard.into(), - payload: sample_wireguard_payload_with_address("10.42.0.3/32", 1381), - }, - ) - .unwrap(); - - upsert_network( - &conn, - &RPCNetwork { - id: 1, - r#type: NetworkType::WireGuard.into(), - payload: sample_wireguard_payload_with_address("10.99.0.2/32", 1400), - }, - ) - .unwrap(); - - let networks = list_networks(&conn).unwrap(); - let ids: Vec = networks.iter().map(|n| n.id).collect(); - assert_eq!(ids, vec![1, 2]); - - let updated = String::from_utf8(networks[0].payload.clone()).unwrap(); - assert!(updated.contains("10.99.0.2/32")); - } - - #[test] - fn select_proxy_subscription_node_updates_payload_without_reorder() { - let conn = Connection::open_in_memory().unwrap(); - initialize_tables(&conn).unwrap(); - - add_network( - &conn, - &RPCNetwork { - id: 1, - r#type: NetworkType::ProxySubscription.into(), - payload: sample_proxy_subscription_payload_with_two_nodes(), - }, - ) - .unwrap(); - add_network( - &conn, - &RPCNetwork { - id: 2, - r#type: NetworkType::WireGuard.into(), - payload: sample_wireguard_payload_with_address("10.42.0.3/32", 1381), - }, - ) - .unwrap(); - - assert_eq!(select_proxy_subscription_node(&conn, 1, 1).unwrap(), 1); - - let networks = list_networks(&conn).unwrap(); - let ids: Vec = networks.iter().map(|n| n.id).collect(); - assert_eq!(ids, vec![1, 2]); - - let payload: ProxySubscriptionPayload = - serde_json::from_slice(&networks[0].payload).unwrap(); - assert_eq!(payload.selected_ordinal, Some(1)); - assert_eq!( - payload.selected_name.as_deref(), - Some("example shadowsocks") - ); - } - #[test] fn get_connection_does_not_seed_a_default_interface() { let dir = tempdir().unwrap(); diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index f0b24d5..7867d18 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -1,9 +1,6 @@ #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod control; -pub mod proxy_runtime; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -pub mod proxy_subscription; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod wireguard; diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 39a4af1..cfa2085 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -5,9 +5,6 @@ use clap::{Args, Parser, Subcommand}; mod control; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; -mod proxy_runtime; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -mod proxy_subscription; pub(crate) mod tracing; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod wireguard; @@ -83,16 +80,6 @@ enum Commands { TailnetPing(TailnetPingArgs), /// Send a UDP echo probe through the active Tailnet tunnel over daemon packet streaming TailnetUdpEcho(TailnetUdpEchoArgs), - /// Send an HTTP probe through the active proxy tunnel over daemon packet streaming - ProxyTcpProbe(ProxyTcpProbeArgs), - /// Send a UDP echo probe through the active proxy tunnel over daemon packet streaming - ProxyUdpEcho(ProxyUdpEchoArgs), - /// Send an HTTPS probe through the active proxy tunnel over daemon packet streaming - ProxyHttpsProbe(ProxyHttpsProbeArgs), - /// Select a proxy subscription node through the daemon - ProxySubscriptionSelect(ProxySubscriptionSelectArgs), - /// Preview proxy nodes from a subscription URL - ProxySubscriptionPreview(ProxySubscriptionPreviewArgs), #[cfg(target_os = "linux")] /// Run a command in an unshared Linux namespace using a Burrow backend Exec(ExecArgs), @@ -161,59 +148,6 @@ struct TailnetUdpEchoArgs { timeout_ms: u64, } -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[derive(Args)] -struct ProxyTcpProbeArgs { - #[arg(long, default_value = "1.1.1.1:80")] - remote: String, - #[arg(long, default_value = "one.one.one.one")] - host: String, - #[arg(long)] - dns_name: Option, - #[arg(long, default_value = "100.64.0.2:53")] - dns_server: String, - #[arg(long, default_value_t = 8000)] - timeout_ms: u64, -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[derive(Args)] -struct ProxyUdpEchoArgs { - remote: String, - #[arg(long, default_value = "burrow-proxy-udp-smoke")] - message: String, - #[arg(long, default_value_t = 5000)] - timeout_ms: u64, -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[derive(Args)] -struct ProxyHttpsProbeArgs { - #[arg(long, default_value = "1.1.1.1:443")] - remote: String, - #[arg(long)] - host: Option, - #[arg(long)] - dns_name: Option, - #[arg(long, default_value = "100.64.0.2:53")] - dns_server: String, - #[arg(long, default_value_t = 15000)] - timeout_ms: u64, -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[derive(Args)] -struct ProxySubscriptionSelectArgs { - id: i32, - selected_ordinal: i32, -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[derive(Args)] -struct ProxySubscriptionPreviewArgs { - url: String, -} - #[cfg(target_os = "linux")] #[derive(Args)] struct TorExecArgs { @@ -455,21 +389,6 @@ async fn try_tailnet_ping(remote: &str, payload: &str, timeout_ms: u64) -> Resul #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_tailnet_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> Result<()> { - try_packet_udp_echo("Tailnet", remote, message, timeout_ms).await -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_proxy_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> Result<()> { - try_packet_udp_echo("Proxy", remote, message, timeout_ms).await -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_packet_udp_echo( - label: &str, - remote: &str, - message: &str, - timeout_ms: u64, -) -> Result<()> { use std::net::SocketAddr; use anyhow::{bail, Context}; @@ -486,7 +405,6 @@ async fn try_packet_udp_echo( let remote_addr: SocketAddr = remote .parse() .with_context(|| format!("invalid remote socket address {remote}"))?; - let label = label.to_owned(); let mut client = BurrowClient::from_uds().await?; client.tunnel_client.tunnel_start(Empty {}).await?; @@ -506,11 +424,9 @@ async fn try_packet_udp_echo( .enable_udp(true) .enable_tcp(true) .build() - .with_context(|| format!("failed to build userspace {label} UDP stack"))?; - let runner = - runner.with_context(|| format!("userspace {label} UDP stack runner unavailable"))?; - let udp_socket = - udp_socket.with_context(|| format!("userspace {label} UDP stack socket unavailable"))?; + .context("failed to build userspace UDP stack")?; + let runner = runner.context("userspace UDP stack runner unavailable")?; + let udp_socket = udp_socket.context("userspace UDP stack socket unavailable")?; let (mut stack_sink, mut stack_stream) = stack.split(); let (mut udp_reader, mut udp_writer) = udp_socket.split(); @@ -521,21 +437,18 @@ async fn try_packet_udp_echo( .await? .into_inner(); - let ingress_label = label.clone(); let ingress_task = tokio::spawn(async move { loop { match tunnel_packets.message().await? { Some(packet) => { log::debug!( - "{} UDP echo received {} bytes from daemon packet stream", - ingress_label, + "tailnet udp echo received {} bytes from daemon packet stream", packet.payload.len() ); - stack_sink.send(packet.payload).await.with_context(|| { - format!( - "failed to feed inbound {ingress_label} packet into userspace stack" - ) - })?; + stack_sink + .send(packet.payload) + .await + .context("failed to feed inbound tailnet packet into userspace stack")?; } None => break, } @@ -543,21 +456,17 @@ async fn try_packet_udp_echo( Result::<()>::Ok(()) }); - let egress_label = label.clone(); let egress_task = tokio::spawn(async move { while let Some(packet) = stack_stream.next().await { let payload = packet.context("failed to read outbound packet from userspace stack")?; log::debug!( - "{} UDP echo sending {} bytes into daemon packet stream", - egress_label, + "tailnet udp echo sending {} bytes into daemon packet stream", payload.len() ); outbound_tx .send(TunnelPacket { payload }) .await - .with_context(|| { - format!("failed to forward outbound {egress_label} packet to daemon") - })?; + .context("failed to forward outbound tailnet packet to daemon")?; } Result::<()>::Ok(()) }); @@ -567,13 +476,13 @@ async fn try_packet_udp_echo( udp_writer .send((message.as_bytes().to_vec(), local_addr, remote_addr)) .await - .with_context(|| format!("failed to send {label} UDP echo probe into userspace stack"))?; - log::debug!("{label} UDP echo probe queued from {local_addr} to {remote_addr}"); + .context("failed to send UDP echo probe into userspace stack")?; + log::debug!("tailnet udp echo probe queued from {local_addr} to {remote_addr}"); let response = timeout(Duration::from_millis(timeout_ms), udp_reader.next()) .await - .with_context(|| format!("timed out waiting for {label} UDP echo from {remote_addr}"))? - .with_context(|| format!("userspace {label} UDP stack ended before returning a reply"))?; + .with_context(|| format!("timed out waiting for UDP echo from {remote_addr}"))? + .context("userspace UDP stack ended before returning a reply")?; let (payload, reply_source, reply_destination) = response; let response_text = String::from_utf8_lossy(&payload); @@ -591,691 +500,9 @@ async fn try_packet_udp_echo( bail!("UDP echo payload mismatch"); } - println!("{label} UDP Echo Source: {reply_source}"); - println!("{label} UDP Echo Destination: {reply_destination}"); - println!("{label} UDP Echo Payload: {response_text}"); - Ok(()) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_proxy_subscription_preview(url: &str) -> Result<()> { - let body = proxy_subscription::fetch_subscription(url).await?; - let preview = proxy_subscription::parse_subscription_body(&body, Some(url)); - - println!("Proxy subscription nodes: {}", preview.nodes.len()); - println!("Rejected entries: {}", preview.rejected.len()); - println!("Detected format: {}", preview.detected_format.as_str()); - for node in preview.nodes.iter() { - println!( - "{}: {} {}:{} ({})", - node.ordinal, - node.name, - node.server, - node.port, - node.protocol.as_str() - ); - } - Ok(()) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn proxy_dns_probe( - dns_name: &str, - dns_server: &str, - timeout_ms: u64, -) -> Result { - use std::net::{IpAddr, SocketAddr}; - - use anyhow::{bail, Context}; - use futures::{SinkExt, StreamExt}; - use netstack_smoltcp::StackBuilder; - use rand::Rng; - use tokio::{ - sync::mpsc, - time::{timeout, Duration}, - }; - use tokio_stream::wrappers::ReceiverStream; - - use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket}; - - let dns_server_addr: SocketAddr = dns_server - .parse() - .with_context(|| format!("invalid DNS server socket address {dns_server}"))?; - - let mut query = proxy_runtime::build_dns_query(dns_name, 1)?; - let query_id = rand::thread_rng().gen::(); - query[0..2].copy_from_slice(&query_id.to_be_bytes()); - - let mut client = BurrowClient::from_uds().await?; - client.tunnel_client.tunnel_start(Empty {}).await?; - let mut config_stream = client - .tunnel_client - .tunnel_configuration(Empty {}) - .await? - .into_inner(); - let config = config_stream - .message() - .await? - .context("tunnel configuration stream ended before yielding a config")?; - let local_addr = select_tailnet_local_socket(&config.addresses, dns_server_addr.ip())?; - - let (stack, runner, udp_socket, _) = StackBuilder::default() - .enable_udp(true) - .enable_tcp(true) - .build() - .context("failed to build userspace DNS probe stack")?; - let runner = runner.context("userspace DNS probe stack runner unavailable")?; - let udp_socket = udp_socket.context("userspace DNS probe socket unavailable")?; - let (mut stack_sink, mut stack_stream) = stack.split(); - let (mut udp_reader, mut udp_writer) = udp_socket.split(); - - let (outbound_tx, outbound_rx) = mpsc::channel::(128); - let mut tunnel_packets = client - .tunnel_client - .tunnel_packets(ReceiverStream::new(outbound_rx)) - .await? - .into_inner(); - - let ingress_task = tokio::spawn(async move { - while let Some(packet) = tunnel_packets.message().await? { - stack_sink - .send(packet.payload) - .await - .context("failed to feed inbound DNS probe packet into userspace stack")?; - } - Result::<()>::Ok(()) - }); - let egress_task = tokio::spawn(async move { - while let Some(packet) = stack_stream.next().await { - let payload = packet.context("failed to read outbound DNS probe packet")?; - outbound_tx - .send(TunnelPacket { payload }) - .await - .context("failed to forward outbound DNS probe packet to daemon")?; - } - Result::<()>::Ok(()) - }); - let runner_task = tokio::spawn(async move { runner.await.map_err(anyhow::Error::from) }); - - udp_writer - .send((query, local_addr, dns_server_addr)) - .await - .context("failed to send DNS probe query")?; - - let response = timeout(Duration::from_millis(timeout_ms), udp_reader.next()) - .await - .with_context(|| format!("timed out waiting for DNS response from {dns_server_addr}"))? - .context("userspace DNS probe stack ended before returning a reply")?; - let (payload, reply_source, _reply_destination) = response; - - ingress_task.abort(); - egress_task.abort(); - runner_task.abort(); - - if reply_source != dns_server_addr { - bail!("received DNS response from unexpected source {reply_source}"); - } - for ip in proxy_runtime::parse_dns_response(&payload, query_id)? { - if let IpAddr::V4(ip) = ip { - return Ok(ip); - } - } - bail!("DNS response for {dns_name} did not include an A record") -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_proxy_tcp_probe( - remote: &str, - host: &str, - dns_name: Option<&str>, - dns_server: &str, - timeout_ms: u64, -) -> Result<()> { - use std::net::{IpAddr, SocketAddr}; - - use anyhow::{bail, Context}; - use rand::Rng; - use tokio::{ - sync::mpsc, - time::{timeout, Duration}, - }; - use tokio_stream::wrappers::ReceiverStream; - - use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket}; - - let mut remote_addr: SocketAddr = remote - .parse() - .with_context(|| format!("invalid remote socket address {remote}"))?; - let mut http_host = host.to_owned(); - if let Some(dns_name) = dns_name { - let resolved_ip = proxy_dns_probe(dns_name, dns_server, timeout_ms).await?; - remote_addr = SocketAddr::new(IpAddr::V4(resolved_ip), remote_addr.port()); - if host == "one.one.one.one" { - http_host = dns_name.to_owned(); - } - println!("Proxy TCP Probe DNS Name: {dns_name}"); - println!("Proxy TCP Probe DNS Resolved: {resolved_ip}"); - } - let remote_ip = match remote_addr.ip() { - IpAddr::V4(ip) => ip, - IpAddr::V6(_) => bail!("proxy tcp probe currently supports IPv4 targets only"), - }; - - let mut client = BurrowClient::from_uds().await?; - client.tunnel_client.tunnel_start(Empty {}).await?; - - let mut config_stream = client - .tunnel_client - .tunnel_configuration(Empty {}) - .await? - .into_inner(); - let config = config_stream - .message() - .await? - .context("tunnel configuration stream ended before yielding a config")?; - let local_addr = select_tailnet_local_socket(&config.addresses, remote_addr.ip())?; - let local_ip = match local_addr.ip() { - IpAddr::V4(ip) => ip, - IpAddr::V6(_) => bail!("proxy tcp probe selected an IPv6 local address"), - }; - let local_port = local_addr.port(); - - let (outbound_tx, outbound_rx) = mpsc::channel::(128); - let mut tunnel_packets = client - .tunnel_client - .tunnel_packets(ReceiverStream::new(outbound_rx)) - .await? - .into_inner(); - - let initial_seq = rand::thread_rng().gen::(); - let syn = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - initial_seq, - 0, - TCP_SYN, - &[], - ); - outbound_tx - .send(TunnelPacket { payload: syn }) - .await - .context("failed to send proxy tcp SYN into daemon packet stream")?; - - let syn_ack = timeout(Duration::from_millis(timeout_ms), async { - loop { - let packet = tunnel_packets - .message() - .await - .context("failed to read packet from daemon packet stream")? - .context("daemon packet stream ended before SYN-ACK")?; - if let Some(segment) = parse_tcp_ipv4_segment( - &packet.payload, - local_ip, - remote_ip, - local_port, - remote_addr.port(), - )? { - if segment.flags & TCP_SYN != 0 && segment.flags & TCP_ACK != 0 { - break Ok::<_, anyhow::Error>(segment); - } - if segment.flags & TCP_RST != 0 { - bail!("proxy tcp probe received RST before SYN-ACK"); - } - } - } - }) - .await - .with_context(|| format!("timed out waiting for proxy tcp SYN-ACK from {remote_addr}"))??; - - let mut client_seq = initial_seq.wrapping_add(1); - let mut peer_ack = syn_ack.sequence.wrapping_add(1); - let ack = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_ACK, - &[], - ); - outbound_tx - .send(TunnelPacket { payload: ack }) - .await - .context("failed to send proxy tcp handshake ACK")?; - - let request = format!( - "GET / HTTP/1.1\r\nHost: {http_host}\r\nConnection: close\r\nUser-Agent: burrow-proxy-tcp-probe\r\n\r\n" - ) - .into_bytes(); - let request_packet = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_PSH | TCP_ACK, - &request, - ); - outbound_tx - .send(TunnelPacket { payload: request_packet }) - .await - .context("failed to send proxy tcp HTTP request")?; - client_seq = client_seq.wrapping_add(request.len() as u32); - - let response = timeout(Duration::from_millis(timeout_ms), async { - loop { - let packet = tunnel_packets - .message() - .await - .context("failed to read packet from daemon packet stream")? - .context("daemon packet stream ended before HTTP response")?; - let Some(segment) = parse_tcp_ipv4_segment( - &packet.payload, - local_ip, - remote_ip, - local_port, - remote_addr.port(), - )? - else { - continue; - }; - - if segment.flags & TCP_RST != 0 { - bail!("proxy tcp probe received RST before HTTP response"); - } - if !segment.payload.is_empty() { - peer_ack = segment.sequence.wrapping_add(segment.payload.len() as u32); - let ack = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_ACK, - &[], - ); - outbound_tx - .send(TunnelPacket { payload: ack }) - .await - .context("failed to ACK proxy tcp response")?; - break Ok::<_, anyhow::Error>(segment.payload); - } - if segment.flags & TCP_FIN != 0 { - bail!("proxy tcp probe received FIN before HTTP response bytes"); - } - } - }) - .await - .with_context(|| { - format!("timed out waiting for HTTP response through proxy to {remote_addr}") - })??; - - let first_line = String::from_utf8_lossy(&response) - .lines() - .next() - .unwrap_or("") - .to_owned(); - println!("Proxy TCP Probe Local: {local_addr}"); - println!("Proxy TCP Probe Remote: {remote_addr}"); - println!("Proxy TCP Probe Response Bytes: {}", response.len()); - println!("Proxy TCP Probe First Line: {first_line}"); - Ok(()) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn open_proxy_packet_tcp_stream( - remote_addr: std::net::SocketAddr, - timeout_ms: u64, -) -> Result<(tokio::io::DuplexStream, std::net::SocketAddr)> { - use std::net::IpAddr; - - use anyhow::{bail, Context}; - use rand::Rng; - use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - sync::mpsc, - time::{timeout, Duration}, - }; - use tokio_stream::wrappers::ReceiverStream; - - use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket}; - - let remote_ip = match remote_addr.ip() { - IpAddr::V4(ip) => ip, - IpAddr::V6(_) => bail!("proxy packet tcp stream currently supports IPv4 targets only"), - }; - - let mut client = BurrowClient::from_uds().await?; - client.tunnel_client.tunnel_start(Empty {}).await?; - - let mut config_stream = client - .tunnel_client - .tunnel_configuration(Empty {}) - .await? - .into_inner(); - let config = config_stream - .message() - .await? - .context("tunnel configuration stream ended before yielding a config")?; - let local_addr = select_tailnet_local_socket(&config.addresses, remote_addr.ip())?; - let local_ip = match local_addr.ip() { - IpAddr::V4(ip) => ip, - IpAddr::V6(_) => bail!("proxy packet tcp stream selected an IPv6 local address"), - }; - let local_port = local_addr.port(); - - let (outbound_tx, outbound_rx) = mpsc::channel::(128); - let mut tunnel_packets = client - .tunnel_client - .tunnel_packets(ReceiverStream::new(outbound_rx)) - .await? - .into_inner(); - - let initial_seq = rand::thread_rng().gen::(); - let syn = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - initial_seq, - 0, - TCP_SYN, - &[], - ); - outbound_tx - .send(TunnelPacket { payload: syn }) - .await - .context("failed to send proxy packet TCP SYN into daemon packet stream")?; - - let syn_ack = timeout(Duration::from_millis(timeout_ms), async { - loop { - let packet = tunnel_packets - .message() - .await - .context("failed to read packet from daemon packet stream")? - .context("daemon packet stream ended before SYN-ACK")?; - if let Some(segment) = parse_tcp_ipv4_segment( - &packet.payload, - local_ip, - remote_ip, - local_port, - remote_addr.port(), - )? { - if segment.flags & TCP_SYN != 0 && segment.flags & TCP_ACK != 0 { - break Ok::<_, anyhow::Error>(segment); - } - if segment.flags & TCP_RST != 0 { - bail!("proxy packet TCP stream received RST before SYN-ACK"); - } - } - } - }) - .await - .with_context(|| format!("timed out waiting for proxy TCP SYN-ACK from {remote_addr}"))??; - - let mut client_seq = initial_seq.wrapping_add(1); - let mut peer_ack = syn_ack.sequence.wrapping_add(1); - let ack = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_ACK, - &[], - ); - outbound_tx - .send(TunnelPacket { payload: ack }) - .await - .context("failed to send proxy packet TCP handshake ACK")?; - - let (client_stream, manager_stream) = tokio::io::duplex(128 * 1024); - let (mut app_reader, mut app_writer) = tokio::io::split(manager_stream); - tokio::spawn(async move { - let mut app_buf = vec![0u8; 8192]; - loop { - tokio::select! { - read = app_reader.read(&mut app_buf) => { - let read = match read { - Ok(read) => read, - Err(error) => { - ::tracing::debug!(%error, "proxy packet TCP stream app read failed"); - break; - } - }; - if read == 0 { - let fin = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_FIN | TCP_ACK, - &[], - ); - let _ = outbound_tx.send(TunnelPacket { payload: fin }).await; - break; - } - - for chunk in app_buf[..read].chunks(1200) { - let packet = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_PSH | TCP_ACK, - chunk, - ); - if let Err(error) = outbound_tx.send(TunnelPacket { payload: packet }).await { - ::tracing::debug!(%error, "proxy packet TCP stream outbound send failed"); - return; - } - client_seq = client_seq.wrapping_add(chunk.len() as u32); - } - } - packet = tunnel_packets.message() => { - let packet = match packet { - Ok(Some(packet)) => packet, - Ok(None) => break, - Err(error) => { - ::tracing::debug!(%error, "proxy packet TCP stream inbound read failed"); - break; - } - }; - let segment = match parse_tcp_ipv4_segment( - &packet.payload, - local_ip, - remote_ip, - local_port, - remote_addr.port(), - ) { - Ok(Some(segment)) => segment, - Ok(None) => continue, - Err(error) => { - ::tracing::debug!(%error, "proxy packet TCP stream segment parse failed"); - break; - } - }; - if segment.flags & TCP_RST != 0 { - ::tracing::debug!("proxy packet TCP stream received RST"); - break; - } - - let mut ack_increment = segment.payload.len() as u32; - if segment.flags & TCP_FIN != 0 { - ack_increment = ack_increment.wrapping_add(1); - } - if ack_increment > 0 { - peer_ack = segment.sequence.wrapping_add(ack_increment); - let ack = build_tcp_ipv4_packet( - local_ip, - remote_ip, - local_port, - remote_addr.port(), - client_seq, - peer_ack, - TCP_ACK, - &[], - ); - if let Err(error) = outbound_tx.send(TunnelPacket { payload: ack }).await { - ::tracing::debug!(%error, "proxy packet TCP stream ACK send failed"); - return; - } - } - - if !segment.payload.is_empty() { - if let Err(error) = app_writer.write_all(&segment.payload).await { - ::tracing::debug!(%error, "proxy packet TCP stream app write failed"); - break; - } - } - if segment.flags & TCP_FIN != 0 { - let _ = app_writer.shutdown().await; - break; - } - } - } - } - }); - - Ok((client_stream, local_addr)) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_proxy_https_probe( - remote: &str, - host: Option<&str>, - dns_name: Option<&str>, - dns_server: &str, - timeout_ms: u64, -) -> Result<()> { - use std::{ - net::{IpAddr, SocketAddr}, - sync::{Arc, Once}, - }; - - use anyhow::{bail, Context}; - use rustls::{pki_types::ServerName, ClientConfig, RootCertStore}; - use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - time::{timeout, Duration}, - }; - use tokio_rustls::TlsConnector; - - static INSTALL_RUSTLS_PROVIDER: Once = Once::new(); - INSTALL_RUSTLS_PROVIDER.call_once(|| { - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - }); - - let mut remote_addr: SocketAddr = remote - .parse() - .with_context(|| format!("invalid remote socket address {remote}"))?; - let tls_host = host - .or(dns_name) - .context("proxy HTTPS probe requires --host or --dns-name for TLS SNI")? - .to_owned(); - if let Some(dns_name) = dns_name { - let resolved_ip = proxy_dns_probe(dns_name, dns_server, timeout_ms).await?; - remote_addr = SocketAddr::new(IpAddr::V4(resolved_ip), remote_addr.port()); - println!("Proxy HTTPS Probe DNS Name: {dns_name}"); - println!("Proxy HTTPS Probe DNS Resolved: {resolved_ip}"); - } - - if !remote_addr.is_ipv4() { - bail!("proxy HTTPS probe currently supports IPv4 targets only"); - } - - let mut root_store = RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let mut tls_config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - tls_config.alpn_protocols = vec![b"http/1.1".to_vec()]; - - let connector = TlsConnector::from(Arc::new(tls_config)); - let server_name = ServerName::try_from(tls_host.clone()) - .with_context(|| format!("invalid TLS server name {tls_host}"))?; - - let (stream, local_addr) = open_proxy_packet_tcp_stream(remote_addr, timeout_ms).await?; - let mut tls_stream = timeout( - Duration::from_millis(timeout_ms), - connector.connect(server_name, stream), - ) - .await - .with_context(|| format!("timed out waiting for TLS handshake with {tls_host}"))? - .with_context(|| format!("TLS handshake with {tls_host} failed"))?; - - let request = format!( - "GET / HTTP/1.1\r\nHost: {tls_host}\r\nConnection: close\r\nUser-Agent: burrow-proxy-https-probe\r\n\r\n" - ); - timeout( - Duration::from_millis(timeout_ms), - tls_stream.write_all(request.as_bytes()), - ) - .await - .with_context(|| format!("timed out sending HTTPS request to {tls_host}"))? - .with_context(|| format!("failed to send HTTPS request to {tls_host}"))?; - - let response = timeout(Duration::from_millis(timeout_ms), async { - let mut response = Vec::new(); - let mut buf = [0u8; 8192]; - loop { - let read = tls_stream - .read(&mut buf) - .await - .context("failed to read HTTPS response")?; - if read == 0 { - break; - } - response.extend_from_slice(&buf[..read]); - if response.iter().any(|byte| *byte == b'\n') { - break; - } - } - if response.is_empty() { - bail!("HTTPS response ended before returning bytes"); - } - Ok::<_, anyhow::Error>(response) - }) - .await - .with_context(|| format!("timed out waiting for HTTPS response from {tls_host}"))??; - - let first_line = String::from_utf8_lossy(&response) - .lines() - .next() - .unwrap_or("") - .to_owned(); - println!("Proxy HTTPS Probe Local: {local_addr}"); - println!("Proxy HTTPS Probe Remote: {remote_addr}"); - println!("Proxy HTTPS Probe Host: {tls_host}"); - println!("Proxy HTTPS Probe Response Bytes: {}", response.len()); - println!("Proxy HTTPS Probe First Line: {first_line}"); - Ok(()) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_proxy_subscription_select(id: i32, selected_ordinal: i32) -> Result<()> { - use crate::daemon::rpc::grpc_defs::ProxySubscriptionSelectRequest; - - let mut client = BurrowClient::from_uds().await?; - let response = client - .proxy_subscription_client - .select_node(ProxySubscriptionSelectRequest { id, selected_ordinal }) - .await? - .into_inner(); - println!( - "Proxy Subscription Selected: id={} selected_ordinal={}", - response.id, response.selected_ordinal - ); + println!("Tailnet UDP Echo Source: {reply_source}"); + println!("Tailnet UDP Echo Destination: {reply_destination}"); + println!("Tailnet UDP Echo Payload: {response_text}"); Ok(()) } @@ -1320,24 +547,6 @@ struct IcmpEchoReply { payload: Vec, } -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -const TCP_FIN: u16 = 0x01; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -const TCP_SYN: u16 = 0x02; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -const TCP_RST: u16 = 0x04; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -const TCP_PSH: u16 = 0x08; -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -const TCP_ACK: u16 = 0x10; - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -struct TcpSegment { - sequence: u32, - flags: u16, - payload: Vec, -} - #[cfg(any(target_os = "linux", target_vendor = "apple"))] fn build_icmp_echo_request( source: std::net::IpAddr, @@ -1434,113 +643,6 @@ fn parse_icmp_echo_reply( })) } -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -fn build_tcp_ipv4_packet( - source: std::net::Ipv4Addr, - destination: std::net::Ipv4Addr, - source_port: u16, - destination_port: u16, - sequence: u32, - ack: u32, - flags: u16, - payload: &[u8], -) -> Vec { - let tcp_len = 20 + payload.len(); - let total_len = 20 + tcp_len; - let mut packet = Vec::with_capacity(total_len); - packet.push(0x45); - packet.push(0); - packet.extend_from_slice(&(total_len as u16).to_be_bytes()); - packet.extend_from_slice(&0u16.to_be_bytes()); - packet.extend_from_slice(&0x4000u16.to_be_bytes()); - packet.push(64); - packet.push(6); - packet.extend_from_slice(&[0, 0]); - packet.extend_from_slice(&source.octets()); - packet.extend_from_slice(&destination.octets()); - let header_checksum = internet_checksum(&packet); - packet[10..12].copy_from_slice(&header_checksum.to_be_bytes()); - - let tcp_start = packet.len(); - packet.extend_from_slice(&source_port.to_be_bytes()); - packet.extend_from_slice(&destination_port.to_be_bytes()); - packet.extend_from_slice(&sequence.to_be_bytes()); - packet.extend_from_slice(&ack.to_be_bytes()); - packet.push(5 << 4); - packet.push((flags & 0xff) as u8); - packet.extend_from_slice(&64240u16.to_be_bytes()); - packet.extend_from_slice(&[0, 0]); - packet.extend_from_slice(&0u16.to_be_bytes()); - packet.extend_from_slice(payload); - - let checksum = tcp_ipv4_checksum(source, destination, &packet[tcp_start..]); - packet[tcp_start + 16..tcp_start + 18].copy_from_slice(&checksum.to_be_bytes()); - packet -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -fn parse_tcp_ipv4_segment( - packet: &[u8], - local_ip: std::net::Ipv4Addr, - remote_ip: std::net::Ipv4Addr, - local_port: u16, - remote_port: u16, -) -> Result> { - use anyhow::bail; - - if packet.len() < 40 { - return Ok(None); - } - let version = packet[0] >> 4; - if version != 4 { - return Ok(None); - } - let ihl = (packet[0] & 0x0f) as usize * 4; - if packet.len() < ihl + 20 || packet[9] != 6 { - return Ok(None); - } - - let source = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]); - let destination = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]); - if source != remote_ip || destination != local_ip { - return Ok(None); - } - - let tcp = &packet[ihl..]; - let source_port = u16::from_be_bytes([tcp[0], tcp[1]]); - let destination_port = u16::from_be_bytes([tcp[2], tcp[3]]); - if source_port != remote_port || destination_port != local_port { - return Ok(None); - } - let data_offset = ((tcp[12] >> 4) as usize) * 4; - if data_offset < 20 || tcp.len() < data_offset { - bail!("invalid TCP data offset in proxy tcp probe response"); - } - let sequence = u32::from_be_bytes([tcp[4], tcp[5], tcp[6], tcp[7]]); - let flags = (tcp[13] as u16) & 0x3f; - Ok(Some(TcpSegment { - sequence, - flags, - payload: tcp[data_offset..].to_vec(), - })) -} - -#[cfg(any(target_os = "linux", target_vendor = "apple"))] -fn tcp_ipv4_checksum( - source: std::net::Ipv4Addr, - destination: std::net::Ipv4Addr, - tcp_segment: &[u8], -) -> u16 { - let mut pseudo = Vec::with_capacity(12 + tcp_segment.len()); - pseudo.extend_from_slice(&source.octets()); - pseudo.extend_from_slice(&destination.octets()); - pseudo.push(0); - pseudo.push(6); - pseudo.extend_from_slice(&(tcp_segment.len() as u16).to_be_bytes()); - pseudo.extend_from_slice(tcp_segment); - internet_checksum(&pseudo) -} - #[cfg(any(target_os = "linux", target_vendor = "apple"))] fn internet_checksum(bytes: &[u8]) -> u16 { let mut sum = 0u32; @@ -1673,35 +775,6 @@ async fn main() -> Result<()> { Commands::TailnetUdpEcho(args) => { try_tailnet_udp_echo(&args.remote, &args.message, args.timeout_ms).await? } - Commands::ProxyTcpProbe(args) => { - try_proxy_tcp_probe( - &args.remote, - &args.host, - args.dns_name.as_deref(), - &args.dns_server, - args.timeout_ms, - ) - .await? - } - Commands::ProxyUdpEcho(args) => { - try_proxy_udp_echo(&args.remote, &args.message, args.timeout_ms).await? - } - Commands::ProxyHttpsProbe(args) => { - try_proxy_https_probe( - &args.remote, - args.host.as_deref(), - args.dns_name.as_deref(), - &args.dns_server, - args.timeout_ms, - ) - .await? - } - Commands::ProxySubscriptionSelect(args) => { - try_proxy_subscription_select(args.id, args.selected_ordinal).await? - } - Commands::ProxySubscriptionPreview(args) => { - try_proxy_subscription_preview(&args.url).await? - } #[cfg(target_os = "linux")] Commands::Exec(args) => { try_exec( diff --git a/burrow/src/proxy_runtime.rs b/burrow/src/proxy_runtime.rs deleted file mode 100644 index 7eacdda..0000000 --- a/burrow/src/proxy_runtime.rs +++ /dev/null @@ -1,8346 +0,0 @@ -#[cfg(target_vendor = "apple")] -use std::{cmp::Reverse, ffi::CStr, process::Command}; -use std::{ - collections::{HashMap, VecDeque}, - fs, - future::Future, - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}, - num::NonZeroU32, - os::fd::AsRawFd, - pin::Pin, - str::FromStr, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex as StdMutex, Once, - }, - task::{Context as TaskContext, Poll}, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; - -use aead::{ - generic_array::{ - typenum::{U12, U16, U24}, - GenericArray, - }, - AeadInPlace, KeyInit, -}; -use aegis::{aegis128l::Aegis128L, aegis256::Aegis256}; -use aes::{ - cipher::{generic_array::GenericArray as AesGenericArray, BlockDecrypt, BlockEncrypt}, - Aes128, Aes192, Aes256, -}; -use aes_gcm::AesGcm; -use anyhow::{anyhow, bail, Context, Result}; -use base64::{ - engine::general_purpose::{ - STANDARD as BASE64_STANDARD, URL_SAFE as BASE64_URL_SAFE, - URL_SAFE_NO_PAD as BASE64_URL_SAFE_NO_PAD, - }, - Engine as _, -}; -use bytes::Bytes; -use ccm::Ccm; -use chacha20::{ - cipher::{KeyIvInit as ChachaKeyIvInit, StreamCipher as ChachaStreamCipher}, - ChaCha20Legacy, XChaCha20, -}; -use chacha20poly1305::{ChaCha8Poly1305, XChaCha8Poly1305}; -use deoxys::{ - aead::{array::Array as DeoxysArray, AeadInOut as DeoxysAeadInOut, KeyInit as DeoxysKeyInit}, - DeoxysII256, -}; -use futures::{SinkExt, StreamExt}; -use http::{Method, Request, StatusCode}; -use lea::{ - cipher::{ - generic_array::GenericArray as LeaGenericArray, BlockEncrypt as LeaBlockEncrypt, - NewBlockCipher as LeaNewBlockCipher, - }, - Lea128, Lea192, Lea256, -}; -use md5::Md5; -#[cfg(target_vendor = "apple")] -use native_tls::{Identity as NativeTlsIdentity, TlsConnector as NativeTlsConnector}; -use netstack_smoltcp::{ - StackBuilder, TcpListener as StackTcpListener, TcpStream as StackTcpStream, - UdpSocket as StackUdpSocket, -}; -use poly1305::{universal_hash::UniversalHash, Poly1305}; -use rabbit::{ - cipher::{KeyIvInit as RabbitKeyIvInit, StreamCipher as RabbitStreamCipher}, - Rabbit, -}; -#[cfg(feature = "boring-browser-fingerprints")] -use rama_net::{ - address::Domain as RamaDomain, - tls::{ - client::{ - ClientHelloExtension as RamaClientHelloExtension, - ServerVerifyMode as RamaServerVerifyMode, - }, - SupportedGroup as RamaSupportedGroup, - }, -}; -#[cfg(feature = "boring-browser-fingerprints")] -use rama_tls_boring::{ - client::{ - ConnectorConfigClientAuth as BoringConnectorConfigClientAuth, - TlsConnectorDataBuilder as BoringTlsConnectorDataBuilder, - }, - core::{ - hash::MessageDigest as BoringMessageDigest, - pkey::PKey as BoringPKey, - x509::{X509Ref as BoringX509Ref, X509 as BoringX509}, - }, -}; -#[cfg(feature = "boring-browser-fingerprints")] -use rama_ua::{ - profile::{try_load_embedded_profiles, TlsProfile}, - PlatformKind, UserAgentKind, -}; -use rand::{rngs::OsRng, Rng, RngCore}; -use ring::{hmac, pbkdf2}; -use rust_tokio_kcp::{ - Aes128BlockCrypt as KcpAes128BlockCrypt, Aes192BlockCrypt as KcpAes192BlockCrypt, - Aes256BlockCrypt as KcpAes256BlockCrypt, AesGcmBlockCrypt as KcpAesGcmBlockCrypt, - BlockCrypt as KcpBlockCrypt, BlowfishBlockCrypt as KcpBlowfishBlockCrypt, - Cast5BlockCrypt as KcpCast5BlockCrypt, KcpConfig, KcpNoDelayConfig, KcpStream, - NoneBlockCrypt as KcpNoneBlockCrypt, Salsa20BlockCrypt as KcpSalsa20BlockCrypt, - SimpleXorBlockCrypt as KcpSimpleXorBlockCrypt, Sm4BlockCrypt as KcpSm4BlockCrypt, - TeaBlockCrypt as KcpTeaBlockCrypt, TripleDesBlockCrypt as KcpTripleDesBlockCrypt, - TwofishBlockCrypt as KcpTwofishBlockCrypt, XteaBlockCrypt as KcpXteaBlockCrypt, -}; -use rustls::{ - client::{ - danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - EchConfig, EchMode, - }, - pki_types::{CertificateDer, EchConfigListBytes, PrivateKeyDer, ServerName, UnixTime}, - version, ClientConfig, DigitallySignedStruct, Error as RustlsError, RootCertStore, - SignatureScheme, -}; -use sha2::{Digest, Sha224, Sha256}; -use shadowsocks::{ - config::{ - ServerAddr as SsServerAddr, ServerConfig as SsServerConfig, ServerType as SsServerType, - }, - context::{Context as SsContext, SharedContext as SsSharedContext}, - crypto::CipherKind as SsCipherKind, - relay::{ - socks5::Address as SsAddress, - udprelay::{ - proxy_socket::UdpSocketType as SsUdpSocketType, DatagramReceive, DatagramSend, - DatagramSocket, ProxySocket as SsProxySocket, - }, - }, - ProxyClientStream as SsProxyClientStream, -}; -use subtle::ConstantTimeEq; -use tokio::{ - io::{split, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}, - net::{TcpSocket, TcpStream, UdpSocket}, - sync::{broadcast, mpsc, oneshot, Mutex, RwLock}, - task::{JoinHandle, JoinSet}, - time::timeout, -}; -#[cfg(target_vendor = "apple")] -use tokio_native_tls::TlsConnector as TokioNativeTlsConnector; -use tokio_rustls::TlsConnector; -use tokio_snappy::SnappyIO; -use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; -use tun::tokio::TunInterface; - -use crate::proxy_subscription::{ - ProxyNode, ProxyNodeConfig, ProxySubscriptionPayload, ShadowsocksNode, ShadowsocksPlugin, - ShadowsocksSmux, TrojanNetwork, TrojanNode, -}; - -const PROXY_TUN_V4: &str = "100.64.0.2/24"; -const PROXY_TUN_V6: &str = "fd00:64::2/64"; -const PROXY_DNS_V4: &str = "100.64.0.2"; -const UDP_IDLE_TIMEOUT: Duration = Duration::from_secs(30); -const TROJAN_MAX_UDP_PAYLOAD: usize = 8192; -const MIHOMO_TROJAN_TCP_DEFAULT_ALPN: &[&str] = &["h2", "http/1.1"]; -const SS_INFO: &[u8] = b"ss-subkey"; -const UOT_VERSION: u8 = 2; -#[allow(dead_code)] -const TLS_RECORD_HEADER_LEN: usize = 5; -#[allow(dead_code)] -const TLS_APPLICATION_DATA_RECORD_TYPE: u8 = 0x17; -#[allow(dead_code)] -const TLS12_RECORD_VERSION: [u8; 2] = [0x03, 0x03]; -const UOT_LEGACY_VERSION: u8 = 1; -const UOT_MAGIC_ADDRESS: &str = "sp.v2.udp-over-tcp.arpa"; -const UOT_LEGACY_MAGIC_ADDRESS: &str = "sp.udp-over-tcp.arpa"; -const SING_MUX_ADDRESS: &str = "sp.mux.sing-box.arpa"; -const SING_MUX_PORT: u16 = 444; -const SING_MUX_PROTOCOL_SMUX: u8 = 0; -const SING_MUX_PROTOCOL_YAMUX: u8 = 1; -const SING_MUX_PROTOCOL_H2MUX: u8 = 2; -const SING_MUX_VERSION_0: u8 = 0; -const SING_MUX_VERSION_1: u8 = 1; -const SING_MUX_PADDING_FRAMES: usize = 16; -const SING_MUX_MIN_PADDING_LEN: usize = 256; -const SING_MUX_PADDING_LEN_RANGE: usize = 512; -const SING_MUX_STREAM_FLAG_UDP: u16 = 1; -const SING_MUX_STREAM_STATUS_SUCCESS: u8 = 0; -const SING_MUX_STREAM_STATUS_ERROR: u8 = 1; -const SING_MUX_BRUTAL_EXCHANGE_DOMAIN: &str = "_BrutalBwExchange"; -const AEAD2022_SESSION_SUBKEY_CONTEXT: &str = "shadowsocks 2022 session subkey"; -const AEAD2022_IDENTITY_SUBKEY_CONTEXT: &str = "shadowsocks 2022 identity subkey"; -const AEAD2022_HEADER_TYPE_CLIENT: u8 = 0; -const AEAD2022_HEADER_TYPE_SERVER: u8 = 1; -const AEAD2022_TIMESTAMP_MAX_DIFF: u64 = 30; -const PUBLIC_DNS_SERVERS: &[&str] = &["1.1.1.1", "8.8.8.8"]; -const SHADOW_TLS_MAX_RECORD_PAYLOAD: usize = 16 * 1024; -const SHADOW_TLS_V3_HMAC_SIZE: usize = 4; -const SHADOW_TLS_V3_TLS_HEADER_SIZE: usize = 5; -const SHADOW_TLS_V3_TLS_RANDOM_SIZE: usize = 32; -const SHADOW_TLS_V3_SESSION_ID_SIZE: usize = 32; -const SHADOW_TLS_V3_SESSION_ID_START: usize = 1 + 3 + 2 + SHADOW_TLS_V3_TLS_RANDOM_SIZE + 1; -const SHADOW_TLS_V3_SERVER_RANDOM_INDEX: usize = SHADOW_TLS_V3_TLS_HEADER_SIZE + 1 + 3 + 2; -const SHADOW_TLS_V3_SESSION_ID_LENGTH_INDEX: usize = - SHADOW_TLS_V3_SERVER_RANDOM_INDEX + SHADOW_TLS_V3_TLS_RANDOM_SIZE; -const SHADOW_TLS_V3_HMAC_HEADER_SIZE: usize = - SHADOW_TLS_V3_TLS_HEADER_SIZE + SHADOW_TLS_V3_HMAC_SIZE; -const KCPTUN_RATE_LIMIT_BURST_BYTES: u64 = 64 * 1500; -const KCPTUN_SMUX_DEFAULT_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(30); -const RESTLS_DEFAULT_SCRIPT: &str = "250?100<1,350~100<1,600~100,300~200,300~100"; -#[allow(dead_code)] -const RESTLS_APP_DATA_MAC_LEN: usize = 8; -#[allow(dead_code)] -const RESTLS_CMD_LEN: usize = 2; -#[allow(dead_code)] -const RESTLS_MASK_LEN: usize = RESTLS_CMD_LEN + 2; -#[allow(dead_code)] -const RESTLS_APP_DATA_AUTH_HEADER_LEN: usize = RESTLS_APP_DATA_MAC_LEN + RESTLS_MASK_LEN; -#[allow(dead_code)] -const RESTLS_APP_DATA_OFFSET: usize = RESTLS_APP_DATA_AUTH_HEADER_LEN; -#[allow(dead_code)] -const RESTLS_APP_DATA_LEN_OFFSET: usize = RESTLS_APP_DATA_MAC_LEN; -#[allow(dead_code)] -const RESTLS_MAX_PLAINTEXT: usize = 16_384; -#[allow(dead_code)] -const RESTLS_RANDOM_RESPONSE_MAGIC: &[u8] = b"restls-random-response"; -#[allow(dead_code)] -const RESTLS_HANDSHAKE_MAC_LEN: usize = 16; -#[allow(dead_code)] -const RESTLS_TLS12_CLIENT_AUTH_LAYOUT_3: [usize; 4] = [0, 11, 22, 32]; -#[allow(dead_code)] -const RESTLS_TLS12_CLIENT_AUTH_LAYOUT_4: [usize; 5] = [0, 8, 16, 24, 32]; -const DIRECT_DNS_TIMEOUT: Duration = Duration::from_secs(2); -const DNS_TYPE_HTTPS: u16 = 65; -const HTTPS_SVC_PARAM_ECH: u16 = 5; -const FAKE_DNS_BASE: u32 = u32::from_be_bytes([198, 18, 64, 1]); - -type Aes192Gcm = AesGcm; -type Aes128Ccm = Ccm; -type Aes192Ccm = Ccm; -type Aes256Ccm = Ccm; - -pub struct ProxyRuntimeConfig { - pub selected_name: String, - pub outbound: ProxyOutbound, -} - -pub struct ProxyPacketBridge { - outbound: mpsc::Sender>, - inbound: broadcast::Sender>, - proxy_outbound: Arc>, - task: JoinHandle>, -} - -impl ProxyPacketBridge { - pub fn outbound_sender(&self) -> mpsc::Sender> { - self.outbound.clone() - } - - pub fn subscribe(&self) -> broadcast::Receiver> { - self.inbound.subscribe() - } - - pub async fn set_proxy_outbound(&self, outbound: ProxyOutbound) { - *self.proxy_outbound.write().await = outbound; - } - - pub fn abort(&self) { - self.task.abort(); - } - - pub async fn join(self) -> Result<()> { - match self.task.await { - Ok(Ok(())) => Ok(()), - Ok(Err(err)) => Err(err), - Err(err) if err.is_cancelled() => Ok(()), - Err(err) => Err(err.into()), - } - } -} - -#[derive(Clone)] -pub enum ProxyOutbound { - Trojan(TrojanOutbound), - Shadowsocks(ShadowsocksOutbound), -} - -type DnsHostnameCache = Arc>; - -#[derive(Debug)] -struct DnsHostnameCacheState { - reverse: HashMap, - fake_by_hostname: HashMap, - next_fake_offset: u32, -} - -impl DnsHostnameCacheState { - fn new() -> Self { - Self { - reverse: HashMap::new(), - fake_by_hostname: HashMap::new(), - next_fake_offset: 0, - } - } - - fn lookup_hostname(&self, ip: IpAddr) -> Option { - self.reverse.get(&ip).cloned() - } - - fn insert_mapping(&mut self, ip: IpAddr, hostname: String) { - self.reverse.insert(ip, hostname); - } - - fn fake_ip_for_hostname(&mut self, hostname: &str) -> Ipv4Addr { - if let Some(ip) = self.fake_by_hostname.get(hostname) { - return *ip; - } - - let raw = FAKE_DNS_BASE.wrapping_add(self.next_fake_offset); - self.next_fake_offset = self.next_fake_offset.wrapping_add(1); - let ip = Ipv4Addr::from(raw); - self.fake_by_hostname.insert(hostname.to_owned(), ip); - self.reverse.insert(IpAddr::V4(ip), hostname.to_owned()); - ip - } -} - -#[derive(Debug, Clone)] -struct ProxyTcpTarget { - address: SocketAddr, - hostname: Option, -} - -impl ProxyOutbound { - async fn bridge_tcp(&self, inbound: StackTcpStream, target: ProxyTcpTarget) -> Result<()> { - match self { - Self::Trojan(outbound) => outbound.bridge_tcp(inbound, target).await, - Self::Shadowsocks(outbound) => outbound.bridge_tcp(inbound, target).await, - } - } - - async fn start_udp_session( - &self, - key: UdpFlowKey, - outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - match self { - Self::Trojan(outbound) => outbound.run_udp_session(key, outbound_rx, reply_tx).await, - Self::Shadowsocks(outbound) => { - outbound.run_udp_session(key, outbound_rx, reply_tx).await - } - } - } -} - -#[derive(Clone)] -pub struct TrojanOutbound { - endpoint: ProxyServerEndpoint, - password: String, - sni: String, - alpn: Vec, - skip_cert_verify: bool, - udp_enabled: bool, - certificate_fingerprint: Option, -} - -#[derive(Clone)] -pub struct ShadowsocksOutbound { - endpoint: ProxyServerEndpoint, - protocol: ShadowsocksProtocol, - plugin: Option, - kcptun_pool: Option>, - sing_mux: Option, - sing_mux_sessions: Arc>>, - udp_enabled: bool, - udp_over_tcp: bool, - udp_over_tcp_version: u8, -} - -#[derive(Clone)] -enum ShadowsocksProtocol { - None, - Standard { - server_config: Arc, - context: SsSharedContext, - }, - CustomAead { - kind: CustomSsAeadKind, - master_key: Arc<[u8]>, - }, - CustomStream { - kind: CustomSsStreamKind, - key: Arc<[u8]>, - }, - Aead2022AesCcm { - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum ShadowsocksPluginTransport { - SimpleObfs { mode: SimpleObfsMode, host: String }, - WebSocket(ShadowsocksWebSocketTransport), - ShadowTls(ShadowsocksShadowTlsTransport), - Kcptun(ShadowsocksKcptunTransport), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol, - max_connections: usize, - min_streams: usize, - max_streams: usize, - padding: bool, - udp: bool, - brutal: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ShadowsocksSingMuxBrutalTransport { - send_bps: u64, - receive_bps: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ShadowsocksSingMuxProtocol { - H2Mux, - Smux, - Yamux, -} - -impl ShadowsocksSingMuxProtocol { - fn name(self) -> &'static str { - match self { - Self::H2Mux => "h2mux", - Self::Smux => "smux", - Self::Yamux => "yamux", - } - } - - fn wire_id(self) -> u8 { - match self { - Self::H2Mux => SING_MUX_PROTOCOL_H2MUX, - Self::Smux => SING_MUX_PROTOCOL_SMUX, - Self::Yamux => SING_MUX_PROTOCOL_YAMUX, - } - } -} - -#[derive(Clone)] -enum ShadowsocksSingMuxSession { - H2Mux(ShadowsocksH2MuxSession), - Smux(Arc), - Yamux(ShadowsocksYamuxSession), -} - -#[derive(Clone)] -struct ShadowsocksH2MuxSession { - send_request: h2::client::SendRequest, - active_streams: Arc, - closed: Arc, -} - -#[derive(Clone)] -struct ShadowsocksYamuxSession { - open_tx: mpsc::Sender, - active_streams: Arc, - closed: Arc, -} - -struct YamuxCommand { - response: oneshot::Sender>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SimpleObfsMode { - Http, - Tls, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind, - host: String, - path: String, - headers: Vec<(String, String)>, - tls: bool, - ech: Option, - skip_cert_verify: bool, - certificate_fingerprint: Option, - client_identity: Option, - mux: ShadowsocksWebSocketMux, - v2ray_http_upgrade: bool, - v2ray_http_upgrade_fast_open: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ShadowsocksWebSocketKind { - V2ray, - Gost, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ShadowsocksWebSocketMux { - None, - V2ray, - Gost, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksEchConfig { - config_list: Option>, - query_server_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksShadowTlsTransport { - host: String, - password: String, - version: u8, - alpn: Vec, - skip_cert_verify: bool, - certificate_fingerprint: Option, - client_identity: Option, - client_fingerprint: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct TlsClientIdentity { - certificate: String, - private_key: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksKcptunTransport { - key: String, - crypt: String, - mode: String, - conn: usize, - auto_expire: u64, - scavenge_ttl: u64, - mtu: usize, - rate_limit: u32, - sndwnd: u16, - rcvwnd: u16, - data_shard: usize, - parity_shard: usize, - dscp: u32, - no_comp: bool, - ack_nodelay: bool, - no_delay: i32, - interval: i32, - resend: i32, - no_congestion: bool, - sockbuf: usize, - smux_ver: u8, - smux_buf: usize, - frame_size: usize, - stream_buf: usize, - keep_alive: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShadowsocksRestlsTransport { - host: String, - password: String, - version_hint: RestlsVersionHint, - script: Vec, - client_fingerprint: RestlsClientFingerprint, - session_tickets_disabled: bool, - secret: [u8; 32], -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RestlsVersionHint { - Tls12, - Tls13, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RestlsClientFingerprint { - Chrome, - Firefox, - Safari, - Ios, -} - -impl RestlsClientFingerprint { - #[cfg(feature = "boring-browser-fingerprints")] - fn browser_profile(self) -> MihomoClientFingerprint { - match self { - Self::Chrome => MihomoClientFingerprint::Chrome, - Self::Firefox => MihomoClientFingerprint::Firefox, - Self::Safari => MihomoClientFingerprint::Safari, - Self::Ios => MihomoClientFingerprint::Ios, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MihomoClientFingerprint { - Chrome, - Firefox, - Safari, - Ios, - Android, - Edge, - Browser360, - Qq, - Random, - Chrome120, - Firefox120, - Safari16, - ChromePsk, - ChromePskShuffle, - ChromePaddingPskShuffle, - ChromePq, - ChromePqPsk, - Randomized, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct RestlsScriptLine { - target_len: RestlsTargetLength, - command: RestlsCommand, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct RestlsTargetLength { - base: u16, - random_range: u16, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RestlsCommand { - Noop, - Response(u16), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -enum RestlsRecordDirection { - ToServer, - ToClient, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsApplicationFrame { - data: Vec, - command: RestlsCommand, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsWriteResult { - record: Vec, - consumed: usize, - command: RestlsCommand, -} - -#[derive(Debug, Clone)] -#[allow(dead_code)] -struct RestlsApplicationCodec { - secret: [u8; 32], - server_random: [u8; 32], - to_server_counter: u64, - to_client_counter: u64, - tls12_gcm: bool, - tls12_gcm_to_client_disable_counter: bool, - client_finished_auth: Option>, -} - -#[derive(Debug, Clone)] -#[allow(dead_code)] -struct RestlsStreamState { - codec: RestlsApplicationCodec, - script: Vec, - send_buffer: Vec, - write_pending: bool, -} - -#[allow(dead_code)] -struct RestlsStream { - inner: S, - state: RestlsStreamState, - read_stage: RestlsRecordReadStage, - read_buf: Vec, - read_offset: usize, - write_buf: Vec, - write_offset: usize, -} - -#[allow(dead_code)] -enum RestlsRecordReadStage { - Header { - bytes: [u8; TLS_RECORD_HEADER_LEN], - filled: usize, - }, - Payload { - header: [u8; TLS_RECORD_HEADER_LEN], - bytes: Vec, - filled: usize, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsStreamWriteResult { - accepted: usize, - records: Vec>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsStreamReadResult { - data: Vec, - records: Vec>, - resumed_pending_write: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsServerAuthRecord { - record: Vec, - tls12_gcm_server_disable_counter: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -struct RestlsTls13ClientHelloAuthMaterial { - key_shares: Vec<(u16, Vec)>, - psk_labels: Vec>, -} - -struct KcptunSessionPool { - endpoint: ProxyServerEndpoint, - config: ShadowsocksKcptunTransport, - state: Mutex, -} - -struct KcptunSessionPoolState { - sessions: Vec>, - rr: usize, -} - -struct KcptunTimedSession { - session: Arc, - expiry_at: Option, -} - -impl Default for ShadowsocksKcptunTransport { - fn default() -> Self { - let mut config = Self::empty(); - config.fill_defaults(); - config - } -} - -impl ShadowsocksKcptunTransport { - fn empty() -> Self { - Self { - key: String::new(), - crypt: String::new(), - mode: String::new(), - conn: 0, - auto_expire: 0, - scavenge_ttl: 0, - mtu: 0, - rate_limit: 0, - sndwnd: 0, - rcvwnd: 0, - data_shard: 0, - parity_shard: 0, - dscp: 0, - no_comp: false, - ack_nodelay: false, - no_delay: 0, - interval: 0, - resend: 0, - no_congestion: false, - sockbuf: 0, - smux_ver: 0, - smux_buf: 0, - frame_size: 0, - stream_buf: 0, - keep_alive: 0, - } - } - - fn fill_defaults(&mut self) { - if self.key.is_empty() { - self.key = "it's a secrect".to_owned(); - } - if self.crypt.is_empty() { - self.crypt = "aes".to_owned(); - } - if self.mode.is_empty() { - self.mode = "fast".to_owned(); - } - if self.conn == 0 { - self.conn = 1; - } - if self.scavenge_ttl == 0 { - self.scavenge_ttl = 600; - } - if self.mtu == 0 { - self.mtu = 1350; - } - if self.sndwnd == 0 { - self.sndwnd = 128; - } - if self.rcvwnd == 0 { - self.rcvwnd = 512; - } - if self.data_shard == 0 { - self.data_shard = 10; - } - if self.parity_shard == 0 { - self.parity_shard = 3; - } - if self.interval == 0 { - self.interval = 50; - } - if self.sockbuf == 0 { - self.sockbuf = 4_194_304; - } - if self.smux_ver == 0 { - self.smux_ver = 1; - } - if self.smux_buf == 0 { - self.smux_buf = 4_194_304; - } - if self.frame_size == 0 { - self.frame_size = 8192; - } - if self.stream_buf == 0 { - self.stream_buf = 2_097_152; - } - if self.keep_alive == 0 { - self.keep_alive = 10; - } - match self.mode.trim().to_ascii_lowercase().as_str() { - "normal" => { - self.no_delay = 0; - self.interval = 40; - self.resend = 2; - self.no_congestion = true; - } - "fast" => { - self.no_delay = 0; - self.interval = 30; - self.resend = 2; - self.no_congestion = true; - } - "fast2" => { - self.no_delay = 1; - self.interval = 20; - self.resend = 2; - self.no_congestion = true; - } - "fast3" => { - self.no_delay = 1; - self.interval = 10; - self.resend = 2; - self.no_congestion = true; - } - _ => {} - } - if self.smux_ver > 2 { - self.smux_ver = 2; - } - } -} - -impl KcptunSessionPool { - fn new(endpoint: ProxyServerEndpoint, config: ShadowsocksKcptunTransport) -> Self { - let conn = config.conn.max(1); - Self { - endpoint, - config, - state: Mutex::new(KcptunSessionPoolState { - sessions: std::iter::repeat_with(|| None).take(conn).collect(), - rr: 0, - }), - } - } - - async fn open_stream(&self) -> Result<(Box, SocketAddr)> { - let address = *self - .endpoint - .addresses - .first() - .ok_or_else(|| anyhow!("Shadowsocks kcptun endpoint has no resolved address"))?; - for attempt in 0..2 { - let session = self.session_for_next_slot(address).await?; - match kcptun_open_smux_stream(session.clone()).await { - Ok(stream) => return Ok((stream, address)), - Err(err) if attempt == 0 => { - tracing::debug!( - error = %err, - server = %self.endpoint.host, - port = self.endpoint.port, - "discarding failed Shadowsocks kcptun smux session and retrying" - ); - self.invalidate_session(&session).await; - } - Err(err) => return Err(err), - } - } - unreachable!("kcptun retry loop has fixed attempts") - } - - async fn session_for_next_slot(&self, address: SocketAddr) -> Result> { - let mut state = self.state.lock().await; - if state.sessions.is_empty() { - state.sessions.push(None); - } - let index = state.rr % state.sessions.len(); - state.rr = state.rr.wrapping_add(1); - let now = Instant::now(); - let expired = state.sessions[index] - .as_ref() - .and_then(|session| session.expiry_at) - .is_some_and(|expiry| now >= expiry); - let needs_reconnect = state.sessions[index] - .as_ref() - .map(|session| session.session.is_closed() || expired) - .unwrap_or(true); - if needs_reconnect { - if let Some(previous) = state.sessions[index].take() { - if expired && !previous.session.is_closed() { - schedule_kcptun_session_close(previous.session, self.config.scavenge_ttl); - } - } - let session = kcptun_open_session(&self.endpoint, address, &self.config).await?; - let expiry_at = if self.config.auto_expire == 0 { - None - } else { - Some(Instant::now() + Duration::from_secs(self.config.auto_expire)) - }; - state.sessions[index] = Some(KcptunTimedSession { - session: session.clone(), - expiry_at, - }); - return Ok(session); - } - Ok(state.sessions[index] - .as_ref() - .expect("kcptun session exists when reconnect is not needed") - .session - .clone()) - } - - async fn invalidate_session(&self, session: &Arc) { - let mut state = self.state.lock().await; - let stale = state - .sessions - .iter_mut() - .find(|slot| { - slot.as_ref() - .map(|candidate| Arc::ptr_eq(&candidate.session, session)) - .unwrap_or(false) - }) - .and_then(Option::take); - drop(state); - if let Some(stale) = stale { - let _ = stale.session.close().await; - } - } -} - -#[derive(Clone)] -struct ProxyServerEndpoint { - host: String, - port: u16, - addresses: Arc<[SocketAddr]>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CustomSsAeadKind { - Aes192Gcm, - Aes192Ccm, - Chacha8IetfPoly1305, - XChacha8IetfPoly1305, - Rabbit128Poly1305, - Aegis128L, - Aegis256, - Aez384, - DeoxysII256128, - Ascon128, - Ascon128A, - Lea128Gcm, - Lea192Gcm, - Lea256Gcm, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CustomSsStreamKind { - Chacha20, - XChacha20, -} - -impl CustomSsStreamKind { - fn from_name(name: &str) -> Option { - match name.trim().to_ascii_lowercase().as_str() { - "chacha20" => Some(Self::Chacha20), - "xchacha20" => Some(Self::XChacha20), - _ => None, - } - } - - fn name(self) -> &'static str { - match self { - Self::Chacha20 => "chacha20", - Self::XChacha20 => "xchacha20", - } - } - - fn key_len(self) -> usize { - 32 - } - - fn salt_len(self) -> usize { - match self { - Self::Chacha20 => 8, - Self::XChacha20 => 24, - } - } -} - -impl CustomSsAeadKind { - fn from_name(name: &str) -> Option { - match name.trim().to_ascii_lowercase().as_str() { - "aes-192-gcm" => Some(Self::Aes192Gcm), - "aes-192-ccm" => Some(Self::Aes192Ccm), - "chacha8-ietf-poly1305" => Some(Self::Chacha8IetfPoly1305), - "xchacha8-ietf-poly1305" => Some(Self::XChacha8IetfPoly1305), - "rabbit128-poly1305" => Some(Self::Rabbit128Poly1305), - "aegis-128l" => Some(Self::Aegis128L), - "aegis-256" => Some(Self::Aegis256), - "aez-384" => Some(Self::Aez384), - "deoxys-ii-256-128" => Some(Self::DeoxysII256128), - "ascon128" => Some(Self::Ascon128), - "ascon128a" => Some(Self::Ascon128A), - "lea-128-gcm" => Some(Self::Lea128Gcm), - "lea-192-gcm" => Some(Self::Lea192Gcm), - "lea-256-gcm" => Some(Self::Lea256Gcm), - _ => None, - } - } - - fn key_len(self) -> usize { - match self { - Self::Aes192Gcm | Self::Aes192Ccm | Self::Lea192Gcm => 24, - Self::Chacha8IetfPoly1305 | Self::XChacha8IetfPoly1305 => 32, - Self::Rabbit128Poly1305 | Self::Aegis128L | Self::Lea128Gcm => 16, - Self::Aegis256 | Self::DeoxysII256128 | Self::Lea256Gcm => 32, - Self::Aez384 => 48, - Self::Ascon128 | Self::Ascon128A => 16, - } - } - - fn salt_len(self) -> usize { - self.key_len() - } - - fn tag_len(self) -> usize { - 16 - } - - fn nonce_len(self) -> usize { - match self { - Self::Aes192Gcm - | Self::Aes192Ccm - | Self::Lea128Gcm - | Self::Lea192Gcm - | Self::Lea256Gcm => 12, - Self::Chacha8IetfPoly1305 => 12, - Self::XChacha8IetfPoly1305 => 24, - Self::Rabbit128Poly1305 => 8, - Self::Aegis128L => 16, - Self::Aegis256 => 32, - Self::Aez384 => 16, - Self::DeoxysII256128 => 15, - Self::Ascon128 | Self::Ascon128A => 16, - } - } - - fn name(self) -> &'static str { - match self { - Self::Aes192Gcm => "aes-192-gcm", - Self::Aes192Ccm => "aes-192-ccm", - Self::Chacha8IetfPoly1305 => "chacha8-ietf-poly1305", - Self::XChacha8IetfPoly1305 => "xchacha8-ietf-poly1305", - Self::Rabbit128Poly1305 => "rabbit128-poly1305", - Self::Aegis128L => "aegis-128l", - Self::Aegis256 => "aegis-256", - Self::Aez384 => "aez-384", - Self::DeoxysII256128 => "deoxys-ii-256-128", - Self::Ascon128 => "ascon128", - Self::Ascon128A => "ascon128a", - Self::Lea128Gcm => "lea-128-gcm", - Self::Lea192Gcm => "lea-192-gcm", - Self::Lea256Gcm => "lea-256-gcm", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Aead2022AesCcmKind { - Aes128, - Aes256, -} - -impl Aead2022AesCcmKind { - fn from_name(name: &str) -> Option { - match name.trim().to_ascii_lowercase().as_str() { - "2022-blake3-aes-128-ccm" => Some(Self::Aes128), - "2022-blake3-aes-256-ccm" => Some(Self::Aes256), - _ => None, - } - } - - fn name(self) -> &'static str { - match self { - Self::Aes128 => "2022-blake3-aes-128-ccm", - Self::Aes256 => "2022-blake3-aes-256-ccm", - } - } - - fn key_len(self) -> usize { - match self { - Self::Aes128 => 16, - Self::Aes256 => 32, - } - } - - fn tag_len(self) -> usize { - 16 - } - - fn nonce_len(self) -> usize { - 12 - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct CertificateFingerprint([u8; 32]); - -#[derive(Debug)] -struct UdpReply { - payload: Vec, - source: SocketAddr, - destination: SocketAddr, -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -struct UdpFlowKey { - local: SocketAddr, - remote: SocketAddr, -} - -pub fn proxy_server_config_name(payload: &ProxySubscriptionPayload) -> String { - match selected_node(payload) { - Ok(node) => format!("{} ({})", payload.name, node.name), - Err(_) => payload.name.clone(), - } -} - -pub fn proxy_server_addresses() -> Vec { - vec![PROXY_TUN_V4.to_owned(), PROXY_TUN_V6.to_owned()] -} - -pub fn proxy_dns_servers() -> Vec { - let servers = vec![PROXY_DNS_V4.to_owned()]; - tracing::info!( - dns_servers = ?servers, - "configured proxy subscription DNS servers from daemon fake-IP resolver" - ); - servers -} - -pub fn proxy_dns_excluded_routes(dns_servers: &[String]) -> Vec { - let mut routes = Vec::new(); - for server in dns_servers { - let Ok(ip) = server.parse::() else { - continue; - }; - if ip == IpAddr::V4(Ipv4Addr::new(100, 64, 0, 2)) { - continue; - } - let route = host_route_for_ip(ip); - if !routes.contains(&route) { - routes.push(route); - } - } - tracing::info!( - excluded_routes = ?routes, - "configured proxy DNS excluded routes" - ); - routes -} - -pub fn proxy_excluded_routes(payload: &ProxySubscriptionPayload) -> Result> { - let node = selected_node(payload)?; - let routes = excluded_routes_for_server(&node.server, node.port)?; - tracing::info!( - server = %node.server, - port = node.port, - excluded_routes = ?routes, - "configured proxy server excluded routes" - ); - Ok(routes) -} - -pub fn proxy_runtime_config(payload: &ProxySubscriptionPayload) -> Result { - let node = selected_node(payload)?; - tracing::info!( - subscription = %payload.name, - node = %node.name, - protocol = %node.protocol.as_str(), - server = %node.server, - port = node.port, - "selected proxy subscription node" - ); - let outbound = match &node.config { - ProxyNodeConfig::Trojan(config) => { - ProxyOutbound::Trojan(TrojanOutbound::from_node(node, config)?) - } - ProxyNodeConfig::Shadowsocks(config) => { - ProxyOutbound::Shadowsocks(ShadowsocksOutbound::from_node(node, config)?) - } - }; - Ok(ProxyRuntimeConfig { - selected_name: node.name.clone(), - outbound, - }) -} - -pub fn selected_node(payload: &ProxySubscriptionPayload) -> Result<&ProxyNode> { - if let Some(name) = payload.selected_name.as_deref() { - let node = payload - .nodes - .iter() - .find(|node| node.name == name) - .ok_or_else(|| anyhow!("selected proxy subscription node {name} was not found"))?; - validate_runtime_supported_node(node)?; - return Ok(node); - } - - if let Some(ordinal) = payload.selected_ordinal { - let node = payload - .nodes - .iter() - .find(|node| node.ordinal == ordinal) - .ok_or_else(|| anyhow!("selected proxy subscription node {ordinal} was not found"))?; - validate_runtime_supported_node(node)?; - return Ok(node); - } - - payload - .nodes - .iter() - .find(|node| validate_runtime_supported_node(node).is_ok()) - .ok_or_else(|| { - anyhow!("proxy subscription contains no runtime-supported Trojan or Shadowsocks nodes") - }) -} - -fn validate_runtime_supported_node(node: &ProxyNode) -> Result<()> { - match &node.config { - ProxyNodeConfig::Trojan(config) => { - if !matches!(config.network, TrojanNetwork::Tcp) { - bail!( - "Trojan node {} uses {:?}; only tcp transport is supported by the packet runtime", - node.name, - config.network - ); - } - } - ProxyNodeConfig::Shadowsocks(config) => { - shadowsocks_plugin_transport( - config.plugin.as_ref(), - config.client_fingerprint.as_deref(), - )?; - if config.udp_over_tcp { - normalize_uot_version(config.udp_over_tcp_version)?; - } - validate_shadowsocks_cipher(&config.cipher)?; - let protocol = shadowsocks_protocol_for_node(node, config)?; - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol)?; - } - } - Ok(()) -} - -pub fn runtime_support_error(node: &ProxyNode) -> Option { - validate_runtime_supported_node(node) - .err() - .map(|err| err.to_string()) -} - -pub fn start_proxy_packet_bridge(outbound: ProxyOutbound) -> ProxyPacketBridge { - let (outbound_tx, outbound_rx) = mpsc::channel(128); - let (inbound_tx, _) = broadcast::channel(128); - let proxy_outbound = Arc::new(RwLock::new(outbound)); - let task = tokio::spawn(run_proxy_packet_stack( - outbound_rx, - inbound_tx.clone(), - proxy_outbound.clone(), - )); - ProxyPacketBridge { - outbound: outbound_tx, - inbound: inbound_tx, - proxy_outbound, - task, - } -} - -pub async fn run_proxy_tun_bridge( - tun: Arc>>, - outbound_tx: mpsc::Sender>, - mut inbound_rx: broadcast::Receiver>, -) -> Result<()> { - let inbound_tun = tun.clone(); - let inbound = tokio::spawn(async move { - loop { - let packet = match inbound_rx.recv().await { - Ok(packet) => packet, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => break, - }; - let guard = inbound_tun.read().await; - let Some(tun) = guard.as_ref() else { - bail!("proxy subscription tun interface unavailable"); - }; - tun.send(&packet) - .await - .context("failed to write proxy packet to tun")?; - } - Result::<()>::Ok(()) - }); - - let outbound_tun = tun.clone(); - let outbound = tokio::spawn(async move { - let mut buf = vec![0u8; 65_535]; - loop { - let len = { - let guard = outbound_tun.read().await; - let Some(tun) = guard.as_ref() else { - bail!("proxy subscription tun interface unavailable"); - }; - tun.recv(&mut buf) - .await - .context("failed to read proxy packet from tun")? - }; - outbound_tx - .send(buf[..len].to_vec()) - .await - .context("failed to forward packet to proxy stack")?; - } - #[allow(unreachable_code)] - Result::<()>::Ok(()) - }); - - let (inbound_result, outbound_result) = tokio::try_join!(inbound, outbound)?; - inbound_result?; - outbound_result?; - Ok(()) -} - -async fn run_proxy_packet_stack( - mut packet_rx: mpsc::Receiver>, - packet_tx: broadcast::Sender>, - outbound: Arc>, -) -> Result<()> { - let (stack, runner, udp_socket, tcp_listener) = StackBuilder::default() - .stack_buffer_size(1024) - .udp_buffer_size(1024) - .tcp_buffer_size(1024) - .enable_udp(true) - .enable_tcp(true) - .enable_icmp(true) - .build() - .context("failed to build proxy userspace netstack")?; - let (mut stack_sink, mut stack_stream) = stack.split(); - - let mut tasks = JoinSet::new(); - if let Some(runner) = runner { - tasks.spawn(async move { runner.await.map_err(anyhow::Error::from) }); - } - - tasks.spawn(async move { - while let Some(packet) = packet_rx.recv().await { - stack_sink - .send(packet) - .await - .context("failed to send tunnel packet into proxy netstack")?; - } - Result::<()>::Ok(()) - }); - - tasks.spawn(async move { - while let Some(packet) = stack_stream.next().await { - let packet = packet.context("failed to receive packet from proxy netstack")?; - let _ = packet_tx.send(packet); - } - Result::<()>::Ok(()) - }); - - if let Some(tcp_listener) = tcp_listener { - let tcp_outbound = outbound.clone(); - let dns_cache = Arc::new(RwLock::new(DnsHostnameCacheState::new())); - let tcp_dns_cache = dns_cache.clone(); - tasks.spawn( - async move { tcp_dispatch_loop(tcp_listener, tcp_outbound, tcp_dns_cache).await }, - ); - - if let Some(udp_socket) = udp_socket { - tasks.spawn(async move { udp_dispatch_loop(udp_socket, outbound, dns_cache).await }); - } - } else if let Some(udp_socket) = udp_socket { - let dns_cache = Arc::new(RwLock::new(DnsHostnameCacheState::new())); - tasks.spawn(async move { udp_dispatch_loop(udp_socket, outbound, dns_cache).await }); - } - - while let Some(result) = tasks.join_next().await { - match result { - Ok(Ok(())) => {} - Ok(Err(err)) => return Err(err), - Err(err) if err.is_cancelled() => {} - Err(err) => return Err(err.into()), - } - } - Ok(()) -} - -async fn tcp_dispatch_loop( - mut listener: StackTcpListener, - outbound: Arc>, - dns_cache: DnsHostnameCache, -) -> Result<()> { - let mut tasks = JoinSet::new(); - loop { - tokio::select! { - Some(result) = tasks.join_next(), if !tasks.is_empty() => { - match result { - Ok(Ok(())) => {} - Ok(Err(err)) => tracing::warn!(?err, "proxy tcp bridge task failed"), - Err(err) if err.is_cancelled() => {} - Err(err) => tracing::warn!(?err, "proxy tcp bridge task panicked"), - } - } - next = listener.next() => match next { - Some((stream, local_addr, remote_addr)) => { - let hostname = dns_cache.read().await.lookup_hostname(remote_addr.ip()); - tracing::debug!( - %local_addr, - %remote_addr, - hostname = hostname.as_deref(), - "accepted proxy tcp stream" - ); - let outbound = outbound.read().await.clone(); - let target = ProxyTcpTarget { - address: remote_addr, - hostname, - }; - tasks.spawn(async move { - outbound.bridge_tcp(stream, target).await - }); - } - None => break, - } - } - } - - tasks.abort_all(); - Ok(()) -} - -async fn udp_dispatch_loop( - socket: StackUdpSocket, - outbound: Arc>, - dns_cache: DnsHostnameCache, -) -> Result<()> { - let (mut udp_reader, mut udp_writer) = socket.split(); - let (reply_tx, mut reply_rx) = mpsc::channel::(128); - let sessions = Arc::new(Mutex::new( - HashMap::>>::new(), - )); - let mut session_tasks = JoinSet::new(); - - loop { - tokio::select! { - Some(result) = session_tasks.join_next(), if !session_tasks.is_empty() => { - match result { - Ok(Ok(())) => {} - Ok(Err(err)) => tracing::warn!(?err, "proxy udp session failed"), - Err(err) if err.is_cancelled() => {} - Err(err) => tracing::warn!(?err, "proxy udp session panicked"), - } - } - maybe_reply = reply_rx.recv() => match maybe_reply { - Some(reply) => { - record_dns_mappings(&dns_cache, &reply.payload).await; - udp_writer - .send((reply.payload, reply.source, reply.destination)) - .await - .context("failed to write udp reply into proxy netstack")?; - } - None => break, - }, - maybe_datagram = udp_reader.next() => match maybe_datagram { - Some((payload, local_addr, remote_addr)) => { - if is_proxy_dns_request(remote_addr) { - if let Some(response) = handle_fake_dns_request(&dns_cache, &payload).await { - udp_writer - .send((response, remote_addr, local_addr)) - .await - .context("failed to write fake DNS reply into proxy netstack")?; - continue; - } - } - dispatch_udp( - outbound.clone(), - payload, - local_addr, - remote_addr, - reply_tx.clone(), - sessions.clone(), - &mut session_tasks, - ).await?; - } - None => break, - } - } - } - - session_tasks.abort_all(); - Ok(()) -} - -fn is_proxy_dns_request(remote_addr: SocketAddr) -> bool { - remote_addr.port() == 53 && remote_addr.ip() == IpAddr::V4(Ipv4Addr::new(100, 64, 0, 2)) -} - -async fn handle_fake_dns_request(cache: &DnsHostnameCache, payload: &[u8]) -> Option> { - match build_fake_dns_response(cache, payload).await { - Ok(response) => Some(response), - Err(err) => { - tracing::debug!(error = ?err, "failed to answer fake proxy DNS request"); - None - } - } -} - -async fn build_fake_dns_response(cache: &DnsHostnameCache, query: &[u8]) -> Result> { - let questions = parse_dns_query(query)?; - let mut answers = Vec::new(); - { - let mut guard = cache.write().await; - for question in &questions { - if question.class != 1 || question.name.is_empty() { - continue; - } - if question.record_type == 1 { - let ip = guard.fake_ip_for_hostname(&question.name); - tracing::debug!( - hostname = %question.name, - %ip, - "answered proxy fake DNS A query" - ); - answers.push((question.name.clone(), ip)); - } - } - } - Ok(encode_fake_dns_response(query, &questions, &answers)) -} - -#[derive(Debug)] -struct DnsQuestion { - name: String, - record_type: u16, - class: u16, -} - -fn parse_dns_query(query: &[u8]) -> Result> { - if query.len() < 12 { - bail!("truncated DNS query"); - } - let flags = u16::from_be_bytes([query[2], query[3]]); - if flags & 0x8000 != 0 { - bail!("DNS packet is not a query"); - } - let question_count = u16::from_be_bytes([query[4], query[5]]) as usize; - let mut offset = 12; - let mut questions = Vec::new(); - for _ in 0..question_count { - let (name, next_offset) = read_dns_name(query, offset)?; - offset = next_offset; - if query.len() < offset + 4 { - bail!("truncated DNS question"); - } - let record_type = u16::from_be_bytes([query[offset], query[offset + 1]]); - let class = u16::from_be_bytes([query[offset + 2], query[offset + 3]]); - offset += 4; - questions.push(DnsQuestion { name, record_type, class }); - } - Ok(questions) -} - -fn encode_fake_dns_response( - query: &[u8], - questions: &[DnsQuestion], - answers: &[(String, Ipv4Addr)], -) -> Vec { - let question_end = dns_question_section_end(query).unwrap_or(query.len()); - let mut response = Vec::with_capacity(question_end + answers.len() * 32); - response.extend_from_slice(&query[..2]); - response.extend_from_slice(&0x8180u16.to_be_bytes()); - response.extend_from_slice(&(questions.len() as u16).to_be_bytes()); - response.extend_from_slice(&(answers.len() as u16).to_be_bytes()); - response.extend_from_slice(&0u16.to_be_bytes()); - response.extend_from_slice(&0u16.to_be_bytes()); - response.extend_from_slice(&query[12..question_end]); - - for (hostname, ip) in answers { - encode_dns_name(&mut response, hostname); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&60u32.to_be_bytes()); - response.extend_from_slice(&4u16.to_be_bytes()); - response.extend_from_slice(&ip.octets()); - } - response -} - -fn dns_question_section_end(query: &[u8]) -> Result { - if query.len() < 12 { - bail!("truncated DNS query"); - } - let question_count = u16::from_be_bytes([query[4], query[5]]) as usize; - let mut offset = 12; - for _ in 0..question_count { - offset = skip_dns_name(query, offset)?; - if query.len() < offset + 4 { - bail!("truncated DNS question"); - } - offset += 4; - } - Ok(offset) -} - -fn encode_dns_name(out: &mut Vec, hostname: &str) { - for label in hostname.trim_end_matches('.').split('.') { - out.push(label.len().min(63) as u8); - out.extend_from_slice( - label - .as_bytes() - .get(..label.len().min(63)) - .unwrap_or_default(), - ); - } - out.push(0); -} - -async fn dispatch_udp( - outbound: Arc>, - payload: Vec, - local_addr: SocketAddr, - remote_addr: SocketAddr, - reply_tx: mpsc::Sender, - sessions: Arc>>>>, - session_tasks: &mut JoinSet>, -) -> Result<()> { - let key = UdpFlowKey { - local: local_addr, - remote: remote_addr, - }; - let existing = { sessions.lock().await.get(&key).cloned() }; - if let Some(sender) = existing { - if sender.send(payload.clone()).await.is_ok() { - return Ok(()); - } - sessions.lock().await.remove(&key); - } - - let (tx, rx) = mpsc::channel::>(32); - tx.send(payload) - .await - .context("failed to enqueue proxy udp payload")?; - sessions.lock().await.insert(key.clone(), tx); - - session_tasks.spawn(async move { - let outbound = outbound.read().await.clone(); - let result = outbound - .start_udp_session(key.clone(), rx, reply_tx) - .await - .with_context(|| format!("proxy udp session {} -> {} failed", key.local, key.remote)); - sessions.lock().await.remove(&key); - result - }); - Ok(()) -} - -impl TrojanOutbound { - fn from_node(node: &ProxyNode, config: &TrojanNode) -> Result { - if !matches!(config.network, TrojanNetwork::Tcp) { - bail!( - "Trojan node {} uses {:?}; only tcp transport is supported by the packet runtime", - node.name, - config.network - ); - } - let sni = config.sni.clone().unwrap_or_else(|| node.server.clone()); - let certificate_fingerprint = config - .fingerprint - .as_deref() - .map(parse_certificate_fingerprint) - .transpose()?; - let endpoint = ProxyServerEndpoint::resolve(&node.server, node.port) - .with_context(|| format!("failed to resolve Trojan server for node {}", node.name))?; - if let Some(client_fingerprint) = unsupported_client_fingerprint(config) { - tracing::warn!( - node = %node.name, - client_fingerprint, - "Trojan client-fingerprint is stored but uTLS ClientHello impersonation is not implemented; using platform TLS ClientHello" - ); - } - tracing::info!( - node = %node.name, - server = %endpoint.host, - port = endpoint.port, - sni = %sni, - alpn = ?trojan_alpn(config), - alpn_present = config.alpn_present, - skip_cert_verify = config.skip_cert_verify, - udp = config.udp, - certificate_fingerprint = config.fingerprint.is_some(), - resolved_addresses = ?endpoint.addresses, - "configured Trojan proxy outbound" - ); - Ok(Self { - endpoint, - password: config.password.clone(), - sni, - alpn: trojan_alpn(config), - skip_cert_verify: config.skip_cert_verify, - udp_enabled: config.udp, - certificate_fingerprint, - }) - } - - async fn bridge_tcp(&self, inbound: StackTcpStream, target: ProxyTcpTarget) -> Result<()> { - let remote_addr = target.address; - let mut outbound = self.connect_tls().await?; - let socks_target = socks_addr_for_target(&target)?; - write_trojan_header(&mut outbound, &self.password, 0x01, &socks_target).await?; - tracing::debug!( - %remote_addr, - hostname = target.hostname.as_deref(), - "wrote Trojan TCP request header" - ); - bridge_tcp_streams(inbound, outbound, remote_addr, target.hostname).await?; - Ok(()) - } - - async fn run_udp_session( - &self, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - if !self.udp_enabled { - bail!("Trojan UDP is disabled for this node"); - } - let mut stream = self.connect_tls().await?; - write_trojan_header(&mut stream, &self.password, 0x03, &socks_addr(key.remote)?).await?; - let (mut reader, mut writer) = split(stream); - let remote_socks = socks_addr(key.remote)?; - - let mut write_task = tokio::spawn(async move { - while let Some(payload) = outbound_rx.recv().await { - write_trojan_udp_packet(&mut writer, &remote_socks, &payload).await?; - } - Result::<()>::Ok(()) - }); - - let mut read_task = tokio::spawn(async move { - let mut buf = vec![0u8; 65_535]; - loop { - let (source, payload) = read_trojan_udp_packet(&mut reader, &mut buf).await?; - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Trojan udp reply")?; - } - #[allow(unreachable_code)] - Result::<()>::Ok(()) - }); - - tokio::select! { - result = &mut write_task => { - read_task.abort(); - result??; - } - result = &mut read_task => { - write_task.abort(); - result??; - } - } - Ok(()) - } - - async fn connect_tls(&self) -> Result { - let (tcp, address) = connect_proxy_tcp(&self.endpoint).await.with_context(|| { - format!( - "failed to connect Trojan server {}:{}", - self.endpoint.host, self.endpoint.port - ) - })?; - #[cfg(target_vendor = "apple")] - { - return self.connect_native_tls(tcp, address).await; - } - #[cfg(not(target_vendor = "apple"))] - { - self.connect_rustls(tcp, address).await - } - } - - #[cfg(not(target_vendor = "apple"))] - async fn connect_rustls(&self, tcp: TcpStream, address: SocketAddr) -> Result { - let config = trojan_tls_config( - &self.alpn, - self.skip_cert_verify, - self.certificate_fingerprint.as_ref(), - )?; - let connector = TlsConnector::from(Arc::new(config)); - let name = ServerName::try_from(self.sni.clone()) - .with_context(|| format!("invalid Trojan SNI {}", self.sni))?; - tracing::debug!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - alpn = ?self.alpn, - skip_cert_verify = self.skip_cert_verify, - "starting Trojan TLS handshake" - ); - let tls = match connector.connect(name, tcp).await { - Ok(tls) => { - let negotiated_alpn = tls - .get_ref() - .1 - .alpn_protocol() - .map(|protocol| String::from_utf8_lossy(protocol).into_owned()); - tracing::debug!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - alpn = ?negotiated_alpn, - "completed Trojan TLS handshake" - ); - tls - } - Err(err) => { - tracing::warn!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - error = %err, - "failed Trojan TLS handshake" - ); - return Err(err).with_context(|| { - format!("failed to complete Trojan TLS handshake with {}", self.sni) - }); - } - }; - Ok(Box::new(tls)) - } - - #[cfg(target_vendor = "apple")] - async fn connect_native_tls( - &self, - tcp: TcpStream, - address: SocketAddr, - ) -> Result { - let mut builder = NativeTlsConnector::builder(); - if self.skip_cert_verify || self.certificate_fingerprint.is_some() { - builder.danger_accept_invalid_certs(true); - } - let alpn = self.alpn.iter().map(String::as_str).collect::>(); - if !alpn.is_empty() { - builder.request_alpns(&alpn); - } - let connector = TokioNativeTlsConnector::from( - builder - .build() - .context("failed to build native TLS connector for Trojan")?, - ); - tracing::debug!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - alpn = ?self.alpn, - skip_cert_verify = self.skip_cert_verify, - certificate_fingerprint = self.certificate_fingerprint.is_some(), - "starting Trojan native TLS handshake" - ); - let tls = match connector.connect(&self.sni, tcp).await { - Ok(tls) => { - if let Some(fingerprint) = self.certificate_fingerprint.as_ref() { - verify_native_tls_peer_certificate(&tls, fingerprint, "Trojan")?; - } - let negotiated_alpn = tls - .get_ref() - .negotiated_alpn() - .ok() - .flatten() - .map(|protocol| String::from_utf8_lossy(&protocol).into_owned()); - tracing::debug!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - alpn = ?negotiated_alpn, - "completed Trojan native TLS handshake" - ); - tls - } - Err(err) => { - tracing::warn!( - server = %self.endpoint.host, - port = self.endpoint.port, - %address, - sni = %self.sni, - error = %err, - "failed Trojan native TLS handshake" - ); - return Err(err).with_context(|| { - format!( - "failed to complete Trojan native TLS handshake with {}", - self.sni - ) - }); - } - }; - Ok(Box::new(tls)) - } -} - -async fn bridge_tcp_streams( - inbound: StackTcpStream, - outbound: TrojanStream, - remote_addr: SocketAddr, - hostname: Option, -) -> Result<()> { - let (inbound_reader, inbound_writer) = split(inbound); - let (outbound_reader, outbound_writer) = split(outbound); - let hostname_for_client = hostname.clone(); - let mut client_to_proxy = tokio::spawn(async move { - copy_tcp_direction( - "client_to_proxy", - inbound_reader, - outbound_writer, - remote_addr, - hostname_for_client, - ) - .await - }); - let mut proxy_to_client = tokio::spawn(async move { - copy_tcp_direction( - "proxy_to_client", - outbound_reader, - inbound_writer, - remote_addr, - hostname, - ) - .await - }); - - tokio::select! { - result = &mut client_to_proxy => { - proxy_to_client.abort(); - let bytes = result.context("client-to-proxy bridge task panicked")??; - tracing::debug!(%remote_addr, bytes, "Trojan tcp client-to-proxy bridge completed first"); - } - result = &mut proxy_to_client => { - client_to_proxy.abort(); - let bytes = result.context("proxy-to-client bridge task panicked")??; - tracing::debug!(%remote_addr, bytes, "Trojan tcp proxy-to-client bridge completed first"); - } - } - Ok(()) -} - -async fn copy_tcp_direction( - direction: &'static str, - mut reader: R, - mut writer: W, - remote_addr: SocketAddr, - hostname: Option, -) -> Result -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - let mut total = 0u64; - let mut logged_first_chunk = false; - let mut buf = vec![0u8; 16 * 1024]; - loop { - let len = reader - .read(&mut buf) - .await - .with_context(|| format!("failed to read {direction} proxy tcp bytes"))?; - if len == 0 { - tracing::debug!( - %remote_addr, - hostname = hostname.as_deref(), - direction, - total, - "proxy tcp direction reached EOF" - ); - writer - .shutdown() - .await - .with_context(|| format!("failed to shutdown {direction} proxy tcp writer"))?; - return Ok(total); - } - if !logged_first_chunk { - logged_first_chunk = true; - tracing::debug!( - %remote_addr, - hostname = hostname.as_deref(), - direction, - len, - "proxy tcp direction forwarding first bytes" - ); - } - writer - .write_all(&buf[..len]) - .await - .with_context(|| format!("failed to write {direction} proxy tcp bytes"))?; - total += len as u64; - } -} - -fn trojan_alpn(config: &TrojanNode) -> Vec { - if config.alpn_present { - config.alpn.clone() - } else { - MIHOMO_TROJAN_TCP_DEFAULT_ALPN - .iter() - .map(|value| (*value).to_owned()) - .collect() - } -} - -async fn bridge_streams(inbound: StackTcpStream, stream: S) -> Result<()> -where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - let (mut inbound_reader, mut inbound_writer) = split(inbound); - let (mut stream_reader, mut stream_writer) = split(stream); - let mut upload = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = inbound_reader - .read(&mut buf) - .await - .context("failed to read tcp payload from proxy netstack")?; - if n == 0 { - break; - } - stream_writer.write_all(&buf[..n]).await?; - } - stream_writer - .shutdown() - .await - .context("failed to shutdown proxy tcp upload")?; - Result::<()>::Ok(()) - }); - - let mut download = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = stream_reader - .read(&mut buf) - .await - .context("failed to read proxy tcp payload")?; - if n == 0 { - break; - } - inbound_writer - .write_all(&buf[..n]) - .await - .context("failed to write tcp payload to proxy netstack")?; - } - inbound_writer - .shutdown() - .await - .context("failed to shutdown proxy netstack tcp download")?; - Result::<()>::Ok(()) - }); - - tokio::select! { - result = &mut upload => { - download.abort(); - result??; - } - result = &mut download => { - upload.abort(); - result??; - } - } - Ok(()) -} - -impl ShadowsocksOutbound { - fn from_node(node: &ProxyNode, config: &ShadowsocksNode) -> Result { - let plugin = shadowsocks_plugin_transport( - config.plugin.as_ref(), - config.client_fingerprint.as_deref(), - ) - .with_context(|| { - format!( - "Shadowsocks node {} uses an unsupported plugin transport", - node.name - ) - })?; - let force_udp_over_tcp = matches!(plugin, Some(ShadowsocksPluginTransport::Kcptun(_))); - let udp_over_tcp = config.udp_over_tcp || force_udp_over_tcp; - let udp_over_tcp_version = if udp_over_tcp { - normalize_uot_version(config.udp_over_tcp_version)? - } else { - UOT_LEGACY_VERSION - }; - let endpoint = - ProxyServerEndpoint::resolve(&node.server, node.port).with_context(|| { - format!( - "failed to resolve Shadowsocks server for node {}", - node.name - ) - })?; - let protocol = shadowsocks_protocol_for_node(node, config)?; - let sing_mux = shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol) - .with_context(|| { - format!( - "Shadowsocks node {} uses an unsupported top-level sing-mux mode", - node.name - ) - })?; - let kcptun_pool = match plugin.as_ref() { - Some(ShadowsocksPluginTransport::Kcptun(config)) => Some(Arc::new( - KcptunSessionPool::new(endpoint.clone(), config.clone()), - )), - _ => None, - }; - tracing::info!( - node = %node.name, - server = %endpoint.host, - port = endpoint.port, - cipher = %config.cipher, - udp = config.udp, - udp_over_tcp, - udp_over_tcp_version, - plugin = ?plugin, - sing_mux = ?sing_mux, - resolved_addresses = ?endpoint.addresses, - "configured Shadowsocks proxy outbound" - ); - Ok(Self { - endpoint, - protocol, - plugin, - kcptun_pool, - udp_enabled: config.udp - || udp_over_tcp - || sing_mux - .as_ref() - .map(|sing_mux| sing_mux.udp) - .unwrap_or(false), - sing_mux, - sing_mux_sessions: Arc::new(Mutex::new(Vec::new())), - udp_over_tcp, - udp_over_tcp_version, - }) - } - - async fn connect_tcp(&self) -> Result<(Box, SocketAddr)> { - if let Some(pool) = self.kcptun_pool.as_ref() { - return pool.open_stream().await; - } - let (tcp, address) = connect_proxy_tcp(&self.endpoint).await.with_context(|| { - format!( - "failed to connect Shadowsocks server {}:{}", - self.endpoint.host, self.endpoint.port - ) - })?; - let stream: Box = match self.plugin.as_ref() { - Some(ShadowsocksPluginTransport::SimpleObfs { mode, host }) => Box::new( - SimpleObfsStream::new(tcp, *mode, host.clone(), self.endpoint.port), - ), - Some(ShadowsocksPluginTransport::WebSocket(config)) => { - let stream = - websocket_plugin_connect(Box::new(tcp), config, self.endpoint.port).await?; - match config.mux { - ShadowsocksWebSocketMux::None => stream, - ShadowsocksWebSocketMux::V2ray => Box::new(V2rayMuxStream::new(stream)), - ShadowsocksWebSocketMux::Gost => gost_smux_stream(stream).await?, - } - } - Some(ShadowsocksPluginTransport::ShadowTls(config)) => { - shadow_tls_connect(Box::new(tcp), config).await? - } - Some(ShadowsocksPluginTransport::Kcptun(_)) => { - bail!("internal Shadowsocks kcptun transport dispatch error") - } - None => Box::new(tcp), - }; - Ok((stream, address)) - } - - async fn connect_tcp_to_target( - &self, - target: &ProxyTcpTarget, - ) -> Result<(Box, SocketAddr)> { - let (mut tcp, address) = self.connect_tcp().await?; - match &self.protocol { - ShadowsocksProtocol::None => { - let socks_target = socks_addr_for_target(target)?; - tcp.write_all(&socks_target).await.with_context(|| { - format!( - "failed to write Shadowsocks none target address for {}", - target.address - ) - })?; - tcp.flush() - .await - .context("failed to flush Shadowsocks none target address")?; - Ok((tcp, address)) - } - ShadowsocksProtocol::Standard { server_config, context } => { - let stream = SsProxyClientStream::from_stream( - context.clone(), - tcp, - server_config, - shadowsocks_addr_for_target(target)?, - ); - Ok((Box::new(stream), address)) - } - ShadowsocksProtocol::CustomAead { kind, master_key } => { - let stream = - custom_aead_plain_tcp_stream(tcp, target, *kind, master_key.clone()).await?; - Ok((stream, address)) - } - ShadowsocksProtocol::CustomStream { kind, key } => { - let stream = - custom_stream_plain_tcp_stream(tcp, target, *kind, key.clone()).await?; - Ok((stream, address)) - } - ShadowsocksProtocol::Aead2022AesCcm { kind, user_key, identity_keys } => { - let stream = aead2022_aes_ccm_plain_tcp_stream( - tcp, - target, - *kind, - user_key.clone(), - identity_keys.clone(), - ) - .await?; - Ok((stream, address)) - } - } - } - - async fn open_sing_mux_tcp_stream(&self, target: &ProxyTcpTarget) -> Result> { - let session = self.sing_mux_session().await?; - let mut stream = session.open_stream().await?; - write_sing_mux_tcp_request(&mut stream, target).await?; - Ok(Box::new(SingMuxTcpStream::new(stream))) - } - - async fn sing_mux_session(&self) -> Result { - let config = self - .sing_mux - .as_ref() - .context("Shadowsocks sing-mux is not configured")?; - let mut sessions = self.sing_mux_sessions.lock().await; - sessions.retain(|session| !session.is_closed()); - - if config.brutal.is_some() { - if let Some(session) = sessions.first() { - return Ok(session.clone()); - } - let session = self.connect_sing_mux_session().await?; - sessions.push(session.clone()); - return Ok(session); - } - - let mut best = None; - for (index, session) in sessions.iter().enumerate() { - let streams = session.num_streams().await; - match best { - None => best = Some((index, streams)), - Some((_, current)) if streams < current => best = Some((index, streams)), - _ => {} - } - } - - if let Some((index, streams)) = best { - if streams == 0 - || (config.max_connections > 0 - && (sessions.len() >= config.max_connections || streams < config.min_streams)) - || (config.max_connections == 0 - && config.max_streams > 0 - && streams < config.max_streams) - { - return Ok(sessions[index].clone()); - } - } - - let session = self.connect_sing_mux_session().await?; - sessions.push(session.clone()); - Ok(session) - } - - async fn connect_sing_mux_session(&self) -> Result { - let target = ProxyTcpTarget { - address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), SING_MUX_PORT), - hostname: Some(SING_MUX_ADDRESS.to_owned()), - }; - let sing_mux = self - .sing_mux - .as_ref() - .context("Shadowsocks sing-mux session requested without sing-mux config")?; - let (mut stream, address) = self.connect_tcp_to_target(&target).await?; - write_sing_mux_session_preface(&mut stream, sing_mux.protocol, sing_mux.padding).await?; - let stream = if sing_mux.padding { - sing_mux_padding_stream(stream) - } else { - stream - }; - - let session = match sing_mux.protocol { - ShadowsocksSingMuxProtocol::H2Mux => start_sing_mux_h2mux_session(stream).await?, - ShadowsocksSingMuxProtocol::Smux => { - let mut config = smux_rust::Config::default(); - config.keep_alive_disabled = true; - let session = - smux_rust::client(Box::new(ProxyIoAdapter::new(stream)), Some(config)) - .await - .map_err(|err| { - anyhow!("failed to start Shadowsocks sing-mux smux session: {err}") - })?; - ShadowsocksSingMuxSession::Smux(session) - } - ShadowsocksSingMuxProtocol::Yamux => start_sing_mux_yamux_session(stream).await?, - }; - if let Some(brutal) = sing_mux.brutal { - sing_mux_brutal_exchange(&session, brutal).await?; - } - tracing::debug!( - %address, - mux_host = SING_MUX_ADDRESS, - mux_port = SING_MUX_PORT, - protocol = sing_mux.protocol.name(), - brutal = sing_mux.brutal.is_some(), - "opened Shadowsocks sing-mux session" - ); - Ok(session) - } - - async fn bridge_tcp(&self, inbound: StackTcpStream, target: ProxyTcpTarget) -> Result<()> { - if self.sing_mux.is_some() { - let stream = self.open_sing_mux_tcp_stream(&target).await?; - return bridge_streams(inbound, stream).await; - } - let (tcp, _) = self.connect_tcp().await?; - match &self.protocol { - ShadowsocksProtocol::None => self.bridge_none_tcp(inbound, tcp, target).await, - ShadowsocksProtocol::Standard { server_config, context } => { - let stream = SsProxyClientStream::from_stream( - context.clone(), - tcp, - server_config, - shadowsocks_addr_for_target(&target)?, - ); - bridge_streams(inbound, stream).await - } - ShadowsocksProtocol::CustomAead { kind, master_key } => { - self.bridge_custom_aead_tcp(inbound, tcp, target, *kind, master_key.clone()) - .await - } - ShadowsocksProtocol::CustomStream { kind, key } => { - self.bridge_custom_stream_tcp(inbound, tcp, target, *kind, key.clone()) - .await - } - ShadowsocksProtocol::Aead2022AesCcm { kind, user_key, identity_keys } => { - self.bridge_aead2022_aes_ccm_tcp( - inbound, - tcp, - target, - *kind, - user_key.clone(), - identity_keys.clone(), - ) - .await - } - } - } - - async fn bridge_none_tcp( - &self, - inbound: StackTcpStream, - mut tcp: S, - target: ProxyTcpTarget, - ) -> Result<()> - where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, - { - let remote_addr = target.address; - let socks_target = socks_addr_for_target(&target)?; - tcp.write_all(&socks_target).await.with_context(|| { - format!("failed to write Shadowsocks none target address for {remote_addr}") - })?; - tcp.flush() - .await - .context("failed to flush Shadowsocks none target address")?; - bridge_streams(inbound, tcp).await - } - - async fn bridge_custom_aead_tcp( - &self, - inbound: StackTcpStream, - tcp: S, - target: ProxyTcpTarget, - kind: CustomSsAeadKind, - master_key: Arc<[u8]>, - ) -> Result<()> - where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, - { - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_reader = CustomSsAeadReader::new(reader, kind, master_key.clone()); - let mut ss_writer = CustomSsAeadWriter::new(writer, kind, master_key) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let socks_target = socks_addr_for_target(&target)?; - ss_writer - .write_chunk(&socks_target) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks target address for {remote_addr} with {}", - kind.name() - ) - })?; - let (mut inbound_reader, mut inbound_writer) = split(inbound); - let mut upload = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = inbound_reader - .read(&mut buf) - .await - .context("failed to read tcp payload from proxy netstack")?; - if n == 0 { - break; - } - ss_writer.write_chunk(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp upload")?; - Result::<()>::Ok(()) - }); - - let mut download = tokio::spawn(async move { - let mut buf = Vec::new(); - loop { - buf.clear(); - match ss_reader.read_chunk(&mut buf).await { - Ok(0) => break, - Ok(_) => { - inbound_writer - .write_all(&buf) - .await - .context("failed to write tcp payload to proxy netstack")?; - } - Err(err) => return Err(err), - } - } - inbound_writer - .shutdown() - .await - .context("failed to shutdown proxy netstack tcp download")?; - Result::<()>::Ok(()) - }); - - tokio::select! { - result = &mut upload => { - download.abort(); - result??; - } - result = &mut download => { - upload.abort(); - result??; - } - } - Ok(()) - } - - async fn bridge_custom_stream_tcp( - &self, - inbound: StackTcpStream, - tcp: S, - target: ProxyTcpTarget, - kind: CustomSsStreamKind, - key: Arc<[u8]>, - ) -> Result<()> - where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, - { - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_reader = CustomSsStreamReader::new(reader, kind, key.clone()); - let mut ss_writer = CustomSsStreamWriter::new(writer, kind, key) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let socks_target = socks_addr_for_target(&target)?; - ss_writer - .write_all_encrypted(&socks_target) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks target address for {remote_addr} with {}", - kind.name() - ) - })?; - let (mut inbound_reader, mut inbound_writer) = split(inbound); - let mut upload = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = inbound_reader - .read(&mut buf) - .await - .context("failed to read tcp payload from proxy netstack")?; - if n == 0 { - break; - } - ss_writer.write_all_encrypted(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp upload")?; - Result::<()>::Ok(()) - }); - - let mut download = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = ss_reader.read_decrypted(&mut buf).await?; - if n == 0 { - break; - } - inbound_writer - .write_all(&buf[..n]) - .await - .context("failed to write tcp payload to proxy netstack")?; - } - inbound_writer - .shutdown() - .await - .context("failed to shutdown proxy netstack tcp download")?; - Result::<()>::Ok(()) - }); - - tokio::select! { - result = &mut upload => { - download.abort(); - result??; - } - result = &mut download => { - upload.abort(); - result??; - } - } - Ok(()) - } - - async fn bridge_aead2022_aes_ccm_tcp( - &self, - inbound: StackTcpStream, - tcp: S, - target: ProxyTcpTarget, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - ) -> Result<()> - where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, - { - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_writer = - Aead2022AesCcmWriter::new(writer, kind, user_key.clone(), identity_keys) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let request_salt = ss_writer - .write_request(&socks_addr_for_target(&target)?, 1) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks 2022 target address for {remote_addr} with {}", - kind.name() - ) - })?; - let mut ss_reader = Aead2022AesCcmReader::new(reader, kind, user_key, request_salt); - let (mut inbound_reader, mut inbound_writer) = split(inbound); - let mut upload = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = inbound_reader - .read(&mut buf) - .await - .context("failed to read tcp payload from proxy netstack")?; - if n == 0 { - break; - } - ss_writer.write_chunk(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks 2022 tcp upload")?; - Result::<()>::Ok(()) - }); - - let mut download = tokio::spawn(async move { - let mut buf = Vec::new(); - loop { - buf.clear(); - match ss_reader.read_chunk(&mut buf).await { - Ok(0) => break, - Ok(_) => { - inbound_writer - .write_all(&buf) - .await - .context("failed to write tcp payload to proxy netstack")?; - } - Err(err) => return Err(err), - } - } - inbound_writer - .shutdown() - .await - .context("failed to shutdown proxy netstack tcp download")?; - Result::<()>::Ok(()) - }); - - tokio::select! { - result = &mut upload => { - download.abort(); - result??; - } - result = &mut download => { - upload.abort(); - result??; - } - } - Ok(()) - } - - async fn run_udp_session( - &self, - key: UdpFlowKey, - outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - if self - .sing_mux - .as_ref() - .map(|sing_mux| sing_mux.udp) - .unwrap_or(false) - { - return self - .run_sing_mux_udp_session(key, outbound_rx, reply_tx) - .await; - } - if self.udp_over_tcp { - return self - .run_udp_over_tcp_session(key, outbound_rx, reply_tx) - .await; - } - if !self.udp_enabled { - bail!("Shadowsocks UDP is disabled for this node"); - } - let (socket, server) = connect_proxy_udp(&self.endpoint).await.with_context(|| { - format!( - "failed to connect Shadowsocks udp socket to {}:{}", - self.endpoint.host, self.endpoint.port - ) - })?; - socket.connect(server).await.with_context(|| { - format!("failed to connect udp socket to Shadowsocks server {server}") - })?; - match &self.protocol { - ShadowsocksProtocol::None => { - self.run_none_udp_loop(socket, server, key, outbound_rx, reply_tx) - .await - } - ShadowsocksProtocol::Standard { server_config, context } => { - let socket = SsProxySocket::from_socket( - SsUdpSocketType::Client, - context.clone(), - server_config, - BurrowDatagramSocket(socket), - ); - self.run_standard_udp_loop(socket, server, key, outbound_rx, reply_tx) - .await - } - ShadowsocksProtocol::CustomAead { kind, master_key } => { - self.run_custom_aead_udp_loop( - socket, - server, - key, - outbound_rx, - reply_tx, - *kind, - master_key.clone(), - ) - .await - } - ShadowsocksProtocol::CustomStream { kind, key: stream_key } => { - self.run_custom_stream_udp_loop( - socket, - server, - key, - outbound_rx, - reply_tx, - *kind, - stream_key.clone(), - ) - .await - } - ShadowsocksProtocol::Aead2022AesCcm { kind, user_key, identity_keys } => { - self.run_aead2022_aes_ccm_udp_loop( - socket, - server, - key, - outbound_rx, - reply_tx, - *kind, - user_key.clone(), - identity_keys.clone(), - ) - .await - } - } - } - - async fn run_sing_mux_udp_session( - &self, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - let session = self.sing_mux_session().await?; - let mut stream = session.open_stream().await?; - let mut request_written = false; - let mut response_read = false; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - if !request_written { - write_sing_mux_udp_request(&mut stream, key.remote, &payload).await?; - request_written = true; - } else { - write_sing_mux_udp_payload(&mut stream, &payload).await?; - } - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, read_sing_mux_udp_payload(&mut stream, &mut response_read)), if request_written => match recv { - Ok(Ok(payload)) => { - reply_tx - .send(UdpReply { - payload, - source: key.remote, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks sing-mux udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks sing-mux udp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_udp_over_tcp_session( - &self, - key: UdpFlowKey, - outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - let (tcp, server) = self.connect_tcp().await.with_context(|| { - format!( - "failed to connect Shadowsocks tcp socket to {}:{} for udp-over-tcp", - self.endpoint.host, self.endpoint.port - ) - })?; - match &self.protocol { - ShadowsocksProtocol::None => { - let mut stream = tcp; - let uot_target = socks_domain_addr(uot_magic_domain(self.udp_over_tcp_version), 0)?; - stream.write_all(&uot_target).await.with_context(|| { - "failed to write Shadowsocks none udp-over-tcp target".to_owned() - })?; - stream - .flush() - .await - .context("failed to flush Shadowsocks none udp-over-tcp target")?; - self.run_standard_uot_loop( - stream, - server, - key, - outbound_rx, - reply_tx, - self.udp_over_tcp_version, - ) - .await - } - ShadowsocksProtocol::Standard { server_config, context } => { - let stream = SsProxyClientStream::from_stream( - context.clone(), - tcp, - server_config, - uot_magic_shadowsocks_addr(self.udp_over_tcp_version), - ); - self.run_standard_uot_loop( - stream, - server, - key, - outbound_rx, - reply_tx, - self.udp_over_tcp_version, - ) - .await - } - ShadowsocksProtocol::CustomAead { kind, master_key } => { - let (reader, writer) = split(tcp); - let mut ss_writer = CustomSsAeadWriter::new(writer, *kind, master_key.clone()) - .await - .with_context(|| { - format!("failed to initialize {} udp-over-tcp stream", kind.name()) - })?; - let uot_target = socks_domain_addr(uot_magic_domain(self.udp_over_tcp_version), 0)?; - ss_writer.write_chunk(&uot_target).await.with_context(|| { - format!( - "failed to write Shadowsocks udp-over-tcp target for {}", - kind.name() - ) - })?; - let ss_reader = CustomSsAeadReader::new(reader, *kind, master_key.clone()); - self.run_custom_aead_uot_loop( - CustomSsAeadByteReader::new(ss_reader), - ss_writer, - server, - key, - outbound_rx, - reply_tx, - self.udp_over_tcp_version, - ) - .await - } - ShadowsocksProtocol::CustomStream { kind, key: stream_key } => { - let (reader, writer) = split(tcp); - let mut ss_writer = CustomSsStreamWriter::new(writer, *kind, stream_key.clone()) - .await - .with_context(|| { - format!("failed to initialize {} udp-over-tcp stream", kind.name()) - })?; - let uot_target = socks_domain_addr(uot_magic_domain(self.udp_over_tcp_version), 0)?; - ss_writer - .write_all_encrypted(&uot_target) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks udp-over-tcp target for {}", - kind.name() - ) - })?; - let ss_reader = CustomSsStreamReader::new(reader, *kind, stream_key.clone()); - self.run_custom_stream_uot_loop( - CustomSsStreamByteReader::new(ss_reader), - ss_writer, - server, - key, - outbound_rx, - reply_tx, - self.udp_over_tcp_version, - ) - .await - } - ShadowsocksProtocol::Aead2022AesCcm { kind, user_key, identity_keys } => { - let (reader, writer) = split(tcp); - let mut ss_writer = Aead2022AesCcmWriter::new( - writer, - *kind, - user_key.clone(), - identity_keys.clone(), - ) - .await - .with_context(|| { - format!("failed to initialize {} udp-over-tcp stream", kind.name()) - })?; - let uot_target = socks_domain_addr(uot_magic_domain(self.udp_over_tcp_version), 0)?; - let request_salt = - ss_writer - .write_request(&uot_target, 1) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks 2022 udp-over-tcp target for {}", - kind.name() - ) - })?; - let ss_reader = - Aead2022AesCcmReader::new(reader, *kind, user_key.clone(), request_salt); - self.run_aead2022_aes_ccm_uot_loop( - Aead2022AesCcmByteReader::new(ss_reader), - ss_writer, - server, - key, - outbound_rx, - reply_tx, - self.udp_over_tcp_version, - ) - .await - } - } - } - - async fn run_none_udp_loop( - &self, - socket: UdpSocket, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - let remote = socks_addr(key.remote)?; - let mut buf = vec![0u8; 65_535]; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let mut packet = Vec::with_capacity(remote.len() + payload.len()); - packet.extend_from_slice(&remote); - packet.extend_from_slice(&payload); - socket - .send(&packet) - .await - .with_context(|| format!("failed to send Shadowsocks none udp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, socket.recv(&mut buf)) => match recv { - Ok(Ok(packet_len)) => { - let (source, used) = parse_socks_addr(&buf[..packet_len]) - .context("failed to parse Shadowsocks none udp response source")?; - reply_tx - .send(UdpReply { - payload: buf[used..packet_len].to_vec(), - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks none udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks none udp response"), - Err(_) => break, - }, - } - } - Ok(()) - } - - async fn run_standard_udp_loop( - &self, - socket: SsProxySocket, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - ) -> Result<()> { - let remote = SsAddress::SocketAddress(key.remote); - let mut buf = vec![0u8; 65_535]; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - socket - .send(&remote, &payload) - .await - .with_context(|| format!("failed to send Shadowsocks udp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, socket.recv(&mut buf)) => match recv { - Ok(Ok((payload_len, source, _packet_len))) => { - let source = shadowsocks_addr_to_socket_addr(source)?; - reply_tx - .send(UdpReply { - payload: buf[..payload_len].to_vec(), - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks udp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_standard_uot_loop( - &self, - mut stream: S, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - version: u8, - ) -> Result<()> - where - S: AsyncRead + AsyncWrite + Unpin, - { - if version == UOT_VERSION { - write_uot_request(&mut stream, key.remote).await?; - } - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - write_uot_frame(&mut stream, key.remote, &payload) - .await - .with_context(|| format!("failed to send Shadowsocks udp-over-tcp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, read_uot_frame(&mut stream)) => match recv { - Ok(Ok((source, payload))) => { - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks udp-over-tcp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks udp-over-tcp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_custom_aead_udp_loop( - &self, - socket: UdpSocket, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - kind: CustomSsAeadKind, - master_key: Arc<[u8]>, - ) -> Result<()> { - let mut buf = vec![0u8; 65_535]; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let packet = encrypt_custom_ss_udp_packet(kind, &master_key, key.remote, &payload)?; - socket - .send(&packet) - .await - .with_context(|| format!("failed to send {} udp payload to {server}", kind.name()))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, socket.recv(&mut buf)) => match recv { - Ok(Ok(len)) => { - let (source, payload) = - decrypt_custom_ss_udp_packet(kind, &master_key, &buf[..len])?; - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks udp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_custom_aead_uot_loop( - &self, - mut reader: CustomSsAeadByteReader, - mut writer: CustomSsAeadWriter, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - version: u8, - ) -> Result<()> - where - W: AsyncWrite + Unpin, - R: AsyncRead + Unpin, - { - if version == UOT_VERSION { - writer.write_chunk(&uot_request(key.remote)?).await?; - } - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let frame = uot_frame(key.remote, &payload)?; - writer.write_chunk(&frame) - .await - .with_context(|| format!("failed to send custom Shadowsocks udp-over-tcp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, reader.read_frame()) => match recv { - Ok(Ok(Some((source, payload)))) => { - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue custom Shadowsocks udp-over-tcp reply")?; - } - Ok(Ok(None)) => break, - Ok(Err(err)) => return Err(err).context("failed to receive custom Shadowsocks udp-over-tcp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_custom_stream_udp_loop( - &self, - socket: UdpSocket, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - kind: CustomSsStreamKind, - stream_key: Arc<[u8]>, - ) -> Result<()> { - let mut buf = vec![0u8; 65_535]; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let packet = encrypt_custom_ss_stream_udp_packet(kind, &stream_key, key.remote, &payload)?; - socket - .send(&packet) - .await - .with_context(|| format!("failed to send {} udp payload to {server}", kind.name()))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, socket.recv(&mut buf)) => match recv { - Ok(Ok(len)) => { - let (source, payload) = - decrypt_custom_ss_stream_udp_packet(kind, &stream_key, &buf[..len])?; - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks udp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_custom_stream_uot_loop( - &self, - mut reader: CustomSsStreamByteReader, - mut writer: CustomSsStreamWriter, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - version: u8, - ) -> Result<()> - where - W: AsyncWrite + Unpin, - R: AsyncRead + Unpin, - { - if version == UOT_VERSION { - writer - .write_all_encrypted(&uot_request(key.remote)?) - .await?; - } - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let frame = uot_frame(key.remote, &payload)?; - writer.write_all_encrypted(&frame) - .await - .with_context(|| format!("failed to send custom Shadowsocks udp-over-tcp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, reader.read_frame()) => match recv { - Ok(Ok(Some((source, payload)))) => { - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue custom Shadowsocks udp-over-tcp reply")?; - } - Ok(Ok(None)) => break, - Ok(Err(err)) => return Err(err).context("failed to receive custom Shadowsocks udp-over-tcp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_aead2022_aes_ccm_udp_loop( - &self, - socket: UdpSocket, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - ) -> Result<()> { - let mut session = Aead2022AesCcmUdpSession::new(kind, user_key, identity_keys)?; - let mut buf = vec![0u8; 65_535]; - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let packet = session.encrypt_client_packet(key.remote, &payload)?; - socket - .send(&packet) - .await - .with_context(|| format!("failed to send {} udp payload to {server}", kind.name()))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, socket.recv(&mut buf)) => match recv { - Ok(Ok(len)) => { - let (source, payload) = session.decrypt_server_packet(&buf[..len])?; - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks 2022 udp reply")?; - } - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks 2022 udp response"), - Err(_) => break, - } - } - } - Ok(()) - } - - async fn run_aead2022_aes_ccm_uot_loop( - &self, - mut reader: Aead2022AesCcmByteReader, - mut writer: Aead2022AesCcmWriter, - server: SocketAddr, - key: UdpFlowKey, - mut outbound_rx: mpsc::Receiver>, - reply_tx: mpsc::Sender, - version: u8, - ) -> Result<()> - where - W: AsyncWrite + Unpin, - R: AsyncRead + Unpin, - { - if version == UOT_VERSION { - writer.write_chunk(&uot_request(key.remote)?).await?; - } - loop { - tokio::select! { - maybe_payload = outbound_rx.recv() => match maybe_payload { - Some(payload) => { - let frame = uot_frame(key.remote, &payload)?; - writer.write_chunk(&frame) - .await - .with_context(|| format!("failed to send Shadowsocks 2022 udp-over-tcp payload to {server}"))?; - } - None => break, - }, - recv = timeout(UDP_IDLE_TIMEOUT, reader.read_frame()) => match recv { - Ok(Ok(Some((source, payload)))) => { - reply_tx - .send(UdpReply { - payload, - source, - destination: key.local, - }) - .await - .context("failed to enqueue Shadowsocks 2022 udp-over-tcp reply")?; - } - Ok(Ok(None)) => break, - Ok(Err(err)) => return Err(err).context("failed to receive Shadowsocks 2022 udp-over-tcp response"), - Err(_) => break, - } - } - } - Ok(()) - } -} - -trait ProxyIo: AsyncRead + AsyncWrite + Unpin + Send {} - -impl ProxyIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {} - -impl ShadowsocksSingMuxSession { - fn is_closed(&self) -> bool { - match self { - Self::H2Mux(session) => session.closed.load(Ordering::Relaxed), - Self::Smux(session) => session.is_closed(), - Self::Yamux(session) => session.closed.load(Ordering::Relaxed), - } - } - - async fn num_streams(&self) -> usize { - match self { - Self::H2Mux(session) => session.active_streams.load(Ordering::Relaxed), - Self::Smux(session) => session.num_streams().await, - Self::Yamux(session) => session.active_streams.load(Ordering::Relaxed), - } - } - - async fn open_stream(&self) -> Result> { - match self { - Self::H2Mux(session) => session.open_stream().await, - Self::Smux(session) => { - let stream = session.open_stream().await.map_err(|err| { - anyhow!("failed to open Shadowsocks sing-mux smux stream: {err}") - })?; - Ok(Box::new(stream)) - } - Self::Yamux(session) => { - let (response, stream_rx) = oneshot::channel(); - session - .open_tx - .send(YamuxCommand { response }) - .await - .context("Shadowsocks sing-mux yamux session is closed")?; - let stream = stream_rx - .await - .context("Shadowsocks sing-mux yamux session stopped before opening stream")? - .map_err(|err| { - anyhow!("failed to open Shadowsocks sing-mux yamux stream: {err}") - })?; - session.active_streams.fetch_add(1, Ordering::Relaxed); - Ok(Box::new(CountingProxyIo::new( - stream.compat(), - session.active_streams.clone(), - ))) - } - } - } -} - -impl ShadowsocksH2MuxSession { - async fn open_stream(&self) -> Result> { - let mut send_request = self - .send_request - .clone() - .ready() - .await - .context("Shadowsocks sing-mux h2mux session is not ready")?; - let request = Request::builder() - .method(Method::CONNECT) - .uri("https://localhost") - .body(()) - .context("failed to build Shadowsocks sing-mux h2mux CONNECT request")?; - let (response, send_stream) = send_request - .send_request(request, false) - .context("failed to open Shadowsocks sing-mux h2mux stream")?; - self.active_streams.fetch_add(1, Ordering::Relaxed); - Ok(Box::new(H2MuxStream::new( - Box::pin(response), - send_stream, - self.active_streams.clone(), - ))) - } -} - -async fn start_sing_mux_h2mux_session( - stream: Box, -) -> Result { - let (send_request, connection) = h2::client::handshake(ProxyIoAdapter::new(stream)) - .await - .context("failed to start Shadowsocks sing-mux h2mux session")?; - let active_streams = Arc::new(AtomicUsize::new(0)); - let closed = Arc::new(AtomicBool::new(false)); - let driver_closed = closed.clone(); - - tokio::spawn(async move { - if let Err(err) = connection.await { - tracing::debug!(?err, "Shadowsocks sing-mux h2mux session stopped"); - } - driver_closed.store(true, Ordering::Relaxed); - }); - - Ok(ShadowsocksSingMuxSession::H2Mux(ShadowsocksH2MuxSession { - send_request, - active_streams, - closed, - })) -} - -async fn start_sing_mux_yamux_session( - stream: Box, -) -> Result { - let connection = yamux::Connection::new( - ProxyIoAdapter::new(stream).compat(), - yamux::Config::default(), - yamux::Mode::Client, - ); - let (open_tx, open_rx) = mpsc::channel(32); - let active_streams = Arc::new(AtomicUsize::new(0)); - let closed = Arc::new(AtomicBool::new(false)); - let driver_closed = closed.clone(); - - tokio::spawn(async move { - YamuxDriver::new(connection, open_rx).await; - driver_closed.store(true, Ordering::Relaxed); - }); - - Ok(ShadowsocksSingMuxSession::Yamux(ShadowsocksYamuxSession { - open_tx, - active_streams, - closed, - })) -} - -struct YamuxDriver { - connection: yamux::Connection, - open_rx: mpsc::Receiver, - pending_open: VecDeque>>, -} - -impl YamuxDriver { - fn new(connection: yamux::Connection, open_rx: mpsc::Receiver) -> Self { - Self { - connection, - open_rx, - pending_open: VecDeque::new(), - } - } -} - -impl Future for YamuxDriver -where - T: futures::io::AsyncRead + futures::io::AsyncWrite + Unpin, -{ - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll { - loop { - let mut progressed = false; - - while let Poll::Ready(command) = self.open_rx.poll_recv(cx) { - let Some(command) = command else { - break; - }; - self.pending_open.push_back(command.response); - progressed = true; - } - - while let Some(response) = self.pending_open.pop_front() { - match self.connection.poll_new_outbound(cx) { - Poll::Ready(Ok(stream)) => { - let _ = response.send(Ok(stream)); - progressed = true; - } - Poll::Ready(Err(err)) => { - let _ = response.send(Err(err.to_string())); - progressed = true; - } - Poll::Pending => { - self.pending_open.push_front(response); - break; - } - } - } - - match self.connection.poll_next_inbound(cx) { - Poll::Ready(Some(Ok(_stream))) => { - tracing::debug!("discarding unexpected server-opened Shadowsocks yamux stream"); - progressed = true; - } - Poll::Ready(Some(Err(err))) => { - tracing::debug!(?err, "Shadowsocks sing-mux yamux session stopped"); - return Poll::Ready(()); - } - Poll::Ready(None) => return Poll::Ready(()), - Poll::Pending => {} - } - - if !progressed { - return Poll::Pending; - } - } - } -} - -type H2MuxResponseFuture = Pin< - Box, h2::Error>> + Send>, ->; - -struct H2MuxStream { - response: Option, - recv: Option, - send: h2::SendStream, - read_buf: Bytes, - active_streams: Arc, - send_closed: bool, -} - -impl H2MuxStream { - fn new( - response: H2MuxResponseFuture, - send: h2::SendStream, - active_streams: Arc, - ) -> Self { - Self { - response: Some(response), - recv: None, - send, - read_buf: Bytes::new(), - active_streams, - send_closed: false, - } - } -} - -impl Drop for H2MuxStream { - fn drop(&mut self) { - if !self.send_closed { - self.send.send_reset(h2::Reason::CANCEL); - } - self.active_streams.fetch_sub(1, Ordering::Relaxed); - } -} - -impl AsyncRead for H2MuxStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - if !self.read_buf.is_empty() { - let n = buf.remaining().min(self.read_buf.len()); - buf.put_slice(&self.read_buf[..n]); - let _ = self.read_buf.split_to(n); - return Poll::Ready(Ok(())); - } - - if self.recv.is_none() { - let Some(response) = self.response.as_mut() else { - return Poll::Ready(Ok(())); - }; - let response = match response.as_mut().poll(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Ok(response)) => response, - Poll::Ready(Err(err)) => return Poll::Ready(Err(io::Error::other(err))), - }; - if response.status() != StatusCode::OK { - return Poll::Ready(Err(io::Error::other(format!( - "unexpected Shadowsocks sing-mux h2mux status {}", - response.status() - )))); - } - self.recv = Some(response.into_body()); - self.response = None; - } - - let recv = self.recv.as_mut().expect("h2mux response body initialized"); - match recv.poll_data(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Some(Ok(bytes))) => { - if bytes.is_empty() { - continue; - } - let len = bytes.len(); - if let Err(err) = recv.flow_control().release_capacity(len) { - return Poll::Ready(Err(io::Error::other(err))); - } - self.read_buf = bytes; - } - Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(io::Error::other(err))), - Poll::Ready(None) => return Poll::Ready(Ok(())), - } - } - } -} - -impl AsyncWrite for H2MuxStream { - fn poll_write( - mut self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - if self.send_closed { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "Shadowsocks sing-mux h2mux stream is closed", - ))); - } - if buf.is_empty() { - return Poll::Ready(Ok(0)); - } - self.send - .send_data(Bytes::copy_from_slice(buf), false) - .map_err(io::Error::other)?; - Poll::Ready(Ok(buf.len())) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - if !self.send_closed { - self.send - .send_data(Bytes::new(), true) - .map_err(io::Error::other)?; - self.send_closed = true; - } - Poll::Ready(Ok(())) - } -} - -struct CountingProxyIo { - inner: S, - active_streams: Arc, -} - -impl CountingProxyIo { - fn new(inner: S, active_streams: Arc) -> Self { - Self { inner, active_streams } - } -} - -impl Drop for CountingProxyIo { - fn drop(&mut self) { - self.active_streams.fetch_sub(1, Ordering::Relaxed); - } -} - -impl AsyncRead for CountingProxyIo -where - S: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -impl AsyncWrite for CountingProxyIo -where - S: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -struct SingMuxTcpStream { - inner: S, - response_read: bool, -} - -impl SingMuxTcpStream { - fn new(inner: S) -> Self { - Self { inner, response_read: false } - } -} - -impl AsyncRead for SingMuxTcpStream -where - S: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if !self.response_read { - let mut status = [0u8; 1]; - let mut status_buf = ReadBuf::new(&mut status); - match Pin::new(&mut self.inner).poll_read(cx, &mut status_buf) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if status_buf.filled().is_empty() => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "Shadowsocks sing-mux stream closed before response status", - ))); - } - Poll::Ready(Ok(())) => match status[0] { - SING_MUX_STREAM_STATUS_SUCCESS => self.response_read = true, - SING_MUX_STREAM_STATUS_ERROR => { - return Poll::Ready(Err(io::Error::other( - "Shadowsocks sing-mux remote error", - ))); - } - other => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("unknown Shadowsocks sing-mux stream response status {other}"), - ))); - } - }, - } - } - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -impl AsyncWrite for SingMuxTcpStream -where - S: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -struct ProxyIoAdapter { - inner: Box, -} - -impl ProxyIoAdapter { - fn new(inner: Box) -> Self { - Self { inner } - } -} - -impl AsyncRead for ProxyIoAdapter { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -impl AsyncWrite for ProxyIoAdapter { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -async fn custom_aead_plain_tcp_stream( - tcp: S, - target: &ProxyTcpTarget, - kind: CustomSsAeadKind, - master_key: Arc<[u8]>, -) -> Result> -where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_reader = CustomSsAeadReader::new(reader, kind, master_key.clone()); - let mut ss_writer = CustomSsAeadWriter::new(writer, kind, master_key) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let socks_target = socks_addr_for_target(target)?; - ss_writer - .write_chunk(&socks_target) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks target address for {remote_addr} with {}", - kind.name() - ) - })?; - - let (plain, bridge) = tokio::io::duplex(64 * 1024); - let (mut plain_reader, mut plain_writer) = split(bridge); - let _upload = tokio::spawn(async move { - let result = async { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = plain_reader - .read(&mut buf) - .await - .context("failed to read plaintext payload for Shadowsocks tcp upload")?; - if n == 0 { - break; - } - ss_writer.write_chunk(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp upload")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!( - ?err, - "Shadowsocks custom AEAD plaintext upload task stopped" - ); - } - }); - - let _download = tokio::spawn(async move { - let result = async { - let mut buf = Vec::new(); - loop { - buf.clear(); - match ss_reader.read_chunk(&mut buf).await { - Ok(0) => break, - Ok(_) => { - plain_writer - .write_all(&buf) - .await - .context("failed to write plaintext payload from Shadowsocks tcp")?; - } - Err(err) => return Err(err), - } - } - plain_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp download")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!( - ?err, - "Shadowsocks custom AEAD plaintext download task stopped" - ); - } - }); - - Ok(Box::new(plain)) -} - -async fn custom_stream_plain_tcp_stream( - tcp: S, - target: &ProxyTcpTarget, - kind: CustomSsStreamKind, - key: Arc<[u8]>, -) -> Result> -where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_reader = CustomSsStreamReader::new(reader, kind, key.clone()); - let mut ss_writer = CustomSsStreamWriter::new(writer, kind, key) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let socks_target = socks_addr_for_target(target)?; - ss_writer - .write_all_encrypted(&socks_target) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks target address for {remote_addr} with {}", - kind.name() - ) - })?; - - let (plain, bridge) = tokio::io::duplex(64 * 1024); - let (mut plain_reader, mut plain_writer) = split(bridge); - let _upload = tokio::spawn(async move { - let result = async { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = plain_reader - .read(&mut buf) - .await - .context("failed to read plaintext payload for Shadowsocks tcp upload")?; - if n == 0 { - break; - } - ss_writer.write_all_encrypted(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp upload")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!( - ?err, - "Shadowsocks custom stream plaintext upload task stopped" - ); - } - }); - - let _download = tokio::spawn(async move { - let result = async { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = ss_reader.read_decrypted(&mut buf).await?; - if n == 0 { - break; - } - plain_writer - .write_all(&buf[..n]) - .await - .context("failed to write plaintext payload from Shadowsocks tcp")?; - } - plain_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks tcp download")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!( - ?err, - "Shadowsocks custom stream plaintext download task stopped" - ); - } - }); - - Ok(Box::new(plain)) -} - -async fn aead2022_aes_ccm_plain_tcp_stream( - tcp: S, - target: &ProxyTcpTarget, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, -) -> Result> -where - S: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - let remote_addr = target.address; - let (reader, writer) = split(tcp); - let mut ss_writer = Aead2022AesCcmWriter::new(writer, kind, user_key.clone(), identity_keys) - .await - .with_context(|| format!("failed to initialize {} tcp stream", kind.name()))?; - let request_salt = ss_writer - .write_request(&socks_addr_for_target(target)?, 1) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks 2022 target address for {remote_addr} with {}", - kind.name() - ) - })?; - let mut ss_reader = Aead2022AesCcmReader::new(reader, kind, user_key, request_salt); - - let (plain, bridge) = tokio::io::duplex(64 * 1024); - let (mut plain_reader, mut plain_writer) = split(bridge); - let _upload = tokio::spawn(async move { - let result = async { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = plain_reader - .read(&mut buf) - .await - .context("failed to read plaintext payload for Shadowsocks 2022 tcp upload")?; - if n == 0 { - break; - } - ss_writer.write_chunk(&buf[..n]).await?; - } - ss_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks 2022 tcp upload")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!(?err, "Shadowsocks 2022 plaintext upload task stopped"); - } - }); - - let _download = tokio::spawn(async move { - let result = async { - let mut buf = Vec::new(); - loop { - buf.clear(); - match ss_reader.read_chunk(&mut buf).await { - Ok(0) => break, - Ok(_) => { - plain_writer.write_all(&buf).await.context( - "failed to write plaintext payload from Shadowsocks 2022 tcp", - )?; - } - Err(err) => return Err(err), - } - } - plain_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks 2022 tcp download")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!(?err, "Shadowsocks 2022 plaintext download task stopped"); - } - }); - - Ok(Box::new(plain)) -} - -type TrojanStream = Box; - -struct SimpleObfsStream { - inner: S, - mode: SimpleObfsMode, - host: String, - port: u16, - first_write: bool, - first_read: bool, - write_buf: Vec, - write_offset: usize, - read_buf: Vec, - read_offset: usize, - tls_read_stage: Option, -} - -enum SimpleObfsTlsReadStage { - Discard { remaining: usize }, - Length { bytes: [u8; 2], filled: usize }, - Payload { bytes: Vec, filled: usize }, -} - -impl SimpleObfsStream { - fn new(inner: S, mode: SimpleObfsMode, host: String, port: u16) -> Self { - Self { - inner, - mode, - host, - port, - first_write: true, - first_read: true, - write_buf: Vec::new(), - write_offset: 0, - read_buf: Vec::new(), - read_offset: 0, - tls_read_stage: None, - } - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } -} - -impl AsyncRead for SimpleObfsStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - match self.mode { - SimpleObfsMode::Http => self.poll_read_http(cx, buf), - SimpleObfsMode::Tls => self.poll_read_tls(cx, buf), - } - } -} - -impl SimpleObfsStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read_http( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if !self.first_read { - return Pin::new(&mut self.inner).poll_read(cx, buf); - } - let mut response = vec![0u8; 16 * 1024]; - let mut response_buf = ReadBuf::new(&mut response); - match Pin::new(&mut self.inner).poll_read(cx, &mut response_buf) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => { - let filled = response_buf.filled().len(); - let Some(header_end) = response[..filled] - .windows(4) - .position(|window| window == b"\r\n\r\n") - .map(|index| index + 4) - else { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "simple-obfs HTTP response header was incomplete", - ))); - }; - self.first_read = false; - self.read_buf - .extend_from_slice(&response[header_end..filled]); - self.copy_read_buffer(buf); - Poll::Ready(Ok(())) - } - } - } - - fn poll_read_tls( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - if self.tls_read_stage.is_none() { - let discard = if self.first_read { 105 } else { 3 }; - self.first_read = false; - self.tls_read_stage = Some(SimpleObfsTlsReadStage::Discard { remaining: discard }); - } - - match self.tls_read_stage.take().expect("stage initialized") { - SimpleObfsTlsReadStage::Discard { mut remaining } => { - while remaining > 0 { - let mut scratch = [0u8; 128]; - let read_len = remaining.min(scratch.len()); - let mut discard_buf = ReadBuf::new(&mut scratch[..read_len]); - match Pin::new(&mut self.inner).poll_read(cx, &mut discard_buf) { - Poll::Pending => { - self.tls_read_stage = - Some(SimpleObfsTlsReadStage::Discard { remaining }); - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if discard_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - remaining -= discard_buf.filled().len(); - } - } - } - self.tls_read_stage = - Some(SimpleObfsTlsReadStage::Length { bytes: [0u8; 2], filled: 0 }); - } - SimpleObfsTlsReadStage::Length { mut bytes, mut filled } => { - while filled < 2 { - let mut len_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut len_buf) { - Poll::Pending => { - self.tls_read_stage = - Some(SimpleObfsTlsReadStage::Length { bytes, filled }); - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if len_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - filled += len_buf.filled().len(); - } - } - } - let len = u16::from_be_bytes(bytes) as usize; - self.tls_read_stage = Some(SimpleObfsTlsReadStage::Payload { - bytes: vec![0u8; len], - filled: 0, - }); - } - SimpleObfsTlsReadStage::Payload { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut payload_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.tls_read_stage = - Some(SimpleObfsTlsReadStage::Payload { bytes, filled }); - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - filled += payload_buf.filled().len(); - } - } - } - self.tls_read_stage = None; - self.read_buf = bytes; - } - } - } - } -} - -impl AsyncWrite for SimpleObfsStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => { - return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); - } - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - - if self.first_write { - self.first_write = false; - self.write_buf = match self.mode { - SimpleObfsMode::Http => simple_obfs_http_request(&self.host, self.port, buf), - SimpleObfsMode::Tls => simple_obfs_tls_client_hello(&self.host, buf)?, - }; - self.write_offset = 0; - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => { - return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); - } - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - return Poll::Ready(Ok(buf.len())); - } - - match self.mode { - SimpleObfsMode::Http => Pin::new(&mut self.inner).poll_write(cx, buf), - SimpleObfsMode::Tls => { - self.write_buf = simple_obfs_tls_record(buf)?; - self.write_offset = 0; - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => { - return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); - } - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(buf.len())) - } - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -struct HmacReadStream { - inner: S, - hmac: hmac::Context, -} - -#[cfg(feature = "boring-browser-fingerprints")] -struct DetachableStream { - inner: Option, -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl DetachableStream { - fn new(inner: S) -> Self { - Self { inner: Some(inner) } - } - - fn take(&mut self) -> io::Result { - self.inner.take().ok_or_else(|| { - io::Error::new( - io::ErrorKind::BrokenPipe, - "detachable TLS stream already taken", - ) - }) - } - - fn inner_mut(&mut self) -> io::Result<&mut S> { - self.inner.as_mut().ok_or_else(|| { - io::Error::new( - io::ErrorKind::BrokenPipe, - "detachable TLS stream already taken", - ) - }) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncRead for DetachableStream -where - S: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let inner = match self.inner_mut() { - Ok(inner) => inner, - Err(err) => return Poll::Ready(Err(err)), - }; - Pin::new(inner).poll_read(cx, buf) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncWrite for DetachableStream -where - S: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - let inner = match self.inner_mut() { - Ok(inner) => inner, - Err(err) => return Poll::Ready(Err(err)), - }; - Pin::new(inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let inner = match self.inner_mut() { - Ok(inner) => inner, - Err(err) => return Poll::Ready(Err(err)), - }; - Pin::new(inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let inner = match self.inner_mut() { - Ok(inner) => inner, - Err(err) => return Poll::Ready(Err(err)), - }; - Pin::new(inner).poll_shutdown(cx) - } -} - -impl HmacReadStream { - fn new(inner: S, password: &str) -> Self { - Self { - inner, - hmac: hmac::Context::with_key(&hmac::Key::new( - hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, - password.as_bytes(), - )), - } - } - - fn finish(self) -> (S, [u8; 8]) { - let digest = self.hmac.sign(); - let mut prefix = [0u8; 8]; - prefix.copy_from_slice(&digest.as_ref()[..8]); - (self.inner, prefix) - } -} - -impl AsyncRead for HmacReadStream -where - S: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let before = buf.filled().len(); - match Pin::new(&mut self.inner).poll_read(cx, buf) { - Poll::Ready(Ok(())) => { - let filled = buf.filled(); - if filled.len() > before { - self.hmac.update(&filled[before..]); - } - Poll::Ready(Ok(())) - } - other => other, - } - } -} - -impl AsyncWrite for HmacReadStream -where - S: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -struct ShadowTlsV2Stream { - inner: S, - first_write_hmac: Option<[u8; 8]>, - read_stage: ShadowTlsReadStage, - write_buf: Vec, - write_offset: usize, -} - -enum ShadowTlsReadStage { - Header { bytes: [u8; 5], filled: usize }, - Payload { remaining: usize }, -} - -impl ShadowTlsV2Stream { - fn new(inner: S, first_write_hmac: [u8; 8]) -> Self { - Self { - inner, - first_write_hmac: Some(first_write_hmac), - read_stage: ShadowTlsReadStage::Header { bytes: [0u8; 5], filled: 0 }, - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } -} - -impl AsyncRead for ShadowTlsV2Stream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - match self.read_stage { - ShadowTlsReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut header_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut header_buf) { - Poll::Pending => { - self.read_stage = ShadowTlsReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if header_buf.filled().is_empty() => { - return Poll::Ready(Ok(())); - } - Poll::Ready(Ok(())) => { - filled += header_buf.filled().len(); - } - } - } - if bytes[0] != 23 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("unexpected ShadowTLS record type {}", bytes[0]), - ))); - } - let remaining = u16::from_be_bytes([bytes[3], bytes[4]]) as usize; - self.read_stage = ShadowTlsReadStage::Payload { remaining }; - } - ShadowTlsReadStage::Payload { remaining } => { - if remaining == 0 { - self.read_stage = ShadowTlsReadStage::Header { bytes: [0u8; 5], filled: 0 }; - continue; - } - let before = buf.filled().len(); - let read_len = remaining.min(buf.remaining()); - if read_len == 0 { - return Poll::Ready(Ok(())); - } - let initialized = buf.initialize_unfilled_to(read_len); - let mut payload_buf = ReadBuf::new(initialized); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.read_stage = ShadowTlsReadStage::Payload { remaining }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - let read = payload_buf.filled().len(); - buf.advance(read); - let remaining = remaining - read; - self.read_stage = if remaining == 0 { - ShadowTlsReadStage::Header { bytes: [0u8; 5], filled: 0 } - } else { - ShadowTlsReadStage::Payload { remaining } - }; - if buf.filled().len() > before { - return Poll::Ready(Ok(())); - } - } - } - } - } - } - } -} - -impl AsyncWrite for ShadowTlsV2Stream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - - let write_len = buf.len().min(SHADOW_TLS_MAX_RECORD_PAYLOAD); - self.write_buf = shadow_tls_v2_record(buf, write_len, self.first_write_hmac.take())?; - self.write_offset = 0; - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(write_len)), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(write_len)) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -struct ShadowTlsV3HandshakeStream { - inner: S, - password: String, - read_stage: ShadowTlsV3ReadStage, - read_buf: Vec, - read_offset: usize, - server_random: Option<[u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE]>, - read_hmac: Option, - read_hmac_key: Option<[u8; 32]>, - is_tls13: bool, - authorized: bool, -} - -#[cfg(feature = "boring-browser-fingerprints")] -struct ShadowTlsV3SignedClientHelloStream { - inner: ShadowTlsV3HandshakeStream, - password: String, - client_hello_signed: bool, - client_hello_buf: Vec, - write_buf: Vec, - write_offset: usize, -} - -enum ShadowTlsV3ReadStage { - Header { - bytes: [u8; 5], - filled: usize, - }, - Payload { - header: [u8; 5], - bytes: Vec, - filled: usize, - }, -} - -impl ShadowTlsV3HandshakeStream { - fn new(inner: S, password: &str) -> Self { - Self { - inner, - password: password.to_owned(), - read_stage: ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }, - read_buf: Vec::new(), - read_offset: 0, - server_random: None, - read_hmac: None, - read_hmac_key: None, - is_tls13: false, - authorized: false, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn process_frame(&mut self, mut frame: Vec) -> io::Result<()> { - match frame.first().copied() { - Some(0x16) => { - if self.read_hmac.is_none() - && frame.len() - >= SHADOW_TLS_V3_SERVER_RANDOM_INDEX + SHADOW_TLS_V3_TLS_RANDOM_SIZE - && frame[SHADOW_TLS_V3_TLS_HEADER_SIZE] == 0x02 - { - let mut server_random = [0u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE]; - server_random.copy_from_slice( - &frame[SHADOW_TLS_V3_SERVER_RANDOM_INDEX - ..SHADOW_TLS_V3_SERVER_RANDOM_INDEX + SHADOW_TLS_V3_TLS_RANDOM_SIZE], - ); - let read_hmac = shadow_tls_v3_hmac(&self.password, &server_random, b""); - let read_hmac_key = shadow_tls_v3_kdf(&self.password, &server_random); - self.is_tls13 = shadow_tls_v3_server_hello_supports_tls13(&frame); - if !self.is_tls13 { - self.authorized = true; - } - self.server_random = Some(server_random); - self.read_hmac_key = Some(read_hmac_key); - self.read_hmac = Some(read_hmac); - } - self.read_buf = frame; - } - Some(0x17) => { - self.authorized = false; - if frame.len() > SHADOW_TLS_V3_HMAC_HEADER_SIZE { - if let (Some(read_hmac), Some(read_hmac_key)) = - (self.read_hmac.as_mut(), self.read_hmac_key.as_ref()) - { - read_hmac.update(&frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..]); - let expected = - shadow_tls_v3_hmac_prefix(read_hmac, SHADOW_TLS_V3_HMAC_SIZE); - if expected[..] - == frame[SHADOW_TLS_V3_TLS_HEADER_SIZE..SHADOW_TLS_V3_HMAC_HEADER_SIZE] - { - shadow_tls_v3_xor( - &mut frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..], - read_hmac_key, - ); - let payload_len = frame.len() - SHADOW_TLS_V3_HMAC_HEADER_SIZE; - let mut rewritten = - Vec::with_capacity(SHADOW_TLS_V3_TLS_HEADER_SIZE + payload_len); - rewritten.extend_from_slice(&frame[..SHADOW_TLS_V3_TLS_HEADER_SIZE]); - rewritten[3..5].copy_from_slice(&(payload_len as u16).to_be_bytes()); - rewritten.extend_from_slice(&frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..]); - self.read_buf = rewritten; - self.authorized = true; - } else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 handshake HMAC mismatch", - )); - } - } else { - self.read_buf = frame; - } - } else { - self.read_buf = frame; - } - } - _ => { - self.read_buf = frame; - } - } - self.read_offset = 0; - Ok(()) - } - - fn into_inner(self) -> (S, ShadowTlsV3HandshakeState) { - ( - self.inner, - ShadowTlsV3HandshakeState { - server_random: self.server_random, - read_hmac: self.read_hmac, - is_tls13: self.is_tls13, - authorized: self.authorized, - }, - ) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl ShadowTlsV3SignedClientHelloStream { - fn new(inner: ShadowTlsV3HandshakeStream, password: &str) -> Self { - Self { - inner, - password: password.to_owned(), - client_hello_signed: false, - client_hello_buf: Vec::new(), - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn into_inner(self) -> (ShadowTlsV3HandshakeStream, bool) { - (self.inner, self.client_hello_signed) - } - - fn queue_write(&mut self, buf: &[u8]) -> io::Result<()> { - if self.client_hello_signed { - self.write_buf.extend_from_slice(buf); - return Ok(()); - } - self.client_hello_buf.extend_from_slice(buf); - if let Some(signed) = - shadow_tls_v3_sign_client_hello_record(&self.password, &self.client_hello_buf)? - { - self.write_buf.extend_from_slice(&signed); - self.client_hello_buf.clear(); - self.client_hello_signed = true; - } - Ok(()) - } - - fn poll_write_pending(&mut self, cx: &mut TaskContext<'_>) -> Poll> - where - S: AsyncRead + AsyncWrite + Unpin, - { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - } - } - } - self.write_buf.clear(); - self.write_offset = 0; - Poll::Ready(Ok(())) - } -} - -struct ShadowTlsV3HandshakeState { - server_random: Option<[u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE]>, - read_hmac: Option, - is_tls13: bool, - authorized: bool, -} - -#[allow(dead_code)] -struct RestlsHandshakeStream { - inner: S, - secret: [u8; 32], - tls12_gcm: bool, - read_stage: RestlsHandshakeReadStage, - read_buf: Vec, - read_offset: usize, - write_buf: Vec, - write_offset: usize, - write_capture_buf: Vec, - server_random: Option<[u8; 32]>, - client_finished_auth: Option>, - server_auth_unmasked: bool, - tls12_gcm_server_disable_counter: bool, -} - -#[cfg(feature = "boring-browser-fingerprints")] -struct RestlsSignedClientHelloStream { - inner: RestlsHandshakeStream, - secret: [u8; 32], - client_hello_signed: bool, - client_hello_buf: Vec, - write_buf: Vec, - write_offset: usize, -} - -#[allow(dead_code)] -enum RestlsHandshakeReadStage { - Header { - bytes: [u8; TLS_RECORD_HEADER_LEN], - filled: usize, - }, - Payload { - header: [u8; TLS_RECORD_HEADER_LEN], - bytes: Vec, - filled: usize, - }, -} - -#[allow(dead_code)] -struct RestlsHandshakeState { - server_random: Option<[u8; 32]>, - client_finished_auth: Option>, - server_auth_unmasked: bool, - tls12_gcm_server_disable_counter: bool, -} - -#[allow(dead_code)] -impl RestlsHandshakeStream { - fn new(inner: S, secret: [u8; 32], tls12_gcm: bool) -> Self { - Self { - inner, - secret, - tls12_gcm, - read_stage: RestlsHandshakeReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }, - read_buf: Vec::new(), - read_offset: 0, - write_buf: Vec::new(), - write_offset: 0, - write_capture_buf: Vec::new(), - server_random: None, - client_finished_auth: None, - server_auth_unmasked: false, - tls12_gcm_server_disable_counter: false, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } - - fn process_read_frame(&mut self, frame: Vec) -> io::Result<()> { - if let Some(server_random) = restls_server_hello_random(&frame) { - self.server_random = Some(server_random); - self.read_buf = frame; - } else if !self.server_auth_unmasked && frame.first().copied() == Some(0x17) { - let server_random = self.server_random.ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - "Restls server-auth record arrived before ServerHello random", - ) - })?; - let unmasked = restls_unmask_server_auth_record( - &self.secret, - &server_random, - &frame, - self.tls12_gcm, - ) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; - self.tls12_gcm_server_disable_counter = unmasked.tls12_gcm_server_disable_counter; - self.server_auth_unmasked = true; - self.read_buf = unmasked.record; - } else { - self.read_buf = frame; - } - self.read_offset = 0; - Ok(()) - } - - fn process_write_bytes(&mut self, buf: &[u8]) { - if self.client_finished_auth.is_some() { - return; - } - self.write_capture_buf.extend_from_slice(buf); - loop { - if self.write_capture_buf.len() < TLS_RECORD_HEADER_LEN { - return; - } - let payload_len = usize::from(u16::from_be_bytes([ - self.write_capture_buf[3], - self.write_capture_buf[4], - ])); - let record_len = TLS_RECORD_HEADER_LEN + payload_len; - if self.write_capture_buf.len() < record_len { - return; - } - let record = self.write_capture_buf[..record_len].to_vec(); - self.write_capture_buf.drain(..record_len); - if record.first().copied() == Some(TLS_APPLICATION_DATA_RECORD_TYPE) { - self.client_finished_auth = Some(record); - self.write_capture_buf.clear(); - return; - } - } - } - - fn into_inner(self) -> (S, RestlsHandshakeState) { - ( - self.inner, - RestlsHandshakeState { - server_random: self.server_random, - client_finished_auth: self.client_finished_auth, - server_auth_unmasked: self.server_auth_unmasked, - tls12_gcm_server_disable_counter: self.tls12_gcm_server_disable_counter, - }, - ) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl RestlsSignedClientHelloStream { - fn new(inner: RestlsHandshakeStream, secret: [u8; 32]) -> Self { - Self { - inner, - secret, - client_hello_signed: false, - client_hello_buf: Vec::new(), - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn into_inner(self) -> (RestlsHandshakeStream, bool) { - (self.inner, self.client_hello_signed) - } - - fn queue_write(&mut self, buf: &[u8]) -> io::Result<()> { - if self.client_hello_signed { - self.write_buf.extend_from_slice(buf); - return Ok(()); - } - self.client_hello_buf.extend_from_slice(buf); - if let Some(signed) = - restls_tls13_sign_client_hello_record(&self.secret, &self.client_hello_buf)? - { - self.write_buf.extend_from_slice(&signed); - self.client_hello_buf.clear(); - self.client_hello_signed = true; - } - Ok(()) - } - - fn poll_write_pending(&mut self, cx: &mut TaskContext<'_>) -> Poll> - where - S: AsyncRead + AsyncWrite + Unpin, - { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - } - } - } - self.write_buf.clear(); - self.write_offset = 0; - Poll::Ready(Ok(())) - } -} - -impl AsyncRead for RestlsHandshakeStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - loop { - match std::mem::replace( - &mut self.read_stage, - RestlsHandshakeReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }, - ) { - RestlsHandshakeReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut header_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut header_buf) { - Poll::Pending => { - self.read_stage = - RestlsHandshakeReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if header_buf.filled().is_empty() => { - self.read_stage = - RestlsHandshakeReadStage::Header { bytes, filled }; - return Poll::Ready(Ok(())); - } - Poll::Ready(Ok(())) => { - filled += header_buf.filled().len(); - } - } - } - let remaining = usize::from(u16::from_be_bytes([bytes[3], bytes[4]])); - self.read_stage = RestlsHandshakeReadStage::Payload { - header: bytes, - bytes: vec![0u8; remaining], - filled: 0, - }; - } - RestlsHandshakeReadStage::Payload { header, mut bytes, mut filled } => { - while filled < bytes.len() { - let mut payload_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.read_stage = - RestlsHandshakeReadStage::Payload { header, bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - filled += payload_buf.filled().len(); - } - } - } - let mut frame = Vec::with_capacity(TLS_RECORD_HEADER_LEN + bytes.len()); - frame.extend_from_slice(&header); - frame.extend_from_slice(&bytes); - self.process_read_frame(frame)?; - self.read_stage = RestlsHandshakeReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }; - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - } - } - } - } -} - -impl AsyncWrite for RestlsHandshakeStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - self.process_write_bytes(buf); - match Pin::new(&mut self.inner).poll_write(cx, buf) { - Poll::Pending => Poll::Pending, - Poll::Ready(result) => Poll::Ready(result), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncRead for RestlsSignedClientHelloStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.poll_write_pending(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => {} - } - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncWrite for RestlsSignedClientHelloStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - if let Err(err) = self.queue_write(buf) { - return Poll::Ready(Err(err)); - } - match self.poll_write_pending(cx) { - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Pending | Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - if !self.client_hello_signed && !self.client_hello_buf.is_empty() { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "incomplete Restls ClientHello before flush", - ))); - } - match self.poll_write_pending(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_flush(cx), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -#[allow(dead_code)] -impl RestlsStream { - fn new(inner: S, state: RestlsStreamState) -> Self { - Self { - inner, - state, - read_stage: RestlsRecordReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }, - read_buf: Vec::new(), - read_offset: 0, - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn queue_records(&mut self, records: Vec>) { - for record in records { - self.write_buf.extend_from_slice(&record); - } - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } - - fn process_read_frame(&mut self, frame: Vec) -> io::Result<()> { - let read = self - .state - .read_to_client_record(&frame) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; - self.queue_records(read.records); - self.read_buf = read.data; - self.read_offset = 0; - Ok(()) - } -} - -impl AsyncRead for RestlsStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - match std::mem::replace( - &mut self.read_stage, - RestlsRecordReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }, - ) { - RestlsRecordReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut header_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut header_buf) { - Poll::Pending => { - self.read_stage = RestlsRecordReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if header_buf.filled().is_empty() => { - self.read_stage = RestlsRecordReadStage::Header { bytes, filled }; - return Poll::Ready(Ok(())); - } - Poll::Ready(Ok(())) => { - filled += header_buf.filled().len(); - } - } - } - let remaining = usize::from(u16::from_be_bytes([bytes[3], bytes[4]])); - self.read_stage = RestlsRecordReadStage::Payload { - header: bytes, - bytes: vec![0u8; remaining], - filled: 0, - }; - } - RestlsRecordReadStage::Payload { header, mut bytes, mut filled } => { - while filled < bytes.len() { - let mut payload_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.read_stage = - RestlsRecordReadStage::Payload { header, bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - filled += payload_buf.filled().len(); - } - } - } - let mut frame = Vec::with_capacity(TLS_RECORD_HEADER_LEN + bytes.len()); - frame.extend_from_slice(&header); - frame.extend_from_slice(&bytes); - self.process_read_frame(frame)?; - self.read_stage = RestlsRecordReadStage::Header { - bytes: [0u8; TLS_RECORD_HEADER_LEN], - filled: 0, - }; - } - } - } - } -} - -impl AsyncWrite for RestlsStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - let write = self - .state - .write(buf) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; - let accepted = write.accepted; - self.queue_records(write.records); - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(accepted)), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(accepted)) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -impl AsyncRead for ShadowTlsV3HandshakeStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - match std::mem::replace( - &mut self.read_stage, - ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }, - ) { - ShadowTlsV3ReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut header_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut header_buf) { - Poll::Pending => { - self.read_stage = ShadowTlsV3ReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if header_buf.filled().is_empty() => { - return Poll::Ready(Ok(())); - } - Poll::Ready(Ok(())) => { - filled += header_buf.filled().len(); - } - } - } - let len = u16::from_be_bytes([bytes[3], bytes[4]]) as usize; - self.read_stage = ShadowTlsV3ReadStage::Payload { - header: bytes, - bytes: vec![0u8; len], - filled: 0, - }; - } - ShadowTlsV3ReadStage::Payload { header, mut bytes, mut filled } => { - while filled < bytes.len() { - let mut payload_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.read_stage = - ShadowTlsV3ReadStage::Payload { header, bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - filled += payload_buf.filled().len(); - } - } - } - let mut frame = Vec::with_capacity(header.len() + bytes.len()); - frame.extend_from_slice(&header); - frame.extend_from_slice(&bytes); - if let Err(err) = self.process_frame(frame) { - return Poll::Ready(Err(err)); - } - self.read_stage = ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }; - } - } - } - } -} - -impl AsyncWrite for ShadowTlsV3HandshakeStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncRead for ShadowTlsV3SignedClientHelloStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.poll_write_pending(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => {} - } - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -impl AsyncWrite for ShadowTlsV3SignedClientHelloStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - if let Err(err) = self.queue_write(buf) { - return Poll::Ready(Err(err)); - } - match self.poll_write_pending(cx) { - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Pending | Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - if !self.client_hello_signed && !self.client_hello_buf.is_empty() { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "incomplete ShadowTLS v3 ClientHello before flush", - ))); - } - match self.poll_write_pending(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_flush(cx), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -struct ShadowTlsV3Stream { - inner: S, - hmac_add: hmac::Context, - hmac_verify: hmac::Context, - hmac_ignore: Option, - read_buf: Vec, - read_offset: usize, - read_stage: ShadowTlsV3ReadStage, - write_buf: Vec, - write_offset: usize, -} - -impl ShadowTlsV3Stream { - fn new( - inner: S, - password: &str, - server_random: [u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE], - hmac_ignore: Option, - ) -> Self { - Self { - inner, - hmac_add: shadow_tls_v3_hmac(password, &server_random, b"C"), - hmac_verify: shadow_tls_v3_hmac(password, &server_random, b"S"), - hmac_ignore, - read_buf: Vec::new(), - read_offset: 0, - read_stage: ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }, - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn process_frame(&mut self, frame: Vec) -> io::Result { - match frame.first().copied() { - Some(0x15) => Err(io::Error::new( - io::ErrorKind::ConnectionAborted, - "ShadowTLS v3 remote alert", - )), - Some(0x17) => { - if let Some(hmac_ignore) = self.hmac_ignore.as_ref() { - if shadow_tls_v3_application_data_hmac(&frame, hmac_ignore).is_some_and( - |expected| { - expected[..] - == frame - [SHADOW_TLS_V3_TLS_HEADER_SIZE..SHADOW_TLS_V3_HMAC_HEADER_SIZE] - }, - ) { - return Ok(false); - } - } - self.hmac_ignore = None; - let Some(expected) = shadow_tls_v3_application_data_hmac(&frame, &self.hmac_verify) - else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 application data frame was invalid", - )); - }; - if expected[..] - != frame[SHADOW_TLS_V3_TLS_HEADER_SIZE..SHADOW_TLS_V3_HMAC_HEADER_SIZE] - { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 application data verification failed", - )); - } - self.hmac_verify - .update(&frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..]); - self.hmac_verify.update(&expected); - self.read_buf = frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..].to_vec(); - self.read_offset = 0; - Ok(true) - } - Some(other) => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("unexpected ShadowTLS v3 record type {other}"), - )), - None => Ok(false), - } - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } -} - -impl AsyncRead for ShadowTlsV3Stream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - match std::mem::replace( - &mut self.read_stage, - ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }, - ) { - ShadowTlsV3ReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut header_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut header_buf) { - Poll::Pending => { - self.read_stage = ShadowTlsV3ReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if header_buf.filled().is_empty() => { - return Poll::Ready(Ok(())); - } - Poll::Ready(Ok(())) => filled += header_buf.filled().len(), - } - } - let len = u16::from_be_bytes([bytes[3], bytes[4]]) as usize; - self.read_stage = ShadowTlsV3ReadStage::Payload { - header: bytes, - bytes: vec![0u8; len], - filled: 0, - }; - } - ShadowTlsV3ReadStage::Payload { header, mut bytes, mut filled } => { - while filled < bytes.len() { - let mut payload_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut payload_buf) { - Poll::Pending => { - self.read_stage = - ShadowTlsV3ReadStage::Payload { header, bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if payload_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += payload_buf.filled().len(), - } - } - let mut frame = Vec::with_capacity(header.len() + bytes.len()); - frame.extend_from_slice(&header); - frame.extend_from_slice(&bytes); - match self.process_frame(frame) { - Ok(true) => { - self.read_stage = - ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }; - continue; - } - Ok(false) => { - self.read_stage = - ShadowTlsV3ReadStage::Header { bytes: [0u8; 5], filled: 0 }; - continue; - } - Err(err) => return Poll::Ready(Err(err)), - } - } - } - } - } -} - -impl AsyncWrite for ShadowTlsV3Stream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - let write_len = buf.len().min(SHADOW_TLS_MAX_RECORD_PAYLOAD); - self.write_buf = shadow_tls_v3_record(buf, write_len, &mut self.hmac_add)?; - self.write_offset = 0; - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(write_len)), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(write_len)) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let offset = self.write_offset; - let pending = self.write_buf[offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -struct RateLimitedStream { - inner: S, - bytes_per_second: u64, - burst_capacity: u64, - available: u64, - last_refill: Instant, - sleep: Option>>, -} - -impl RateLimitedStream { - fn new(inner: S, bytes_per_second: u32) -> Self { - let burst_capacity = KCPTUN_RATE_LIMIT_BURST_BYTES; - Self { - inner, - bytes_per_second: u64::from(bytes_per_second), - burst_capacity, - available: burst_capacity, - last_refill: Instant::now(), - sleep: None, - } - } - - fn refill(&mut self) { - if self.bytes_per_second == 0 { - self.available = self.burst_capacity; - self.last_refill = Instant::now(); - return; - } - let now = Instant::now(); - let elapsed = now.saturating_duration_since(self.last_refill); - let tokens = (elapsed - .as_nanos() - .saturating_mul(u128::from(self.bytes_per_second)) - / 1_000_000_000) as u64; - if tokens > 0 { - self.available = self - .available - .saturating_add(tokens) - .min(self.burst_capacity); - self.last_refill = now; - } - } - - fn wait_duration(&self) -> Duration { - if self.bytes_per_second == 0 { - return Duration::ZERO; - } - let nanos = - 1_000_000_000_u64.saturating_add(self.bytes_per_second - 1) / self.bytes_per_second; - Duration::from_nanos(nanos.max(1)) - } -} - -impl AsyncRead for RateLimitedStream -where - S: AsyncRead + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -impl AsyncWrite for RateLimitedStream -where - S: AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - if buf.is_empty() || self.bytes_per_second == 0 { - return Pin::new(&mut self.inner).poll_write(cx, buf); - } - - loop { - self.refill(); - if self.available > 0 { - break; - } - if self.sleep.is_none() { - self.sleep = Some(Box::pin(tokio::time::sleep(self.wait_duration()))); - } - let sleep = self.sleep.as_mut().expect("rate limit sleep exists"); - match Future::poll(sleep.as_mut(), cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(()) => { - self.sleep = None; - self.refill(); - if self.available == 0 { - self.available = 1; - } - } - } - } - - let write_len = buf.len().min(self.available as usize); - match Pin::new(&mut self.inner).poll_write(cx, &buf[..write_len]) { - Poll::Ready(Ok(written)) => { - self.available = self.available.saturating_sub(written as u64); - Poll::Ready(Ok(written)) - } - other => other, - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -struct WebSocketStream { - inner: S, - read_stage: WebSocketReadStage, - read_buf: Vec, - read_offset: usize, - write_buf: Vec, - write_offset: usize, -} - -enum WebSocketReadStage { - Header { - bytes: [u8; 2], - filled: usize, - }, - ExtendedLength { - header: [u8; 2], - bytes: [u8; 8], - needed: usize, - filled: usize, - }, - Mask { - opcode: u8, - len: usize, - bytes: [u8; 4], - filled: usize, - }, - Payload { - opcode: u8, - len: usize, - mask: Option<[u8; 4]>, - bytes: Vec, - filled: usize, - }, -} - -impl WebSocketStream { - fn new(inner: S) -> Self { - Self { - inner, - read_stage: WebSocketReadStage::Header { bytes: [0u8; 2], filled: 0 }, - read_buf: Vec::new(), - read_offset: 0, - write_buf: Vec::new(), - write_offset: 0, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } -} - -impl AsyncRead for WebSocketStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - - loop { - let stage = std::mem::replace( - &mut self.read_stage, - WebSocketReadStage::Header { bytes: [0u8; 2], filled: 0 }, - ); - match stage { - WebSocketReadStage::Header { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = WebSocketReadStage::Header { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - if filled == 0 { - return Poll::Ready(Ok(())); - } - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - let len_marker = bytes[1] & 0x7f; - if len_marker < 126 { - let len = usize::from(len_marker); - self.read_stage = if bytes[1] & 0x80 == 0 { - WebSocketReadStage::Payload { - opcode: bytes[0] & 0x0f, - len, - mask: None, - bytes: vec![0u8; len], - filled: 0, - } - } else { - WebSocketReadStage::Mask { - opcode: bytes[0] & 0x0f, - len, - bytes: [0u8; 4], - filled: 0, - } - }; - } else { - let needed = if len_marker == 126 { 2 } else { 8 }; - self.read_stage = WebSocketReadStage::ExtendedLength { - header: bytes, - bytes: [0u8; 8], - needed, - filled: 0, - }; - } - } - WebSocketReadStage::ExtendedLength { - header, - mut bytes, - needed, - mut filled, - } => { - while filled < needed { - let mut read_buf = ReadBuf::new(&mut bytes[filled..needed]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = WebSocketReadStage::ExtendedLength { - header, - bytes, - needed, - filled, - }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - let len = if needed == 2 { - u16::from_be_bytes([bytes[0], bytes[1]]) as usize - } else { - let len = u64::from_be_bytes(bytes); - match usize::try_from(len) { - Ok(len) => len, - Err(_) => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket frame is too large", - ))); - } - } - }; - if len > 16 * 1024 * 1024 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket frame exceeds Burrow frame limit", - ))); - } - self.read_stage = if header[1] & 0x80 == 0 { - WebSocketReadStage::Payload { - opcode: header[0] & 0x0f, - len, - mask: None, - bytes: vec![0u8; len], - filled: 0, - } - } else { - WebSocketReadStage::Mask { - opcode: header[0] & 0x0f, - len, - bytes: [0u8; 4], - filled: 0, - } - }; - } - WebSocketReadStage::Mask { - opcode, - len, - mut bytes, - mut filled, - } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = - WebSocketReadStage::Mask { opcode, len, bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - self.read_stage = WebSocketReadStage::Payload { - opcode, - len, - mask: Some(bytes), - bytes: vec![0u8; len], - filled: 0, - }; - } - WebSocketReadStage::Payload { - opcode, - len, - mask, - mut bytes, - mut filled, - } => { - while filled < len { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = WebSocketReadStage::Payload { - opcode, - len, - mask, - bytes, - filled, - }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - if let Some(mask) = mask { - apply_websocket_mask(&mut bytes, mask); - } - self.read_stage = WebSocketReadStage::Header { bytes: [0u8; 2], filled: 0 }; - match opcode { - 0x0 | 0x1 | 0x2 => { - self.read_buf = bytes; - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - } - 0x8 => return Poll::Ready(Ok(())), - _ => {} - } - } - } - } - } -} - -impl AsyncWrite for WebSocketStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - - self.write_buf = websocket_binary_frame(buf)?; - self.write_offset = 0; - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(buf.len())), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(buf.len())) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -struct HttpUpgradeFastOpenStream { - inner: Box, - response: Vec, - validated: bool, -} - -impl HttpUpgradeFastOpenStream { - fn new(inner: Box) -> Self { - Self { - inner, - response: Vec::with_capacity(1024), - validated: false, - } - } -} - -impl AsyncRead for HttpUpgradeFastOpenStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - while !self.validated { - if self.response.len() >= 16 * 1024 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket plugin response headers exceeded Burrow limit", - ))); - } - - let mut byte = [0u8; 1]; - let mut read_buf = ReadBuf::new(&mut byte); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => { - self.response.push(read_buf.filled()[0]); - if self.response.ends_with(b"\r\n\r\n") { - let response = match std::str::from_utf8(&self.response) { - Ok(response) => response, - Err(_) => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket plugin response headers were not valid UTF-8", - ))); - } - }; - if let Err(err) = validate_websocket_upgrade_response(response, true, None) - { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - err.to_string(), - ))); - } - self.validated = true; - self.response.clear(); - } - } - } - } - - Pin::new(&mut self.inner).poll_read(cx, buf) - } -} - -impl AsyncWrite for HttpUpgradeFastOpenStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.inner).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } -} - -struct WebSocketEarlyDataStream { - inner: WebSocketEarlyDataInner, - config: ShadowsocksWebSocketTransport, - path: String, - port: u16, - max_early_data: usize, - request: Vec, - request_offset: usize, - request_flushed: bool, - response: Vec, - first_write_consumed: Option, - websocket_key: Option, -} - -enum WebSocketEarlyDataInner { - Pending(Box), - Raw(Box), - Framed(WebSocketStream>), - Empty, -} - -impl WebSocketEarlyDataStream { - fn new( - inner: Box, - config: ShadowsocksWebSocketTransport, - port: u16, - path: String, - max_early_data: usize, - ) -> Self { - Self { - inner: WebSocketEarlyDataInner::Pending(inner), - config, - path, - port, - max_early_data, - request: Vec::new(), - request_offset: 0, - request_flushed: false, - response: Vec::with_capacity(1024), - first_write_consumed: None, - websocket_key: None, - } - } - - fn start_handshake(&mut self, payload: &[u8]) -> io::Result<()> { - if self.first_write_consumed.is_some() { - return Ok(()); - } - let consumed = payload.len().min(self.max_early_data); - let (request, websocket_key) = websocket_plugin_request( - &self.config, - self.port, - &self.path, - (consumed > 0).then_some(&payload[..consumed]), - ) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err.to_string()))?; - self.request = request; - self.websocket_key = websocket_key; - self.first_write_consumed = Some(consumed); - Ok(()) - } - - fn poll_handshake(&mut self, cx: &mut TaskContext<'_>) -> Poll> { - loop { - let WebSocketEarlyDataInner::Pending(stream) = &mut self.inner else { - return Poll::Ready(Ok(())); - }; - while self.request_offset < self.request.len() { - let pending = self.request[self.request_offset..].to_vec(); - match Pin::new(&mut **stream).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => { - return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); - } - Poll::Ready(Ok(written)) => self.request_offset += written, - } - } - - if !self.request_flushed { - match Pin::new(&mut **stream).poll_flush(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => self.request_flushed = true, - } - } - - if self.config.v2ray_http_upgrade && self.config.v2ray_http_upgrade_fast_open { - let stream = - match std::mem::replace(&mut self.inner, WebSocketEarlyDataInner::Empty) { - WebSocketEarlyDataInner::Pending(stream) => stream, - _ => unreachable!("early-data fast-open stream must be pending"), - }; - self.inner = - WebSocketEarlyDataInner::Raw(Box::new(HttpUpgradeFastOpenStream::new(stream))); - self.request.clear(); - self.response.clear(); - return Poll::Ready(Ok(())); - } - - while !self.response.ends_with(b"\r\n\r\n") { - if self.response.len() >= 16 * 1024 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket plugin response headers exceeded Burrow limit", - ))); - } - let mut byte = [0u8; 1]; - let mut read_buf = ReadBuf::new(&mut byte); - match Pin::new(&mut **stream).poll_read(cx, &mut read_buf) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => self.response.push(read_buf.filled()[0]), - } - } - - let response = match std::str::from_utf8(&self.response) { - Ok(response) => response, - Err(_) => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "websocket plugin response headers were not valid UTF-8", - ))); - } - }; - validate_websocket_upgrade_response( - response, - self.config.v2ray_http_upgrade, - self.websocket_key.as_deref(), - ) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?; - let stream = match std::mem::replace(&mut self.inner, WebSocketEarlyDataInner::Empty) { - WebSocketEarlyDataInner::Pending(stream) => stream, - _ => unreachable!("early-data stream must be pending before validation"), - }; - self.inner = if self.config.v2ray_http_upgrade { - WebSocketEarlyDataInner::Raw(stream) - } else { - WebSocketEarlyDataInner::Framed(WebSocketStream::new(stream)) - }; - self.request.clear(); - self.response.clear(); - return Poll::Ready(Ok(())); - } - } -} - -impl AsyncRead for WebSocketEarlyDataStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if matches!(self.inner, WebSocketEarlyDataInner::Pending(_)) { - if let Err(err) = self.start_handshake(&[]) { - return Poll::Ready(Err(err)); - } - match self.poll_handshake(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => {} - } - } - - match &mut self.inner { - WebSocketEarlyDataInner::Raw(stream) => Pin::new(&mut **stream).poll_read(cx, buf), - WebSocketEarlyDataInner::Framed(stream) => Pin::new(stream).poll_read(cx, buf), - WebSocketEarlyDataInner::Pending(_) => { - unreachable!("pending early-data stream handled") - } - WebSocketEarlyDataInner::Empty => Poll::Ready(Err(io::ErrorKind::NotConnected.into())), - } - } -} - -impl AsyncWrite for WebSocketEarlyDataStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - if matches!(self.inner, WebSocketEarlyDataInner::Pending(_)) { - if let Err(err) = self.start_handshake(buf) { - return Poll::Ready(Err(err)); - } - match self.poll_handshake(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => { - return Poll::Ready(Ok(self.first_write_consumed.take().unwrap_or(0))); - } - } - } - - match &mut self.inner { - WebSocketEarlyDataInner::Raw(stream) => Pin::new(&mut **stream).poll_write(cx, buf), - WebSocketEarlyDataInner::Framed(stream) => Pin::new(stream).poll_write(cx, buf), - WebSocketEarlyDataInner::Pending(_) => { - unreachable!("pending early-data stream handled") - } - WebSocketEarlyDataInner::Empty => Poll::Ready(Err(io::ErrorKind::NotConnected.into())), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match &mut self.inner { - WebSocketEarlyDataInner::Pending(stream) => Pin::new(&mut **stream).poll_flush(cx), - WebSocketEarlyDataInner::Raw(stream) => Pin::new(&mut **stream).poll_flush(cx), - WebSocketEarlyDataInner::Framed(stream) => Pin::new(stream).poll_flush(cx), - WebSocketEarlyDataInner::Empty => Poll::Ready(Err(io::ErrorKind::NotConnected.into())), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match &mut self.inner { - WebSocketEarlyDataInner::Pending(stream) => Pin::new(&mut **stream).poll_shutdown(cx), - WebSocketEarlyDataInner::Raw(stream) => Pin::new(&mut **stream).poll_shutdown(cx), - WebSocketEarlyDataInner::Framed(stream) => Pin::new(stream).poll_shutdown(cx), - WebSocketEarlyDataInner::Empty => Poll::Ready(Err(io::ErrorKind::NotConnected.into())), - } - } -} - -struct V2rayMuxStream { - inner: S, - read_stage: V2rayMuxReadStage, - read_buf: Vec, - read_offset: usize, - write_buf: Vec, - write_offset: usize, - wrote_open: bool, -} - -enum V2rayMuxReadStage { - MetadataLength { bytes: [u8; 2], filled: usize }, - Metadata { bytes: Vec, filled: usize }, - DataLength { bytes: [u8; 2], filled: usize }, - Data { bytes: Vec, filled: usize }, -} - -impl V2rayMuxStream { - fn new(inner: S) -> Self { - Self { - inner, - read_stage: V2rayMuxReadStage::MetadataLength { bytes: [0u8; 2], filled: 0 }, - read_buf: Vec::new(), - read_offset: 0, - write_buf: Vec::new(), - write_offset: 0, - wrote_open: false, - } - } - - fn copy_read_buffer(&mut self, buf: &mut ReadBuf<'_>) -> bool { - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - return false; - } - let len = (self.read_buf.len() - self.read_offset).min(buf.remaining()); - buf.put_slice(&self.read_buf[self.read_offset..self.read_offset + len]); - self.read_offset += len; - if self.read_offset == self.read_buf.len() { - self.read_buf.clear(); - self.read_offset = 0; - } - true - } - - fn compact_write_buffer(&mut self) { - if self.write_offset == self.write_buf.len() { - self.write_buf.clear(); - self.write_offset = 0; - } - } -} - -impl AsyncRead for V2rayMuxStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - - loop { - let stage = std::mem::replace( - &mut self.read_stage, - V2rayMuxReadStage::MetadataLength { bytes: [0u8; 2], filled: 0 }, - ); - match stage { - V2rayMuxReadStage::MetadataLength { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = - V2rayMuxReadStage::MetadataLength { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - if filled == 0 { - return Poll::Ready(Ok(())); - } - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - let len = u16::from_be_bytes(bytes) as usize; - if len > 512 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "v2ray-plugin mux metadata is too large", - ))); - } - self.read_stage = V2rayMuxReadStage::Metadata { - bytes: vec![0u8; len], - filled: 0, - }; - } - V2rayMuxReadStage::Metadata { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = V2rayMuxReadStage::Metadata { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - if bytes.len() < 4 { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "v2ray-plugin mux metadata is incomplete", - ))); - } - let status = bytes[2]; - let option = bytes[3]; - if status == 0x04 || option != 0x01 { - self.read_stage = - V2rayMuxReadStage::MetadataLength { bytes: [0u8; 2], filled: 0 }; - } else { - self.read_stage = - V2rayMuxReadStage::DataLength { bytes: [0u8; 2], filled: 0 }; - } - } - V2rayMuxReadStage::DataLength { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = V2rayMuxReadStage::DataLength { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - let len = u16::from_be_bytes(bytes) as usize; - self.read_stage = V2rayMuxReadStage::Data { - bytes: vec![0u8; len], - filled: 0, - }; - } - V2rayMuxReadStage::Data { mut bytes, mut filled } => { - while filled < bytes.len() { - let mut read_buf = ReadBuf::new(&mut bytes[filled..]); - match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) { - Poll::Pending => { - self.read_stage = V2rayMuxReadStage::Data { bytes, filled }; - return Poll::Pending; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) if read_buf.filled().is_empty() => { - return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())); - } - Poll::Ready(Ok(())) => filled += read_buf.filled().len(), - } - } - self.read_stage = - V2rayMuxReadStage::MetadataLength { bytes: [0u8; 2], filled: 0 }; - self.read_buf = bytes; - if self.copy_read_buffer(buf) { - return Poll::Ready(Ok(())); - } - } - } - } - } -} - -impl AsyncWrite for V2rayMuxStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - - let payload_len = buf.len().min(u16::MAX as usize); - let payload = &buf[..payload_len]; - self.write_buf = if self.wrote_open { - v2ray_mux_data_frame(payload)? - } else { - self.wrote_open = true; - let mut frame = v2ray_mux_open_frame(); - frame.extend_from_slice(&v2ray_mux_data_frame(payload)?); - frame - }; - self.write_offset = 0; - - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Ready(Ok(payload_len)), - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Poll::Ready(Ok(payload_len)) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - while self.write_offset < self.write_buf.len() { - let pending = self.write_buf[self.write_offset..].to_vec(); - match Pin::new(&mut self.inner).poll_write(cx, &pending) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(0)) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Poll::Ready(Ok(written)) => { - self.write_offset += written; - self.compact_write_buffer(); - } - } - } - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - match self.as_mut().poll_flush(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => Pin::new(&mut self.inner).poll_shutdown(cx), - } - } -} - -#[cfg(target_vendor = "apple")] -fn verify_native_tls_peer_certificate( - tls: &tokio_native_tls::TlsStream, - fingerprint: &CertificateFingerprint, - protocol: &str, -) -> Result<()> { - let cert = tls - .get_ref() - .peer_certificate() - .with_context(|| format!("failed to read {protocol} native TLS peer certificate"))? - .ok_or_else(|| anyhow!("{protocol} native TLS peer did not provide a certificate"))?; - let digest = Sha256::digest( - cert.to_der() - .with_context(|| format!("failed to encode {protocol} native TLS peer certificate"))?, - ); - if digest.as_slice() != fingerprint.0 { - bail!("{protocol} certificate fingerprint mismatch"); - } - Ok(()) -} - -#[cfg(not(target_vendor = "apple"))] -fn trojan_tls_config( - alpn: &[String], - skip_cert_verify: bool, - certificate_fingerprint: Option<&CertificateFingerprint>, -) -> Result { - rustls_tls_config(alpn, skip_cert_verify, certificate_fingerprint, None, None) -} - -fn rustls_tls_config( - alpn: &[String], - skip_cert_verify: bool, - certificate_fingerprint: Option<&CertificateFingerprint>, - client_identity: Option<&TlsClientIdentity>, - ech_config_list: Option<&[u8]>, -) -> Result { - install_rustls_crypto_provider(); - let mut root_store = RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let builder = if let Some(ech_config_list) = ech_config_list { - let ech_config = rustls_ech_config(ech_config_list)?; - ClientConfig::builder_with_provider(Arc::new(rustls::crypto::aws_lc_rs::default_provider())) - .with_ech(EchMode::Enable(ech_config)) - .context("failed to enable ECH for Shadowsocks websocket plugin")? - } else { - ClientConfig::builder() - }; - let builder = builder.with_root_certificates(root_store); - let mut config = if let Some(identity) = client_identity { - let (cert_chain, private_key) = load_rustls_client_identity(identity)?; - builder - .with_client_auth_cert(cert_chain, private_key) - .context("failed to configure TLS client certificate")? - } else { - builder.with_no_client_auth() - }; - config.alpn_protocols = alpn.iter().map(|value| value.as_bytes().to_vec()).collect(); - if let Some(fingerprint) = certificate_fingerprint { - config - .dangerous() - .set_certificate_verifier(Arc::new(PinnedCertificateVerifier { - fingerprint: fingerprint.clone(), - })); - } else if skip_cert_verify { - config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerifier)); - } - Ok(config) -} - -fn rustls_ech_config(config_list: &[u8]) -> Result { - EchConfig::new( - EchConfigListBytes::from(config_list), - rustls::crypto::aws_lc_rs::hpke::ALL_SUPPORTED_SUITES, - ) - .context("failed to parse Shadowsocks websocket ECH config list") -} - -fn install_rustls_crypto_provider() { - static INSTALL: Once = Once::new(); - INSTALL.call_once( - || match rustls::crypto::aws_lc_rs::default_provider().install_default() { - Ok(()) => tracing::debug!("installed aws-lc-rs rustls crypto provider"), - Err(_) => tracing::debug!("rustls crypto provider was already installed"), - }, - ); -} - -fn parse_certificate_fingerprint(value: &str) -> Result { - let normalized = value - .chars() - .filter(|ch| *ch != ':' && *ch != '-' && !ch.is_whitespace()) - .collect::(); - if matches!( - normalized.to_ascii_lowercase().as_str(), - "chrome" | "firefox" | "safari" | "ios" | "android" | "edge" | "360" | "qq" - ) { - bail!( - "`fingerprint` is for TLS certificate pinning; use `client-fingerprint` for browser fingerprints" - ); - } - if normalized.len() != 64 { - bail!("TLS certificate fingerprint must be a SHA-256 hex digest"); - } - let mut bytes = [0u8; 32]; - for (idx, chunk) in normalized.as_bytes().chunks_exact(2).enumerate() { - let hi = hex_nibble(chunk[0]).ok_or_else(|| anyhow!("invalid certificate fingerprint"))?; - let lo = hex_nibble(chunk[1]).ok_or_else(|| anyhow!("invalid certificate fingerprint"))?; - bytes[idx] = (hi << 4) | lo; - } - Ok(CertificateFingerprint(bytes)) -} - -fn tls_material_bytes(value: &str, label: &str) -> Result> { - let trimmed = value.trim(); - if trimmed.contains("-----BEGIN ") { - return Ok(trimmed.as_bytes().to_vec()); - } - fs::read(trimmed).with_context(|| format!("failed to read TLS {label} from {trimmed}")) -} - -fn load_rustls_client_identity( - identity: &TlsClientIdentity, -) -> Result<(Vec>, PrivateKeyDer<'static>)> { - let certificate = tls_material_bytes(&identity.certificate, "client certificate")?; - let private_key = tls_material_bytes(&identity.private_key, "client private key")?; - - let mut certificate_reader = io::Cursor::new(certificate); - let certificate_chain = rustls_pemfile::certs(&mut certificate_reader) - .collect::, _>>() - .context("failed to parse TLS client certificate PEM")?; - if certificate_chain.is_empty() { - bail!("TLS client certificate PEM did not contain any certificates"); - } - - let mut private_key_reader = io::Cursor::new(private_key); - let private_key = rustls_pemfile::private_key(&mut private_key_reader) - .context("failed to parse TLS client private key PEM")? - .ok_or_else(|| anyhow!("TLS client private key PEM did not contain a private key"))?; - - Ok((certificate_chain, private_key)) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn load_boring_client_identity( - identity: &TlsClientIdentity, -) -> Result { - let certificate = tls_material_bytes(&identity.certificate, "client certificate")?; - let private_key = tls_material_bytes(&identity.private_key, "client private key")?; - - let mut certificate_reader = io::Cursor::new(certificate); - let certificate_chain = rustls_pemfile::certs(&mut certificate_reader) - .collect::, _>>() - .context("failed to parse TLS client certificate PEM")? - .into_iter() - .map(|certificate| { - BoringX509::from_der(certificate.as_ref()) - .context("failed to parse TLS client certificate as X.509") - }) - .collect::>>()?; - if certificate_chain.is_empty() { - bail!("TLS client certificate PEM did not contain any certificates"); - } - - let mut private_key_reader = io::Cursor::new(private_key); - let private_key = rustls_pemfile::private_key(&mut private_key_reader) - .context("failed to parse TLS client private key PEM")? - .ok_or_else(|| anyhow!("TLS client private key PEM did not contain a private key"))?; - let private_key = BoringPKey::private_key_from_der(private_key.secret_der()) - .context("failed to parse TLS client private key for BoringSSL")?; - - Ok(BoringConnectorConfigClientAuth { - cert_chain: certificate_chain, - private_key, - }) -} - -#[cfg(target_vendor = "apple")] -fn load_native_tls_identity(identity: &TlsClientIdentity) -> Result { - let certificate = tls_material_bytes(&identity.certificate, "client certificate")?; - let private_key = tls_material_bytes(&identity.private_key, "client private key")?; - NativeTlsIdentity::from_pkcs8(&certificate, &private_key) - .context("failed to configure TLS client certificate") -} - -fn unsupported_client_fingerprint(config: &TrojanNode) -> Option<&str> { - unsupported_client_fingerprint_value(config.client_fingerprint.as_deref()) -} - -fn unsupported_client_fingerprint_value(value: Option<&str>) -> Option<&str> { - let value = value?.trim(); - if value.is_empty() || value.eq_ignore_ascii_case("none") { - return None; - } - MihomoClientFingerprint::from_mihomo_name(value) - .is_some() - .then_some(value) -} - -impl MihomoClientFingerprint { - fn from_mihomo_name(value: &str) -> Option { - Some(match value.trim().to_ascii_lowercase().as_str() { - "chrome" => Self::Chrome, - "firefox" => Self::Firefox, - "safari" => Self::Safari, - "ios" => Self::Ios, - "android" => Self::Android, - "edge" => Self::Edge, - "360" => Self::Browser360, - "qq" => Self::Qq, - "random" => Self::Random, - "chrome120" => Self::Chrome120, - "firefox120" => Self::Firefox120, - "safari16" => Self::Safari16, - "chrome_psk" => Self::ChromePsk, - "chrome_psk_shuffle" => Self::ChromePskShuffle, - "chrome_padding_psk_shuffle" => Self::ChromePaddingPskShuffle, - "chrome_pq" => Self::ChromePq, - "chrome_pq_psk" => Self::ChromePqPsk, - "randomized" => Self::Randomized, - _ => return None, - }) - } - - fn mihomo_name(self) -> &'static str { - match self { - Self::Chrome => "chrome", - Self::Firefox => "firefox", - Self::Safari => "safari", - Self::Ios => "ios", - Self::Android => "android", - Self::Edge => "edge", - Self::Browser360 => "360", - Self::Qq => "qq", - Self::Random => "random", - Self::Chrome120 => "chrome120", - Self::Firefox120 => "firefox120", - Self::Safari16 => "safari16", - Self::ChromePsk => "chrome_psk", - Self::ChromePskShuffle => "chrome_psk_shuffle", - Self::ChromePaddingPskShuffle => "chrome_padding_psk_shuffle", - Self::ChromePq => "chrome_pq", - Self::ChromePqPsk => "chrome_pq_psk", - Self::Randomized => "randomized", - } - } - - fn shadow_tls_v2_boring_supported(self) -> bool { - #[cfg(not(feature = "boring-browser-fingerprints"))] - { - let _ = self; - false - } - #[cfg(feature = "boring-browser-fingerprints")] - matches!( - self, - Self::Chrome - | Self::Firefox - | Self::Safari - | Self::Ios - | Self::Android - | Self::Edge - | Self::Random - | Self::Safari16 - ) - } - -} - -fn hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - -#[derive(Debug)] -struct PinnedCertificateVerifier { - fingerprint: CertificateFingerprint, -} - -impl ServerCertVerifier for PinnedCertificateVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - let mut certificates = std::iter::once(end_entity).chain(intermediates.iter()); - if certificates - .any(|certificate| certificate_fingerprint_matches(certificate, &self.fingerprint)) - { - Ok(ServerCertVerified::assertion()) - } else { - Err(RustlsError::General( - "certificate chain does not match pinned fingerprint".to_owned(), - )) - } - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn supported_verify_schemes(&self) -> Vec { - supported_verify_schemes() - } -} - -fn certificate_fingerprint_matches( - certificate: &CertificateDer<'_>, - fingerprint: &CertificateFingerprint, -) -> bool { - let digest = Sha256::digest(certificate.as_ref()); - digest[..] == fingerprint.0[..] -} - -#[derive(Debug)] -struct NoCertificateVerifier; - -impl ServerCertVerifier for NoCertificateVerifier { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn supported_verify_schemes(&self) -> Vec { - supported_verify_schemes() - } -} - -fn supported_verify_schemes() -> Vec { - vec![ - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ED25519, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - ] -} - -async fn write_trojan_header( - writer: &mut W, - password: &str, - command: u8, - socks_address: &[u8], -) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - let password_hash = trojan_password_hash(password); - let mut header = Vec::with_capacity(password_hash.len() + 2 + 1 + socks_address.len() + 2); - header.extend_from_slice(password_hash.as_bytes()); - header.extend_from_slice(b"\r\n"); - header.push(command); - header.extend_from_slice(socks_address); - header.extend_from_slice(b"\r\n"); - writer.write_all(&header).await?; - writer.flush().await?; - Ok(()) -} - -fn trojan_password_hash(password: &str) -> String { - let digest = Sha224::digest(password.as_bytes()); - hex_lower(&digest) -} - -async fn write_trojan_udp_packet( - writer: &mut W, - socks_address: &[u8], - payload: &[u8], -) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - if payload.len() <= TROJAN_MAX_UDP_PAYLOAD { - write_trojan_udp_frame(writer, socks_address, payload).await?; - } else { - for chunk in payload.chunks(TROJAN_MAX_UDP_PAYLOAD) { - write_trojan_udp_frame(writer, socks_address, chunk).await?; - } - } - writer.flush().await?; - Ok(()) -} - -async fn write_trojan_udp_frame( - writer: &mut W, - socks_address: &[u8], - payload: &[u8], -) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer.write_all(socks_address).await?; - writer - .write_all(&(payload.len() as u16).to_be_bytes()) - .await?; - writer.write_all(b"\r\n").await?; - writer.write_all(payload).await?; - Ok(()) -} - -async fn read_trojan_udp_packet(reader: &mut R, buf: &mut [u8]) -> Result<(SocketAddr, Vec)> -where - R: AsyncRead + Unpin, -{ - let source = read_socks_addr_async(reader).await?; - let mut len = [0u8; 2]; - reader.read_exact(&mut len).await?; - let payload_len = u16::from_be_bytes(len) as usize; - let mut crlf = [0u8; 2]; - reader.read_exact(&mut crlf).await?; - if crlf != *b"\r\n" { - bail!("invalid Trojan UDP packet delimiter"); - } - if payload_len > buf.len() { - bail!("Trojan UDP packet too large"); - } - reader.read_exact(&mut buf[..payload_len]).await?; - Ok((source, buf[..payload_len].to_vec())) -} - -include!("proxy_runtime/shadowsocks_crypto.rs"); - -include!("proxy_runtime/shadowsocks_plugins.rs"); - -fn validate_shadowsocks_cipher(name: &str) -> Result<()> { - if shadowsocks_cipher_kind(name).is_ok() - || CustomSsAeadKind::from_name(name).is_some() - || CustomSsStreamKind::from_name(name).is_some() - || Aead2022AesCcmKind::from_name(name).is_some() - { - return Ok(()); - } - bail!("unsupported Shadowsocks cipher {name}") -} - -fn normalize_uot_version(version: u8) -> Result { - match version { - 0 | UOT_LEGACY_VERSION => Ok(UOT_LEGACY_VERSION), - UOT_VERSION => Ok(UOT_VERSION), - other => bail!("unsupported Shadowsocks udp-over-tcp version {other}"), - } -} - -fn shadowsocks_sing_mux_transport( - smux: Option<&ShadowsocksSmux>, - _config: &ShadowsocksNode, - protocol: &ShadowsocksProtocol, -) -> Result> { - let Some(smux) = smux.filter(|smux| smux.enabled) else { - return Ok(None); - }; - let protocol_name = smux.protocol.as_deref().unwrap_or("h2mux"); - let sing_mux_protocol = if protocol_name.eq_ignore_ascii_case("h2mux") { - ShadowsocksSingMuxProtocol::H2Mux - } else if protocol_name.eq_ignore_ascii_case("smux") { - ShadowsocksSingMuxProtocol::Smux - } else if protocol_name.eq_ignore_ascii_case("yamux") { - ShadowsocksSingMuxProtocol::Yamux - } else { - bail!("top-level Shadowsocks sing-mux protocol {protocol_name} is not supported yet"); - }; - let brutal = smux - .brutal - .as_ref() - .filter(|brutal| brutal.enabled) - .map(|brutal| ShadowsocksSingMuxBrutalTransport { - send_bps: mihomo_string_to_bps(brutal.up.as_deref().unwrap_or("")), - receive_bps: mihomo_string_to_bps(brutal.down.as_deref().unwrap_or("")), - }); - match protocol { - ShadowsocksProtocol::None - | ShadowsocksProtocol::Standard { .. } - | ShadowsocksProtocol::CustomAead { .. } - | ShadowsocksProtocol::CustomStream { .. } - | ShadowsocksProtocol::Aead2022AesCcm { .. } => {} - } - Ok(Some(ShadowsocksSingMuxTransport { - protocol: sing_mux_protocol, - max_connections: smux.max_connections.unwrap_or(0) as usize, - min_streams: smux.min_streams.unwrap_or(0) as usize, - max_streams: smux.max_streams.unwrap_or(0) as usize, - padding: smux.padding, - udp: !smux.only_tcp, - brutal, - })) -} - -fn mihomo_string_to_bps(value: &str) -> u64 { - let value = value.trim(); - if value.is_empty() { - return 0; - } - if let Ok(mbps) = value.parse::() { - return mbps.saturating_mul(1_000_000 / 8); - } - - let digit_len = value - .as_bytes() - .iter() - .take_while(|byte| byte.is_ascii_digit()) - .count(); - if digit_len == 0 { - return 0; - } - let Ok(amount) = value[..digit_len].parse::() else { - return 0; - }; - let unit = value[digit_len..].trim(); - let Some(suffix) = unit.strip_suffix("ps") else { - return 0; - }; - let (scale, bits) = match suffix { - "b" => (1, true), - "B" => (1, false), - "Kb" => (1_000, true), - "KB" => (1_000, false), - "Mb" => (1_000_000, true), - "MB" => (1_000_000, false), - "Gb" => (1_000_000_000, true), - "GB" => (1_000_000_000, false), - "Tb" => (1_000_000_000_000, true), - "TB" => (1_000_000_000_000, false), - _ => return 0, - }; - let bytes = amount.saturating_mul(scale); - if bits { - bytes / 8 - } else { - bytes - } -} - -fn shadowsocks_protocol_for_node( - node: &ProxyNode, - config: &ShadowsocksNode, -) -> Result { - if config.cipher.trim().eq_ignore_ascii_case("none") { - return Ok(ShadowsocksProtocol::None); - } - - if let Some(kind) = Aead2022AesCcmKind::from_name(&config.cipher) { - let (user_key, identity_keys) = parse_aead2022_keys(kind, &config.password)?; - return Ok(ShadowsocksProtocol::Aead2022AesCcm { kind, user_key, identity_keys }); - } - - if let Ok(cipher) = shadowsocks_cipher_kind(&config.cipher) { - validate_standard_aead2022_mihomo_password(cipher, &config.password)?; - let server_config = SsServerConfig::new( - SsServerAddr::DomainName(node.server.clone(), node.port), - config.password.clone(), - cipher, - ) - .with_context(|| { - format!( - "failed to configure Shadowsocks cipher {} for node {}", - config.cipher, node.name - ) - })?; - return Ok(ShadowsocksProtocol::Standard { - server_config: Arc::new(server_config), - context: SsContext::new_shared(SsServerType::Local), - }); - } - - if let Some(kind) = CustomSsAeadKind::from_name(&config.cipher) { - let master_key = evp_bytes_to_key(config.password.as_bytes(), kind.key_len()); - return Ok(ShadowsocksProtocol::CustomAead { - kind, - master_key: Arc::from(master_key), - }); - } - - if let Some(kind) = CustomSsStreamKind::from_name(&config.cipher) { - let key = evp_bytes_to_key(config.password.as_bytes(), kind.key_len()); - return Ok(ShadowsocksProtocol::CustomStream { kind, key: Arc::from(key) }); - } - - bail!("unsupported Shadowsocks cipher {}", config.cipher) -} - -fn shadowsocks_cipher_kind(name: &str) -> Result { - let canonical = canonical_shadowsocks_cipher_name(name); - SsCipherKind::from_str(&canonical).map_err(|_| anyhow!("unsupported Shadowsocks cipher {name}")) -} - -fn validate_standard_aead2022_mihomo_password(cipher: SsCipherKind, password: &str) -> Result<()> { - if !cipher.is_aead_2022() { - return Ok(()); - } - - if password.is_empty() { - bail!("Shadowsocks 2022 password is missing"); - } - - let supports_eih = matches!( - cipher, - SsCipherKind::AEAD2022_BLAKE3_AES_128_GCM | SsCipherKind::AEAD2022_BLAKE3_AES_256_GCM - ); - let segments = password.split(':').collect::>(); - if segments.len() > 1 && !supports_eih { - bail!("Shadowsocks 2022 EIH support only available in AES ciphers"); - } - - for segment in segments { - let decoded = BASE64_STANDARD - .decode(segment) - .context("failed to decode Shadowsocks 2022 base64 key")?; - if decoded.len() != cipher.key_len() { - bail!( - "Shadowsocks 2022 key length mismatch: required {}, got {}", - cipher.key_len(), - decoded.len() - ); - } - } - - Ok(()) -} - -fn canonical_shadowsocks_cipher_name(name: &str) -> String { - match name.trim().to_ascii_lowercase().as_str() { - "aead_aes_128_gcm" => "aes-128-gcm".to_owned(), - "aead_aes_256_gcm" => "aes-256-gcm".to_owned(), - "chacha20-poly1305" | "aead_chacha20_poly1305" => "chacha20-ietf-poly1305".to_owned(), - other => other.to_owned(), - } -} - -fn shadowsocks_addr_for_target(target: &ProxyTcpTarget) -> Result { - if let Some(hostname) = target.hostname.as_deref() { - let hostname = hostname.trim_end_matches('.'); - if !hostname.is_empty() && hostname.len() <= u8::MAX as usize { - return Ok(SsAddress::DomainNameAddress( - hostname.to_owned(), - target.address.port(), - )); - } - } - Ok(SsAddress::SocketAddress(target.address)) -} - -fn shadowsocks_addr_to_socket_addr(addr: SsAddress) -> Result { - match addr { - SsAddress::SocketAddress(addr) => Ok(addr), - SsAddress::DomainNameAddress(domain, port) => resolve_server(&domain, port), - } -} - -fn socks_addr(addr: SocketAddr) -> Result> { - let mut encoded = Vec::with_capacity(1 + 16 + 2); - match addr.ip() { - IpAddr::V4(ip) => { - encoded.push(1); - encoded.extend_from_slice(&ip.octets()); - } - IpAddr::V6(ip) => { - encoded.push(4); - encoded.extend_from_slice(&ip.octets()); - } - } - encoded.extend_from_slice(&addr.port().to_be_bytes()); - Ok(encoded) -} - -fn socks_domain_addr(domain: &str, port: u16) -> Result> { - let domain = domain.trim_end_matches('.'); - if domain.is_empty() || domain.len() > u8::MAX as usize { - bail!("invalid SOCKS domain length"); - } - let mut encoded = Vec::with_capacity(1 + 1 + domain.len() + 2); - encoded.push(3); - encoded.push(domain.len() as u8); - encoded.extend_from_slice(domain.as_bytes()); - encoded.extend_from_slice(&port.to_be_bytes()); - Ok(encoded) -} - -fn socks_addr_for_target(target: &ProxyTcpTarget) -> Result> { - if let Some(hostname) = target.hostname.as_deref() { - let hostname = hostname.trim_end_matches('.'); - if !hostname.is_empty() && hostname.len() <= u8::MAX as usize { - return socks_domain_addr(hostname, target.address.port()); - } - } - socks_addr(target.address) -} - -async fn sing_mux_brutal_exchange( - session: &ShadowsocksSingMuxSession, - brutal: ShadowsocksSingMuxBrutalTransport, -) -> Result<()> { - let mut stream = session.open_stream().await?; - write_sing_mux_brutal_request(&mut stream, brutal.receive_bps).await?; - let server_receive_bps = read_sing_mux_brutal_response(&mut stream).await?; - let send_bps = if server_receive_bps < brutal.send_bps { - server_receive_bps - } else { - brutal.send_bps - }; - tracing::debug!( - send_bps, - receive_bps = brutal.receive_bps, - server_receive_bps, - "completed Shadowsocks sing-mux brutal bandwidth exchange" - ); - Ok(()) -} - -async fn write_sing_mux_brutal_request(writer: &mut W, receive_bps: u64) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer - .write_all(&0u16.to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux brutal tcp flags")?; - writer - .write_all(&socks_domain_addr(SING_MUX_BRUTAL_EXCHANGE_DOMAIN, 0)?) - .await - .context("failed to write Shadowsocks sing-mux brutal exchange destination")?; - writer - .write_all(&receive_bps.to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux brutal receive bps")?; - writer - .flush() - .await - .context("failed to flush Shadowsocks sing-mux brutal request")?; - Ok(()) -} - -async fn read_sing_mux_brutal_response(reader: &mut R) -> Result -where - R: AsyncRead + Unpin, -{ - read_sing_mux_stream_response(reader).await?; - let ok = reader - .read_u8() - .await - .context("failed to read Shadowsocks sing-mux brutal response status")?; - match ok { - 0 => { - let len = read_sing_mux_uvarint(reader).await?; - if len > 4096 { - bail!("Shadowsocks sing-mux brutal remote error message is too long"); - } - let mut message = vec![0u8; len as usize]; - reader - .read_exact(&mut message) - .await - .context("failed to read Shadowsocks sing-mux brutal remote error")?; - let message = String::from_utf8_lossy(&message); - bail!("Shadowsocks sing-mux brutal remote error: {message}"); - } - 1 => reader - .read_u64() - .await - .context("failed to read Shadowsocks sing-mux brutal server receive bps"), - other => bail!("unknown Shadowsocks sing-mux brutal response status {other}"), - } -} - -async fn write_sing_mux_tcp_request(writer: &mut W, target: &ProxyTcpTarget) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer - .write_all(&0u16.to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux tcp flags")?; - writer - .write_all(&socks_addr_for_target(target)?) - .await - .context("failed to write Shadowsocks sing-mux tcp destination")?; - writer - .flush() - .await - .context("failed to flush Shadowsocks sing-mux tcp request")?; - Ok(()) -} - -async fn write_sing_mux_udp_request( - writer: &mut W, - destination: SocketAddr, - payload: &[u8], -) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer - .write_all(&SING_MUX_STREAM_FLAG_UDP.to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux udp flags")?; - writer - .write_all(&socks_addr(destination)?) - .await - .context("failed to write Shadowsocks sing-mux udp destination")?; - if !payload.is_empty() { - write_sing_mux_udp_payload(writer, payload).await?; - } else { - writer - .flush() - .await - .context("failed to flush Shadowsocks sing-mux udp request")?; - } - Ok(()) -} - -async fn write_sing_mux_udp_payload(writer: &mut W, payload: &[u8]) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - if payload.len() > u16::MAX as usize { - bail!("Shadowsocks sing-mux udp payload too large"); - } - writer - .write_all(&(payload.len() as u16).to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux udp payload length")?; - writer - .write_all(payload) - .await - .context("failed to write Shadowsocks sing-mux udp payload")?; - writer - .flush() - .await - .context("failed to flush Shadowsocks sing-mux udp payload")?; - Ok(()) -} - -async fn read_sing_mux_udp_payload(reader: &mut R, response_read: &mut bool) -> Result> -where - R: AsyncRead + Unpin, -{ - if !*response_read { - read_sing_mux_stream_response(reader).await?; - *response_read = true; - } - let payload_len = reader - .read_u16() - .await - .context("failed to read Shadowsocks sing-mux udp payload length")? - as usize; - let mut payload = vec![0u8; payload_len]; - reader - .read_exact(&mut payload) - .await - .context("failed to read Shadowsocks sing-mux udp payload")?; - Ok(payload) -} - -fn sing_mux_padding_len() -> usize { - SING_MUX_MIN_PADDING_LEN + (OsRng.next_u32() as usize % SING_MUX_PADDING_LEN_RANGE) -} - -async fn write_sing_mux_session_preface( - writer: &mut W, - protocol: ShadowsocksSingMuxProtocol, - padding: bool, -) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - if !padding { - writer - .write_all(&[SING_MUX_VERSION_0, protocol.wire_id()]) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks sing-mux {} preface", - protocol.name() - ) - })?; - writer.flush().await.with_context(|| { - format!( - "failed to flush Shadowsocks sing-mux {} preface", - protocol.name() - ) - })?; - return Ok(()); - } - - let padding_len = sing_mux_padding_len(); - writer - .write_all(&[SING_MUX_VERSION_1, protocol.wire_id(), 1]) - .await - .with_context(|| { - format!( - "failed to write Shadowsocks sing-mux padded {} preface", - protocol.name() - ) - })?; - writer - .write_all(&(padding_len as u16).to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux preface padding length")?; - writer - .write_all(&vec![0u8; padding_len]) - .await - .context("failed to write Shadowsocks sing-mux preface padding")?; - writer - .flush() - .await - .context("failed to flush Shadowsocks sing-mux padded preface")?; - Ok(()) -} - -fn sing_mux_padding_stream(stream: Box) -> Box { - let (plain, bridge) = tokio::io::duplex(64 * 1024); - let (mut plain_reader, mut plain_writer) = split(bridge); - let (mut stream_reader, mut stream_writer) = split(stream); - - let _upload = tokio::spawn(async move { - let result = async { - let mut buf = vec![0u8; 16 * 1024]; - let mut frames = 0usize; - loop { - let n = plain_reader - .read(&mut buf) - .await - .context("failed to read Shadowsocks sing-mux padded upload payload")?; - if n == 0 { - break; - } - if frames < SING_MUX_PADDING_FRAMES { - let padding_len = sing_mux_padding_len(); - stream_writer - .write_all(&(n as u16).to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux padded upload length")?; - stream_writer - .write_all(&(padding_len as u16).to_be_bytes()) - .await - .context("failed to write Shadowsocks sing-mux upload padding length")?; - stream_writer - .write_all(&buf[..n]) - .await - .context("failed to write Shadowsocks sing-mux padded upload payload")?; - stream_writer - .write_all(&vec![0u8; padding_len]) - .await - .context("failed to write Shadowsocks sing-mux upload padding")?; - frames += 1; - } else { - stream_writer - .write_all(&buf[..n]) - .await - .context("failed to write Shadowsocks sing-mux upload payload")?; - } - } - stream_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks sing-mux padded upload")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!(?err, "Shadowsocks sing-mux padded upload task stopped"); - } - }); - - let _download = tokio::spawn(async move { - let result = async { - let mut header = [0u8; 4]; - let mut padding = vec![0u8; SING_MUX_MIN_PADDING_LEN + SING_MUX_PADDING_LEN_RANGE - 1]; - let mut frames = 0usize; - while frames < SING_MUX_PADDING_FRAMES { - stream_reader - .read_exact(&mut header) - .await - .context("failed to read Shadowsocks sing-mux padded download header")?; - let payload_len = u16::from_be_bytes([header[0], header[1]]) as usize; - let padding_len = u16::from_be_bytes([header[2], header[3]]) as usize; - let mut payload = vec![0u8; payload_len]; - stream_reader - .read_exact(&mut payload) - .await - .context("failed to read Shadowsocks sing-mux padded download payload")?; - let mut remaining_padding = padding_len; - while remaining_padding > 0 { - let read_len = remaining_padding.min(padding.len()); - stream_reader - .read_exact(&mut padding[..read_len]) - .await - .context("failed to read Shadowsocks sing-mux download padding")?; - remaining_padding -= read_len; - } - plain_writer - .write_all(&payload) - .await - .context("failed to write Shadowsocks sing-mux padded download payload")?; - frames += 1; - } - - let mut buf = vec![0u8; 16 * 1024]; - loop { - let n = stream_reader - .read(&mut buf) - .await - .context("failed to read Shadowsocks sing-mux download payload")?; - if n == 0 { - break; - } - plain_writer - .write_all(&buf[..n]) - .await - .context("failed to write Shadowsocks sing-mux download payload")?; - } - plain_writer - .shutdown() - .await - .context("failed to shutdown Shadowsocks sing-mux padded download")?; - Result::<()>::Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!(?err, "Shadowsocks sing-mux padded download task stopped"); - } - }); - - Box::new(plain) -} - -async fn read_sing_mux_stream_response(reader: &mut R) -> Result<()> -where - R: AsyncRead + Unpin, -{ - let mut status = [0u8; 1]; - reader - .read_exact(&mut status) - .await - .context("failed to read Shadowsocks sing-mux stream response")?; - match status[0] { - SING_MUX_STREAM_STATUS_SUCCESS => Ok(()), - SING_MUX_STREAM_STATUS_ERROR => { - let len = read_sing_mux_uvarint(reader).await?; - if len > 4096 { - bail!("Shadowsocks sing-mux remote error message is too long"); - } - let mut message = vec![0u8; len as usize]; - reader - .read_exact(&mut message) - .await - .context("failed to read Shadowsocks sing-mux remote error message")?; - bail!( - "Shadowsocks sing-mux remote error: {}", - String::from_utf8_lossy(&message) - ) - } - other => bail!("unknown Shadowsocks sing-mux stream response status {other}"), - } -} - -async fn read_sing_mux_uvarint(reader: &mut R) -> Result -where - R: AsyncRead + Unpin, -{ - let mut value = 0u64; - for shift in (0..64).step_by(7) { - let mut byte = [0u8; 1]; - reader - .read_exact(&mut byte) - .await - .context("failed to read Shadowsocks sing-mux uvarint")?; - value |= u64::from(byte[0] & 0x7f) << shift; - if byte[0] < 0x80 { - return Ok(value); - } - } - bail!("Shadowsocks sing-mux uvarint is too large") -} - -fn uot_magic_domain(version: u8) -> &'static str { - match version { - UOT_VERSION => UOT_MAGIC_ADDRESS, - _ => UOT_LEGACY_MAGIC_ADDRESS, - } -} - -fn uot_magic_shadowsocks_addr(version: u8) -> SsAddress { - SsAddress::DomainNameAddress(uot_magic_domain(version).to_owned(), 0) -} - -fn uot_request(destination: SocketAddr) -> Result> { - let mut request = Vec::with_capacity(1 + 1 + 16 + 2); - request.push(0); - request.extend_from_slice(&socks_addr(destination)?); - Ok(request) -} - -async fn write_uot_request(writer: &mut W, destination: SocketAddr) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer.write_all(&uot_request(destination)?).await?; - writer.flush().await?; - Ok(()) -} - -fn uot_frame(destination: SocketAddr, payload: &[u8]) -> Result> { - if payload.len() > u16::MAX as usize { - bail!("udp-over-tcp payload too large"); - } - let mut frame = uot_addr(destination); - frame.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - frame.extend_from_slice(payload); - Ok(frame) -} - -async fn write_uot_frame(writer: &mut W, destination: SocketAddr, payload: &[u8]) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer.write_all(&uot_frame(destination, payload)?).await?; - writer.flush().await?; - Ok(()) -} - -fn uot_addr(addr: SocketAddr) -> Vec { - let mut encoded = Vec::with_capacity(1 + 16 + 2); - match addr.ip() { - IpAddr::V4(ip) => { - encoded.push(0x00); - encoded.extend_from_slice(&ip.octets()); - } - IpAddr::V6(ip) => { - encoded.push(0x01); - encoded.extend_from_slice(&ip.octets()); - } - } - encoded.extend_from_slice(&addr.port().to_be_bytes()); - encoded -} - -async fn read_uot_frame(reader: &mut R) -> Result<(SocketAddr, Vec)> -where - R: AsyncRead + Unpin, -{ - let source = read_uot_addr_async(reader).await?; - let payload_len = reader.read_u16().await? as usize; - let mut payload = vec![0u8; payload_len]; - reader.read_exact(&mut payload).await?; - Ok((source, payload)) -} - -async fn read_uot_addr_async(reader: &mut R) -> Result -where - R: AsyncRead + Unpin, -{ - let atyp = reader.read_u8().await?; - match atyp { - 0x00 => { - let mut ip = [0u8; 4]; - reader.read_exact(&mut ip).await?; - let port = reader.read_u16().await?; - Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip)), port)) - } - 0x01 => { - let mut ip = [0u8; 16]; - reader.read_exact(&mut ip).await?; - let port = reader.read_u16().await?; - Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port)) - } - 0x02 => { - let len = reader.read_u8().await? as usize; - let mut domain = vec![0u8; len]; - reader.read_exact(&mut domain).await?; - let port = reader.read_u16().await?; - let domain = String::from_utf8(domain).context("invalid udp-over-tcp domain")?; - resolve_server(&domain, port) - } - other => bail!("unsupported udp-over-tcp address type {other}"), - } -} - -async fn read_socks_addr_async(reader: &mut R) -> Result -where - R: AsyncRead + Unpin, -{ - let atyp = reader.read_u8().await?; - match atyp { - 1 => { - let mut ip = [0u8; 4]; - reader.read_exact(&mut ip).await?; - let port = reader.read_u16().await?; - Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip)), port)) - } - 3 => { - let len = reader.read_u8().await? as usize; - let mut domain = vec![0u8; len]; - reader.read_exact(&mut domain).await?; - let port = reader.read_u16().await?; - let domain = String::from_utf8(domain).context("invalid SOCKS domain")?; - resolve_server(&domain, port) - } - 4 => { - let mut ip = [0u8; 16]; - reader.read_exact(&mut ip).await?; - let port = reader.read_u16().await?; - Ok(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port)) - } - other => bail!("unsupported SOCKS address type {other}"), - } -} - -fn parse_socks_addr(buf: &[u8]) -> Result<(SocketAddr, usize)> { - if buf.is_empty() { - bail!("missing SOCKS address"); - } - match buf[0] { - 1 => { - if buf.len() < 7 { - bail!("truncated IPv4 SOCKS address"); - } - let ip = Ipv4Addr::new(buf[1], buf[2], buf[3], buf[4]); - let port = u16::from_be_bytes([buf[5], buf[6]]); - Ok((SocketAddr::new(IpAddr::V4(ip), port), 7)) - } - 3 => { - if buf.len() < 2 { - bail!("truncated domain SOCKS address"); - } - let len = buf[1] as usize; - if buf.len() < 2 + len + 2 { - bail!("truncated domain SOCKS address"); - } - let domain = std::str::from_utf8(&buf[2..2 + len]).context("invalid SOCKS domain")?; - let port = u16::from_be_bytes([buf[2 + len], buf[3 + len]]); - Ok((resolve_server(domain, port)?, 2 + len + 2)) - } - 4 => { - if buf.len() < 19 { - bail!("truncated IPv6 SOCKS address"); - } - let mut ip = [0u8; 16]; - ip.copy_from_slice(&buf[1..17]); - let port = u16::from_be_bytes([buf[17], buf[18]]); - Ok((SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port), 19)) - } - other => bail!("unsupported SOCKS address type {other}"), - } -} - -fn resolve_server(host: &str, port: u16) -> Result { - (host, port) - .to_socket_addrs() - .with_context(|| format!("failed to resolve {host}:{port}"))? - .next() - .ok_or_else(|| anyhow!("no addresses resolved for {host}:{port}")) -} - -include!("proxy_runtime/networking.rs"); - -fn hex_lower(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len() * 2); - for byte in bytes { - out.push(HEX[(byte >> 4) as usize] as char); - out.push(HEX[(byte & 0x0f) as usize] as char); - } - out -} - -#[cfg(test)] -include!("proxy_runtime/tests.rs"); diff --git a/burrow/src/proxy_runtime/networking.rs b/burrow/src/proxy_runtime/networking.rs deleted file mode 100644 index f186543..0000000 --- a/burrow/src/proxy_runtime/networking.rs +++ /dev/null @@ -1,961 +0,0 @@ -impl ProxyServerEndpoint { - fn resolve(host: &str, port: u16) -> Result { - let addresses = resolve_server_addresses(host, port)?; - let endpoint = Self { - host: host.to_owned(), - port, - addresses: Arc::from(addresses), - }; - if endpoint - .addresses - .iter() - .any(|address| is_fake_or_benchmark_ip(address.ip())) - { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - resolved_addresses = ?endpoint.addresses, - "proxy server resolved to a fake-IP or benchmarking range; outbound dial may recurse or fail" - ); - } else { - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - resolved_addresses = ?endpoint.addresses, - "resolved proxy server addresses" - ); - } - Ok(endpoint) - } -} - -async fn connect_proxy_tcp(endpoint: &ProxyServerEndpoint) -> Result<(TcpStream, SocketAddr)> { - let mut last_error = None; - for address in endpoint.addresses.iter().copied() { - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - %address, - "dialing proxy tcp server" - ); - let socket = match address { - SocketAddr::V4(_) => TcpSocket::new_v4(), - SocketAddr::V6(_) => TcpSocket::new_v6(), - } - .with_context(|| format!("failed to create tcp socket for {address}"))?; - - if let Err(err) = bind_proxy_outbound_socket(socket.as_raw_fd(), address.ip()) { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = ?err, - "failed to bind proxy tcp socket to physical interface" - ); - last_error = Some(err); - continue; - } - - match socket.connect(address).await { - Ok(stream) => { - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - %address, - "connected proxy tcp server" - ); - return Ok((stream, address)); - } - Err(err) => { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = %err, - "failed to connect proxy tcp server" - ); - last_error = Some(anyhow!(err).context(format!("failed to connect {address}"))); - } - } - } - - Err(last_error.unwrap_or_else(|| { - anyhow!( - "no cached addresses available for {}:{}", - endpoint.host, - endpoint.port - ) - })) -} - -async fn connect_proxy_udp(endpoint: &ProxyServerEndpoint) -> Result<(UdpSocket, SocketAddr)> { - let mut last_error = None; - for address in endpoint.addresses.iter().copied() { - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - %address, - "dialing proxy udp server" - ); - let bind_addr = match address { - SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), - SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), - }; - let socket = match std::net::UdpSocket::bind(bind_addr) { - Ok(socket) => socket, - Err(err) => { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = %err, - "failed to bind proxy udp socket" - ); - last_error = - Some(anyhow!(err).context(format!("failed to bind udp socket for {address}"))); - continue; - } - }; - - if let Err(err) = bind_proxy_outbound_socket(socket.as_raw_fd(), address.ip()) { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = ?err, - "failed to bind proxy udp socket to physical interface" - ); - last_error = Some(err); - continue; - } - if let Err(err) = socket.set_nonblocking(true) { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = %err, - "failed to make proxy udp socket nonblocking" - ); - last_error = Some(anyhow!(err).context("failed to make proxy udp socket nonblocking")); - continue; - } - - match UdpSocket::from_std(socket) { - Ok(socket) => { - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - %address, - "connected proxy udp server" - ); - return Ok((socket, address)); - } - Err(err) => { - tracing::warn!( - server = %endpoint.host, - port = endpoint.port, - %address, - error = %err, - "failed to wrap proxy udp socket" - ); - last_error = Some(anyhow!(err).context("failed to wrap proxy udp socket")); - } - } - } - - Err(last_error.unwrap_or_else(|| { - anyhow!( - "no cached addresses available for {}:{}", - endpoint.host, - endpoint.port - ) - })) -} - -fn resolve_server_addresses(host: &str, port: u16) -> Result> { - let addresses: Vec<_> = (host, port) - .to_socket_addrs() - .with_context(|| format!("failed to resolve {host}:{port}"))? - .collect(); - if addresses.is_empty() { - bail!("no addresses resolved for {host}:{port}"); - } - if addresses - .iter() - .all(|address| is_fake_or_benchmark_ip(address.ip())) - { - tracing::warn!( - server = %host, - port, - resolved_addresses = ?addresses, - "system DNS returned only fake-IP proxy server addresses; trying direct DNS" - ); - let direct_addresses = resolve_server_addresses_direct(host, port)?; - tracing::info!( - server = %host, - port, - resolved_addresses = ?direct_addresses, - "direct DNS resolved proxy server addresses" - ); - return Ok(direct_addresses); - } - Ok(addresses) -} - -fn resolve_server_addresses_direct(host: &str, port: u16) -> Result> { - if let Ok(ip) = host.parse::() { - return Ok(vec![SocketAddr::new(ip, port)]); - } - - let mut dns_servers = Vec::new(); - for server in platform_proxy_dns_servers() - .into_iter() - .chain(PUBLIC_DNS_SERVERS.iter().map(|server| (*server).to_owned())) - { - if dns_servers.iter().any(|existing| existing == &server) { - continue; - } - let Ok(ip) = server.parse::() else { - continue; - }; - if is_usable_proxy_dns_ip(ip) { - dns_servers.push(server); - } - } - - let mut addresses = Vec::new(); - let mut last_error = None; - for dns_server in dns_servers { - let Ok(ip) = dns_server.parse::() else { - continue; - }; - let dns_addr = SocketAddr::new(ip, 53); - match direct_dns_lookup(host, dns_addr) { - Ok(resolved) => { - for ip in resolved { - if is_fake_or_benchmark_ip(ip) { - continue; - } - let address = SocketAddr::new(ip, port); - if !addresses.contains(&address) { - addresses.push(address); - } - } - if !addresses.is_empty() { - break; - } - } - Err(err) => { - tracing::debug!( - server = %host, - %dns_addr, - error = ?err, - "direct DNS lookup failed" - ); - last_error = Some(err); - } - } - } - - if addresses.is_empty() { - return Err(last_error.unwrap_or_else(|| { - anyhow!("direct DNS did not resolve any usable addresses for {host}:{port}") - })); - } - Ok(addresses) -} - -fn direct_dns_lookup(host: &str, dns_server: SocketAddr) -> Result> { - let mut addresses = Vec::new(); - let mut last_error = None; - for query_type in [1u16, 28u16] { - let mut query = build_dns_query(host, query_type)?; - let id = (OsRng.next_u32() & 0xffff) as u16; - query[0..2].copy_from_slice(&id.to_be_bytes()); - - let bind_addr = match dns_server { - SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), - SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), - }; - let socket = std::net::UdpSocket::bind(bind_addr) - .with_context(|| format!("failed to bind DNS socket for {dns_server}"))?; - bind_proxy_outbound_socket(socket.as_raw_fd(), dns_server.ip()).with_context(|| { - format!("failed to bind DNS socket to physical interface for {dns_server}") - })?; - socket - .set_read_timeout(Some(DIRECT_DNS_TIMEOUT)) - .context("failed to set DNS read timeout")?; - socket - .set_write_timeout(Some(DIRECT_DNS_TIMEOUT)) - .context("failed to set DNS write timeout")?; - if let Err(err) = socket.send_to(&query, dns_server) { - last_error = - Some(anyhow!(err).context(format!("failed to send DNS query to {dns_server}"))); - continue; - } - - let mut response = [0u8; 1500]; - match socket.recv_from(&mut response) { - Ok((len, _)) => match parse_dns_response(&response[..len], id) - .with_context(|| format!("failed to parse DNS response from {dns_server}")) - { - Ok(mut resolved) => addresses.append(&mut resolved), - Err(err) => last_error = Some(err), - }, - Err(err) => { - last_error = Some( - anyhow!(err) - .context(format!("failed to receive DNS response from {dns_server}")), - ); - } - } - } - if addresses.is_empty() { - return Err(last_error.unwrap_or_else(|| anyhow!("DNS lookup returned no answers"))); - } - Ok(addresses) -} - -fn direct_dns_https_ech_config_list(host: &str, dns_server: SocketAddr) -> Result>> { - let mut query = build_dns_query(host, DNS_TYPE_HTTPS)?; - let id = (OsRng.next_u32() & 0xffff) as u16; - query[0..2].copy_from_slice(&id.to_be_bytes()); - - let bind_addr = match dns_server { - SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), - SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), - }; - let socket = std::net::UdpSocket::bind(bind_addr) - .with_context(|| format!("failed to bind DNS socket for {dns_server}"))?; - bind_proxy_outbound_socket(socket.as_raw_fd(), dns_server.ip()).with_context(|| { - format!("failed to bind DNS socket to physical interface for {dns_server}") - })?; - socket - .set_read_timeout(Some(DIRECT_DNS_TIMEOUT)) - .context("failed to set DNS read timeout")?; - socket - .set_write_timeout(Some(DIRECT_DNS_TIMEOUT)) - .context("failed to set DNS write timeout")?; - socket - .send_to(&query, dns_server) - .with_context(|| format!("failed to send DNS HTTPS query to {dns_server}"))?; - - let mut response = [0u8; 1500]; - let (len, _) = socket - .recv_from(&mut response) - .with_context(|| format!("failed to receive DNS HTTPS response from {dns_server}"))?; - parse_dns_https_ech_config_list(&response[..len], id) - .with_context(|| format!("failed to parse DNS HTTPS response from {dns_server}")) -} - -pub(crate) fn build_dns_query(host: &str, query_type: u16) -> Result> { - let mut query = Vec::with_capacity(512); - query.extend_from_slice(&0u16.to_be_bytes()); - query.extend_from_slice(&0x0100u16.to_be_bytes()); - query.extend_from_slice(&1u16.to_be_bytes()); - query.extend_from_slice(&0u16.to_be_bytes()); - query.extend_from_slice(&0u16.to_be_bytes()); - query.extend_from_slice(&0u16.to_be_bytes()); - - for label in host.trim_end_matches('.').split('.') { - if label.is_empty() || label.len() > 63 { - bail!("invalid DNS label in {host}"); - } - query.push(label.len() as u8); - query.extend_from_slice(label.as_bytes()); - } - query.push(0); - query.extend_from_slice(&query_type.to_be_bytes()); - query.extend_from_slice(&1u16.to_be_bytes()); - Ok(query) -} - -pub(crate) fn parse_dns_response(response: &[u8], expected_id: u16) -> Result> { - if response.len() < 12 { - bail!("truncated DNS response"); - } - let id = u16::from_be_bytes([response[0], response[1]]); - if id != expected_id { - bail!("DNS response id mismatch"); - } - let flags = u16::from_be_bytes([response[2], response[3]]); - if flags & 0x8000 == 0 { - bail!("DNS response is not marked as a response"); - } - let rcode = flags & 0x000f; - if rcode != 0 { - bail!("DNS response error code {rcode}"); - } - - let question_count = u16::from_be_bytes([response[4], response[5]]) as usize; - let answer_count = u16::from_be_bytes([response[6], response[7]]) as usize; - let mut offset = 12; - for _ in 0..question_count { - offset = skip_dns_name(response, offset)?; - if response.len() < offset + 4 { - bail!("truncated DNS question"); - } - offset += 4; - } - - let mut addresses = Vec::new(); - for _ in 0..answer_count { - offset = skip_dns_name(response, offset)?; - if response.len() < offset + 10 { - bail!("truncated DNS answer"); - } - let record_type = u16::from_be_bytes([response[offset], response[offset + 1]]); - let record_class = u16::from_be_bytes([response[offset + 2], response[offset + 3]]); - let data_len = u16::from_be_bytes([response[offset + 8], response[offset + 9]]) as usize; - offset += 10; - if response.len() < offset + data_len { - bail!("truncated DNS answer data"); - } - if record_class == 1 && record_type == 1 && data_len == 4 { - addresses.push(IpAddr::V4(Ipv4Addr::new( - response[offset], - response[offset + 1], - response[offset + 2], - response[offset + 3], - ))); - } else if record_class == 1 && record_type == 28 && data_len == 16 { - let mut octets = [0u8; 16]; - octets.copy_from_slice(&response[offset..offset + 16]); - addresses.push(IpAddr::V6(Ipv6Addr::from(octets))); - } - offset += data_len; - } - Ok(addresses) -} - -fn parse_dns_https_ech_config_list(response: &[u8], expected_id: u16) -> Result>> { - if response.len() < 12 { - bail!("truncated DNS response"); - } - let id = u16::from_be_bytes([response[0], response[1]]); - if id != expected_id { - bail!("DNS response id mismatch"); - } - let flags = u16::from_be_bytes([response[2], response[3]]); - if flags & 0x8000 == 0 { - bail!("DNS response is not marked as a response"); - } - let rcode = flags & 0x000f; - if rcode != 0 { - bail!("DNS response error code {rcode}"); - } - - let question_count = u16::from_be_bytes([response[4], response[5]]) as usize; - let answer_count = u16::from_be_bytes([response[6], response[7]]) as usize; - let mut offset = 12; - for _ in 0..question_count { - offset = skip_dns_name(response, offset)?; - if response.len() < offset + 4 { - bail!("truncated DNS question"); - } - offset += 4; - } - - for _ in 0..answer_count { - offset = skip_dns_name(response, offset)?; - if response.len() < offset + 10 { - bail!("truncated DNS answer"); - } - let record_type = u16::from_be_bytes([response[offset], response[offset + 1]]); - let record_class = u16::from_be_bytes([response[offset + 2], response[offset + 3]]); - let data_len = u16::from_be_bytes([response[offset + 8], response[offset + 9]]) as usize; - offset += 10; - if response.len() < offset + data_len { - bail!("truncated DNS answer data"); - } - if record_class == 1 && record_type == DNS_TYPE_HTTPS { - if let Some(config_list) = parse_https_rr_ech_config_list(response, offset, data_len)? { - return Ok(Some(config_list)); - } - } - offset += data_len; - } - Ok(None) -} - -fn parse_https_rr_ech_config_list( - response: &[u8], - offset: usize, - data_len: usize, -) -> Result>> { - let end = offset - .checked_add(data_len) - .ok_or_else(|| anyhow!("DNS HTTPS record offset overflow"))?; - if response.len() < end || data_len < 3 { - bail!("truncated DNS HTTPS record"); - } - let (_target, mut cursor) = read_dns_name(response, offset + 2)?; - if cursor > end { - bail!("DNS HTTPS record target exceeded RDATA"); - } - - while cursor < end { - if response.len() < cursor + 4 || cursor + 4 > end { - bail!("truncated DNS HTTPS service parameter"); - } - let key = u16::from_be_bytes([response[cursor], response[cursor + 1]]); - let value_len = u16::from_be_bytes([response[cursor + 2], response[cursor + 3]]) as usize; - cursor += 4; - let value_end = cursor - .checked_add(value_len) - .ok_or_else(|| anyhow!("DNS HTTPS parameter offset overflow"))?; - if value_end > end { - bail!("truncated DNS HTTPS service parameter value"); - } - if key == HTTPS_SVC_PARAM_ECH { - return Ok(Some(response[cursor..value_end].to_vec())); - } - cursor = value_end; - } - Ok(None) -} - -async fn record_dns_mappings(cache: &DnsHostnameCache, response: &[u8]) { - let mappings = match dns_response_host_mappings(response) { - Ok(mappings) => mappings, - Err(_) => return, - }; - if mappings.is_empty() { - return; - } - - let mut guard = cache.write().await; - for (ip, hostname) in mappings { - tracing::debug!(%ip, %hostname, "cached proxy DNS hostname mapping"); - guard.insert_mapping(ip, hostname); - } -} - -fn dns_response_host_mappings(response: &[u8]) -> Result> { - if response.len() < 12 { - bail!("truncated DNS response"); - } - let flags = u16::from_be_bytes([response[2], response[3]]); - if flags & 0x8000 == 0 { - bail!("DNS packet is not a response"); - } - if flags & 0x000f != 0 { - bail!("DNS response returned an error"); - } - - let question_count = u16::from_be_bytes([response[4], response[5]]) as usize; - let answer_count = u16::from_be_bytes([response[6], response[7]]) as usize; - let mut offset = 12; - let mut question_names = Vec::new(); - for _ in 0..question_count { - let (name, next_offset) = read_dns_name(response, offset)?; - offset = next_offset; - if response.len() < offset + 4 { - bail!("truncated DNS question"); - } - if !name.is_empty() { - question_names.push(name); - } - offset += 4; - } - - let mut mappings = Vec::new(); - for _ in 0..answer_count { - let (answer_name, next_offset) = read_dns_name(response, offset)?; - offset = next_offset; - if response.len() < offset + 10 { - bail!("truncated DNS answer"); - } - let record_type = u16::from_be_bytes([response[offset], response[offset + 1]]); - let record_class = u16::from_be_bytes([response[offset + 2], response[offset + 3]]); - let data_len = u16::from_be_bytes([response[offset + 8], response[offset + 9]]) as usize; - offset += 10; - if response.len() < offset + data_len { - bail!("truncated DNS answer data"); - } - - let hostname = question_names - .first() - .cloned() - .filter(|name| !name.is_empty()) - .unwrap_or(answer_name); - if record_class == 1 && record_type == 1 && data_len == 4 { - mappings.push(( - IpAddr::V4(Ipv4Addr::new( - response[offset], - response[offset + 1], - response[offset + 2], - response[offset + 3], - )), - hostname, - )); - } else if record_class == 1 && record_type == 28 && data_len == 16 { - let mut octets = [0u8; 16]; - octets.copy_from_slice(&response[offset..offset + 16]); - mappings.push((IpAddr::V6(Ipv6Addr::from(octets)), hostname)); - } - offset += data_len; - } - Ok(mappings) -} - -fn read_dns_name(response: &[u8], offset: usize) -> Result<(String, usize)> { - let mut labels = Vec::new(); - let mut cursor = offset; - let mut next_offset = None; - let mut jumps = 0usize; - - loop { - if cursor >= response.len() { - bail!("truncated DNS name"); - } - let len = response[cursor]; - if len & 0xc0 == 0xc0 { - if response.len() < cursor + 2 { - bail!("truncated compressed DNS name"); - } - if next_offset.is_none() { - next_offset = Some(cursor + 2); - } - let pointer = (((len & 0x3f) as usize) << 8) | response[cursor + 1] as usize; - cursor = pointer; - jumps += 1; - if jumps > 16 { - bail!("too many compressed DNS name jumps"); - } - continue; - } - if len & 0xc0 != 0 { - bail!("unsupported DNS label encoding"); - } - cursor += 1; - if len == 0 { - return Ok((labels.join("."), next_offset.unwrap_or(cursor))); - } - let end = cursor - .checked_add(len as usize) - .ok_or_else(|| anyhow!("DNS name offset overflow"))?; - if end > response.len() { - bail!("truncated DNS label"); - } - let label = - std::str::from_utf8(&response[cursor..end]).context("DNS label is not valid UTF-8")?; - labels.push(label.to_owned()); - cursor = end; - } -} - -fn skip_dns_name(response: &[u8], mut offset: usize) -> Result { - loop { - if offset >= response.len() { - bail!("truncated DNS name"); - } - let len = response[offset]; - if len & 0xc0 == 0xc0 { - if response.len() < offset + 2 { - bail!("truncated compressed DNS name"); - } - return Ok(offset + 2); - } - if len & 0xc0 != 0 { - bail!("unsupported DNS label encoding"); - } - offset += 1; - if len == 0 { - return Ok(offset); - } - offset = offset - .checked_add(len as usize) - .ok_or_else(|| anyhow!("DNS name offset overflow"))?; - } -} - -fn is_fake_or_benchmark_ip(ip: IpAddr) -> bool { - match ip { - IpAddr::V4(ip) => { - let octets = ip.octets(); - octets[0] == 198 && matches!(octets[1], 18 | 19) - } - IpAddr::V6(_) => false, - } -} - -#[cfg(target_vendor = "apple")] -fn platform_proxy_dns_servers() -> Vec { - match std::fs::read_to_string("/etc/resolv.conf") { - Ok(contents) => { - let servers = parse_resolv_conf_dns_servers(&contents); - if !servers.is_empty() { - return servers; - } - } - Err(err) => { - tracing::warn!(error = %err, "failed to read /etc/resolv.conf"); - } - } - - match Command::new("scutil").arg("--dns").output() { - Ok(output) if output.status.success() => { - parse_apple_physical_dns_servers(&String::from_utf8_lossy(&output.stdout)) - } - Ok(output) => { - tracing::warn!( - status = ?output.status.code(), - stderr = %String::from_utf8_lossy(&output.stderr), - "failed to read macOS DNS configuration" - ); - Vec::new() - } - Err(err) => { - tracing::warn!(error = %err, "failed to run scutil --dns"); - Vec::new() - } - } -} - -#[cfg(not(target_vendor = "apple"))] -fn platform_proxy_dns_servers() -> Vec { - Vec::new() -} - -fn parse_apple_physical_dns_servers(output: &str) -> Vec { - let mut servers = Vec::new(); - let mut resolver_servers = Vec::::new(); - let mut resolver_interface = None::; - - let mut flush_resolver = - |resolver_servers: &mut Vec, resolver_interface: &mut Option| { - let is_tunnel_resolver = resolver_interface - .as_deref() - .is_some_and(should_skip_dns_resolver_interface); - if !is_tunnel_resolver { - for server in resolver_servers.drain(..) { - if !servers.contains(&server) { - servers.push(server); - } - } - } else { - resolver_servers.clear(); - } - *resolver_interface = None; - }; - - for line in output.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("resolver #") { - flush_resolver(&mut resolver_servers, &mut resolver_interface); - continue; - } - if trimmed.starts_with("nameserver[") { - if let Some((_, value)) = trimmed.split_once(':') { - let server = value.trim(); - if let Ok(ip) = server.parse::() { - if is_usable_proxy_dns_ip(ip) { - resolver_servers.push(server.to_owned()); - } - } - } - continue; - } - if trimmed.starts_with("if_index") { - if let (Some(start), Some(end)) = (trimmed.find('('), trimmed.find(')')) { - resolver_interface = Some(trimmed[start + 1..end].to_owned()); - } - } - } - flush_resolver(&mut resolver_servers, &mut resolver_interface); - - servers -} - -fn parse_resolv_conf_dns_servers(contents: &str) -> Vec { - let mut servers = Vec::new(); - for line in contents.lines() { - let line = line.split('#').next().unwrap_or("").trim(); - let mut fields = line.split_whitespace(); - if fields.next() != Some("nameserver") { - continue; - } - let Some(server) = fields.next() else { - continue; - }; - let Ok(ip) = server.parse::() else { - continue; - }; - if is_usable_proxy_dns_ip(ip) && !servers.iter().any(|existing| existing == server) { - servers.push(server.to_owned()); - } - } - servers -} - -fn should_skip_dns_resolver_interface(interface: &str) -> bool { - interface.starts_with("utun") - || interface.starts_with("lo") - || interface.starts_with("awdl") - || interface.starts_with("llw") -} - -fn is_usable_proxy_dns_ip(ip: IpAddr) -> bool { - match ip { - IpAddr::V4(ip) => { - !ip.is_unspecified() - && !ip.is_loopback() - && !ip.is_multicast() - && !is_fake_or_benchmark_ip(IpAddr::V4(ip)) - } - IpAddr::V6(ip) => !ip.is_unspecified() && !ip.is_loopback() && !ip.is_multicast(), - } -} - -#[cfg(target_vendor = "apple")] -fn bind_proxy_outbound_socket(fd: std::os::fd::RawFd, ip: IpAddr) -> Result<()> { - if !should_bind_proxy_outbound_socket(ip) { - return Ok(()); - } - let interface_index = default_physical_interface_index().ok_or_else(|| { - anyhow!("no non-tunnel default interface found for proxy outbound socket") - })?; - let interface_index = interface_index as libc::c_uint; - let (level, option) = match ip { - IpAddr::V4(_) => (libc::IPPROTO_IP, libc::IP_BOUND_IF), - IpAddr::V6(_) => (libc::IPPROTO_IPV6, libc::IPV6_BOUND_IF), - }; - let result = unsafe { - libc::setsockopt( - fd, - level, - option, - (&interface_index as *const libc::c_uint).cast::(), - std::mem::size_of_val(&interface_index) as libc::socklen_t, - ) - }; - if result != 0 { - return Err(std::io::Error::last_os_error()).with_context(|| { - format!("failed to bind proxy outbound socket to interface index {interface_index}") - }); - } - Ok(()) -} - -#[cfg(not(target_vendor = "apple"))] -fn bind_proxy_outbound_socket(_fd: std::os::fd::RawFd, _ip: IpAddr) -> Result<()> { - Ok(()) -} - -fn should_bind_proxy_outbound_socket(ip: IpAddr) -> bool { - !ip.is_loopback() -} - -#[cfg(target_vendor = "apple")] -fn default_physical_interface_index() -> Option { - #[derive(Clone)] - struct Candidate { - name: String, - index: u32, - score: i32, - } - - let mut addrs = std::ptr::null_mut(); - if unsafe { libc::getifaddrs(&mut addrs) } != 0 || addrs.is_null() { - return None; - } - - struct IfAddrs(*mut libc::ifaddrs); - impl Drop for IfAddrs { - fn drop(&mut self) { - unsafe { libc::freeifaddrs(self.0) }; - } - } - let _guard = IfAddrs(addrs); - - let mut candidates = Vec::::new(); - let mut current = addrs; - while !current.is_null() { - let ifa = unsafe { &*current }; - current = ifa.ifa_next; - if ifa.ifa_addr.is_null() || ifa.ifa_name.is_null() { - continue; - } - let flags = ifa.ifa_flags as libc::c_uint; - if flags & libc::IFF_UP as libc::c_uint == 0 - || flags & libc::IFF_RUNNING as libc::c_uint == 0 - || flags & libc::IFF_LOOPBACK as libc::c_uint != 0 - || flags & libc::IFF_POINTOPOINT as libc::c_uint != 0 - { - continue; - } - - let family = unsafe { (*ifa.ifa_addr).sa_family as libc::c_int }; - if family != libc::AF_INET && family != libc::AF_INET6 { - continue; - } - - let name = unsafe { CStr::from_ptr(ifa.ifa_name) } - .to_string_lossy() - .into_owned(); - if should_skip_outbound_interface(&name) { - continue; - } - let index = unsafe { libc::if_nametoindex(ifa.ifa_name) }; - if index == 0 { - continue; - } - - let mut score = if name.starts_with("en") { 100 } else { 10 }; - if family == libc::AF_INET { - score += 20; - } - if name == "en0" { - score += 10; - } - - candidates.push(Candidate { name, index, score }); - } - - candidates.sort_by_key(|candidate| (Reverse(candidate.score), candidate.name.clone())); - candidates.first().map(|candidate| { - tracing::debug!( - interface = %candidate.name, - index = candidate.index, - score = candidate.score, - "selected proxy outbound physical interface" - ); - candidate.index - }) -} - -#[cfg(target_vendor = "apple")] -fn should_skip_outbound_interface(name: &str) -> bool { - name.starts_with("lo") - || name.starts_with("utun") - || name.starts_with("awdl") - || name.starts_with("llw") - || name.starts_with("bridge") - || name.starts_with("gif") - || name.starts_with("stf") - || name.starts_with("p2p") -} - -fn excluded_routes_for_server(host: &str, port: u16) -> Result> { - let mut routes = Vec::new(); - for address in resolve_server_addresses(host, port) - .with_context(|| format!("failed to resolve proxy server {host}:{port}"))? - { - let route = host_route_for_ip(address.ip()); - if !routes.contains(&route) { - routes.push(route); - } - } - if routes.is_empty() { - bail!("no addresses resolved for proxy server {host}:{port}"); - } - Ok(routes) -} - -fn host_route_for_ip(ip: IpAddr) -> String { - match ip { - IpAddr::V4(ip) => format!("{ip}/32"), - IpAddr::V6(ip) => format!("{ip}/128"), - } -} diff --git a/burrow/src/proxy_runtime/shadowsocks_crypto.rs b/burrow/src/proxy_runtime/shadowsocks_crypto.rs deleted file mode 100644 index 773a37d..0000000 --- a/burrow/src/proxy_runtime/shadowsocks_crypto.rs +++ /dev/null @@ -1,2004 +0,0 @@ -struct BurrowDatagramSocket(UdpSocket); - -impl DatagramSocket for BurrowDatagramSocket { - fn local_addr(&self) -> std::io::Result { - self.0.local_addr() - } -} - -impl DatagramReceive for BurrowDatagramSocket { - fn poll_recv( - &self, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - self.0.poll_recv(cx, buf) - } - - fn poll_recv_from( - &self, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - self.0.poll_recv_from(cx, buf) - } - - fn poll_recv_ready(&self, cx: &mut TaskContext<'_>) -> Poll> { - self.0.poll_recv_ready(cx) - } -} - -impl DatagramSend for BurrowDatagramSocket { - fn poll_send(&self, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll> { - self.0.poll_send(cx, buf) - } - - fn poll_send_to( - &self, - cx: &mut TaskContext<'_>, - buf: &[u8], - target: SocketAddr, - ) -> Poll> { - self.0.poll_send_to(cx, buf, target) - } - - fn poll_send_ready(&self, cx: &mut TaskContext<'_>) -> Poll> { - self.0.poll_send_ready(cx) - } -} - -struct CustomSsStreamWriter { - writer: W, - cipher: CustomSsStreamCipher, -} - -impl CustomSsStreamWriter -where - W: AsyncWrite + Unpin, -{ - async fn new(mut writer: W, kind: CustomSsStreamKind, key: Arc<[u8]>) -> Result { - let mut salt = vec![0u8; kind.salt_len()]; - OsRng.fill_bytes(&mut salt); - let cipher = CustomSsStreamCipher::new(kind, &key, &salt)?; - writer - .write_all(&salt) - .await - .context("failed to write Shadowsocks request salt")?; - Ok(Self { writer, cipher }) - } - - async fn write_all_encrypted(&mut self, payload: &[u8]) -> Result<()> { - if payload.is_empty() { - return Ok(()); - } - let mut encrypted = payload.to_vec(); - self.cipher.apply_keystream(&mut encrypted); - self.writer - .write_all(&encrypted) - .await - .context("failed to write Shadowsocks encrypted stream payload")?; - self.writer.flush().await?; - Ok(()) - } - - async fn shutdown(&mut self) -> Result<()> { - self.writer.shutdown().await?; - Ok(()) - } -} - -struct CustomSsStreamReader { - reader: R, - kind: CustomSsStreamKind, - key: Arc<[u8]>, - cipher: Option, -} - -impl CustomSsStreamReader -where - R: AsyncRead + Unpin, -{ - fn new(reader: R, kind: CustomSsStreamKind, key: Arc<[u8]>) -> Self { - Self { - reader, - kind, - key, - cipher: None, - } - } - - async fn read_decrypted(&mut self, output: &mut [u8]) -> Result { - if self.cipher.is_none() { - let mut salt = vec![0u8; self.kind.salt_len()]; - if let Err(err) = self.reader.read_exact(&mut salt).await { - if err.kind() == std::io::ErrorKind::UnexpectedEof { - return Ok(0); - } - return Err(err).context("failed to read Shadowsocks response salt"); - } - self.cipher = Some(CustomSsStreamCipher::new(self.kind, &self.key, &salt)?); - } - let n = self - .reader - .read(output) - .await - .context("failed to read Shadowsocks encrypted stream payload")?; - if n == 0 { - return Ok(0); - } - self.cipher - .as_mut() - .expect("cipher initialized") - .apply_keystream(&mut output[..n]); - Ok(n) - } -} - -struct CustomSsStreamByteReader { - reader: CustomSsStreamReader, - buffer: Vec, - offset: usize, -} - -impl CustomSsStreamByteReader -where - R: AsyncRead + Unpin, -{ - fn new(reader: CustomSsStreamReader) -> Self { - Self { - reader, - buffer: Vec::new(), - offset: 0, - } - } - - async fn read_frame(&mut self) -> Result)>> { - let source = match self.read_uot_addr().await? { - Some(source) => source, - None => return Ok(None), - }; - let Some(payload_len) = self.read_u16().await? else { - bail!("truncated udp-over-tcp payload length"); - }; - let payload = self.read_exact_vec(payload_len as usize).await?; - Ok(Some((source, payload))) - } - - async fn read_uot_addr(&mut self) -> Result> { - let Some(atyp) = self.read_u8().await? else { - return Ok(None); - }; - match atyp { - 0x00 => { - let ip = self.read_exact_array::<4>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip)), port))) - } - 0x01 => { - let ip = self.read_exact_array::<16>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port))) - } - 0x02 => { - let len = self.read_u8_required().await? as usize; - let domain = self.read_exact_vec(len).await?; - let port = self.read_u16_required().await?; - let domain = String::from_utf8(domain).context("invalid udp-over-tcp domain")?; - Ok(Some(resolve_server(&domain, port)?)) - } - other => bail!("unsupported udp-over-tcp address type {other}"), - } - } - - async fn read_u8_required(&mut self) -> Result { - self.read_u8() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u16_required(&mut self) -> Result { - self.read_u16() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u8(&mut self) -> Result> { - if !self.ensure_available(1).await? { - return Ok(None); - } - let value = self.buffer[self.offset]; - self.offset += 1; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_u16(&mut self) -> Result> { - if !self.ensure_available(2).await? { - return Ok(None); - } - let value = u16::from_be_bytes([self.buffer[self.offset], self.buffer[self.offset + 1]]); - self.offset += 2; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_exact_array(&mut self) -> Result<[u8; N]> { - let bytes = self.read_exact_vec(N).await?; - Ok(bytes - .try_into() - .expect("read_exact_vec returned requested length")) - } - - async fn read_exact_vec(&mut self, len: usize) -> Result> { - if !self.ensure_available(len).await? { - bail!("truncated udp-over-tcp frame"); - } - let value = self.buffer[self.offset..self.offset + len].to_vec(); - self.offset += len; - self.compact_buffer(); - Ok(value) - } - - async fn ensure_available(&mut self, len: usize) -> Result { - while self.buffer.len().saturating_sub(self.offset) < len { - if self.offset > 0 { - self.buffer.drain(..self.offset); - self.offset = 0; - } - let mut chunk = vec![0u8; 16 * 1024]; - let read = self.reader.read_decrypted(&mut chunk).await?; - if read == 0 { - return Ok(false); - } - self.buffer.extend_from_slice(&chunk[..read]); - } - Ok(true) - } - - fn compact_buffer(&mut self) { - if self.offset == self.buffer.len() { - self.buffer.clear(); - self.offset = 0; - } else if self.offset > 4096 && self.offset * 2 > self.buffer.len() { - self.buffer.drain(..self.offset); - self.offset = 0; - } - } -} - -enum CustomSsStreamCipher { - Chacha20(ChaCha20Legacy), - XChacha20(XChaCha20), -} - -impl CustomSsStreamCipher { - fn new(kind: CustomSsStreamKind, key: &[u8], salt: &[u8]) -> Result { - match kind { - CustomSsStreamKind::Chacha20 => Ok(Self::Chacha20( - ChaCha20Legacy::new_from_slices(key, salt) - .map_err(|err| anyhow!("invalid chacha20 stream cipher material: {err}"))?, - )), - CustomSsStreamKind::XChacha20 => Ok(Self::XChacha20( - XChaCha20::new_from_slices(key, salt) - .map_err(|err| anyhow!("invalid xchacha20 stream cipher material: {err}"))?, - )), - } - } - - fn apply_keystream(&mut self, buf: &mut [u8]) { - match self { - Self::Chacha20(cipher) => cipher.apply_keystream(buf), - Self::XChacha20(cipher) => cipher.apply_keystream(buf), - } - } -} - -struct CustomSsAeadWriter { - writer: W, - cipher: CustomSsAeadCipher, - nonce: Vec, -} - -impl CustomSsAeadWriter -where - W: AsyncWrite + Unpin, -{ - async fn new(mut writer: W, kind: CustomSsAeadKind, master_key: Arc<[u8]>) -> Result { - let mut salt = vec![0u8; kind.salt_len()]; - OsRng.fill_bytes(&mut salt); - let cipher = custom_ss_subkey(kind, &master_key, &salt)?; - writer - .write_all(&salt) - .await - .context("failed to write Shadowsocks request salt")?; - Ok(Self { - writer, - cipher, - nonce: vec![0u8; kind.nonce_len()], - }) - } - - async fn write_chunk(&mut self, payload: &[u8]) -> Result<()> { - if payload.is_empty() { - return Ok(()); - } - let mut offset = 0; - while offset < payload.len() { - let end = (offset + 0x3fff).min(payload.len()); - let chunk = &payload[offset..end]; - - let mut encrypted_len = (chunk.len() as u16).to_be_bytes().to_vec(); - self.cipher - .seal_in_place(&mut self.nonce, &mut encrypted_len)?; - self.writer.write_all(&encrypted_len).await?; - - let mut encrypted_payload = chunk.to_vec(); - self.cipher - .seal_in_place(&mut self.nonce, &mut encrypted_payload)?; - self.writer.write_all(&encrypted_payload).await?; - offset = end; - } - self.writer.flush().await?; - Ok(()) - } - - async fn shutdown(&mut self) -> Result<()> { - self.writer.shutdown().await?; - Ok(()) - } -} - -struct CustomSsAeadReader { - reader: R, - kind: CustomSsAeadKind, - master_key: Arc<[u8]>, - cipher: Option, - nonce: Vec, -} - -impl CustomSsAeadReader -where - R: AsyncRead + Unpin, -{ - fn new(reader: R, kind: CustomSsAeadKind, master_key: Arc<[u8]>) -> Self { - Self { - reader, - kind, - master_key, - cipher: None, - nonce: vec![0u8; kind.nonce_len()], - } - } - - async fn read_chunk(&mut self, output: &mut Vec) -> Result { - if self.cipher.is_none() { - let mut salt = vec![0u8; self.kind.salt_len()]; - self.reader - .read_exact(&mut salt) - .await - .context("failed to read Shadowsocks response salt")?; - self.cipher = Some(custom_ss_subkey(self.kind, &self.master_key, &salt)?); - } - let cipher = self.cipher.as_ref().expect("cipher initialized"); - let tag_len = self.kind.tag_len(); - let mut encrypted_len = vec![0u8; 2 + tag_len]; - if let Err(err) = self.reader.read_exact(&mut encrypted_len).await { - if err.kind() == std::io::ErrorKind::UnexpectedEof { - return Ok(0); - } - return Err(err).context("failed to read Shadowsocks encrypted chunk length"); - } - let plaintext_len = cipher.open_in_place(&mut self.nonce, &mut encrypted_len)?; - let payload_len = u16::from_be_bytes([plaintext_len[0], plaintext_len[1]]) as usize; - if payload_len == 0 { - return Ok(0); - } - let mut encrypted_payload = vec![0u8; payload_len + tag_len]; - self.reader - .read_exact(&mut encrypted_payload) - .await - .context("failed to read Shadowsocks encrypted chunk")?; - let plaintext = cipher.open_in_place(&mut self.nonce, &mut encrypted_payload)?; - output.extend_from_slice(plaintext); - Ok(plaintext.len()) - } -} - -struct CustomSsAeadByteReader { - reader: CustomSsAeadReader, - buffer: Vec, - offset: usize, -} - -impl CustomSsAeadByteReader -where - R: AsyncRead + Unpin, -{ - fn new(reader: CustomSsAeadReader) -> Self { - Self { - reader, - buffer: Vec::new(), - offset: 0, - } - } - - async fn read_frame(&mut self) -> Result)>> { - let source = match self.read_uot_addr().await? { - Some(source) => source, - None => return Ok(None), - }; - let Some(payload_len) = self.read_u16().await? else { - bail!("truncated udp-over-tcp payload length"); - }; - let payload = self.read_exact_vec(payload_len as usize).await?; - Ok(Some((source, payload))) - } - - async fn read_uot_addr(&mut self) -> Result> { - let Some(atyp) = self.read_u8().await? else { - return Ok(None); - }; - match atyp { - 0x00 => { - let ip = self.read_exact_array::<4>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip)), port))) - } - 0x01 => { - let ip = self.read_exact_array::<16>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port))) - } - 0x02 => { - let len = self.read_u8_required().await? as usize; - let domain = self.read_exact_vec(len).await?; - let port = self.read_u16_required().await?; - let domain = String::from_utf8(domain).context("invalid udp-over-tcp domain")?; - Ok(Some(resolve_server(&domain, port)?)) - } - other => bail!("unsupported udp-over-tcp address type {other}"), - } - } - - async fn read_u8_required(&mut self) -> Result { - self.read_u8() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u16_required(&mut self) -> Result { - self.read_u16() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u8(&mut self) -> Result> { - if !self.ensure_available(1).await? { - return Ok(None); - } - let value = self.buffer[self.offset]; - self.offset += 1; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_u16(&mut self) -> Result> { - if !self.ensure_available(2).await? { - return Ok(None); - } - let value = u16::from_be_bytes([self.buffer[self.offset], self.buffer[self.offset + 1]]); - self.offset += 2; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_exact_array(&mut self) -> Result<[u8; N]> { - let bytes = self.read_exact_vec(N).await?; - Ok(bytes - .try_into() - .expect("read_exact_vec returned requested length")) - } - - async fn read_exact_vec(&mut self, len: usize) -> Result> { - if !self.ensure_available(len).await? { - bail!("truncated udp-over-tcp frame"); - } - let value = self.buffer[self.offset..self.offset + len].to_vec(); - self.offset += len; - self.compact_buffer(); - Ok(value) - } - - async fn ensure_available(&mut self, len: usize) -> Result { - while self.buffer.len().saturating_sub(self.offset) < len { - if self.offset > 0 { - self.buffer.drain(..self.offset); - self.offset = 0; - } - let mut chunk = Vec::new(); - let read = self.reader.read_chunk(&mut chunk).await?; - if read == 0 { - return Ok(false); - } - self.buffer.extend_from_slice(&chunk); - } - Ok(true) - } - - fn compact_buffer(&mut self) { - if self.offset == self.buffer.len() { - self.buffer.clear(); - self.offset = 0; - } else if self.offset > 4096 && self.offset * 2 > self.buffer.len() { - self.buffer.drain(..self.offset); - self.offset = 0; - } - } -} - -struct Aead2022AesCcmWriter { - writer: W, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - cipher: Option, -} - -impl Aead2022AesCcmWriter -where - W: AsyncWrite + Unpin, -{ - async fn new( - writer: W, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - ) -> Result { - Ok(Self { - writer, - kind, - user_key, - identity_keys, - cipher: None, - }) - } - - async fn write_request( - &mut self, - socks_target: &[u8], - padding_len: usize, - ) -> Result> { - if padding_len > u16::MAX as usize { - bail!("Shadowsocks 2022 TCP request padding is too large"); - } - let mut salt = vec![0u8; self.kind.key_len()]; - OsRng.fill_bytes(&mut salt); - - let mut header = salt.clone(); - header.extend_from_slice(&aead2022_eih_blocks( - self.kind, - &self.identity_keys, - &self.user_key, - &salt, - )?); - - let mut cipher = Aead2022TcpCipher::new(self.kind, &self.user_key, &salt)?; - let variable_len = socks_target - .len() - .checked_add(2) - .and_then(|len| len.checked_add(padding_len)) - .ok_or_else(|| anyhow!("Shadowsocks 2022 TCP request header is too large"))?; - if variable_len > u16::MAX as usize { - bail!("Shadowsocks 2022 TCP request header is too large"); - } - - let mut fixed = Vec::with_capacity(1 + 8 + 2); - fixed.push(AEAD2022_HEADER_TYPE_CLIENT); - fixed.extend_from_slice(&aead2022_now_timestamp()?.to_be_bytes()); - fixed.extend_from_slice(&(variable_len as u16).to_be_bytes()); - header.extend_from_slice(&cipher.seal_packet(&fixed)?); - - let mut variable = Vec::with_capacity(variable_len); - variable.extend_from_slice(socks_target); - variable.extend_from_slice(&(padding_len as u16).to_be_bytes()); - variable.resize(variable.len() + padding_len, 0); - header.extend_from_slice(&cipher.seal_packet(&variable)?); - - self.writer - .write_all(&header) - .await - .context("failed to write Shadowsocks 2022 request header")?; - self.writer - .flush() - .await - .context("failed to flush Shadowsocks 2022 request header")?; - self.cipher = Some(cipher); - Ok(Arc::from(salt)) - } - - async fn write_chunk(&mut self, payload: &[u8]) -> Result<()> { - if payload.is_empty() { - return Ok(()); - } - let cipher = self - .cipher - .as_mut() - .ok_or_else(|| anyhow!("Shadowsocks 2022 TCP request was not initialized"))?; - let mut offset = 0; - while offset < payload.len() { - let end = (offset + u16::MAX as usize).min(payload.len()); - let chunk = &payload[offset..end]; - - let encrypted_len = cipher.seal_packet(&(chunk.len() as u16).to_be_bytes())?; - self.writer.write_all(&encrypted_len).await?; - - let encrypted_payload = cipher.seal_packet(chunk)?; - self.writer.write_all(&encrypted_payload).await?; - offset = end; - } - self.writer.flush().await?; - Ok(()) - } - - async fn shutdown(&mut self) -> Result<()> { - self.writer.shutdown().await?; - Ok(()) - } -} - -struct Aead2022AesCcmReader { - reader: R, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - request_salt: Arc<[u8]>, - cipher: Option, - response_read: bool, -} - -impl Aead2022AesCcmReader -where - R: AsyncRead + Unpin, -{ - fn new( - reader: R, - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - request_salt: Arc<[u8]>, - ) -> Self { - Self { - reader, - kind, - user_key, - request_salt, - cipher: None, - response_read: false, - } - } - - async fn read_response(&mut self) -> Result<()> { - if self.response_read { - return Ok(()); - } - let mut salt = vec![0u8; self.kind.key_len()]; - self.reader - .read_exact(&mut salt) - .await - .context("failed to read Shadowsocks 2022 response salt")?; - let mut cipher = Aead2022TcpCipher::new(self.kind, &self.user_key, &salt)?; - - let fixed_len = 1 + 8 + self.kind.key_len() + 2; - let mut encrypted_fixed = vec![0u8; fixed_len + self.kind.tag_len()]; - self.reader - .read_exact(&mut encrypted_fixed) - .await - .context("failed to read Shadowsocks 2022 response fixed header")?; - let fixed = cipher.open_packet(&mut encrypted_fixed)?; - if fixed[0] != AEAD2022_HEADER_TYPE_SERVER { - bail!( - "unexpected Shadowsocks 2022 response header type {}", - fixed[0] - ); - } - aead2022_validate_timestamp(u64::from_be_bytes( - fixed[1..9].try_into().expect("fixed timestamp"), - ))?; - let response_request_salt = &fixed[9..9 + self.kind.key_len()]; - if !bool::from(response_request_salt.ct_eq(self.request_salt.as_ref())) { - bail!("Shadowsocks 2022 response request salt mismatch"); - } - let padding_len_offset = 9 + self.kind.key_len(); - let padding_len = - u16::from_be_bytes([fixed[padding_len_offset], fixed[padding_len_offset + 1]]) as usize; - if padding_len > 0 { - let mut encrypted_padding = vec![0u8; padding_len + self.kind.tag_len()]; - self.reader - .read_exact(&mut encrypted_padding) - .await - .context("failed to read Shadowsocks 2022 response padding")?; - let padding = cipher.open_packet(&mut encrypted_padding)?; - if padding.len() != padding_len { - bail!("Shadowsocks 2022 response padding length mismatch"); - } - } - - self.cipher = Some(cipher); - self.response_read = true; - Ok(()) - } - - async fn read_chunk(&mut self, output: &mut Vec) -> Result { - self.read_response().await?; - let cipher = self - .cipher - .as_mut() - .ok_or_else(|| anyhow!("Shadowsocks 2022 TCP response was not initialized"))?; - let mut encrypted_len = vec![0u8; 2 + self.kind.tag_len()]; - if let Err(err) = self.reader.read_exact(&mut encrypted_len).await { - if err.kind() == std::io::ErrorKind::UnexpectedEof { - return Ok(0); - } - return Err(err).context("failed to read Shadowsocks 2022 encrypted chunk length"); - } - let plaintext_len = cipher.open_packet(&mut encrypted_len)?; - let payload_len = u16::from_be_bytes([plaintext_len[0], plaintext_len[1]]) as usize; - if payload_len == 0 { - return Ok(0); - } - let mut encrypted_payload = vec![0u8; payload_len + self.kind.tag_len()]; - self.reader - .read_exact(&mut encrypted_payload) - .await - .context("failed to read Shadowsocks 2022 encrypted chunk")?; - let plaintext = cipher.open_packet(&mut encrypted_payload)?; - output.extend_from_slice(plaintext); - Ok(plaintext.len()) - } -} - -struct Aead2022AesCcmByteReader { - reader: Aead2022AesCcmReader, - buffer: Vec, - offset: usize, -} - -impl Aead2022AesCcmByteReader -where - R: AsyncRead + Unpin, -{ - fn new(reader: Aead2022AesCcmReader) -> Self { - Self { - reader, - buffer: Vec::new(), - offset: 0, - } - } - - async fn read_frame(&mut self) -> Result)>> { - let source = match self.read_uot_addr().await? { - Some(source) => source, - None => return Ok(None), - }; - let Some(payload_len) = self.read_u16().await? else { - bail!("truncated udp-over-tcp payload length"); - }; - let payload = self.read_exact_vec(payload_len as usize).await?; - Ok(Some((source, payload))) - } - - async fn read_uot_addr(&mut self) -> Result> { - let Some(atyp) = self.read_u8().await? else { - return Ok(None); - }; - match atyp { - 0x00 => { - let ip = self.read_exact_array::<4>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(ip)), port))) - } - 0x01 => { - let ip = self.read_exact_array::<16>().await?; - let port = self.read_u16_required().await?; - Ok(Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(ip)), port))) - } - 0x02 => { - let len = self.read_u8_required().await? as usize; - let domain = self.read_exact_vec(len).await?; - let port = self.read_u16_required().await?; - let domain = String::from_utf8(domain).context("invalid udp-over-tcp domain")?; - Ok(Some(resolve_server(&domain, port)?)) - } - other => bail!("unsupported udp-over-tcp address type {other}"), - } - } - - async fn read_u8_required(&mut self) -> Result { - self.read_u8() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u16_required(&mut self) -> Result { - self.read_u16() - .await? - .ok_or_else(|| anyhow!("truncated udp-over-tcp frame")) - } - - async fn read_u8(&mut self) -> Result> { - if !self.ensure_available(1).await? { - return Ok(None); - } - let value = self.buffer[self.offset]; - self.offset += 1; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_u16(&mut self) -> Result> { - if !self.ensure_available(2).await? { - return Ok(None); - } - let value = u16::from_be_bytes([self.buffer[self.offset], self.buffer[self.offset + 1]]); - self.offset += 2; - self.compact_buffer(); - Ok(Some(value)) - } - - async fn read_exact_array(&mut self) -> Result<[u8; N]> { - let bytes = self.read_exact_vec(N).await?; - Ok(bytes - .try_into() - .expect("read_exact_vec returned requested length")) - } - - async fn read_exact_vec(&mut self, len: usize) -> Result> { - if !self.ensure_available(len).await? { - bail!("truncated udp-over-tcp frame"); - } - let value = self.buffer[self.offset..self.offset + len].to_vec(); - self.offset += len; - self.compact_buffer(); - Ok(value) - } - - async fn ensure_available(&mut self, len: usize) -> Result { - while self.buffer.len().saturating_sub(self.offset) < len { - if self.offset > 0 { - self.buffer.drain(..self.offset); - self.offset = 0; - } - let mut chunk = Vec::new(); - let read = self.reader.read_chunk(&mut chunk).await?; - if read == 0 { - return Ok(false); - } - self.buffer.extend_from_slice(&chunk); - } - Ok(true) - } - - fn compact_buffer(&mut self) { - if self.offset == self.buffer.len() { - self.buffer.clear(); - self.offset = 0; - } else if self.offset > 4096 && self.offset * 2 > self.buffer.len() { - self.buffer.drain(..self.offset); - self.offset = 0; - } - } -} - -enum Aead2022AesCcmCipher { - Aes128(Aes128Ccm), - Aes256(Aes256Ccm), -} - -impl Aead2022AesCcmCipher { - fn new(kind: Aead2022AesCcmKind, key: &[u8]) -> Result { - match kind { - Aead2022AesCcmKind::Aes128 => Ok(Self::Aes128( - Aes128Ccm::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-128-CCM key"))?, - )), - Aead2022AesCcmKind::Aes256 => Ok(Self::Aes256( - Aes256Ccm::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-256-CCM key"))?, - )), - } - } - - fn seal_in_place(&self, nonce: &[u8], buf: &mut Vec) -> Result<()> { - let tag = match self { - Self::Aes128(cipher) => cipher - .encrypt_in_place_detached(GenericArray::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("Shadowsocks 2022 AES-128-CCM seal failed"))? - .as_slice() - .to_vec(), - Self::Aes256(cipher) => cipher - .encrypt_in_place_detached(GenericArray::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("Shadowsocks 2022 AES-256-CCM seal failed"))? - .as_slice() - .to_vec(), - }; - buf.extend_from_slice(&tag); - Ok(()) - } - - fn open_in_place<'a>(&self, nonce: &[u8], buf: &'a mut [u8]) -> Result<&'a [u8]> { - if buf.len() < 16 { - bail!("Shadowsocks 2022 AEAD packet missing tag"); - } - let (ciphertext, tag) = buf.split_at_mut(buf.len() - 16); - match self { - Self::Aes128(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks 2022 AES-128-CCM open failed"))?, - Self::Aes256(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks 2022 AES-256-CCM open failed"))?, - }; - Ok(ciphertext) - } -} - -struct Aead2022TcpCipher { - cipher: Aead2022AesCcmCipher, - nonce: Vec, -} - -impl Aead2022TcpCipher { - fn new(kind: Aead2022AesCcmKind, user_key: &[u8], salt: &[u8]) -> Result { - Ok(Self { - cipher: Aead2022AesCcmCipher::new(kind, &aead2022_session_key(kind, user_key, salt)?)?, - nonce: vec![0u8; kind.nonce_len()], - }) - } - - fn seal_packet(&mut self, payload: &[u8]) -> Result> { - let mut packet = payload.to_vec(); - self.cipher.seal_in_place(&self.nonce, &mut packet)?; - increment_nonce(&mut self.nonce); - Ok(packet) - } - - fn open_packet<'a>(&mut self, packet: &'a mut [u8]) -> Result<&'a [u8]> { - let plaintext = self.cipher.open_in_place(&self.nonce, packet)?; - increment_nonce(&mut self.nonce); - Ok(plaintext) - } -} - -struct Aead2022AesCcmUdpSession { - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - client_session_id: u64, - packet_id: u64, - client_cipher: Aead2022AesCcmCipher, -} - -impl Aead2022AesCcmUdpSession { - fn new( - kind: Aead2022AesCcmKind, - user_key: Arc<[u8]>, - identity_keys: Arc<[Arc<[u8]>]>, - ) -> Result { - let client_session_id = OsRng.next_u64(); - let client_session_id_bytes = client_session_id.to_be_bytes(); - let client_cipher = Aead2022AesCcmCipher::new( - kind, - &aead2022_session_key(kind, &user_key, &client_session_id_bytes)?, - )?; - Ok(Self { - kind, - user_key, - identity_keys, - client_session_id, - packet_id: 0, - client_cipher, - }) - } - - fn encrypt_client_packet( - &mut self, - destination: SocketAddr, - payload: &[u8], - ) -> Result> { - let packet_id = self.packet_id; - self.packet_id = self.packet_id.wrapping_add(1); - - let padding_len = aead2022_udp_padding_len(destination, payload); - let mut packet = Vec::with_capacity( - 16 + self.identity_keys.len() * 16 + 1 + 8 + 2 + padding_len + 19 + payload.len() + 16, - ); - packet.extend_from_slice(&self.client_session_id.to_be_bytes()); - packet.extend_from_slice(&packet_id.to_be_bytes()); - packet.extend_from_slice(&aead2022_udp_eih_blocks( - self.kind, - &self.identity_keys, - &self.user_key, - &packet[..16], - )?); - let data_index = packet.len(); - packet.push(AEAD2022_HEADER_TYPE_CLIENT); - packet.extend_from_slice(&aead2022_now_timestamp()?.to_be_bytes()); - packet.extend_from_slice(&(padding_len as u16).to_be_bytes()); - packet.resize(packet.len() + padding_len, 0); - packet.extend_from_slice(&socks_addr(destination)?); - packet.extend_from_slice(payload); - - let nonce: [u8; 12] = packet[4..16].try_into().expect("fixed packet nonce"); - let mut encrypted_body = packet[data_index..].to_vec(); - self.client_cipher - .seal_in_place(&nonce, &mut encrypted_body)?; - packet.truncate(data_index); - packet.extend_from_slice(&encrypted_body); - aead2022_aes_encrypt_block( - self.kind, - aead2022_udp_header_key(&self.identity_keys, &self.user_key), - &mut packet[..16], - )?; - Ok(packet) - } - - fn decrypt_server_packet(&mut self, packet: &[u8]) -> Result<(SocketAddr, Vec)> { - if packet.len() < 16 + 1 + 8 + 8 + 2 + self.kind.tag_len() { - bail!("Shadowsocks 2022 UDP packet is too short"); - } - let mut buf = packet.to_vec(); - aead2022_aes_decrypt_block(self.kind, &self.user_key, &mut buf[..16])?; - let session_id = u64::from_be_bytes(buf[..8].try_into().expect("fixed session id")); - let session_id_bytes = session_id.to_be_bytes(); - let cipher = Aead2022AesCcmCipher::new( - self.kind, - &aead2022_session_key(self.kind, &self.user_key, &session_id_bytes)?, - )?; - let nonce: [u8; 12] = buf[4..16].try_into().expect("fixed packet nonce"); - let plaintext = cipher.open_in_place(&nonce, &mut buf[16..])?; - if plaintext.len() < 1 + 8 + 8 + 2 { - bail!("Shadowsocks 2022 UDP response is truncated"); - } - if plaintext[0] != AEAD2022_HEADER_TYPE_SERVER { - bail!( - "unexpected Shadowsocks 2022 UDP response header type {}", - plaintext[0] - ); - } - aead2022_validate_timestamp(u64::from_be_bytes( - plaintext[1..9].try_into().expect("fixed timestamp"), - ))?; - let client_session_id = u64::from_be_bytes( - plaintext[9..17] - .try_into() - .expect("fixed client session id"), - ); - if client_session_id != self.client_session_id { - bail!("Shadowsocks 2022 UDP response client session id mismatch"); - } - let padding_len = u16::from_be_bytes([plaintext[17], plaintext[18]]) as usize; - let address_offset = 19usize - .checked_add(padding_len) - .ok_or_else(|| anyhow!("Shadowsocks 2022 UDP response padding is too large"))?; - if plaintext.len() < address_offset { - bail!("Shadowsocks 2022 UDP response padding is truncated"); - } - let (source, used) = parse_socks_addr(&plaintext[address_offset..])?; - Ok((source, plaintext[address_offset + used..].to_vec())) - } -} - -enum CustomSsAeadCipher { - Aes192Gcm(Aes192Gcm), - Aes192Ccm(Aes192Ccm), - Chacha8IetfPoly1305(ChaCha8Poly1305), - XChacha8IetfPoly1305(XChaCha8Poly1305), - Rabbit128Poly1305([u8; 16]), - Aegis128L([u8; 16]), - Aegis256([u8; 32]), - Aez384([u8; 48]), - DeoxysII256128(DeoxysII256), - Ascon128([u8; 16]), - Ascon128A([u8; 16]), - Lea128Gcm(Lea128), - Lea192Gcm(Lea192), - Lea256Gcm(Lea256), -} - -impl CustomSsAeadCipher { - fn new(kind: CustomSsAeadKind, key: &[u8]) -> Result { - match kind { - CustomSsAeadKind::Aes192Gcm => Ok(Self::Aes192Gcm( - Aes192Gcm::new_from_slice(key).map_err(|_| anyhow!("invalid AES-192-GCM key"))?, - )), - CustomSsAeadKind::Aes192Ccm => Ok(Self::Aes192Ccm( - Aes192Ccm::new_from_slice(key).map_err(|_| anyhow!("invalid AES-192-CCM key"))?, - )), - CustomSsAeadKind::Chacha8IetfPoly1305 => Ok(Self::Chacha8IetfPoly1305( - ChaCha8Poly1305::new_from_slice(key) - .map_err(|_| anyhow!("invalid ChaCha8-Poly1305 key"))?, - )), - CustomSsAeadKind::XChacha8IetfPoly1305 => Ok(Self::XChacha8IetfPoly1305( - XChaCha8Poly1305::new_from_slice(key) - .map_err(|_| anyhow!("invalid XChaCha8-Poly1305 key"))?, - )), - CustomSsAeadKind::Rabbit128Poly1305 => Ok(Self::Rabbit128Poly1305( - key.try_into() - .map_err(|_| anyhow!("invalid Rabbit128-Poly1305 key"))?, - )), - CustomSsAeadKind::Aegis128L => Ok(Self::Aegis128L( - key.try_into() - .map_err(|_| anyhow!("invalid AEGIS-128L key"))?, - )), - CustomSsAeadKind::Aegis256 => Ok(Self::Aegis256( - key.try_into() - .map_err(|_| anyhow!("invalid AEGIS-256 key"))?, - )), - CustomSsAeadKind::Aez384 => Ok(Self::Aez384( - key.try_into().map_err(|_| anyhow!("invalid AEZ-384 key"))?, - )), - CustomSsAeadKind::DeoxysII256128 => Ok(Self::DeoxysII256128( - DeoxysII256::new_from_slice(key) - .map_err(|_| anyhow!("invalid Deoxys-II-256-128 key"))?, - )), - CustomSsAeadKind::Ascon128 => Ok(Self::Ascon128( - key.try_into() - .map_err(|_| anyhow!("invalid Ascon128 key"))?, - )), - CustomSsAeadKind::Ascon128A => Ok(Self::Ascon128A( - key.try_into() - .map_err(|_| anyhow!("invalid Ascon128a key"))?, - )), - CustomSsAeadKind::Lea128Gcm => Ok(Self::Lea128Gcm(Lea128::new( - LeaGenericArray::from_slice(key), - ))), - CustomSsAeadKind::Lea192Gcm => Ok(Self::Lea192Gcm(Lea192::new( - LeaGenericArray::from_slice(key), - ))), - CustomSsAeadKind::Lea256Gcm => Ok(Self::Lea256Gcm(Lea256::new( - LeaGenericArray::from_slice(key), - ))), - } - } - - fn seal_in_place(&self, nonce: &mut [u8], buf: &mut Vec) -> Result<()> { - let tag = match self { - Self::Aes192Gcm(cipher) => cipher - .encrypt_in_place_detached(GenericArray::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("AES-192-GCM Shadowsocks seal failed"))? - .as_slice() - .to_vec(), - Self::Aes192Ccm(cipher) => cipher - .encrypt_in_place_detached(GenericArray::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("AES-192-CCM Shadowsocks seal failed"))? - .as_slice() - .to_vec(), - Self::Chacha8IetfPoly1305(cipher) => cipher - .encrypt_in_place_detached(GenericArray::::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("ChaCha8-Poly1305 Shadowsocks seal failed"))? - .as_slice() - .to_vec(), - Self::XChacha8IetfPoly1305(cipher) => cipher - .encrypt_in_place_detached(GenericArray::::from_slice(nonce), &[], buf) - .map_err(|_| anyhow!("XChaCha8-Poly1305 Shadowsocks seal failed"))? - .as_slice() - .to_vec(), - Self::Rabbit128Poly1305(key) => { - let nonce: [u8; 8] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Rabbit128-Poly1305 nonce"))?; - rabbit_poly1305_seal_in_place(key, &nonce, buf)? - } - Self::Aegis128L(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-128L nonce"))?; - Aegis128L::<16>::new(key, &nonce) - .encrypt_in_place(buf, &[]) - .to_vec() - } - Self::Aegis256(key) => { - let nonce: [u8; 32] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-256 nonce"))?; - Aegis256::<16>::new(key, &nonce) - .encrypt_in_place(buf, &[]) - .to_vec() - } - Self::Aez384(key) => { - let aead_nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEZ-384 nonce"))?; - let cipher = zears::Aez::new(key); - cipher.encrypt_vec(&aead_nonce, &[], 16, buf); - increment_nonce(nonce); - return Ok(()); - } - Self::DeoxysII256128(cipher) => cipher - .encrypt_inout_detached( - &DeoxysArray::try_from(&nonce[..]) - .map_err(|_| anyhow!("invalid Deoxys-II-256-128 nonce"))?, - &[], - buf.as_mut_slice().into(), - ) - .map_err(|_| anyhow!("Deoxys-II-256-128 Shadowsocks seal failed"))? - .as_slice() - .to_vec(), - Self::Ascon128(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Ascon128 nonce"))?; - ascon_seal_in_place(key, &nonce, AsconMode::Ascon128, buf) - } - Self::Ascon128A(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Ascon128a nonce"))?; - ascon_seal_in_place(key, &nonce, AsconMode::Ascon128A, buf) - } - Self::Lea128Gcm(_) | Self::Lea192Gcm(_) | Self::Lea256Gcm(_) => { - let nonce: [u8; 12] = nonce - .try_into() - .map_err(|_| anyhow!("invalid LEA-GCM nonce"))?; - lea_gcm_seal_in_place(self, &nonce, buf) - } - }; - buf.extend_from_slice(&tag); - increment_nonce(nonce); - Ok(()) - } - - fn open_in_place<'a>(&self, nonce: &mut [u8], buf: &'a mut [u8]) -> Result<&'a [u8]> { - if buf.len() < 16 { - bail!("Shadowsocks AEAD packet missing tag"); - } - let (ciphertext, tag) = buf.split_at_mut(buf.len() - 16); - match self { - Self::Aes192Gcm(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))?, - Self::Aes192Ccm(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))?, - Self::Chacha8IetfPoly1305(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))?, - Self::XChacha8IetfPoly1305(cipher) => cipher - .decrypt_in_place_detached( - GenericArray::::from_slice(nonce), - &[], - ciphertext, - GenericArray::from_slice(tag), - ) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))?, - Self::Rabbit128Poly1305(key) => { - let nonce: [u8; 8] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Rabbit128-Poly1305 nonce"))?; - rabbit_poly1305_open_in_place(key, &nonce, ciphertext, tag)? - } - Self::Aegis128L(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-128L nonce"))?; - let tag: [u8; 16] = tag - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-128L tag"))?; - Aegis128L::<16>::new(key, &nonce) - .decrypt_in_place(ciphertext, &tag, &[]) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))? - } - Self::Aegis256(key) => { - let nonce: [u8; 32] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-256 nonce"))?; - let tag: [u8; 16] = tag - .try_into() - .map_err(|_| anyhow!("invalid AEGIS-256 tag"))?; - Aegis256::<16>::new(key, &nonce) - .decrypt_in_place(ciphertext, &tag, &[]) - .map_err(|_| anyhow!("Shadowsocks AEAD open failed"))? - } - Self::Aez384(key) => { - let aead_nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid AEZ-384 nonce"))?; - let cipher = zears::Aez::new(key); - let mut encrypted = Vec::with_capacity(ciphertext.len() + tag.len()); - encrypted.extend_from_slice(ciphertext); - encrypted.extend_from_slice(tag); - let plaintext = cipher - .decrypt(&aead_nonce, &[], 16, &encrypted) - .ok_or_else(|| anyhow!("Shadowsocks AEZ-384 open failed"))?; - ciphertext[..plaintext.len()].copy_from_slice(&plaintext); - increment_nonce(nonce); - return Ok(&ciphertext[..plaintext.len()]); - } - Self::DeoxysII256128(cipher) => cipher - .decrypt_inout_detached( - &DeoxysArray::try_from(&nonce[..]) - .map_err(|_| anyhow!("invalid Deoxys-II-256-128 nonce"))?, - &[], - ciphertext.into(), - &DeoxysArray::try_from(&tag[..]) - .map_err(|_| anyhow!("invalid Deoxys-II-256-128 tag"))?, - ) - .map_err(|_| anyhow!("Shadowsocks Deoxys-II-256-128 open failed"))?, - Self::Ascon128(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Ascon128 nonce"))?; - ascon_open_in_place(key, &nonce, AsconMode::Ascon128, ciphertext, tag)? - } - Self::Ascon128A(key) => { - let nonce: [u8; 16] = nonce - .try_into() - .map_err(|_| anyhow!("invalid Ascon128a nonce"))?; - ascon_open_in_place(key, &nonce, AsconMode::Ascon128A, ciphertext, tag)? - } - Self::Lea128Gcm(_) | Self::Lea192Gcm(_) | Self::Lea256Gcm(_) => { - let nonce: [u8; 12] = nonce - .try_into() - .map_err(|_| anyhow!("invalid LEA-GCM nonce"))?; - lea_gcm_open_in_place(self, &nonce, ciphertext, tag)? - } - }; - increment_nonce(nonce); - Ok(ciphertext) - } -} - -fn lea_gcm_seal_in_place( - cipher: &CustomSsAeadCipher, - nonce: &[u8; 12], - plaintext_in_ciphertext_out: &mut [u8], -) -> Vec { - lea_gcm_apply_ctr(cipher, nonce, plaintext_in_ciphertext_out); - lea_gcm_tag(cipher, nonce, plaintext_in_ciphertext_out) -} - -fn lea_gcm_open_in_place( - cipher: &CustomSsAeadCipher, - nonce: &[u8; 12], - ciphertext: &mut [u8], - tag: &[u8], -) -> Result<()> { - let expected = lea_gcm_tag(cipher, nonce, ciphertext); - if !bool::from(expected.as_slice().ct_eq(tag)) { - bail!("Shadowsocks LEA-GCM tag verification failed"); - } - lea_gcm_apply_ctr(cipher, nonce, ciphertext); - Ok(()) -} - -fn lea_gcm_apply_ctr(cipher: &CustomSsAeadCipher, nonce: &[u8; 12], buf: &mut [u8]) { - let mut counter = [0u8; 16]; - counter[..12].copy_from_slice(nonce); - counter[15] = 1; - lea_gcm_increment_counter(&mut counter); - - for chunk in buf.chunks_mut(16) { - let mut key_stream = counter; - lea_encrypt_block(cipher, &mut key_stream); - for (byte, key_stream_byte) in chunk.iter_mut().zip(key_stream) { - *byte ^= key_stream_byte; - } - lea_gcm_increment_counter(&mut counter); - } -} - -fn lea_gcm_tag(cipher: &CustomSsAeadCipher, nonce: &[u8; 12], ciphertext: &[u8]) -> Vec { - let mut h = [0u8; 16]; - lea_encrypt_block(cipher, &mut h); - - let mut tag_mask = [0u8; 16]; - tag_mask[..12].copy_from_slice(nonce); - tag_mask[15] = 1; - lea_encrypt_block(cipher, &mut tag_mask); - - let auth = ghash(&h, &[], ciphertext); - tag_mask - .iter() - .zip(auth) - .map(|(left, right)| left ^ right) - .collect() -} - -fn lea_encrypt_block(cipher: &CustomSsAeadCipher, block: &mut [u8; 16]) { - let block = LeaGenericArray::from_mut_slice(block); - match cipher { - CustomSsAeadCipher::Lea128Gcm(cipher) => cipher.encrypt_block(block), - CustomSsAeadCipher::Lea192Gcm(cipher) => cipher.encrypt_block(block), - CustomSsAeadCipher::Lea256Gcm(cipher) => cipher.encrypt_block(block), - _ => unreachable!("LEA-GCM block encryption called with non-LEA cipher"), - } -} - -fn lea_gcm_increment_counter(counter: &mut [u8; 16]) { - let value = - u32::from_be_bytes(counter[12..16].try_into().expect("fixed GCM counter")).wrapping_add(1); - counter[12..16].copy_from_slice(&value.to_be_bytes()); -} - -fn ghash(h: &[u8; 16], associated_data: &[u8], ciphertext: &[u8]) -> [u8; 16] { - let h = u128::from_be_bytes(*h); - let mut y = 0u128; - ghash_update(&mut y, h, associated_data); - ghash_update(&mut y, h, ciphertext); - let length_block = - ((associated_data.len() as u128) * 8) << 64 | ((ciphertext.len() as u128) * 8); - y = ghash_mul(y ^ length_block, h); - y.to_be_bytes() -} - -fn ghash_update(y: &mut u128, h: u128, mut input: &[u8]) { - while input.len() >= 16 { - *y = ghash_mul( - *y ^ u128::from_be_bytes(input[..16].try_into().expect("full GHASH block")), - h, - ); - input = &input[16..]; - } - if !input.is_empty() { - let mut block = [0u8; 16]; - block[..input.len()].copy_from_slice(input); - *y = ghash_mul(*y ^ u128::from_be_bytes(block), h); - } -} - -fn ghash_mul(mut x: u128, mut y: u128) -> u128 { - let reduction = 0xe1000000000000000000000000000000u128; - let mut z = 0u128; - for _ in 0..128 { - if x & (1u128 << 127) != 0 { - z ^= y; - } - let carry = y & 1 != 0; - y >>= 1; - if carry { - y ^= reduction; - } - x <<= 1; - } - z -} - -#[derive(Debug, Clone, Copy)] -enum AsconMode { - Ascon128, - Ascon128A, -} - -impl AsconMode { - fn block_size(self) -> usize { - match self { - Self::Ascon128 => 8, - Self::Ascon128A => 16, - } - } - - fn perm_b(self) -> usize { - match self { - Self::Ascon128 => 6, - Self::Ascon128A => 8, - } - } -} - -fn ascon_seal_in_place( - key: &[u8; 16], - nonce: &[u8; 16], - mode: AsconMode, - plaintext_in_ciphertext_out: &mut [u8], -) -> Vec { - let mut state = ascon_initialize(key, nonce, mode); - ascon_assoc_data(&mut state, mode, &[]); - ascon_proc_text(&mut state, mode, plaintext_in_ciphertext_out, true); - ascon_finalize(&mut state, mode, key) -} - -fn ascon_open_in_place( - key: &[u8; 16], - nonce: &[u8; 16], - mode: AsconMode, - ciphertext: &mut [u8], - tag: &[u8], -) -> Result<()> { - let mut state = ascon_initialize(key, nonce, mode); - ascon_assoc_data(&mut state, mode, &[]); - ascon_proc_text(&mut state, mode, ciphertext, false); - let expected = ascon_finalize(&mut state, mode, key); - if !bool::from(expected.as_slice().ct_eq(tag)) { - bail!("Shadowsocks Ascon tag verification failed"); - } - Ok(()) -} - -fn ascon_initialize(key: &[u8; 16], nonce: &[u8; 16], mode: AsconMode) -> [u64; 5] { - let k1 = u64::from_be_bytes(key[..8].try_into().expect("fixed key half")); - let k2 = u64::from_be_bytes(key[8..].try_into().expect("fixed key half")); - let block_bits = (mode.block_size() as u64) * 8; - let perm_b = mode.perm_b() as u64; - let n1 = u64::from_be_bytes(nonce[..8].try_into().expect("fixed nonce half")); - let n2 = u64::from_be_bytes(nonce[8..].try_into().expect("fixed nonce half")); - let mut state = [ - (128u64 << 56) | (block_bits << 48) | (12u64 << 40) | (perm_b << 32), - k1, - k2, - n1, - n2, - ]; - ascon_perm(12, &mut state); - state[3] ^= k1; - state[4] ^= k2; - state -} - -fn ascon_assoc_data(state: &mut [u64; 5], mode: AsconMode, mut associated_data: &[u8]) { - let block_size = mode.block_size(); - let perm_b = mode.perm_b(); - if !associated_data.is_empty() { - while associated_data.len() >= block_size { - for index in 0..(block_size / 8) { - let start = index * 8; - state[index] ^= u64::from_be_bytes( - associated_data[start..start + 8] - .try_into() - .expect("full Ascon AD block"), - ); - } - ascon_perm(perm_b, state); - associated_data = &associated_data[block_size..]; - } - for (index, byte) in associated_data.iter().enumerate() { - state[index / 8] ^= (*byte as u64) << (56 - 8 * (index % 8)); - } - state[associated_data.len() / 8] ^= 0x80u64 << (56 - 8 * (associated_data.len() % 8)); - ascon_perm(perm_b, state); - } - state[4] ^= 0x01; -} - -fn ascon_proc_text(state: &mut [u64; 5], mode: AsconMode, buf: &mut [u8], encrypt: bool) { - let block_size = mode.block_size(); - let perm_b = mode.perm_b(); - let mut offset = 0; - while buf.len() - offset >= block_size { - for index in 0..(block_size / 8) { - let start = offset + index * 8; - let input = u64::from_be_bytes(buf[start..start + 8].try_into().expect("full block")); - let output = state[index] ^ input; - buf[start..start + 8].copy_from_slice(&output.to_be_bytes()); - state[index] = if encrypt { output } else { input }; - } - ascon_perm(perm_b, state); - offset += block_size; - } - - let remaining = buf.len() - offset; - for index in 0..remaining { - let state_index = index / 8; - let shift = 56 - 8 * (index % 8); - let state_byte = ((state[state_index] >> shift) & 0xff) as u8; - let input = buf[offset + index]; - let output = state_byte ^ input; - buf[offset + index] = output; - let state_value = if encrypt { output } else { input }; - state[state_index] = - (state[state_index] & !(0xffu64 << shift)) | ((state_value as u64) << shift); - } - state[remaining / 8] ^= 0x80u64 << (56 - 8 * (remaining % 8)); -} - -fn ascon_finalize(state: &mut [u64; 5], mode: AsconMode, key: &[u8; 16]) -> Vec { - let k1 = u64::from_be_bytes(key[..8].try_into().expect("fixed key half")); - let k2 = u64::from_be_bytes(key[8..].try_into().expect("fixed key half")); - let block_words = mode.block_size() / 8; - state[block_words] ^= k1; - state[block_words + 1] ^= k2; - ascon_perm(12, state); - - let mut tag = Vec::with_capacity(16); - tag.extend_from_slice(&(state[3] ^ k1).to_be_bytes()); - tag.extend_from_slice(&(state[4] ^ k2).to_be_bytes()); - tag -} - -fn ascon_perm(rounds: usize, state: &mut [u64; 5]) { - let [mut x0, mut x1, mut x2, mut x3, mut x4] = *state; - for round in (12 - rounds)..12 { - x2 ^= (((0xf - round) << 4) | round) as u64; - - x0 ^= x4; - x4 ^= x3; - x2 ^= x1; - let t0 = x0 & !x4; - let mut t1 = x2 & !x1; - x0 ^= t1; - t1 = x4 & !x3; - x2 ^= t1; - t1 = x1 & !x0; - x4 ^= t1; - t1 = x3 & !x2; - x1 ^= t1; - x3 ^= t0; - x1 ^= x0; - x3 ^= x2; - x0 ^= x4; - x2 = !x2; - - x0 ^= x0.rotate_right(19) ^ x0.rotate_right(28); - x1 ^= x1.rotate_right(61) ^ x1.rotate_right(39); - x2 ^= x2.rotate_right(1) ^ x2.rotate_right(6); - x3 ^= x3.rotate_right(10) ^ x3.rotate_right(17); - x4 ^= x4.rotate_right(7) ^ x4.rotate_right(41); - } - *state = [x0, x1, x2, x3, x4]; -} - -fn rabbit_poly1305_seal_in_place( - key: &[u8; 16], - nonce: &[u8; 8], - plaintext_in_ciphertext_out: &mut [u8], -) -> Result> { - let mut poly_key = [0u8; 32]; - rabbit_apply_keystream(key, nonce, &mut poly_key)?; - rabbit_apply_keystream(key, nonce, plaintext_in_ciphertext_out)?; - Ok(rabbit_poly1305_tag(&poly_key, &[], plaintext_in_ciphertext_out).to_vec()) -} - -fn rabbit_poly1305_open_in_place( - key: &[u8; 16], - nonce: &[u8; 8], - ciphertext: &mut [u8], - tag: &[u8], -) -> Result<()> { - let mut poly_key = [0u8; 32]; - rabbit_apply_keystream(key, nonce, &mut poly_key)?; - let expected = rabbit_poly1305_tag(&poly_key, &[], ciphertext); - if !bool::from(expected.as_slice().ct_eq(tag)) { - bail!("Shadowsocks Rabbit128-Poly1305 tag verification failed"); - } - rabbit_apply_keystream(key, nonce, ciphertext)?; - Ok(()) -} - -fn rabbit_apply_keystream(key: &[u8; 16], nonce: &[u8; 8], buf: &mut [u8]) -> Result<()> { - let mut cipher = Rabbit::new_from_slices(key, nonce) - .map_err(|_| anyhow!("invalid Rabbit128-Poly1305 key or nonce"))?; - cipher.apply_keystream(buf); - Ok(()) -} - -fn rabbit_poly1305_tag(poly_key: &[u8; 32], associated_data: &[u8], ciphertext: &[u8]) -> [u8; 16] { - let mut mac = Poly1305::new(GenericArray::from_slice(poly_key)); - mac.update_padded(associated_data); - mac.update_padded(ciphertext); - let mut lengths = [0u8; 16]; - lengths[..8].copy_from_slice(&(associated_data.len() as u64).to_le_bytes()); - lengths[8..].copy_from_slice(&(ciphertext.len() as u64).to_le_bytes()); - mac.update_padded(&lengths); - mac.finalize().into() -} - -fn encrypt_custom_ss_udp_packet( - kind: CustomSsAeadKind, - master_key: &[u8], - target: SocketAddr, - payload: &[u8], -) -> Result> { - let mut salt = vec![0u8; kind.salt_len()]; - OsRng.fill_bytes(&mut salt); - let cipher = custom_ss_subkey(kind, master_key, &salt)?; - let mut nonce = vec![0u8; kind.nonce_len()]; - let mut plaintext = socks_addr(target)?; - plaintext.extend_from_slice(payload); - cipher.seal_in_place(&mut nonce, &mut plaintext)?; - let mut packet = salt; - packet.extend_from_slice(&plaintext); - Ok(packet) -} - -fn decrypt_custom_ss_udp_packet( - kind: CustomSsAeadKind, - master_key: &[u8], - packet: &[u8], -) -> Result<(SocketAddr, Vec)> { - let salt_len = kind.salt_len(); - if packet.len() <= salt_len { - bail!("Shadowsocks UDP packet missing salt"); - } - let (salt, encrypted) = packet.split_at(salt_len); - let cipher = custom_ss_subkey(kind, master_key, salt)?; - let mut nonce = vec![0u8; kind.nonce_len()]; - let mut buf = encrypted.to_vec(); - let plaintext = cipher.open_in_place(&mut nonce, &mut buf)?; - let (source, used) = parse_socks_addr(plaintext)?; - Ok((source, plaintext[used..].to_vec())) -} - -fn encrypt_custom_ss_stream_udp_packet( - kind: CustomSsStreamKind, - key: &[u8], - target: SocketAddr, - payload: &[u8], -) -> Result> { - let mut salt = vec![0u8; kind.salt_len()]; - OsRng.fill_bytes(&mut salt); - let mut cipher = CustomSsStreamCipher::new(kind, key, &salt)?; - let mut plaintext = socks_addr(target)?; - plaintext.extend_from_slice(payload); - cipher.apply_keystream(&mut plaintext); - let mut packet = salt; - packet.extend_from_slice(&plaintext); - Ok(packet) -} - -fn decrypt_custom_ss_stream_udp_packet( - kind: CustomSsStreamKind, - key: &[u8], - packet: &[u8], -) -> Result<(SocketAddr, Vec)> { - let salt_len = kind.salt_len(); - if packet.len() <= salt_len { - bail!("Shadowsocks UDP packet missing salt"); - } - let (salt, encrypted) = packet.split_at(salt_len); - let mut cipher = CustomSsStreamCipher::new(kind, key, salt)?; - let mut plaintext = encrypted.to_vec(); - cipher.apply_keystream(&mut plaintext); - let (source, used) = parse_socks_addr(&plaintext)?; - Ok((source, plaintext[used..].to_vec())) -} - -fn parse_aead2022_keys( - kind: Aead2022AesCcmKind, - password: &str, -) -> Result<(Arc<[u8]>, Arc<[Arc<[u8]>]>)> { - if password.is_empty() { - bail!("Shadowsocks 2022 password is missing"); - } - let mut keys = Vec::new(); - for segment in password.split(':') { - let decoded = BASE64_STANDARD - .decode(segment) - .context("failed to decode Shadowsocks 2022 base64 key")?; - if decoded.len() != kind.key_len() { - bail!( - "Shadowsocks 2022 key length mismatch: required {}, got {}", - kind.key_len(), - decoded.len() - ); - } - keys.push(Arc::<[u8]>::from(decoded)); - } - let user_key = keys - .pop() - .ok_or_else(|| anyhow!("Shadowsocks 2022 password is missing"))?; - Ok((user_key, Arc::from(keys))) -} - -fn aead2022_session_key(kind: Aead2022AesCcmKind, user_key: &[u8], salt: &[u8]) -> Result> { - if user_key.len() != kind.key_len() || salt.is_empty() { - bail!("invalid Shadowsocks 2022 session key material"); - } - let mut material = Vec::with_capacity(user_key.len() + salt.len()); - material.extend_from_slice(user_key); - material.extend_from_slice(salt); - let derived = blake3::derive_key(AEAD2022_SESSION_SUBKEY_CONTEXT, &material); - Ok(derived[..kind.key_len()].to_vec()) -} - -fn aead2022_identity_subkey( - kind: Aead2022AesCcmKind, - identity_key: &[u8], - salt: &[u8], -) -> Result> { - if identity_key.len() != kind.key_len() || salt.len() != kind.key_len() { - bail!("invalid Shadowsocks 2022 identity key material"); - } - let mut material = Vec::with_capacity(identity_key.len() + salt.len()); - material.extend_from_slice(identity_key); - material.extend_from_slice(salt); - let derived = blake3::derive_key(AEAD2022_IDENTITY_SUBKEY_CONTEXT, &material); - Ok(derived[..kind.key_len()].to_vec()) -} - -fn aead2022_eih_blocks( - kind: Aead2022AesCcmKind, - identity_keys: &[Arc<[u8]>], - user_key: &[u8], - salt: &[u8], -) -> Result> { - let mut blocks = Vec::with_capacity(identity_keys.len() * 16); - for (index, identity_key) in identity_keys.iter().enumerate() { - let next_key = identity_keys - .get(index + 1) - .map(AsRef::as_ref) - .unwrap_or(user_key); - let mut block = aead2022_psk_hash_block(next_key); - let subkey = aead2022_identity_subkey(kind, identity_key, salt)?; - aead2022_aes_encrypt_block(kind, &subkey, &mut block)?; - blocks.extend_from_slice(&block); - } - Ok(blocks) -} - -fn aead2022_udp_eih_blocks( - kind: Aead2022AesCcmKind, - identity_keys: &[Arc<[u8]>], - user_key: &[u8], - packet_header: &[u8], -) -> Result> { - let mut blocks = Vec::with_capacity(identity_keys.len() * 16); - for (index, identity_key) in identity_keys.iter().enumerate() { - let next_key = identity_keys - .get(index + 1) - .map(AsRef::as_ref) - .unwrap_or(user_key); - let mut block = aead2022_psk_hash_block(next_key); - for (left, right) in block.iter_mut().zip(packet_header.iter().copied()) { - *left ^= right; - } - aead2022_aes_encrypt_block(kind, identity_key, &mut block)?; - blocks.extend_from_slice(&block); - } - Ok(blocks) -} - -fn aead2022_psk_hash_block(key: &[u8]) -> [u8; 16] { - let hash = blake3::hash(key); - hash.as_bytes()[..16] - .try_into() - .expect("fixed BLAKE3 block") -} - -fn aead2022_udp_header_key<'a>(identity_keys: &'a [Arc<[u8]>], user_key: &'a [u8]) -> &'a [u8] { - identity_keys.first().map(AsRef::as_ref).unwrap_or(user_key) -} - -fn aead2022_aes_encrypt_block( - kind: Aead2022AesCcmKind, - key: &[u8], - block: &mut [u8], -) -> Result<()> { - if block.len() != 16 { - bail!("Shadowsocks 2022 AES block must be 16 bytes"); - } - match kind { - Aead2022AesCcmKind::Aes128 => { - let cipher = Aes128::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-128 block key"))?; - cipher.encrypt_block(AesGenericArray::from_mut_slice(block)); - } - Aead2022AesCcmKind::Aes256 => { - let cipher = Aes256::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-256 block key"))?; - cipher.encrypt_block(AesGenericArray::from_mut_slice(block)); - } - } - Ok(()) -} - -fn aead2022_aes_decrypt_block( - kind: Aead2022AesCcmKind, - key: &[u8], - block: &mut [u8], -) -> Result<()> { - if block.len() != 16 { - bail!("Shadowsocks 2022 AES block must be 16 bytes"); - } - match kind { - Aead2022AesCcmKind::Aes128 => { - let cipher = Aes128::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-128 block key"))?; - cipher.decrypt_block(AesGenericArray::from_mut_slice(block)); - } - Aead2022AesCcmKind::Aes256 => { - let cipher = Aes256::new_from_slice(key) - .map_err(|_| anyhow!("invalid Shadowsocks 2022 AES-256 block key"))?; - cipher.decrypt_block(AesGenericArray::from_mut_slice(block)); - } - } - Ok(()) -} - -fn aead2022_now_timestamp() -> Result { - Ok(SystemTime::now() - .duration_since(UNIX_EPOCH) - .context("system clock is before unix epoch")? - .as_secs()) -} - -fn aead2022_validate_timestamp(timestamp: u64) -> Result<()> { - let now = aead2022_now_timestamp()?; - if now.abs_diff(timestamp) > AEAD2022_TIMESTAMP_MAX_DIFF { - bail!("Shadowsocks 2022 timestamp is outside the allowed skew"); - } - Ok(()) -} - -fn aead2022_udp_padding_len(destination: SocketAddr, payload: &[u8]) -> usize { - if destination.port() == 53 && payload.len() < 900 { - (OsRng.next_u32() as usize % (900 - payload.len())) + 1 - } else { - 0 - } -} - -fn custom_ss_subkey( - kind: CustomSsAeadKind, - master_key: &[u8], - salt: &[u8], -) -> Result { - let prk = hmac::sign( - &hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, salt), - master_key, - ); - let prk = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, prk.as_ref()); - let mut okm = Vec::with_capacity(kind.key_len()); - let mut previous = Vec::new(); - let mut counter = 1u8; - while okm.len() < kind.key_len() { - let mut input = Vec::with_capacity(previous.len() + SS_INFO.len() + 1); - input.extend_from_slice(&previous); - input.extend_from_slice(SS_INFO); - input.push(counter); - previous = hmac::sign(&prk, &input).as_ref().to_vec(); - okm.extend_from_slice(&previous); - counter = counter - .checked_add(1) - .ok_or_else(|| anyhow!("Shadowsocks HKDF output too long"))?; - } - okm.truncate(kind.key_len()); - CustomSsAeadCipher::new(kind, &okm) -} - -fn evp_bytes_to_key(password: &[u8], key_len: usize) -> Vec { - let mut out = Vec::with_capacity(key_len); - let mut previous = Vec::new(); - while out.len() < key_len { - let mut hasher = Md5::new(); - if !previous.is_empty() { - hasher.update(&previous); - } - hasher.update(password); - previous = hasher.finalize().to_vec(); - out.extend_from_slice(&previous); - } - out.truncate(key_len); - out -} - -fn increment_nonce(nonce: &mut [u8]) { - for byte in nonce { - let (next, overflow) = byte.overflowing_add(1); - *byte = next; - if !overflow { - break; - } - } -} diff --git a/burrow/src/proxy_runtime/shadowsocks_plugins.rs b/burrow/src/proxy_runtime/shadowsocks_plugins.rs deleted file mode 100644 index 7f765d9..0000000 --- a/burrow/src/proxy_runtime/shadowsocks_plugins.rs +++ /dev/null @@ -1,2735 +0,0 @@ -async fn websocket_plugin_connect( - mut stream: Box, - config: &ShadowsocksWebSocketTransport, - port: u16, -) -> Result> { - if config.tls { - stream = websocket_plugin_tls_connect( - stream, - config.websocket_host_header(), - config.skip_cert_verify, - config.certificate_fingerprint.as_ref(), - config.client_identity.as_ref(), - config.ech.as_ref(), - ) - .await?; - } - let (path, max_early_data) = websocket_path_and_early_data(&config.path); - tracing::debug!( - kind = ?config.kind, - host = %config.host, - port, - path = %path, - tls = config.tls, - mux = ?config.mux, - v2ray_http_upgrade = config.v2ray_http_upgrade, - v2ray_http_upgrade_fast_open = config.v2ray_http_upgrade_fast_open, - "starting Shadowsocks websocket plugin handshake" - ); - if let Some(max_early_data) = max_early_data { - return Ok(Box::new(WebSocketEarlyDataStream::new( - stream, - config.clone(), - port, - path, - max_early_data, - ))); - } - let (request, websocket_key) = websocket_plugin_request(config, port, &path, None)?; - stream.write_all(&request).await?; - stream.flush().await?; - if config.v2ray_http_upgrade && config.v2ray_http_upgrade_fast_open { - return Ok(Box::new(HttpUpgradeFastOpenStream::new(stream))); - } - let response = read_http_upgrade_response(&mut stream).await?; - validate_websocket_upgrade_response( - &response, - config.v2ray_http_upgrade, - websocket_key.as_deref(), - )?; - tracing::debug!( - kind = ?config.kind, - host = %config.host, - port, - path = %path, - "completed Shadowsocks websocket plugin handshake" - ); - - if config.v2ray_http_upgrade { - Ok(stream) - } else { - Ok(Box::new(WebSocketStream::new(stream))) - } -} - -async fn gost_smux_stream(stream: Box) -> Result> { - let mut smux_config = tokio_smux::SmuxConfig::default(); - smux_config.keep_alive_disable = true; - let mut session = tokio_smux::Session::client(stream, smux_config) - .map_err(|err| anyhow!("failed to start gost-plugin smux session: {err}"))?; - let mut smux_stream = session - .open_stream() - .await - .map_err(|err| anyhow!("failed to open gost-plugin smux stream: {err}"))?; - let (client, bridge) = tokio::io::duplex(64 * 1024); - let (mut bridge_read, mut bridge_write) = split(bridge); - let (upload_tx, mut upload_rx) = mpsc::channel::>(32); - - let upload = tokio::spawn(async move { - let mut buf = vec![0u8; 16 * 1024]; - loop { - let read = bridge_read.read(&mut buf).await?; - if read == 0 { - break; - } - if upload_tx.send(buf[..read].to_vec()).await.is_err() { - break; - } - } - Result::<()>::Ok(()) - }); - - tokio::spawn(async move { - let _session = session; - let result: Result<()> = async { - loop { - tokio::select! { - outbound = upload_rx.recv() => { - let Some(outbound) = outbound else { - break; - }; - smux_stream - .send_message(outbound) - .await - .map_err(|err| anyhow!("failed to write gost-plugin smux stream: {err}"))?; - } - inbound = smux_stream.recv_message() => { - let Some(inbound) = inbound - .map_err(|err| anyhow!("failed to read gost-plugin smux stream: {err}"))? - else { - break; - }; - bridge_write.write_all(&inbound).await?; - bridge_write.flush().await?; - } - } - } - Ok(()) - } - .await; - if let Err(err) = result { - tracing::debug!(error = %err, "gost-plugin smux bridge stopped"); - } - upload.abort(); - }); - - Ok(Box::new(client)) -} - -async fn kcptun_open_session( - endpoint: &ProxyServerEndpoint, - address: SocketAddr, - config: &ShadowsocksKcptunTransport, -) -> Result> { - let kcp_config = KcpConfig { - mtu: config.mtu, - nodelay: KcpNoDelayConfig { - nodelay: config.no_delay != 0, - interval: config.interval, - resend: config.resend, - nc: config.no_congestion, - }, - wnd_size: (config.sndwnd, config.rcvwnd), - flush_write: false, - flush_acks_input: config.ack_nodelay, - stream: true, - fec_data_shards: config.data_shard, - fec_parity_shards: config.parity_shard, - crypt: kcptun_block_crypt(&config.key, &config.crypt)?, - ..Default::default() - }; - let udp = match address.ip() { - IpAddr::V4(_) => UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await?, - IpAddr::V6(_) => UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0)).await?, - }; - bind_proxy_outbound_socket(udp.as_raw_fd(), address.ip())?; - configure_kcptun_udp_socket(udp.as_raw_fd(), address.ip(), config); - let kcp = KcpStream::connect_with_socket(&kcp_config, udp, address) - .await - .with_context(|| { - format!( - "failed to connect Shadowsocks kcptun transport {}:{}", - endpoint.host, endpoint.port - ) - })?; - let keep_alive = if config.keep_alive == 0 { - 10 - } else { - config.keep_alive - }; - let keep_alive_interval = Duration::from_secs(keep_alive); - let smux_config = smux_rust::Config { - version: config.smux_ver, - keep_alive_disabled: false, - keep_alive_interval, - keep_alive_timeout: kcptun_smux_keep_alive_timeout(keep_alive_interval), - max_frame_size: config.frame_size, - max_receive_buffer: config.smux_buf, - max_stream_buffer: config.stream_buf, - }; - let session = match (config.no_comp, config.rate_limit) { - (true, 0) => kcptun_open_smux_session(kcp, smux_config).await?, - (true, rate_limit) => { - kcptun_open_smux_session(RateLimitedStream::new(kcp, rate_limit), smux_config).await? - } - (false, 0) => kcptun_open_smux_session(SnappyIO::new(kcp), smux_config).await?, - (false, rate_limit) => { - kcptun_open_smux_session( - SnappyIO::new(RateLimitedStream::new(kcp, rate_limit)), - smux_config, - ) - .await? - } - }; - tracing::debug!( - server = %endpoint.host, - port = endpoint.port, - crypt = %config.crypt, - mode = %config.mode, - rate_limit = config.rate_limit, - no_comp = config.no_comp, - smux_ver = config.smux_ver, - "opened Shadowsocks kcptun plugin session" - ); - Ok(session) -} - -async fn kcptun_open_smux_session( - transport: T, - smux_config: smux_rust::Config, -) -> Result> -where - T: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - smux_config - .verify() - .map_err(|err| anyhow!("invalid Shadowsocks kcptun smux config: {err}"))?; - let session = smux_rust::client(Box::new(transport), Some(smux_config)) - .await - .map_err(|err| anyhow!("failed to start Shadowsocks kcptun smux session: {err}"))?; - Ok(session) -} - -async fn kcptun_open_smux_stream(session: Arc) -> Result> { - let stream = session - .open_stream() - .await - .map_err(|err| anyhow!("failed to open Shadowsocks kcptun smux stream: {err}"))?; - Ok(Box::new(stream)) -} - -fn kcptun_smux_keep_alive_timeout(keep_alive_interval: Duration) -> Duration { - if keep_alive_interval >= KCPTUN_SMUX_DEFAULT_KEEP_ALIVE_TIMEOUT { - keep_alive_interval.saturating_mul(3) - } else { - KCPTUN_SMUX_DEFAULT_KEEP_ALIVE_TIMEOUT - } -} - -fn schedule_kcptun_session_close(session: Arc, scavenge_ttl: u64) { - tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(scavenge_ttl)).await; - if !session.is_closed() { - let _ = session.close().await; - } - }); -} - -fn configure_kcptun_udp_socket( - fd: std::os::fd::RawFd, - ip: IpAddr, - config: &ShadowsocksKcptunTransport, -) { - if config.dscp > 0 { - if let Err(err) = set_kcptun_dscp(fd, ip, config.dscp) { - tracing::debug!(dscp = config.dscp, error = %err, "failed to set Shadowsocks kcptun DSCP"); - } - } - if config.sockbuf > 0 { - let sockbuf = kcptun_socket_int_value(config.sockbuf as u64); - if let Err(err) = set_socket_int_option(fd, libc::SOL_SOCKET, libc::SO_RCVBUF, sockbuf) { - tracing::debug!(sockbuf = config.sockbuf, error = %err, "failed to set Shadowsocks kcptun receive buffer"); - } - if let Err(err) = set_socket_int_option(fd, libc::SOL_SOCKET, libc::SO_SNDBUF, sockbuf) { - tracing::debug!(sockbuf = config.sockbuf, error = %err, "failed to set Shadowsocks kcptun send buffer"); - } - } -} - -fn set_kcptun_dscp(fd: std::os::fd::RawFd, ip: IpAddr, dscp: u32) -> io::Result<()> { - match ip { - IpAddr::V4(_) => set_socket_int_option( - fd, - libc::IPPROTO_IP, - libc::IP_TOS, - kcptun_ipv4_dscp_tos(dscp), - ), - IpAddr::V6(_) => set_socket_int_option( - fd, - libc::IPPROTO_IPV6, - libc::IPV6_TCLASS, - kcptun_socket_int_value(dscp as u64), - ), - } -} - -fn kcptun_ipv4_dscp_tos(dscp: u32) -> libc::c_int { - dscp.saturating_mul(4).min(u8::MAX as u32) as libc::c_int -} - -fn kcptun_socket_int_value(value: u64) -> libc::c_int { - value.min(libc::c_int::MAX as u64) as libc::c_int -} - -fn set_socket_int_option( - fd: std::os::fd::RawFd, - level: libc::c_int, - option: libc::c_int, - value: libc::c_int, -) -> io::Result<()> { - let result = unsafe { - libc::setsockopt( - fd, - level, - option, - (&value as *const libc::c_int).cast::(), - std::mem::size_of_val(&value) as libc::socklen_t, - ) - }; - if result != 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -fn kcptun_block_crypt(key: &str, crypt: &str) -> Result>> { - let mut derived = [0u8; 32]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA1, - NonZeroU32::new(4096).expect("non-zero PBKDF2 iterations"), - b"kcp-go", - key.as_bytes(), - &mut derived, - ); - let crypt = crypt.trim().to_ascii_lowercase(); - let block: Arc = match crypt.as_str() { - "null" => return Ok(None), - "none" => KcpNoneBlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun none crypt key: {err}"))?, - "tea" => Arc::new( - KcpTeaBlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun tea crypt key: {err}"))?, - ), - "xor" | "simple_xor" => Arc::new( - KcpSimpleXorBlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun xor crypt key: {err}"))?, - ), - "aes" | "aes-256" => Arc::new( - KcpAes256BlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun aes crypt key: {err}"))?, - ), - "aes-128" => Arc::new( - KcpAes128BlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun aes-128 crypt key: {err}"))?, - ), - "aes-192" => Arc::new( - KcpAes192BlockCrypt::new(&derived[..24]) - .map_err(|err| anyhow!("invalid kcptun aes-192 crypt key: {err}"))?, - ), - "blowfish" => Arc::new( - KcpBlowfishBlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun blowfish crypt key: {err}"))?, - ), - "cast5" => Arc::new( - KcpCast5BlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun cast5 crypt key: {err}"))?, - ), - "3des" | "triple_des" => Arc::new( - KcpTripleDesBlockCrypt::new(&derived[..24]) - .map_err(|err| anyhow!("invalid kcptun 3des crypt key: {err}"))?, - ), - "twofish" => Arc::new( - KcpTwofishBlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun twofish crypt key: {err}"))?, - ), - "xtea" => Arc::new( - KcpXteaBlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun xtea crypt key: {err}"))?, - ), - "salsa20" => Arc::new( - KcpSalsa20BlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun salsa20 crypt key: {err}"))?, - ), - "sm4" => Arc::new( - KcpSm4BlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun sm4 crypt key: {err}"))?, - ), - "aes-128-gcm" => Arc::new( - KcpAesGcmBlockCrypt::new(&derived[..16]) - .map_err(|err| anyhow!("invalid kcptun aes-128-gcm crypt key: {err}"))?, - ), - _ => Arc::new( - KcpAes256BlockCrypt::new(&derived) - .map_err(|err| anyhow!("invalid kcptun fallback aes crypt key: {err}"))?, - ), - }; - Ok(Some(block)) -} - -async fn websocket_plugin_tls_connect( - stream: Box, - server_name: &str, - skip_cert_verify: bool, - certificate_fingerprint: Option<&CertificateFingerprint>, - client_identity: Option<&TlsClientIdentity>, - ech: Option<&ShadowsocksEchConfig>, -) -> Result> { - #[cfg(target_vendor = "apple")] - { - if ech.is_none() { - return websocket_plugin_native_tls_connect( - stream, - server_name, - skip_cert_verify, - certificate_fingerprint, - client_identity, - ) - .await; - } - } - websocket_plugin_rustls_tls_connect( - stream, - server_name, - skip_cert_verify, - certificate_fingerprint, - client_identity, - ech, - ) - .await -} - -async fn websocket_plugin_rustls_tls_connect( - stream: Box, - server_name: &str, - skip_cert_verify: bool, - certificate_fingerprint: Option<&CertificateFingerprint>, - client_identity: Option<&TlsClientIdentity>, - ech: Option<&ShadowsocksEchConfig>, -) -> Result> { - let ech_config_list = resolve_shadowsocks_ech_config_list(server_name, ech)?; - let config = rustls_tls_config( - &["http/1.1".to_owned()], - skip_cert_verify, - certificate_fingerprint, - client_identity, - ech_config_list.as_deref(), - )?; - let connector = TlsConnector::from(Arc::new(config)); - let name = ServerName::try_from(server_name.to_owned()) - .with_context(|| format!("invalid websocket plugin SNI {server_name}"))?; - Ok(Box::new(connector.connect(name, stream).await?)) -} - -fn resolve_shadowsocks_ech_config_list( - server_name: &str, - ech: Option<&ShadowsocksEchConfig>, -) -> Result>> { - let Some(ech) = ech else { - return Ok(None); - }; - if let Some(config_list) = ech.config_list.as_ref() { - return Ok(Some(config_list.clone())); - } - - let query_name = ech.query_server_name.as_deref().unwrap_or(server_name); - let mut last_error = None; - for dns_server in platform_proxy_dns_servers() { - let Ok(ip) = dns_server.parse::() else { - continue; - }; - let dns_addr = SocketAddr::new(ip, 53); - match direct_dns_https_ech_config_list(query_name, dns_addr) { - Ok(Some(config_list)) => return Ok(Some(config_list)), - Ok(None) => { - last_error = Some(anyhow!( - "DNS HTTPS response for {query_name} did not include an ECH config" - )); - } - Err(err) => last_error = Some(err), - } - } - Err(last_error.unwrap_or_else(|| { - anyhow!("failed to resolve ECH config for Shadowsocks websocket host {query_name}") - })) -} - -#[cfg(target_vendor = "apple")] -async fn websocket_plugin_native_tls_connect( - stream: Box, - server_name: &str, - skip_cert_verify: bool, - certificate_fingerprint: Option<&CertificateFingerprint>, - client_identity: Option<&TlsClientIdentity>, -) -> Result> { - let mut builder = NativeTlsConnector::builder(); - if skip_cert_verify || certificate_fingerprint.is_some() { - builder.danger_accept_invalid_certs(true); - } - if let Some(identity) = client_identity { - builder.identity(load_native_tls_identity(identity)?); - } - builder.request_alpns(&["http/1.1"]); - let connector = TokioNativeTlsConnector::from( - builder - .build() - .context("failed to build native TLS connector for Shadowsocks websocket plugin")?, - ); - let tls = connector.connect(server_name, stream).await?; - if let Some(fingerprint) = certificate_fingerprint { - verify_native_tls_peer_certificate(&tls, fingerprint, "Shadowsocks websocket plugin")?; - } - Ok(Box::new(tls)) -} - -async fn shadow_tls_connect( - stream: Box, - config: &ShadowsocksShadowTlsTransport, -) -> Result> { - match config.version { - 1 => { - let tls_config = shadow_tls_client_config(config)?; - let connector = TlsConnector::from(Arc::new(tls_config)); - let name = ServerName::try_from(config.host.clone()) - .with_context(|| format!("invalid ShadowTLS host {}", config.host))?; - let tls = connector - .connect(name, stream) - .await - .with_context(|| format!("ShadowTLS v1 handshake failed for {}", config.host))?; - let (stream, _) = tls.into_inner(); - Ok(stream) - } - 2 => { - if config.client_fingerprint.is_some() { - #[cfg(feature = "boring-browser-fingerprints")] - return shadow_tls_v2_boring_connect(stream, config).await; - #[cfg(not(feature = "boring-browser-fingerprints"))] - bail!( - "unsupported ShadowTLS v2 client-fingerprint: browser TLS profiles require the boring-browser-fingerprints feature" - ); - } - let tls_config = shadow_tls_client_config(config)?; - let connector = TlsConnector::from(Arc::new(tls_config)); - let name = ServerName::try_from(config.host.clone()) - .with_context(|| format!("invalid ShadowTLS host {}", config.host))?; - let hashed = HmacReadStream::new(stream, &config.password); - let tls = connector - .connect(name, hashed) - .await - .with_context(|| format!("ShadowTLS v2 handshake failed for {}", config.host))?; - let (hashed, _) = tls.into_inner(); - let (stream, prefix) = hashed.finish(); - Ok(Box::new(ShadowTlsV2Stream::new(stream, prefix))) - } - version => bail!("unsupported ShadowTLS version {version}"), - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -async fn shadow_tls_v2_boring_connect( - stream: Box, - config: &ShadowsocksShadowTlsTransport, -) -> Result> { - let fingerprint = config - .client_fingerprint - .ok_or_else(|| anyhow!("missing ShadowTLS v2 client fingerprint"))?; - let profile = shadow_tls_v2_boring_profile(fingerprint)?; - let domain = RamaDomain::try_from(config.host.clone()) - .with_context(|| format!("invalid ShadowTLS host {}", config.host))?; - let mut builder = BoringTlsConnectorDataBuilder::try_from(profile.client_config.as_ref()) - .with_context(|| { - format!( - "failed to build browser TLS profile for client-fingerprint {}", - fingerprint.mihomo_name() - ) - })?; - builder = builder - .with_server_name(domain) - .with_alpn_protos(Bytes::from(alpn_wire_protocols(&config.alpn)?)); - if config.skip_cert_verify || config.certificate_fingerprint.is_some() { - builder = builder.with_server_verify_mode(RamaServerVerifyMode::Disable); - } - if let Some(identity) = &config.client_identity { - builder = builder.with_client_auth(load_boring_client_identity(identity)?); - } - let data = builder.build().with_context(|| { - format!( - "failed to configure ShadowTLS v2 browser TLS profile for {}", - config.host - ) - })?; - let hashed = DetachableStream::new(HmacReadStream::new(stream, &config.password)); - let mut tls = rama_tls_boring::core::tokio::connect(data.config, config.host.as_str(), hashed) - .await - .map_err(|err| { - anyhow!( - "ShadowTLS v2 browser-profile handshake failed for {} using client-fingerprint {}: {err}", - config.host, - fingerprint.mihomo_name() - ) - })?; - if let Some(certificate_fingerprint) = config.certificate_fingerprint.as_ref() { - verify_boring_tls_peer_certificate( - &tls, - certificate_fingerprint, - "ShadowTLS v2 browser-profile", - )?; - } - let hashed = tls - .get_mut() - .take() - .context("failed to recover ShadowTLS v2 browser-profile transport")?; - let (stream, prefix) = hashed.finish(); - Ok(Box::new(ShadowTlsV2Stream::new(stream, prefix))) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn shadow_tls_v2_boring_profile(fingerprint: MihomoClientFingerprint) -> Result { - let profile = mihomo_browser_tls_profile(fingerprint)?; - Ok(strip_shadow_tls_v2_unsupported_groups(profile)) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn mihomo_browser_tls_profile(fingerprint: MihomoClientFingerprint) -> Result { - Ok(mihomo_browser_user_agent_profile(fingerprint)?.tls.clone()) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn mihomo_browser_user_agent_profile( - fingerprint: MihomoClientFingerprint, -) -> Result<&'static rama_ua::profile::UserAgentProfile> { - let selected = match fingerprint { - MihomoClientFingerprint::Random => mihomo_initial_random_fingerprint(), - other => other, - }; - let (kind, platform, edge, required_ua_marker) = match selected { - MihomoClientFingerprint::Chrome => ( - UserAgentKind::Chromium, - Some(PlatformKind::MacOS), - false, - None, - ), - MihomoClientFingerprint::Firefox => ( - UserAgentKind::Firefox, - Some(PlatformKind::MacOS), - false, - None, - ), - MihomoClientFingerprint::Safari => ( - UserAgentKind::Safari, - Some(PlatformKind::MacOS), - false, - None, - ), - MihomoClientFingerprint::Safari16 => ( - UserAgentKind::Safari, - Some(PlatformKind::MacOS), - false, - Some("Version/16."), - ), - MihomoClientFingerprint::Ios => { - (UserAgentKind::Safari, Some(PlatformKind::IOS), false, None) - } - MihomoClientFingerprint::Android => ( - UserAgentKind::Chromium, - Some(PlatformKind::Android), - false, - None, - ), - MihomoClientFingerprint::Edge => ( - UserAgentKind::Chromium, - Some(PlatformKind::Windows), - true, - None, - ), - other => bail!( - "unsupported client-fingerprint {}: no matching browser TLS profile is available yet", - other.mihomo_name() - ), - }; - embedded_user_agent_profiles()? - .iter() - .filter(|profile| { - profile.ua_kind == kind - && profile.platform == platform - && required_ua_marker - .map(|marker| { - profile - .ua_str() - .map(|ua| ua.contains(marker)) - .unwrap_or(false) - }) - .unwrap_or(true) - && profile - .ua_str() - .map(|ua| ua.contains(" Edg/") == edge) - .unwrap_or(!edge) - }) - .max_by_key(|profile| profile.ua_version.unwrap_or(0)) - .ok_or_else(|| { - anyhow!( - "no embedded browser TLS profile matched client-fingerprint {}", - fingerprint.mihomo_name() - ) - }) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn strip_shadow_tls_v2_unsupported_groups(mut profile: TlsProfile) -> TlsProfile { - let mut config = profile.client_config.as_ref().clone(); - strip_x25519_mlkem768_from_client_config(&mut config); - profile.client_config = Arc::new(config); - profile -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn strip_x25519_mlkem768_from_client_config(config: &mut rama_net::tls::client::ClientConfig) { - if let Some(extensions) = config.extensions.as_mut() { - for extension in extensions { - if let RamaClientHelloExtension::SupportedGroups(groups) = extension { - groups.retain(|group| *group != RamaSupportedGroup::X25519MLKEM768); - } - } - } -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn mihomo_initial_random_fingerprint() -> MihomoClientFingerprint { - use std::sync::OnceLock; - - static FINGERPRINT: OnceLock = OnceLock::new(); - *FINGERPRINT.get_or_init(|| match rand::thread_rng().gen_range(0..12) { - 0..=5 => MihomoClientFingerprint::Chrome, - 6..=8 => MihomoClientFingerprint::Safari, - 9..=10 => MihomoClientFingerprint::Ios, - _ => MihomoClientFingerprint::Firefox, - }) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn embedded_user_agent_profiles() -> Result<&'static [rama_ua::profile::UserAgentProfile]> { - use std::sync::OnceLock; - - static PROFILES: OnceLock, String>> = - OnceLock::new(); - PROFILES - .get_or_init(|| { - try_load_embedded_profiles() - .map(|profiles| profiles.collect()) - .map_err(|err| err.to_string()) - }) - .as_deref() - .map_err(|err| anyhow!("failed to load embedded browser TLS profiles: {err}")) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn alpn_wire_protocols(protocols: &[String]) -> Result> { - let mut out = Vec::new(); - for protocol in protocols { - let protocol = protocol.as_bytes(); - let len = u8::try_from(protocol.len()).context("ALPN protocol identifier is too long")?; - out.push(len); - out.extend_from_slice(protocol); - } - Ok(out) -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn verify_boring_tls_peer_certificate( - tls: &rama_tls_boring::client::BoringTlsStream, - fingerprint: &CertificateFingerprint, - protocol: &str, -) -> Result<()> { - let leaf = tls - .ssl() - .peer_certificate() - .ok_or_else(|| anyhow!("{protocol} peer did not present a certificate"))?; - if boring_certificate_fingerprint_matches(&leaf, fingerprint)? { - return Ok(()); - } - if let Some(chain) = tls.ssl().peer_cert_chain() { - for certificate in chain { - if boring_certificate_fingerprint_matches(certificate, fingerprint)? { - return Ok(()); - } - } - } - bail!("{protocol} certificate fingerprint mismatch"); -} - -#[cfg(feature = "boring-browser-fingerprints")] -fn boring_certificate_fingerprint_matches( - certificate: &BoringX509Ref, - fingerprint: &CertificateFingerprint, -) -> Result { - let digest = certificate - .digest(BoringMessageDigest::sha256()) - .context("failed to compute TLS certificate fingerprint")?; - Ok(&*digest == fingerprint.0) -} - -#[cfg(feature = "boring-browser-fingerprints")] -async fn restls_boring_connect( - stream: Box, - config: &ShadowsocksRestlsTransport, -) -> Result> { - let fingerprint = config.client_fingerprint.browser_profile(); - let profile = mihomo_browser_tls_profile(fingerprint)?; - let domain = RamaDomain::try_from(config.host.clone()) - .with_context(|| format!("invalid Restls host {}", config.host))?; - let builder = BoringTlsConnectorDataBuilder::try_from(profile.client_config.as_ref()) - .with_context(|| { - format!( - "failed to build Restls browser TLS profile for client-fingerprint {}", - fingerprint.mihomo_name() - ) - })? - .with_server_name(domain); - let data = builder.build().with_context(|| { - format!( - "failed to configure Restls browser TLS profile for {}", - config.host - ) - })?; - let handshake_stream = RestlsHandshakeStream::new(stream, config.secret, false); - let signed_stream = DetachableStream::new(RestlsSignedClientHelloStream::new( - handshake_stream, - config.secret, - )); - let mut tls = - rama_tls_boring::core::tokio::connect(data.config, config.host.as_str(), signed_stream) - .await - .map_err(|err| { - anyhow!( - "Restls browser-profile handshake failed for {} using client-fingerprint {}: {err}", - config.host, - fingerprint.mihomo_name() - ) - })?; - let signed_stream = tls - .get_mut() - .take() - .context("failed to recover Restls browser-profile transport")?; - let (handshake_stream, client_hello_signed) = signed_stream.into_inner(); - if !client_hello_signed { - bail!("Restls browser-profile handshake did not sign the ClientHello session ID"); - } - let (stream, handshake) = handshake_stream.into_inner(); - if !handshake.server_auth_unmasked { - bail!("Restls server authentication record was not observed"); - } - let server_random = handshake - .server_random - .ok_or_else(|| anyhow!("Restls handshake did not expose server random"))?; - let client_finished = handshake - .client_finished_auth - .ok_or_else(|| anyhow!("Restls handshake did not capture ClientFinished record"))?; - let mut codec = RestlsApplicationCodec::new(config.secret, server_random, false); - codec.set_client_finished_auth(client_finished); - codec.set_tls12_gcm_to_client_disable_counter(handshake.tls12_gcm_server_disable_counter); - let state = RestlsStreamState::new(codec, config.script.clone()); - Ok(Box::new(RestlsStream::new(stream, state))) -} - -fn shadow_tls_client_config(config: &ShadowsocksShadowTlsTransport) -> Result { - install_rustls_crypto_provider(); - let mut root_store = RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let versions = if config.version == 1 { - &[&version::TLS12][..] - } else { - rustls::DEFAULT_VERSIONS - }; - let builder = - ClientConfig::builder_with_protocol_versions(versions).with_root_certificates(root_store); - let mut tls_config = if let Some(identity) = config.client_identity.as_ref() { - let (cert_chain, private_key) = load_rustls_client_identity(identity)?; - builder - .with_client_auth_cert(cert_chain, private_key) - .context("failed to configure ShadowTLS client certificate")? - } else { - builder.with_no_client_auth() - }; - tls_config.alpn_protocols = config - .alpn - .iter() - .map(|value| value.as_bytes().to_vec()) - .collect(); - if let Some(fingerprint) = config.certificate_fingerprint.as_ref() { - tls_config - .dangerous() - .set_certificate_verifier(Arc::new(PinnedCertificateVerifier { - fingerprint: fingerprint.clone(), - })); - } else if config.skip_cert_verify { - tls_config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerifier)); - } - Ok(tls_config) -} - -async fn read_http_upgrade_response(stream: &mut Box) -> Result { - let mut response = Vec::with_capacity(1024); - let mut byte = [0u8; 1]; - while response.len() < 16 * 1024 { - stream.read_exact(&mut byte).await?; - response.push(byte[0]); - if response.ends_with(b"\r\n\r\n") { - return String::from_utf8(response) - .context("websocket plugin response headers were not valid UTF-8"); - } - } - bail!("websocket plugin response headers exceeded Burrow limit") -} - -fn validate_websocket_upgrade_response( - response: &str, - v2ray_http_upgrade: bool, - _websocket_key: Option<&str>, -) -> Result<()> { - let mut lines = response.split("\r\n"); - let status = lines.next().unwrap_or_default(); - let switching = status.contains(" 101 "); - let mut connection_upgrade = false; - let mut upgrade_websocket = false; - for line in lines { - let Some((key, value)) = line.split_once(':') else { - continue; - }; - let value = value.trim(); - if key.eq_ignore_ascii_case("connection") - && value - .split(',') - .any(|token| token.trim().eq_ignore_ascii_case("upgrade")) - { - connection_upgrade = true; - } - if key.eq_ignore_ascii_case("upgrade") && value.eq_ignore_ascii_case("websocket") { - upgrade_websocket = true; - } - } - if !switching || !connection_upgrade || !upgrade_websocket { - bail!("unexpected websocket plugin response status {status}"); - } - if !v2ray_http_upgrade { - // Mihomo validates Sec-WebSocket-Accept only in debug builds. Burrow keeps - // the same operational behavior and requires the protocol switch headers. - } - Ok(()) -} - -fn websocket_binary_frame(payload: &[u8]) -> io::Result> { - let mut frame = Vec::with_capacity(payload.len() + 14); - frame.push(0x82); - let len = payload.len(); - if len < 126 { - frame.push(0x80 | len as u8); - } else if u16::try_from(len).is_ok() { - frame.push(0x80 | 126); - frame.extend_from_slice(&(len as u16).to_be_bytes()); - } else { - frame.push(0x80 | 127); - frame.extend_from_slice(&(len as u64).to_be_bytes()); - } - let mut mask = [0u8; 4]; - OsRng.fill_bytes(&mut mask); - frame.extend_from_slice(&mask); - let mut masked = payload.to_vec(); - apply_websocket_mask(&mut masked, mask); - frame.extend_from_slice(&masked); - Ok(frame) -} - -fn apply_websocket_mask(payload: &mut [u8], mask: [u8; 4]) { - for (index, byte) in payload.iter_mut().enumerate() { - *byte ^= mask[index % 4]; - } -} - -fn websocket_path(path: &str) -> String { - let path = path.trim(); - if path.is_empty() { - return "/".to_owned(); - } - if path.starts_with('/') { - path.to_owned() - } else { - format!("/{path}") - } -} - -fn websocket_path_and_early_data(path: &str) -> (String, Option) { - let path = websocket_path(path); - let Some((base, query)) = path.split_once('?') else { - return (path, None); - }; - let mut max_early_data = None; - let mut kept = Vec::new(); - for pair in query.split('&') { - if pair.is_empty() { - continue; - } - let (key, value) = pair.split_once('=').unwrap_or((pair, "")); - if key == "ed" { - if max_early_data.is_none() { - max_early_data = value.parse::().ok().filter(|value| *value > 0); - } - continue; - } - kept.push(pair); - } - let path = if kept.is_empty() { - base.to_owned() - } else { - format!("{base}?{}", kept.join("&")) - }; - (path, max_early_data) -} - -fn websocket_plugin_request( - config: &ShadowsocksWebSocketTransport, - _port: u16, - path: &str, - early_data: Option<&[u8]>, -) -> Result<(Vec, Option)> { - let host = config.websocket_host_header(); - let mut request = format!( - "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n" - ); - for (key, value) in config.headers.iter() { - if key.eq_ignore_ascii_case("host") - || key.eq_ignore_ascii_case("connection") - || key.eq_ignore_ascii_case("upgrade") - || (early_data.is_some() && key.eq_ignore_ascii_case("sec-websocket-protocol")) - || (!config.v2ray_http_upgrade - && (key.eq_ignore_ascii_case("sec-websocket-key") - || key.eq_ignore_ascii_case("sec-websocket-version"))) - { - continue; - } - request.push_str(key); - request.push_str(": "); - request.push_str(value); - request.push_str("\r\n"); - } - - let websocket_key = if config.v2ray_http_upgrade { - None - } else { - let mut key = [0u8; 16]; - OsRng.fill_bytes(&mut key); - let key = BASE64_STANDARD.encode(key); - request.push_str("Sec-WebSocket-Version: 13\r\n"); - request.push_str("Sec-WebSocket-Key: "); - request.push_str(&key); - request.push_str("\r\n"); - Some(key) - }; - if let Some(early_data) = early_data { - request.push_str("Sec-WebSocket-Protocol: "); - request.push_str(&BASE64_URL_SAFE_NO_PAD.encode(early_data)); - request.push_str("\r\n"); - } - request.push_str("\r\n"); - Ok((request.into_bytes(), websocket_key)) -} - -impl ShadowsocksWebSocketTransport { - fn websocket_host_header(&self) -> &str { - self.headers - .iter() - .find(|(key, _)| key.eq_ignore_ascii_case("host")) - .map(|(_, value)| value.as_str()) - .unwrap_or(&self.host) - } -} - -fn v2ray_mux_open_frame() -> Vec { - let mut frame = Vec::new(); - frame.extend_from_slice(&[0u8, 0u8]); - frame.extend_from_slice(&[0u8, 0u8]); - frame.push(0x01); - frame.push(0x00); - frame.push(0x01); - frame.extend_from_slice(&0u16.to_be_bytes()); - frame.push(0x02); - frame.extend_from_slice(b"127.0.0.1"); - let len = u16::try_from(frame.len() - 2).expect("v2ray mux open frame fits u16"); - frame[..2].copy_from_slice(&len.to_be_bytes()); - frame -} - -fn v2ray_mux_data_frame(payload: &[u8]) -> io::Result> { - if payload.len() > u16::MAX as usize { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "v2ray-plugin mux payload is too large", - )); - } - let mut frame = Vec::with_capacity(payload.len() + 8); - frame.extend_from_slice(&4u16.to_be_bytes()); - frame.extend_from_slice(&[0u8, 0u8]); - frame.push(0x02); - frame.push(0x01); - frame.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - frame.extend_from_slice(payload); - Ok(frame) -} - -fn shadowsocks_plugin_transport( - plugin: Option<&ShadowsocksPlugin>, - client_fingerprint: Option<&str>, -) -> Result> { - let Some(plugin) = plugin else { - return Ok(None); - }; - let name = plugin.name.trim(); - if name == "obfs" { - let mode = plugin - .opts - .get("mode") - .or_else(|| plugin.opts.get("obfs")) - .map(|value| value.trim().to_ascii_lowercase()) - .ok_or_else(|| anyhow!("Shadowsocks obfs plugin requires mode"))?; - let mode = match mode.as_str() { - "http" => SimpleObfsMode::Http, - "tls" => SimpleObfsMode::Tls, - other => bail!("unsupported Shadowsocks obfs mode {other}"), - }; - let host = plugin - .opts - .get("host") - .or_else(|| plugin.opts.get("obfs-host")) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or("bing.com") - .to_owned(); - return Ok(Some(ShadowsocksPluginTransport::SimpleObfs { mode, host })); - } - if name == "v2ray-plugin" || name == "gost-plugin" { - let mode = plugin - .opts - .get("mode") - .or_else(|| plugin.opts.get("obfs")) - .map(|value| value.trim().to_ascii_lowercase()) - .ok_or_else(|| anyhow!("Shadowsocks {name} plugin requires websocket mode"))?; - if mode != "websocket" { - bail!("unsupported Shadowsocks {name} mode {mode}"); - } - let mux = plugin_bool_opt(&plugin.opts, "mux").unwrap_or(true); - let mux = match (name, mux) { - ("v2ray-plugin", true) => ShadowsocksWebSocketMux::V2ray, - ("gost-plugin", true) => ShadowsocksWebSocketMux::Gost, - (_, false) => ShadowsocksWebSocketMux::None, - _ => ShadowsocksWebSocketMux::None, - }; - let host = plugin - .opts - .get("host") - .or_else(|| plugin.opts.get("obfs-host")) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or("bing.com") - .to_owned(); - let path = plugin - .opts - .get("path") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or("/") - .to_owned(); - let transport = ShadowsocksWebSocketTransport { - kind: if name == "v2ray-plugin" { - ShadowsocksWebSocketKind::V2ray - } else { - ShadowsocksWebSocketKind::Gost - }, - host, - path, - headers: plugin_headers(&plugin.opts), - tls: plugin_bool_opt(&plugin.opts, "tls").unwrap_or(false), - ech: plugin_ech_config(&plugin.opts)?, - skip_cert_verify: plugin_bool_opt(&plugin.opts, "skip-cert-verify").unwrap_or(false), - certificate_fingerprint: plugin - .opts - .get("fingerprint") - .map(|value| parse_certificate_fingerprint(value)) - .transpose()?, - client_identity: plugin_tls_client_identity(&plugin.opts)?, - mux, - v2ray_http_upgrade: plugin_bool_opt(&plugin.opts, "v2ray-http-upgrade") - .unwrap_or(false), - v2ray_http_upgrade_fast_open: name == "v2ray-plugin" - && plugin_bool_opt(&plugin.opts, "v2ray-http-upgrade-fast-open").unwrap_or(false), - }; - return Ok(Some(ShadowsocksPluginTransport::WebSocket(transport))); - } - if name == "shadow-tls" { - let host = plugin - .opts - .get("host") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| anyhow!("Shadowsocks shadow-tls plugin requires host"))? - .to_owned(); - let version = plugin_u8_opt(&plugin.opts, "version").unwrap_or(2); - match version { - 1 | 2 => {} - other => bail!("unsupported Shadowsocks shadow-tls version {other}"), - } - let parsed_client_fingerprint = - client_fingerprint.and_then(MihomoClientFingerprint::from_mihomo_name); - if let Some(client_fingerprint) = parsed_client_fingerprint { - if version == 2 && !client_fingerprint.shadow_tls_v2_boring_supported() { - bail!( - "unsupported Shadowsocks {name} client-fingerprint {}: no matching browser TLS profile is available yet", - client_fingerprint.mihomo_name() - ); - } - } - let password = plugin - .opts - .get("password") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or(""); - if version != 1 && password.is_empty() { - bail!("Shadowsocks shadow-tls plugin requires password for version {version}"); - } - let password = password.to_owned(); - let alpn = plugin_list_opt(&plugin.opts, "alpn") - .unwrap_or_else(|| vec!["h2".to_owned(), "http/1.1".to_owned()]); - return Ok(Some(ShadowsocksPluginTransport::ShadowTls( - ShadowsocksShadowTlsTransport { - host, - password, - version, - alpn, - skip_cert_verify: plugin_bool_opt(&plugin.opts, "skip-cert-verify") - .unwrap_or(false), - certificate_fingerprint: plugin - .opts - .get("fingerprint") - .map(|value| parse_certificate_fingerprint(value)) - .transpose()?, - client_identity: plugin_tls_client_identity(&plugin.opts)?, - client_fingerprint: if version == 2 || version == 3 { - parsed_client_fingerprint - } else { - None - }, - }, - ))); - } - if name == "kcptun" { - let mut transport = ShadowsocksKcptunTransport::empty(); - if let Some(value) = plugin.opts.get("key") { - transport.key = value.trim().to_owned(); - } - if let Some(value) = plugin.opts.get("crypt") { - transport.crypt = value.trim().to_owned(); - } - if let Some(value) = plugin.opts.get("mode") { - transport.mode = value.trim().to_owned(); - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "conn") { - transport.conn = value; - } - if let Some(value) = plugin_u64_opt(&plugin.opts, "autoexpire") { - transport.auto_expire = value; - } - if let Some(value) = plugin_u64_opt(&plugin.opts, "scavengettl") { - transport.scavenge_ttl = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "mtu") { - transport.mtu = value; - } - if let Some(value) = plugin_u32_opt(&plugin.opts, "ratelimit") { - transport.rate_limit = value; - } - if let Some(value) = plugin_u16_opt(&plugin.opts, "sndwnd") { - transport.sndwnd = value; - } - if let Some(value) = plugin_u16_opt(&plugin.opts, "rcvwnd") { - transport.rcvwnd = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "datashard") { - transport.data_shard = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "parityshard") { - transport.parity_shard = value; - } - if let Some(value) = plugin_u32_opt(&plugin.opts, "dscp") { - transport.dscp = value; - } - if let Some(value) = plugin_bool_opt(&plugin.opts, "nocomp") { - transport.no_comp = value; - } - if let Some(value) = plugin_bool_opt(&plugin.opts, "acknodelay") { - transport.ack_nodelay = value; - } - if let Some(value) = plugin_i32_opt(&plugin.opts, "nodelay") { - transport.no_delay = value; - } - if let Some(value) = plugin_i32_opt(&plugin.opts, "interval") { - transport.interval = value; - } - if let Some(value) = plugin_i32_opt(&plugin.opts, "resend") { - transport.resend = value; - } - if let Some(value) = plugin_bool_opt(&plugin.opts, "nc") { - transport.no_congestion = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "sockbuf") { - transport.sockbuf = value; - } - if let Some(value) = plugin_u8_opt(&plugin.opts, "smuxver") { - transport.smux_ver = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "smuxbuf") { - transport.smux_buf = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "framesize") { - transport.frame_size = value; - } - if let Some(value) = plugin_usize_opt(&plugin.opts, "streambuf") { - transport.stream_buf = value; - } - if let Some(value) = plugin_u64_opt(&plugin.opts, "keepalive") { - transport.keep_alive = value; - } - transport.fill_defaults(); - return Ok(Some(ShadowsocksPluginTransport::Kcptun(transport))); - } - Ok(None) -} - -fn restls_transport( - plugin: &ShadowsocksPlugin, - client_fingerprint: Option<&str>, -) -> Result { - let host = plugin - .opts - .get("host") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| anyhow!("Shadowsocks restls plugin requires host"))? - .to_owned(); - let password = plugin - .opts - .get("password") - .map(|value| value.trim()) - .unwrap_or("") - .to_owned(); - let version_hint = plugin - .opts - .get("version-hint") - .map(|value| restls_version_hint(value)) - .transpose()? - .ok_or_else(|| anyhow!("Shadowsocks restls plugin requires version-hint tls12 or tls13"))?; - let script = plugin - .opts - .get("restls-script") - .map(|value| value.as_str()) - .unwrap_or(RESTLS_DEFAULT_SCRIPT); - let script = parse_restls_script(script)?; - let client_fingerprint = restls_client_fingerprint(client_fingerprint); - let secret = blake3::derive_key("restls-traffic-key", password.as_bytes()); - let session_tickets_disabled = version_hint == RestlsVersionHint::Tls13; - Ok(ShadowsocksRestlsTransport { - host, - password, - version_hint, - script, - client_fingerprint, - session_tickets_disabled, - secret, - }) -} - -fn restls_version_hint(value: &str) -> Result { - match value.trim().to_ascii_lowercase().as_str() { - "tls12" => Ok(RestlsVersionHint::Tls12), - "tls13" => Ok(RestlsVersionHint::Tls13), - _ => bail!("invalid Shadowsocks restls version-hint: expected tls12 or tls13"), - } -} - -fn restls_client_fingerprint(value: Option<&str>) -> RestlsClientFingerprint { - match value.map(str::trim).unwrap_or("") { - "firefox" => RestlsClientFingerprint::Firefox, - "safari" => RestlsClientFingerprint::Safari, - "ios" => RestlsClientFingerprint::Ios, - _ => RestlsClientFingerprint::Chrome, - } -} - -fn parse_restls_script(script: &str) -> Result> { - let mut lines = Vec::new(); - for raw in script.replace(' ', "").split(',') { - if raw.is_empty() { - continue; - } - let mut input = raw; - let base = restls_take_integer(&mut input) - .with_context(|| format!("invalid Shadowsocks restls script line {raw}"))?; - let mut target_len = RestlsTargetLength { base, random_range: 0 }; - if input.starts_with('~') || input.starts_with('?') { - let randomize_once = input.starts_with('?'); - input = &input[1..]; - let random_range = restls_take_integer(&mut input) - .with_context(|| format!("invalid Shadowsocks restls script line {raw}"))?; - if u32::from(base) + u32::from(random_range) > 32768 { - bail!("Shadowsocks restls script target length exceeds 32768"); - } - if randomize_once { - let offset = if random_range == 0 { - 0 - } else { - rand::thread_rng().gen_range(0..usize::from(random_range)) as u16 - }; - target_len.base = base + offset; - } else { - target_len.random_range = random_range; - } - } - let command = if input.is_empty() { - RestlsCommand::Noop - } else if let Some(rest) = input.strip_prefix('<') { - input = rest; - RestlsCommand::Response( - restls_take_integer(&mut input) - .with_context(|| format!("invalid Shadowsocks restls script line {raw}"))?, - ) - } else { - bail!("invalid Shadowsocks restls script line {raw}"); - }; - if !input.is_empty() { - bail!("invalid Shadowsocks restls script line {raw}"); - } - lines.push(RestlsScriptLine { target_len, command }); - } - Ok(lines) -} - -fn restls_take_integer(input: &mut &str) -> Result { - let digit_len = input - .bytes() - .take_while(|byte| byte.is_ascii_digit()) - .count(); - if digit_len == 0 { - bail!("missing integer"); - } - let (digits, rest) = input.split_at(digit_len); - let value = digits.parse::().context("invalid integer")?; - if value > 32768 { - bail!("integer exceeds 32768"); - } - *input = rest; - Ok(value) -} - -#[allow(dead_code)] -impl RestlsCommand { - fn to_bytes(self) -> [u8; RESTLS_CMD_LEN] { - match self { - RestlsCommand::Noop => [0, 0], - RestlsCommand::Response(value) => [1, value as u8], - } - } - - fn from_bytes(bytes: [u8; RESTLS_CMD_LEN]) -> Result { - match bytes[0] { - 0 => Ok(RestlsCommand::Noop), - 1 => Ok(RestlsCommand::Response(u16::from(bytes[1]))), - _ => bail!("unsupported Shadowsocks restls command"), - } - } -} - -#[allow(dead_code)] -impl RestlsTargetLength { - fn len(&self) -> usize { - let offset = if self.random_range == 0 { - 0 - } else { - rand::thread_rng().gen_range(0..usize::from(self.random_range)) - }; - usize::from(self.base) + offset - } -} - -#[allow(dead_code)] -fn restls_tls13_session_id( - secret: &[u8; 32], - key_shares: &[(u16, Vec)], - psk_labels: &[Vec], - mut session_id: [u8; 32], -) -> [u8; 32] { - let mut hmac = blake3::Hasher::new_keyed(secret); - for (group, data) in key_shares { - hmac.update(&group.to_be_bytes()); - hmac.update(data); - } - for label in psk_labels { - hmac.update(label); - } - let digest = hmac.finalize(); - session_id[..RESTLS_HANDSHAKE_MAC_LEN] - .copy_from_slice(&digest.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN]); - session_id -} - -#[allow(dead_code)] -fn restls_tls13_session_id_from_client_hello( - secret: &[u8; 32], - client_hello: &[u8], - seed_session_id: [u8; 32], -) -> Result<[u8; 32]> { - let material = restls_tls13_client_hello_auth_material(client_hello)?; - Ok(restls_tls13_session_id( - secret, - &material.key_shares, - &material.psk_labels, - seed_session_id, - )) -} - -fn restls_tls13_session_id_for_client_hello( - secret: &[u8; 32], - client_hello: &[u8], - error: &StdMutex>, -) -> [u8; 32] { - let mut session_id_seed = [0u8; 32]; - OsRng.fill_bytes(&mut session_id_seed[RESTLS_HANDSHAKE_MAC_LEN..]); - match restls_tls13_session_id_from_client_hello(secret, client_hello, session_id_seed) { - Ok(session_id) => session_id, - Err(err) => { - let message = - format!("failed to derive Restls TLS 1.3 session id from ClientHello: {err:#}"); - tracing::warn!(error = %err, "failed to derive Restls TLS 1.3 session id from ClientHello"); - if let Ok(mut slot) = error.lock() { - *slot = Some(message); - } - session_id_seed - } - } -} - -fn restls_tls12_session_id_for_client_hello( - secret: &[u8; 32], - _client_hello: &[u8], - materials: &[Vec], - error: &StdMutex>, -) -> [u8; 32] { - match restls_tls12_session_id(secret, materials) { - Ok(session_id) => session_id, - Err(err) => { - let message = - format!("failed to derive Restls TLS 1.2 session id from key materials: {err:#}"); - tracing::warn!(error = %err, "failed to derive Restls TLS 1.2 session id"); - if let Ok(mut slot) = error.lock() { - *slot = Some(message); - } - let mut fallback = [0u8; 32]; - OsRng.fill_bytes(&mut fallback); - fallback - } - } -} - -fn take_restls_session_id_error(error: &StdMutex>) -> Option { - error.lock().ok().and_then(|mut slot| slot.take()) -} - -#[allow(dead_code)] -fn restls_tls13_sign_client_hello_record( - secret: &[u8; 32], - bytes: &[u8], -) -> io::Result>> { - if bytes.len() < TLS_RECORD_HEADER_LEN { - return Ok(None); - } - if bytes[0] != 0x16 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Restls first TLS record is not a handshake record", - )); - } - let payload_len = usize::from(u16::from_be_bytes([bytes[3], bytes[4]])); - let record_len = TLS_RECORD_HEADER_LEN + payload_len; - if bytes.len() < record_len { - return Ok(None); - } - let payload = &bytes[TLS_RECORD_HEADER_LEN..record_len]; - if payload.first().copied() != Some(0x01) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Restls first handshake message is not ClientHello", - )); - } - let session_id_length_index = SHADOW_TLS_V3_SESSION_ID_START - 1; - if payload.get(session_id_length_index).copied() != Some(32) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Restls browser ClientHello did not reserve a 32-byte session ID", - )); - } - if payload.len() < SHADOW_TLS_V3_SESSION_ID_START + 32 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Restls browser ClientHello session ID is truncated", - )); - } - let mut seed_session_id = [0u8; 32]; - seed_session_id.copy_from_slice( - &payload[SHADOW_TLS_V3_SESSION_ID_START..SHADOW_TLS_V3_SESSION_ID_START + 32], - ); - let session_id = restls_tls13_session_id_from_client_hello(secret, payload, seed_session_id) - .map_err(|err| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("failed to derive Restls TLS 1.3 session ID from ClientHello: {err:#}"), - ) - })?; - let mut signed = bytes.to_vec(); - let session_id_start = TLS_RECORD_HEADER_LEN + SHADOW_TLS_V3_SESSION_ID_START; - signed[session_id_start..session_id_start + 32].copy_from_slice(&session_id); - Ok(Some(signed)) -} - -#[allow(dead_code)] -fn restls_tls13_client_hello_auth_material( - client_hello: &[u8], -) -> Result { - let mut input = client_hello; - let handshake_type = restls_read_u8(&mut input) - .context("Shadowsocks restls ClientHello is missing handshake type")?; - if handshake_type != 0x01 { - bail!("Shadowsocks restls auth material requires a ClientHello handshake"); - } - let handshake_len = usize::try_from(restls_read_u24(&mut input)?).expect("u24 fits into usize"); - let handshake = restls_read_exact(&mut input, handshake_len) - .context("Shadowsocks restls ClientHello body is truncated")?; - if !input.is_empty() { - bail!("Shadowsocks restls ClientHello has trailing bytes"); - } - - let mut body = handshake; - restls_read_exact(&mut body, 2).context("Shadowsocks restls ClientHello misses version")?; - restls_read_exact(&mut body, 32).context("Shadowsocks restls ClientHello misses random")?; - let session_id_len = usize::from( - restls_read_u8(&mut body).context("Shadowsocks restls ClientHello misses session ID")?, - ); - restls_read_exact(&mut body, session_id_len) - .context("Shadowsocks restls ClientHello session ID is truncated")?; - let cipher_suites_len = usize::from( - restls_read_u16(&mut body) - .context("Shadowsocks restls ClientHello misses cipher suites")?, - ); - if cipher_suites_len % 2 != 0 { - bail!("Shadowsocks restls ClientHello cipher suites length is invalid"); - } - restls_read_exact(&mut body, cipher_suites_len) - .context("Shadowsocks restls ClientHello cipher suites are truncated")?; - let compression_len = usize::from( - restls_read_u8(&mut body).context("Shadowsocks restls ClientHello misses compression")?, - ); - restls_read_exact(&mut body, compression_len) - .context("Shadowsocks restls ClientHello compression list is truncated")?; - let extensions_len = usize::from( - restls_read_u16(&mut body).context("Shadowsocks restls ClientHello misses extensions")?, - ); - let mut extensions = restls_read_exact(&mut body, extensions_len) - .context("Shadowsocks restls ClientHello extensions are truncated")?; - if !body.is_empty() { - bail!("Shadowsocks restls ClientHello body has trailing bytes"); - } - - let mut key_shares = Vec::new(); - let mut psk_labels = Vec::new(); - while !extensions.is_empty() { - let extension_type = restls_read_u16(&mut extensions) - .context("Shadowsocks restls ClientHello extension type is truncated")?; - let extension_len = usize::from(restls_read_u16(&mut extensions).with_context(|| { - format!("Shadowsocks restls ClientHello extension {extension_type} misses length") - })?); - let mut extension = - restls_read_exact(&mut extensions, extension_len).with_context(|| { - format!("Shadowsocks restls ClientHello extension {extension_type} is truncated") - })?; - match extension_type { - 0x0033 => key_shares = restls_tls13_key_share_material(&mut extension)?, - 0x0029 => psk_labels = restls_tls13_psk_labels(&mut extension)?, - _ => {} - } - if !extension.is_empty() { - bail!("Shadowsocks restls ClientHello extension {extension_type} has trailing bytes"); - } - } - - if key_shares.is_empty() { - bail!("Shadowsocks restls TLS 1.3 ClientHello has no key share material"); - } - Ok(RestlsTls13ClientHelloAuthMaterial { key_shares, psk_labels }) -} - -#[allow(dead_code)] -fn restls_tls13_key_share_material(extension: &mut &[u8]) -> Result)>> { - let list_len = usize::from( - restls_read_u16(extension) - .context("Shadowsocks restls key_share extension misses length")?, - ); - let mut shares = restls_read_exact(extension, list_len) - .context("Shadowsocks restls key_share extension is truncated")?; - let mut material = Vec::new(); - while !shares.is_empty() { - let group = restls_read_u16(&mut shares) - .context("Shadowsocks restls key_share entry misses group")?; - let key_len = usize::from( - restls_read_u16(&mut shares) - .context("Shadowsocks restls key_share entry misses key length")?, - ); - let key = restls_read_exact(&mut shares, key_len) - .context("Shadowsocks restls key_share entry is truncated")?; - material.push((group, key.to_vec())); - } - Ok(material) -} - -#[allow(dead_code)] -fn restls_tls13_psk_labels(extension: &mut &[u8]) -> Result>> { - let identities_len = usize::from( - restls_read_u16(extension) - .context("Shadowsocks restls pre_shared_key extension misses identities length")?, - ); - let mut identities = restls_read_exact(extension, identities_len) - .context("Shadowsocks restls pre_shared_key identities are truncated")?; - let mut labels = Vec::new(); - while !identities.is_empty() { - let label_len = usize::from( - restls_read_u16(&mut identities) - .context("Shadowsocks restls pre_shared_key identity misses label length")?, - ); - let label = restls_read_exact(&mut identities, label_len) - .context("Shadowsocks restls pre_shared_key identity label is truncated")?; - labels.push(label.to_vec()); - restls_read_exact(&mut identities, 4) - .context("Shadowsocks restls pre_shared_key identity misses ticket age")?; - } - let binders_len = usize::from( - restls_read_u16(extension) - .context("Shadowsocks restls pre_shared_key extension misses binders length")?, - ); - restls_read_exact(extension, binders_len) - .context("Shadowsocks restls pre_shared_key binders are truncated")?; - Ok(labels) -} - -#[allow(dead_code)] -fn restls_read_u8(input: &mut &[u8]) -> Result { - let (byte, rest) = input - .split_first() - .ok_or_else(|| anyhow!("unexpected end of Restls TLS data"))?; - *input = rest; - Ok(*byte) -} - -#[allow(dead_code)] -fn restls_read_u16(input: &mut &[u8]) -> Result { - let bytes = restls_read_exact(input, 2)?; - Ok(u16::from_be_bytes( - bytes.try_into().expect("fixed u16 byte length"), - )) -} - -#[allow(dead_code)] -fn restls_read_u24(input: &mut &[u8]) -> Result { - let bytes = restls_read_exact(input, 3)?; - Ok((u32::from(bytes[0]) << 16) | (u32::from(bytes[1]) << 8) | u32::from(bytes[2])) -} - -#[allow(dead_code)] -fn restls_read_exact<'a>(input: &mut &'a [u8], len: usize) -> Result<&'a [u8]> { - if input.len() < len { - bail!("unexpected end of Restls TLS data"); - } - let (head, tail) = input.split_at(len); - *input = tail; - Ok(head) -} - -#[allow(dead_code)] -fn restls_tls12_session_id(secret: &[u8; 32], materials: &[Vec]) -> Result<[u8; 32]> { - let layout: &[usize] = match materials.len() { - 3 => &RESTLS_TLS12_CLIENT_AUTH_LAYOUT_3, - 4 => &RESTLS_TLS12_CLIENT_AUTH_LAYOUT_4, - len => bail!("Shadowsocks restls TLS 1.2 auth requires 3 or 4 key materials, got {len}"), - }; - let mut session_id = [0u8; 32]; - for (index, material) in materials.iter().enumerate() { - let mut hmac = blake3::Hasher::new_keyed(secret); - hmac.update(material); - let digest = hmac.finalize(); - let start = layout[index]; - let end = layout[index + 1]; - session_id[start..end].copy_from_slice(&digest.as_bytes()[..end - start]); - } - Ok(session_id) -} - -#[allow(dead_code)] -fn restls_unmask_server_auth_record( - secret: &[u8; 32], - server_random: &[u8; 32], - record: &[u8], - tls12_gcm: bool, -) -> Result { - if record.len() < TLS_RECORD_HEADER_LEN { - bail!("Shadowsocks restls server auth record is too short"); - } - let mut hmac = blake3::Hasher::new_keyed(secret); - hmac.update(server_random); - let digest = hmac.finalize(); - let mask = &digest.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN]; - let mut unmasked = record.to_vec(); - let mut tls12_gcm_server_disable_counter = false; - let offset = if tls12_gcm - && record.len() >= TLS_RECORD_HEADER_LEN + 8 - && record[TLS_RECORD_HEADER_LEN..TLS_RECORD_HEADER_LEN + 8] == [0u8; 8] - { - TLS_RECORD_HEADER_LEN + 8 - } else { - tls12_gcm_server_disable_counter = tls12_gcm; - TLS_RECORD_HEADER_LEN - }; - xor_with_restls_mask(&mut unmasked[offset..], mask); - Ok(RestlsServerAuthRecord { - record: unmasked, - tls12_gcm_server_disable_counter, - }) -} - -#[allow(dead_code)] -fn restls_server_hello_random(record: &[u8]) -> Option<[u8; 32]> { - if record.len() < TLS_RECORD_HEADER_LEN + 1 + 3 + 2 + 32 { - return None; - } - if record[0] != 0x16 || record[TLS_RECORD_HEADER_LEN] != 0x02 { - return None; - } - let payload_len = usize::from(u16::from_be_bytes([record[3], record[4]])); - if payload_len + TLS_RECORD_HEADER_LEN != record.len() { - return None; - } - let handshake_len = (usize::from(record[TLS_RECORD_HEADER_LEN + 1]) << 16) - | (usize::from(record[TLS_RECORD_HEADER_LEN + 2]) << 8) - | usize::from(record[TLS_RECORD_HEADER_LEN + 3]); - if handshake_len + 4 > payload_len || handshake_len < 34 { - return None; - } - let random_start = TLS_RECORD_HEADER_LEN + 1 + 3 + 2; - let mut server_random = [0u8; 32]; - server_random.copy_from_slice(&record[random_start..random_start + 32]); - Some(server_random) -} - -#[allow(dead_code)] -fn restls_tls_records(mut bytes: &[u8]) -> Vec<&[u8]> { - let mut records = Vec::new(); - while bytes.len() >= TLS_RECORD_HEADER_LEN { - let record_len = - TLS_RECORD_HEADER_LEN + usize::from(u16::from_be_bytes([bytes[3], bytes[4]])); - if bytes.len() < record_len { - break; - } - let (record, rest) = bytes.split_at(record_len); - records.push(record); - bytes = rest; - } - records -} - -#[allow(dead_code)] -impl RestlsApplicationCodec { - fn new(secret: [u8; 32], server_random: [u8; 32], tls12_gcm: bool) -> Self { - Self { - secret, - server_random, - to_server_counter: 0, - to_client_counter: 0, - tls12_gcm, - tls12_gcm_to_client_disable_counter: false, - client_finished_auth: None, - } - } - - fn set_client_finished_auth(&mut self, record: Vec) { - self.client_finished_auth = Some(record); - } - - fn set_tls12_gcm_to_client_disable_counter(&mut self, disabled: bool) { - self.tls12_gcm_to_client_disable_counter = disabled; - } - - fn auth_header_hash(&self, direction: RestlsRecordDirection) -> blake3::Hasher { - let mut hasher = blake3::Hasher::new_keyed(&self.secret); - hasher.update(&self.server_random); - let mut counter = [0u8; 8]; - match direction { - RestlsRecordDirection::ToServer => { - hasher.update(b"client-to-server"); - counter.copy_from_slice(&self.to_server_counter.to_be_bytes()); - } - RestlsRecordDirection::ToClient => { - hasher.update(b"server-to-client"); - counter.copy_from_slice(&self.to_client_counter.to_be_bytes()); - } - } - hasher.update(&counter); - hasher - } - - fn build_to_server_record( - &mut self, - data: &[u8], - script: &[RestlsScriptLine], - ) -> Result { - self.build_to_server_record_inner(data, script, false) - } - - fn build_to_server_fake_response_record( - &mut self, - script: &[RestlsScriptLine], - ) -> Result { - self.build_to_server_record_inner(&[], script, true) - } - - fn build_to_server_record_inner( - &mut self, - data: &[u8], - script: &[RestlsScriptLine], - allow_empty: bool, - ) -> Result { - if data.is_empty() && !allow_empty { - bail!("Shadowsocks restls application record requires non-empty data"); - } - let mut data_len = data.len(); - let mut padding_len = 0usize; - let mut command = RestlsCommand::Noop; - if let Some(line) = script.get(self.to_server_counter as usize) { - data_len = line.target_len.len(); - command = line.command; - } - if data_len == 0 { - padding_len = 19 + rand::thread_rng().gen_range(0..100); - } - if data.len() < data_len { - padding_len = data_len - data.len(); - data_len = data.len(); - } - - let auth_header_len = RESTLS_APP_DATA_AUTH_HEADER_LEN + if self.tls12_gcm { 8 } else { 0 }; - let mut payload_len = data_len + padding_len + auth_header_len; - if payload_len > RESTLS_MAX_PLAINTEXT { - payload_len = RESTLS_MAX_PLAINTEXT; - } - data_len = payload_len - .checked_sub(auth_header_len + padding_len) - .ok_or_else(|| anyhow!("Shadowsocks restls target length is too large"))?; - let payload_len_u16 = u16::try_from(payload_len) - .context("Shadowsocks restls application record is too large")?; - - let header_len = TLS_RECORD_HEADER_LEN + if self.tls12_gcm { 8 } else { 0 }; - let mut record = Vec::with_capacity(TLS_RECORD_HEADER_LEN + payload_len); - record.push(TLS_APPLICATION_DATA_RECORD_TYPE); - record.extend_from_slice(&TLS12_RECORD_VERSION); - record.extend_from_slice(&payload_len_u16.to_be_bytes()); - if self.tls12_gcm { - record.extend_from_slice(&(self.to_server_counter + 1).to_be_bytes()); - } - record.resize(header_len + RESTLS_APP_DATA_AUTH_HEADER_LEN, 0); - record.extend_from_slice(&data[..data_len]); - let padding_start = record.len(); - record.resize(padding_start + padding_len, 0); - OsRng.fill_bytes(&mut record[padding_start..]); - self.write_auth_header( - &mut record, - data_len, - command, - header_len, - RestlsRecordDirection::ToServer, - )?; - self.to_server_counter += 1; - Ok(RestlsWriteResult { - record, - consumed: data_len, - command, - }) - } - - fn decode_to_client_record(&mut self, record: &[u8]) -> Result { - let frame = self.decode_record(record, RestlsRecordDirection::ToClient)?; - self.to_client_counter += 1; - Ok(frame) - } - - fn decode_to_server_record(&mut self, record: &[u8]) -> Result { - let frame = self.decode_record(record, RestlsRecordDirection::ToServer)?; - self.to_server_counter += 1; - Ok(frame) - } - - #[cfg(test)] - fn build_to_client_test_record( - &mut self, - data: &[u8], - command: RestlsCommand, - ) -> Result> { - let payload_len = - RESTLS_APP_DATA_AUTH_HEADER_LEN + if self.tls12_gcm { 8 } else { 0 } + data.len(); - let payload_len_u16 = u16::try_from(payload_len) - .context("Shadowsocks restls application record is too large")?; - let header_len = TLS_RECORD_HEADER_LEN + if self.tls12_gcm { 8 } else { 0 }; - let mut record = Vec::with_capacity(TLS_RECORD_HEADER_LEN + payload_len); - record.push(TLS_APPLICATION_DATA_RECORD_TYPE); - record.extend_from_slice(&TLS12_RECORD_VERSION); - record.extend_from_slice(&payload_len_u16.to_be_bytes()); - if self.tls12_gcm { - record.extend_from_slice(&(self.to_client_counter + 1).to_be_bytes()); - } - record.resize(header_len + RESTLS_APP_DATA_AUTH_HEADER_LEN, 0); - record.extend_from_slice(data); - self.write_auth_header( - &mut record, - data.len(), - command, - header_len, - RestlsRecordDirection::ToClient, - )?; - self.to_client_counter += 1; - Ok(record) - } - - fn write_auth_header( - &mut self, - record: &mut [u8], - data_len: usize, - command: RestlsCommand, - header_len: usize, - direction: RestlsRecordDirection, - ) -> Result<()> { - let (header, body) = record.split_at_mut(header_len); - let sample_len = 32.min(body[RESTLS_APP_DATA_OFFSET..].len()); - let mut hmac_mask = self.auth_header_hash(direction); - hmac_mask.update(&body[RESTLS_APP_DATA_OFFSET..RESTLS_APP_DATA_OFFSET + sample_len]); - let mask = hmac_mask.finalize(); - let mask = &mask.as_bytes()[..RESTLS_MASK_LEN]; - - body[RESTLS_APP_DATA_LEN_OFFSET..RESTLS_APP_DATA_LEN_OFFSET + 2] - .copy_from_slice(&(data_len as u16).to_be_bytes()); - body[RESTLS_APP_DATA_LEN_OFFSET + 2..RESTLS_APP_DATA_OFFSET] - .copy_from_slice(&command.to_bytes()); - xor_with_restls_mask( - &mut body[RESTLS_APP_DATA_LEN_OFFSET..RESTLS_APP_DATA_OFFSET], - mask, - ); - - let mut hmac_auth = self.auth_header_hash(direction); - if direction == RestlsRecordDirection::ToServer { - if let Some(client_finished) = self.client_finished_auth.take() { - hmac_auth.update(&client_finished); - } - } - hmac_auth.update(header); - hmac_auth.update(&body[RESTLS_APP_DATA_LEN_OFFSET..]); - let auth_mac = hmac_auth.finalize(); - body[..RESTLS_APP_DATA_MAC_LEN] - .copy_from_slice(&auth_mac.as_bytes()[..RESTLS_APP_DATA_MAC_LEN]); - Ok(()) - } - - fn decode_record( - &mut self, - record: &[u8], - direction: RestlsRecordDirection, - ) -> Result { - if record.len() < TLS_RECORD_HEADER_LEN + RESTLS_APP_DATA_AUTH_HEADER_LEN { - bail!("Shadowsocks restls application record is too short"); - } - if record[0] != TLS_APPLICATION_DATA_RECORD_TYPE { - bail!("Shadowsocks restls record is not application data"); - } - if record[1..3] != TLS12_RECORD_VERSION { - bail!("Shadowsocks restls record has invalid TLS record version"); - } - let payload_len = usize::from(u16::from_be_bytes([record[3], record[4]])); - if payload_len + TLS_RECORD_HEADER_LEN != record.len() { - bail!("Shadowsocks restls application record length mismatch"); - } - - let mut header_len = TLS_RECORD_HEADER_LEN; - let tls12_gcm_counter_present = self.tls12_gcm - && !(direction == RestlsRecordDirection::ToClient - && self.tls12_gcm_to_client_disable_counter); - if tls12_gcm_counter_present { - if record.len() < TLS_RECORD_HEADER_LEN + 8 + RESTLS_APP_DATA_AUTH_HEADER_LEN { - bail!("Shadowsocks restls GCM application record is too short"); - } - let expected_counter = match direction { - RestlsRecordDirection::ToServer => self.to_server_counter + 1, - RestlsRecordDirection::ToClient => self.to_client_counter + 1, - }; - let nonce = u64::from_be_bytes( - record[TLS_RECORD_HEADER_LEN..TLS_RECORD_HEADER_LEN + 8] - .try_into() - .expect("nonce slice has fixed length"), - ); - if nonce != expected_counter { - bail!("Shadowsocks restls GCM application record counter mismatch"); - } - header_len += 8; - } - - let header = &record[..header_len]; - let mut body = record[header_len..].to_vec(); - let mut hmac_auth = self.auth_header_hash(direction); - if direction == RestlsRecordDirection::ToServer { - if let Some(client_finished) = self.client_finished_auth.as_ref() { - hmac_auth.update(client_finished); - } - } - hmac_auth.update(header); - hmac_auth.update(&body[RESTLS_APP_DATA_LEN_OFFSET..]); - let auth_mac = hmac_auth.finalize(); - if auth_mac.as_bytes()[..RESTLS_APP_DATA_MAC_LEN] - .ct_eq(&body[..RESTLS_APP_DATA_MAC_LEN]) - .unwrap_u8() - != 1 - { - bail!("Shadowsocks restls application record authentication failed"); - } - if direction == RestlsRecordDirection::ToServer { - self.client_finished_auth = None; - } - - let sample_len = 32.min(body[RESTLS_APP_DATA_OFFSET..].len()); - let mut hmac_mask = self.auth_header_hash(direction); - hmac_mask.update(&body[RESTLS_APP_DATA_OFFSET..RESTLS_APP_DATA_OFFSET + sample_len]); - let mask = hmac_mask.finalize(); - xor_with_restls_mask( - &mut body[RESTLS_APP_DATA_LEN_OFFSET..RESTLS_APP_DATA_OFFSET], - &mask.as_bytes()[..RESTLS_MASK_LEN], - ); - let data_len = usize::from(u16::from_be_bytes([ - body[RESTLS_APP_DATA_LEN_OFFSET], - body[RESTLS_APP_DATA_LEN_OFFSET + 1], - ])); - if body.len() < RESTLS_APP_DATA_OFFSET + data_len { - bail!("Shadowsocks restls application record data length exceeds payload"); - } - let command = RestlsCommand::from_bytes([ - body[RESTLS_APP_DATA_LEN_OFFSET + 2], - body[RESTLS_APP_DATA_LEN_OFFSET + 3], - ])?; - Ok(RestlsApplicationFrame { - data: body[RESTLS_APP_DATA_OFFSET..RESTLS_APP_DATA_OFFSET + data_len].to_vec(), - command, - }) - } -} - -#[allow(dead_code)] -impl RestlsCommand { - fn need_interrupt(self) -> bool { - matches!(self, RestlsCommand::Response(_)) - } -} - -#[allow(dead_code)] -impl RestlsStreamState { - fn new(codec: RestlsApplicationCodec, script: Vec) -> Self { - Self { - codec, - script, - send_buffer: Vec::new(), - write_pending: false, - } - } - - fn write(&mut self, data_new: &[u8]) -> Result { - let fake_response = data_new == RESTLS_RANDOM_RESPONSE_MAGIC; - let accepted = if fake_response { 0 } else { data_new.len() }; - let data_new = if fake_response { &[][..] } else { data_new }; - if fake_response { - self.write_pending = false; - } - - if self.write_pending { - self.send_buffer.extend_from_slice(data_new); - return Ok(RestlsStreamWriteResult { accepted, records: Vec::new() }); - } - - let mut data = if self.send_buffer.is_empty() { - data_new.to_vec() - } else { - self.send_buffer.extend_from_slice(data_new); - std::mem::take(&mut self.send_buffer) - }; - let mut records = Vec::new(); - let mut fake_response_pending = fake_response; - - while !data.is_empty() || fake_response_pending { - let record = if fake_response_pending { - self.codec - .build_to_server_fake_response_record(&self.script)? - } else { - self.codec.build_to_server_record(&data, &self.script)? - }; - let consumed = record.consumed; - let command = record.command; - records.push(record.record); - if command.need_interrupt() && !fake_response_pending { - self.write_pending = true; - } - if consumed > data.len() { - bail!("Shadowsocks restls consumed more data than available"); - } - data.drain(..consumed); - if command.need_interrupt() && !fake_response_pending { - break; - } - fake_response_pending = false; - } - - self.send_buffer = data; - Ok(RestlsStreamWriteResult { accepted, records }) - } - - fn receive_command( - &mut self, - command: RestlsCommand, - resumed_pending_write: bool, - ) -> Result>> { - let mut records = Vec::new(); - let RestlsCommand::Response(count) = command else { - return Ok(records); - }; - let count = restls_response_repeat_count(count, resumed_pending_write); - for _ in 0..count { - records.extend(self.write(RESTLS_RANDOM_RESPONSE_MAGIC)?.records); - } - Ok(records) - } - - fn resume_pending_write(&mut self) -> Result { - self.write_pending = false; - self.write(&[]) - } - - fn read_to_client_record(&mut self, record: &[u8]) -> Result { - let frame = self.codec.decode_to_client_record(record)?; - let mut records = Vec::new(); - let mut resumed_pending_write = false; - if self.write_pending { - let resumed = self.resume_pending_write()?; - resumed_pending_write = !resumed.records.is_empty(); - records.extend(resumed.records); - } - records.extend(self.receive_command(frame.command, resumed_pending_write)?); - Ok(RestlsStreamReadResult { - data: frame.data, - records, - resumed_pending_write, - }) - } -} - -fn restls_response_repeat_count(count: u16, resumed_pending_write: bool) -> usize { - let mut count = (count as u8) as i8; - if resumed_pending_write { - count = count.wrapping_sub(1); - } - if count <= 0 { - 0 - } else { - count as usize - } -} - -#[allow(dead_code)] -fn xor_with_restls_mask(data: &mut [u8], mask: &[u8]) { - for (byte, mask) in data.iter_mut().zip(mask.iter()) { - *byte ^= *mask; - } -} - -fn plugin_bool_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key).map(|value| { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "y" | "on" - ) - }) -} - -fn plugin_u8_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_u16_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_u32_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_u64_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_usize_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_i32_opt(opts: &HashMap, key: &str) -> Option { - opts.get(key) - .and_then(|value| value.trim().parse::().ok()) -} - -fn plugin_list_opt(opts: &HashMap, key: &str) -> Option> { - opts.get(key).map(|value| { - value - .split(',') - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .collect() - }) -} - -fn plugin_tls_client_identity(opts: &HashMap) -> Result> { - let certificate = opts - .get("certificate") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()); - let private_key = opts - .get("private-key") - .map(|value| value.trim()) - .filter(|value| !value.is_empty()); - match (certificate, private_key) { - (None, None) => Ok(None), - (Some(certificate), Some(private_key)) => Ok(Some(TlsClientIdentity { - certificate: certificate.to_owned(), - private_key: private_key.to_owned(), - })), - _ => bail!("Shadowsocks TLS plugin certificate and private-key must be provided together"), - } -} - -fn plugin_ech_config(opts: &HashMap) -> Result> { - let enabled = plugin_bool_opt(opts, "ech-opts.enable") - .or_else(|| plugin_bool_opt(opts, "ech.enable")) - .unwrap_or(false); - if !enabled { - return Ok(None); - } - - let config_list = opts - .get("ech-opts.config") - .or_else(|| opts.get("ech.config")) - .map(|value| { - BASE64_STANDARD - .decode(value.trim()) - .context("failed to decode Shadowsocks websocket ECH config") - }) - .transpose()?; - let query_server_name = opts - .get("ech-opts.query-server-name") - .or_else(|| opts.get("ech.query-server-name")) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned); - - Ok(Some(ShadowsocksEchConfig { - config_list, - query_server_name, - })) -} - -fn plugin_headers(opts: &HashMap) -> Vec<(String, String)> { - opts.iter() - .filter_map(|(key, value)| { - key.strip_prefix("headers.") - .or_else(|| key.strip_prefix("header.")) - .map(|header| (header.to_owned(), value.to_owned())) - }) - .collect() -} - -fn simple_obfs_http_request(host: &str, port: u16, payload: &[u8]) -> Vec { - let mut websocket_key = [0u8; 16]; - OsRng.fill_bytes(&mut websocket_key); - let user_agent_minor = OsRng.next_u32() % 54; - let user_agent_patch = OsRng.next_u32() % 2; - let request_host = if port == 80 { - host.to_owned() - } else { - format!("{host}:{port}") - }; - let mut request = Vec::new(); - request.extend_from_slice(format!("GET http://{host}/ HTTP/1.1\r\n").as_bytes()); - request.extend_from_slice(format!("Host: {request_host}\r\n").as_bytes()); - request.extend_from_slice( - format!("User-Agent: curl/7.{user_agent_minor}.{user_agent_patch}\r\n").as_bytes(), - ); - request.extend_from_slice(b"Upgrade: websocket\r\n"); - request.extend_from_slice(b"Connection: Upgrade\r\n"); - request.extend_from_slice( - format!( - "Sec-WebSocket-Key: {}\r\n", - BASE64_URL_SAFE.encode(websocket_key) - ) - .as_bytes(), - ); - request.extend_from_slice(format!("Content-Length: {}\r\n\r\n", payload.len()).as_bytes()); - request.extend_from_slice(payload); - request -} - -fn simple_obfs_tls_record(payload: &[u8]) -> io::Result> { - if payload.len() > u16::MAX as usize { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS payload is too large", - )); - } - let mut record = Vec::with_capacity(5 + payload.len()); - record.extend_from_slice(&[0x17, 0x03, 0x03]); - record.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - record.extend_from_slice(payload); - Ok(record) -} - -fn shadow_tls_v2_record( - payload: &[u8], - len: usize, - first_write_hmac: Option<[u8; 8]>, -) -> io::Result> { - if len > SHADOW_TLS_MAX_RECORD_PAYLOAD || len > payload.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "ShadowTLS v2 payload is too large", - )); - } - let prefix_len = first_write_hmac.as_ref().map(|_| 8).unwrap_or(0); - let record_len = prefix_len + len; - let mut record = Vec::with_capacity(5 + record_len); - record.extend_from_slice(&[0x17, 0x03, 0x03]); - record.extend_from_slice(&(record_len as u16).to_be_bytes()); - if let Some(prefix) = first_write_hmac { - record.extend_from_slice(&prefix); - } - record.extend_from_slice(&payload[..len]); - Ok(record) -} - -fn shadow_tls_v3_record( - payload: &[u8], - len: usize, - hmac_add: &mut hmac::Context, -) -> io::Result> { - if len > SHADOW_TLS_MAX_RECORD_PAYLOAD || len > payload.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "ShadowTLS v3 payload is too large", - )); - } - let payload = &payload[..len]; - let mut record = Vec::with_capacity(SHADOW_TLS_V3_HMAC_HEADER_SIZE + payload.len()); - record.extend_from_slice(&[0x17, 0x03, 0x03]); - record.extend_from_slice(&((SHADOW_TLS_V3_HMAC_SIZE + payload.len()) as u16).to_be_bytes()); - hmac_add.update(payload); - let hmac_prefix = shadow_tls_v3_hmac_prefix(hmac_add, SHADOW_TLS_V3_HMAC_SIZE); - hmac_add.update(&hmac_prefix); - record.extend_from_slice(&hmac_prefix); - record.extend_from_slice(payload); - Ok(record) -} - -fn shadow_tls_v3_generate_session_id(password: &str, client_hello: &[u8]) -> [u8; 32] { - let mut session_id = [0u8; SHADOW_TLS_V3_SESSION_ID_SIZE]; - OsRng.fill_bytes(&mut session_id[..SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE]); - if client_hello.len() >= SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE { - let mut context = shadow_tls_v3_hmac(password, b"", b""); - context.update(&client_hello[..SHADOW_TLS_V3_SESSION_ID_START]); - context.update(&session_id); - context.update( - &client_hello[SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE..], - ); - let prefix = shadow_tls_v3_hmac_prefix(&context, SHADOW_TLS_V3_HMAC_SIZE); - session_id[SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE..] - .copy_from_slice(&prefix); - } - session_id -} - -#[allow(dead_code)] -fn shadow_tls_v3_sign_client_hello_record( - password: &str, - bytes: &[u8], -) -> io::Result>> { - if bytes.len() < SHADOW_TLS_V3_TLS_HEADER_SIZE { - return Ok(None); - } - if bytes[0] != 0x16 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 first TLS record is not a handshake record", - )); - } - let payload_len = u16::from_be_bytes([bytes[3], bytes[4]]) as usize; - let record_len = SHADOW_TLS_V3_TLS_HEADER_SIZE + payload_len; - if bytes.len() < record_len { - return Ok(None); - } - let payload = &bytes[SHADOW_TLS_V3_TLS_HEADER_SIZE..record_len]; - if payload.first().copied() != Some(0x01) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 first handshake message is not ClientHello", - )); - } - let session_id_length_index = SHADOW_TLS_V3_SESSION_ID_START - 1; - if payload.get(session_id_length_index).copied() != Some(SHADOW_TLS_V3_SESSION_ID_SIZE as u8) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 browser ClientHello did not reserve a 32-byte session ID", - )); - } - if payload.len() < SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "ShadowTLS v3 browser ClientHello session ID is truncated", - )); - } - let session_id = shadow_tls_v3_generate_session_id(password, payload); - let mut signed = bytes.to_vec(); - let session_id_start = SHADOW_TLS_V3_TLS_HEADER_SIZE + SHADOW_TLS_V3_SESSION_ID_START; - signed[session_id_start..session_id_start + SHADOW_TLS_V3_SESSION_ID_SIZE] - .copy_from_slice(&session_id); - Ok(Some(signed)) -} - -fn shadow_tls_v3_hmac(password: &str, first: &[u8], second: &[u8]) -> hmac::Context { - let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, password.as_bytes()); - let mut context = hmac::Context::with_key(&key); - context.update(first); - context.update(second); - context -} - -fn shadow_tls_v3_hmac_prefix(context: &hmac::Context, len: usize) -> Vec { - let digest = context.clone().sign(); - digest.as_ref()[..len].to_vec() -} - -fn shadow_tls_v3_application_data_hmac(frame: &[u8], context: &hmac::Context) -> Option> { - if frame.len() < SHADOW_TLS_V3_HMAC_HEADER_SIZE - || frame[0] != 0x17 - || frame[1] != 0x03 - || frame[2] != 0x03 - { - return None; - } - let mut candidate = context.clone(); - candidate.update(&frame[SHADOW_TLS_V3_HMAC_HEADER_SIZE..]); - Some(shadow_tls_v3_hmac_prefix( - &candidate, - SHADOW_TLS_V3_HMAC_SIZE, - )) -} - -fn shadow_tls_v3_kdf( - password: &str, - server_random: &[u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE], -) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(password.as_bytes()); - hasher.update(server_random); - hasher.finalize().into() -} - -fn shadow_tls_v3_xor(data: &mut [u8], key: &[u8; 32]) { - for (index, value) in data.iter_mut().enumerate() { - *value ^= key[index % key.len()]; - } -} - -fn shadow_tls_v3_server_hello_supports_tls13(frame: &[u8]) -> bool { - if frame.len() < SHADOW_TLS_V3_SESSION_ID_LENGTH_INDEX { - return false; - } - let mut index = SHADOW_TLS_V3_SESSION_ID_LENGTH_INDEX; - let Some(session_id_len) = frame.get(index).copied().map(usize::from) else { - return false; - }; - index += 1 + session_id_len; - if frame.len() < index + 3 { - return false; - } - index += 3; - if frame.len() < index + 2 { - return false; - } - let extensions_len = u16::from_be_bytes([frame[index], frame[index + 1]]) as usize; - index += 2; - let end = index.saturating_add(extensions_len).min(frame.len()); - while index + 4 <= end { - let extension_type = u16::from_be_bytes([frame[index], frame[index + 1]]); - let extension_len = u16::from_be_bytes([frame[index + 2], frame[index + 3]]) as usize; - index += 4; - if index + extension_len > end { - return false; - } - if extension_type == 43 { - return extension_len == 2 - && u16::from_be_bytes([frame[index], frame[index + 1]]) == 0x0304; - } - index += extension_len; - } - false -} - -fn simple_obfs_tls_client_hello(server: &str, payload: &[u8]) -> io::Result> { - let payload_len = u16::try_from(payload.len()).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello payload is too large", - ) - })?; - let server_len = u16::try_from(server.len()).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello server name is too large", - ) - })?; - let record_len = 212u16 - .checked_add(payload_len) - .and_then(|len| len.checked_add(server_len)) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello is too large", - ) - })?; - let handshake_len = 208u16 - .checked_add(payload_len) - .and_then(|len| len.checked_add(server_len)) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello is too large", - ) - })?; - let extension_len = 79u16 - .checked_add(payload_len) - .and_then(|len| len.checked_add(server_len)) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello is too large", - ) - })?; - let sni_ext_len = server_len.checked_add(5).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello server name is too large", - ) - })?; - let sni_list_len = server_len.checked_add(3).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello server name is too large", - ) - })?; - if server_len == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "simple-obfs TLS client hello server name is empty", - )); - } - let mut random = [0u8; 28]; - let mut session_id = [0u8; 32]; - OsRng.fill_bytes(&mut random); - OsRng.fill_bytes(&mut session_id); - - let mut hello = Vec::with_capacity(217 + server.len() + payload.len()); - hello.push(22); - hello.extend_from_slice(&[0x03, 0x01]); - hello.extend_from_slice(&record_len.to_be_bytes()); - hello.push(1); - hello.push(0); - hello.extend_from_slice(&handshake_len.to_be_bytes()); - hello.extend_from_slice(&[0x03, 0x03]); - hello.extend_from_slice(&(aead2022_now_timestamp().unwrap_or(0) as u32).to_be_bytes()); - hello.extend_from_slice(&random); - hello.push(32); - hello.extend_from_slice(&session_id); - hello.extend_from_slice(&[0x00, 0x38]); - hello.extend_from_slice(&[ - 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, - 0x2f, 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, - 0xc0, 0x0a, 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, - 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, - ]); - hello.extend_from_slice(&[0x01, 0x00]); - hello.extend_from_slice(&extension_len.to_be_bytes()); - hello.extend_from_slice(&[0x00, 0x23]); - hello.extend_from_slice(&payload_len.to_be_bytes()); - hello.extend_from_slice(payload); - hello.extend_from_slice(&[0x00, 0x00]); - hello.extend_from_slice(&sni_ext_len.to_be_bytes()); - hello.extend_from_slice(&sni_list_len.to_be_bytes()); - hello.push(0); - hello.extend_from_slice(&server_len.to_be_bytes()); - hello.extend_from_slice(server.as_bytes()); - hello.extend_from_slice(&[0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02]); - hello.extend_from_slice(&[ - 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18, - ]); - hello.extend_from_slice(&[ - 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, - 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, - 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, - ]); - hello.extend_from_slice(&[0x00, 0x16, 0x00, 0x00]); - hello.extend_from_slice(&[0x00, 0x17, 0x00, 0x00]); - Ok(hello) -} diff --git a/burrow/src/proxy_runtime/tests.rs b/burrow/src/proxy_runtime/tests.rs deleted file mode 100644 index 9eb046b..0000000 --- a/burrow/src/proxy_runtime/tests.rs +++ /dev/null @@ -1,3677 +0,0 @@ -#[cfg(test)] -mod tests { - use super::*; - - async fn read_test_tls_record(stream: &mut S) -> Vec - where - S: AsyncRead + Unpin, - { - let mut header = [0u8; TLS_RECORD_HEADER_LEN]; - stream.read_exact(&mut header).await.unwrap(); - let payload_len = usize::from(u16::from_be_bytes([header[3], header[4]])); - let mut record = header.to_vec(); - record.resize(TLS_RECORD_HEADER_LEN + payload_len, 0); - stream - .read_exact(&mut record[TLS_RECORD_HEADER_LEN..]) - .await - .unwrap(); - record - } - - #[test] - fn trojan_hash_is_sha224_hex() { - assert_eq!( - trojan_password_hash("password"), - "d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01" - ); - } - - #[test] - fn socks_addr_round_trips_ipv4_and_ipv6() { - for addr in [ - "1.2.3.4:53".parse::().unwrap(), - "[2001:db8::1]:443".parse::().unwrap(), - ] { - let encoded = socks_addr(addr).unwrap(); - let (decoded, used) = parse_socks_addr(&encoded).unwrap(); - assert_eq!(decoded, addr); - assert_eq!(used, encoded.len()); - } - } - - #[test] - fn shadowsocks_runtime_accepts_expanded_cipher_families() { - for cipher in [ - "none", - "plain", - "table", - "rc4-md5", - "rc4", - "aes-128-ctr", - "aes-192-ctr", - "aes-256-ctr", - "aes-128-cfb", - "aes-128-cfb1", - "aes-128-cfb8", - "aes-128-cfb128", - "aes-192-cfb", - "aes-192-cfb1", - "aes-192-cfb8", - "aes-192-cfb128", - "aes-256-cfb", - "aes-256-cfb1", - "aes-256-cfb8", - "aes-256-cfb128", - "aes-128-ofb", - "aes-192-ofb", - "aes-256-ofb", - "camellia-128-ctr", - "camellia-192-ctr", - "camellia-256-ctr", - "camellia-128-cfb", - "camellia-128-cfb1", - "camellia-128-cfb8", - "camellia-128-cfb128", - "camellia-192-cfb", - "camellia-192-cfb1", - "camellia-192-cfb8", - "camellia-192-cfb128", - "camellia-256-cfb", - "camellia-256-cfb1", - "camellia-256-cfb8", - "camellia-256-cfb128", - "camellia-128-ofb", - "camellia-192-ofb", - "camellia-256-ofb", - "chacha20", - "chacha20-ietf", - "xchacha20", - "aes-128-gcm", - "aes-192-gcm", - "aes-256-gcm", - "aes-128-ccm", - "aes-192-ccm", - "aes-256-ccm", - "aes-128-gcm-siv", - "aes-256-gcm-siv", - "chacha8-ietf-poly1305", - "chacha20-ietf-poly1305", - "rabbit128-poly1305", - "xchacha8-ietf-poly1305", - "xchacha20-ietf-poly1305", - "sm4-gcm", - "sm4-ccm", - "aegis-128l", - "aegis-256", - "aez-384", - "deoxys-ii-256-128", - "ascon128", - "ascon128a", - "lea-128-gcm", - "lea-192-gcm", - "lea-256-gcm", - "2022-blake3-aes-128-gcm", - "2022-blake3-aes-256-gcm", - "2022-blake3-aes-128-ccm", - "2022-blake3-aes-256-ccm", - "2022-blake3-chacha20-poly1305", - "2022-blake3-chacha8-poly1305", - ] { - validate_shadowsocks_cipher(cipher) - .unwrap_or_else(|err| panic!("expected {cipher} to be supported: {err:#}")); - } - assert!(validate_shadowsocks_cipher("not-a-real-cipher").is_err()); - } - - #[test] - fn shadowsocks_runtime_has_no_known_mihomo_cipher_gaps() { - let mihomo_only_ciphers: [&str; 0] = []; - assert!(mihomo_only_ciphers - .iter() - .all(|cipher| validate_shadowsocks_cipher(cipher).is_ok())); - } - - #[test] - fn shadowsocks_none_cipher_builds_runtime_outbound() { - let node = ProxyNode { - ordinal: 0, - name: "ss-none".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "none".to_owned(), - password: "unused".to_owned(), - udp: true, - udp_over_tcp: true, - udp_over_tcp_version: UOT_LEGACY_VERSION, - client_fingerprint: None, - plugin: None, - smux: None, - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks config"); - }; - let outbound = ShadowsocksOutbound::from_node(&node, config).unwrap(); - assert!(matches!(&outbound.protocol, ShadowsocksProtocol::None)); - assert!(outbound.udp_enabled); - assert!(outbound.udp_over_tcp); - } - - #[test] - fn shadowsocks_runtime_keeps_legacy_cipher_aliases() { - for cipher in [ - "aead_aes_128_gcm", - "aead_aes_256_gcm", - "chacha20-poly1305", - "aead_chacha20_poly1305", - ] { - shadowsocks_cipher_kind(cipher) - .unwrap_or_else(|err| panic!("expected {cipher} alias to be supported: {err:#}")); - } - } - - #[test] - fn shadowsocks_runtime_accepts_udp_over_tcp_nodes() { - let payload = ProxySubscriptionPayload { - name: "sub".to_owned(), - source: crate::proxy_subscription::ProxySubscriptionSource { - url: "https://sub.example/path".to_owned(), - }, - detected_format: crate::proxy_subscription::ProxySubscriptionFormat::ClashYaml, - selected_ordinal: Some(0), - selected_name: None, - nodes: vec![ProxyNode { - ordinal: 0, - name: "ss-uot".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: true, - udp_over_tcp: true, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: None, - }), - }], - warnings: Vec::new(), - }; - - assert_eq!(selected_node(&payload).unwrap().name, "ss-uot"); - assert!(normalize_uot_version(0).is_ok()); - assert!(normalize_uot_version(UOT_LEGACY_VERSION).is_ok()); - assert!(normalize_uot_version(UOT_VERSION).is_ok()); - assert!(normalize_uot_version(3).is_err()); - } - - #[test] - fn runtime_support_accepts_tcp_only_shadowsocks_smux() { - let mut node = ProxyNode { - ordinal: 0, - name: "ss-smux".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("smux".to_owned()), - max_connections: Some(4), - min_streams: Some(2), - max_streams: None, - padding: false, - statistic: false, - only_tcp: true, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::Smux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: false, - udp: false, - brutal: None, - }) - ); - - if let ProxyNodeConfig::Shadowsocks(config) = &mut node.config { - config.udp = true; - config.smux.as_mut().unwrap().only_tcp = false; - } - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::Smux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: false, - udp: true, - brutal: None, - }) - ); - } - - #[test] - fn runtime_support_accepts_default_shadowsocks_h2mux() { - let node = ProxyNode { - ordinal: 0, - name: "ss-h2mux".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: None, - max_connections: Some(4), - min_streams: Some(2), - max_streams: None, - padding: false, - statistic: false, - only_tcp: true, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::H2Mux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: false, - udp: false, - brutal: None, - }) - ); - } - - #[test] - fn runtime_support_accepts_padded_shadowsocks_smux_udp_when_base_udp_is_false() { - let node = ProxyNode { - ordinal: 0, - name: "ss-smux-padding".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: false, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("smux".to_owned()), - max_connections: Some(4), - min_streams: Some(2), - max_streams: None, - padding: true, - statistic: false, - only_tcp: false, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::Smux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: true, - udp: true, - brutal: None, - }) - ); - - let outbound = ShadowsocksOutbound::from_node(&node, config).unwrap(); - assert!( - outbound.udp_enabled, - "Mihomo sing-mux supports UDP unless only-tcp is set, even when the underlying Shadowsocks udp flag is false" - ); - } - - #[test] - fn runtime_support_accepts_brutal_shadowsocks_smux() { - let node = ProxyNode { - ordinal: 0, - name: "ss-smux-brutal".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("smux".to_owned()), - max_connections: Some(4), - min_streams: Some(2), - max_streams: None, - padding: false, - statistic: false, - only_tcp: false, - brutal: Some(crate::proxy_subscription::ShadowsocksSmuxBrutal { - enabled: true, - up: Some("10 Mbps".to_owned()), - down: Some("20 Mbps".to_owned()), - }), - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::Smux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: false, - udp: true, - brutal: Some(ShadowsocksSingMuxBrutalTransport { - send_bps: 1_250_000, - receive_bps: 2_500_000, - }), - }) - ); - } - - #[test] - fn runtime_support_accepts_tcp_only_shadowsocks_yamux() { - let node = ProxyNode { - ordinal: 0, - name: "ss-yamux".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("yamux".to_owned()), - max_connections: Some(4), - min_streams: Some(2), - max_streams: None, - padding: false, - statistic: false, - only_tcp: true, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - let ProxyNodeConfig::Shadowsocks(config) = &node.config else { - panic!("expected Shadowsocks node"); - }; - let protocol = shadowsocks_protocol_for_node(&node, config).unwrap(); - let transport = - shadowsocks_sing_mux_transport(config.smux.as_ref(), config, &protocol).unwrap(); - assert_eq!( - transport, - Some(ShadowsocksSingMuxTransport { - protocol: ShadowsocksSingMuxProtocol::Yamux, - max_connections: 4, - min_streams: 2, - max_streams: 0, - padding: false, - udp: false, - brutal: None, - }) - ); - } - - #[test] - fn runtime_support_accepts_custom_cipher_shadowsocks_smux() { - let node = ProxyNode { - ordinal: 0, - name: "ss-custom-smux".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-192-gcm".to_owned(), - password: "pass".to_owned(), - udp: false, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("smux".to_owned()), - max_connections: Some(2), - min_streams: None, - max_streams: Some(16), - padding: false, - statistic: false, - only_tcp: false, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - } - - #[test] - fn runtime_support_accepts_aead2022_ccm_shadowsocks_smux() { - let kind = Aead2022AesCcmKind::Aes128; - let password = BASE64_STANDARD.encode(vec![3u8; kind.key_len()]); - let node = ProxyNode { - ordinal: 0, - name: "ss-2022-ccm-smux".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: kind.name().to_owned(), - password, - udp: false, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: Some(crate::proxy_subscription::ShadowsocksSmux { - enabled: true, - protocol: Some("smux".to_owned()), - max_connections: Some(2), - min_streams: None, - max_streams: Some(16), - padding: false, - statistic: false, - only_tcp: false, - brutal: None, - }), - }), - }; - - assert!(runtime_support_error(&node).is_none()); - } - - #[tokio::test] - async fn sing_mux_tcp_request_uses_mihomo_stream_header_shape() { - let (mut client, mut server) = tokio::io::duplex(64); - let target = ProxyTcpTarget { - address: "198.18.64.1:443".parse().unwrap(), - hostname: Some("example.com.".to_owned()), - }; - - let writer = - tokio::spawn(async move { write_sing_mux_tcp_request(&mut client, &target).await }); - let mut request = vec![0u8; 2 + 1 + 1 + "example.com".len() + 2]; - server.read_exact(&mut request).await.unwrap(); - writer.await.unwrap().unwrap(); - - let mut expected = vec![0, 0, 3, "example.com".len() as u8]; - expected.extend_from_slice(b"example.com"); - expected.extend_from_slice(&443u16.to_be_bytes()); - assert_eq!(request, expected); - } - - #[tokio::test] - async fn sing_mux_udp_request_uses_mihomo_packet_header_shape() { - let (mut client, mut server) = tokio::io::duplex(64); - let target: SocketAddr = "198.18.64.1:53".parse().unwrap(); - let writer = - tokio::spawn( - async move { write_sing_mux_udp_request(&mut client, target, b"query").await }, - ); - let mut request = vec![0u8; 2 + 1 + 4 + 2 + 2 + "query".len()]; - server.read_exact(&mut request).await.unwrap(); - writer.await.unwrap().unwrap(); - - let mut expected = vec![0, SING_MUX_STREAM_FLAG_UDP as u8, 1, 198, 18, 64, 1]; - expected.extend_from_slice(&53u16.to_be_bytes()); - expected.extend_from_slice(&5u16.to_be_bytes()); - expected.extend_from_slice(b"query"); - assert_eq!(request, expected); - } - - #[tokio::test] - async fn sing_mux_udp_payload_reads_status_then_length_prefixed_packet() { - let (mut client, mut server) = tokio::io::duplex(64); - let writer = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_SUCCESS]) - .await - .unwrap(); - server.write_all(&4u16.to_be_bytes()).await.unwrap(); - server.write_all(b"pong").await.unwrap(); - }); - let mut response_read = false; - let payload = read_sing_mux_udp_payload(&mut client, &mut response_read) - .await - .unwrap(); - writer.await.unwrap(); - assert!(response_read); - assert_eq!(&payload, b"pong"); - } - - #[test] - fn mihomo_bps_parser_matches_decimal_rate_strings() { - assert_eq!(mihomo_string_to_bps(""), 0); - assert_eq!(mihomo_string_to_bps("10"), 1_250_000); - assert_eq!(mihomo_string_to_bps("10 Mbps"), 1_250_000); - assert_eq!(mihomo_string_to_bps("10Mbps"), 1_250_000); - assert_eq!(mihomo_string_to_bps("10 MBps"), 10_000_000); - assert_eq!(mihomo_string_to_bps("1 Kbps"), 125); - assert_eq!(mihomo_string_to_bps("not-a-rate"), 0); - } - - #[tokio::test] - async fn sing_mux_brutal_request_uses_mihomo_exchange_shape() { - let (mut client, mut server) = tokio::io::duplex(128); - let writer = - tokio::spawn( - async move { write_sing_mux_brutal_request(&mut client, 2_500_000).await }, - ); - let mut request = vec![0u8; 2 + 1 + 1 + SING_MUX_BRUTAL_EXCHANGE_DOMAIN.len() + 2 + 8]; - server.read_exact(&mut request).await.unwrap(); - writer.await.unwrap().unwrap(); - - let mut expected = vec![0, 0, 3, SING_MUX_BRUTAL_EXCHANGE_DOMAIN.len() as u8]; - expected.extend_from_slice(SING_MUX_BRUTAL_EXCHANGE_DOMAIN.as_bytes()); - expected.extend_from_slice(&0u16.to_be_bytes()); - expected.extend_from_slice(&2_500_000u64.to_be_bytes()); - assert_eq!(request, expected); - } - - #[tokio::test] - async fn sing_mux_brutal_response_reads_stream_status_then_server_rate() { - let (mut client, mut server) = tokio::io::duplex(64); - let writer = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_SUCCESS, 1]) - .await - .unwrap(); - server.write_all(&3_000_000u64.to_be_bytes()).await.unwrap(); - }); - let receive_bps = read_sing_mux_brutal_response(&mut client).await.unwrap(); - writer.await.unwrap(); - assert_eq!(receive_bps, 3_000_000); - - let (mut client, mut server) = tokio::io::duplex(64); - let writer = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_SUCCESS, 0, 4]) - .await - .unwrap(); - server.write_all(b"oops").await.unwrap(); - }); - let err = read_sing_mux_brutal_response(&mut client) - .await - .unwrap_err(); - writer.await.unwrap(); - assert!(err.to_string().contains("oops"), "{err}"); - } - - #[tokio::test] - async fn sing_mux_session_preface_uses_mihomo_padding_shape() { - let (mut client, mut server) = tokio::io::duplex(1024); - let writer = tokio::spawn(async move { - write_sing_mux_session_preface(&mut client, ShadowsocksSingMuxProtocol::Smux, false) - .await - }); - let mut plain = [0u8; 2]; - server.read_exact(&mut plain).await.unwrap(); - writer.await.unwrap().unwrap(); - assert_eq!(plain, [SING_MUX_VERSION_0, SING_MUX_PROTOCOL_SMUX]); - - let (mut client, mut server) = tokio::io::duplex(1024); - let writer = tokio::spawn(async move { - write_sing_mux_session_preface(&mut client, ShadowsocksSingMuxProtocol::Yamux, false) - .await - }); - let mut plain = [0u8; 2]; - server.read_exact(&mut plain).await.unwrap(); - writer.await.unwrap().unwrap(); - assert_eq!(plain, [SING_MUX_VERSION_0, SING_MUX_PROTOCOL_YAMUX]); - - let (mut client, mut server) = tokio::io::duplex(1024); - let writer = tokio::spawn(async move { - write_sing_mux_session_preface(&mut client, ShadowsocksSingMuxProtocol::H2Mux, false) - .await - }); - let mut plain = [0u8; 2]; - server.read_exact(&mut plain).await.unwrap(); - writer.await.unwrap().unwrap(); - assert_eq!(plain, [SING_MUX_VERSION_0, SING_MUX_PROTOCOL_H2MUX]); - - let (mut client, mut server) = tokio::io::duplex(2048); - let writer = tokio::spawn(async move { - write_sing_mux_session_preface(&mut client, ShadowsocksSingMuxProtocol::Smux, true) - .await - }); - let mut header = [0u8; 5]; - server.read_exact(&mut header).await.unwrap(); - assert_eq!( - &header[..3], - &[SING_MUX_VERSION_1, SING_MUX_PROTOCOL_SMUX, 1] - ); - let padding_len = u16::from_be_bytes([header[3], header[4]]) as usize; - assert!(padding_len >= SING_MUX_MIN_PADDING_LEN); - assert!(padding_len < SING_MUX_MIN_PADDING_LEN + SING_MUX_PADDING_LEN_RANGE); - let mut padding = vec![0u8; padding_len]; - server.read_exact(&mut padding).await.unwrap(); - writer.await.unwrap().unwrap(); - assert!(padding.iter().all(|byte| *byte == 0)); - } - - #[tokio::test] - async fn sing_mux_yamux_session_opens_mihomo_style_streams() { - let (client, server) = tokio::io::duplex(4096); - let session = start_sing_mux_yamux_session(Box::new(client)) - .await - .unwrap(); - let mut server_connection = yamux::Connection::new( - server.compat(), - yamux::Config::default(), - yamux::Mode::Server, - ); - let (stream_tx, mut stream_rx) = mpsc::channel(1); - let server_driver = tokio::spawn(async move { - let mut stream_tx = Some(stream_tx); - std::future::poll_fn(move |cx| loop { - match server_connection.poll_next_inbound(cx) { - Poll::Ready(Some(Ok(stream))) => { - if let Some(sender) = stream_tx.take() { - let _ = sender.try_send(stream.compat()); - } - } - Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(anyhow!(err))), - Poll::Ready(None) => return Poll::Ready(Ok(())), - Poll::Pending => return Poll::Pending, - } - }) - .await - }); - - let mut client_stream = session.open_stream().await.unwrap(); - client_stream.write_all(b"ping").await.unwrap(); - let mut server_stream = stream_rx.recv().await.unwrap(); - let mut request = [0u8; 4]; - server_stream.read_exact(&mut request).await.unwrap(); - assert_eq!(&request, b"ping"); - - drop(client_stream); - assert_eq!(session.num_streams().await, 0); - server_driver.abort(); - } - - #[tokio::test] - async fn sing_mux_h2mux_session_opens_mihomo_style_connect_streams() { - let (client, server) = tokio::io::duplex(4096); - let session = start_sing_mux_h2mux_session(Box::new(client)) - .await - .unwrap(); - let (done_tx, mut done_rx) = oneshot::channel(); - let server_driver = tokio::spawn(async move { - let mut server = h2::server::handshake(server).await?; - let Some(request) = server.accept().await else { - bail!("h2mux server closed before receiving CONNECT stream"); - }; - let (request, mut respond) = request?; - assert_eq!(request.method(), Method::CONNECT); - assert_eq!(request.uri().to_string(), "localhost"); - - let mut body = request.into_body(); - let response = http::Response::builder() - .status(StatusCode::OK) - .body(()) - .unwrap(); - let mut send = respond.send_response(response, false)?; - let data = body.data().await.unwrap()?; - body.flow_control().release_capacity(data.len())?; - assert_eq!(&data[..], b"ping"); - send.reserve_capacity(4); - std::future::poll_fn(|cx| send.poll_capacity(cx)) - .await - .context("h2mux test response stream closed before capacity")??; - send.send_data(Bytes::from_static(b"pong"), true)?; - loop { - tokio::select! { - _ = &mut done_rx => break, - accepted = server.accept() => { - if accepted.is_none() { - break; - } - } - } - } - Ok::<_, anyhow::Error>(()) - }); - - let mut client_stream = session.open_stream().await.unwrap(); - client_stream.write_all(b"ping").await.unwrap(); - let mut response = [0u8; 4]; - client_stream.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"pong"); - let _ = done_tx.send(()); - - drop(client_stream); - assert_eq!(session.num_streams().await, 0); - server_driver.await.unwrap().unwrap(); - } - - #[tokio::test] - async fn sing_mux_padding_stream_frames_first_reads_and_writes() { - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = sing_mux_padding_stream(Box::new(client)); - - stream.write_all(b"ping").await.unwrap(); - let mut header = [0u8; 4]; - server.read_exact(&mut header).await.unwrap(); - let payload_len = u16::from_be_bytes([header[0], header[1]]) as usize; - let padding_len = u16::from_be_bytes([header[2], header[3]]) as usize; - assert_eq!(payload_len, 4); - assert!(padding_len >= SING_MUX_MIN_PADDING_LEN); - assert!(padding_len < SING_MUX_MIN_PADDING_LEN + SING_MUX_PADDING_LEN_RANGE); - let mut payload = vec![0u8; payload_len]; - server.read_exact(&mut payload).await.unwrap(); - assert_eq!(&payload, b"ping"); - let mut padding = vec![0u8; padding_len]; - server.read_exact(&mut padding).await.unwrap(); - - let response_padding = SING_MUX_MIN_PADDING_LEN; - server.write_all(&4u16.to_be_bytes()).await.unwrap(); - server - .write_all(&(response_padding as u16).to_be_bytes()) - .await - .unwrap(); - server.write_all(b"pong").await.unwrap(); - server - .write_all(&vec![0u8; response_padding]) - .await - .unwrap(); - - let mut response = [0u8; 4]; - stream.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"pong"); - } - - #[tokio::test] - async fn sing_mux_stream_response_accepts_success_and_reports_remote_errors() { - let (mut client, mut server) = tokio::io::duplex(64); - let success = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_SUCCESS]) - .await - .unwrap(); - }); - read_sing_mux_stream_response(&mut client).await.unwrap(); - success.await.unwrap(); - - let (mut client, mut server) = tokio::io::duplex(64); - let failure = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_ERROR, 4, b'o', b'o', b'p', b's']) - .await - .unwrap(); - }); - let err = read_sing_mux_stream_response(&mut client) - .await - .unwrap_err(); - failure.await.unwrap(); - assert!(err.to_string().contains("oops"), "{err}"); - } - - #[tokio::test] - async fn sing_mux_tcp_stream_reads_lazy_success_status_before_payload() { - let (client, mut server) = tokio::io::duplex(64); - let mut stream = SingMuxTcpStream::new(client); - let writer = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_SUCCESS, b'o', b'k']) - .await - .unwrap(); - }); - - let mut payload = [0u8; 2]; - stream.read_exact(&mut payload).await.unwrap(); - writer.await.unwrap(); - assert_eq!(&payload, b"ok"); - - let (client, mut server) = tokio::io::duplex(64); - let mut stream = SingMuxTcpStream::new(client); - let writer = tokio::spawn(async move { - server - .write_all(&[SING_MUX_STREAM_STATUS_ERROR]) - .await - .unwrap(); - }); - let err = stream.read_exact(&mut payload).await.unwrap_err(); - writer.await.unwrap(); - assert!(err.to_string().contains("remote error"), "{err}"); - } - - #[tokio::test] - async fn custom_aead_plain_tcp_stream_exposes_plaintext_for_sing_mux() { - let kind = CustomSsAeadKind::Aes192Gcm; - let key: Arc<[u8]> = Arc::from(vec![7u8; kind.key_len()]); - let (client_tcp, server_tcp) = tokio::io::duplex(4096); - let target = ProxyTcpTarget { - address: "198.18.64.1:443".parse().unwrap(), - hostname: Some("example.com.".to_owned()), - }; - let mut plain = custom_aead_plain_tcp_stream(client_tcp, &target, kind, key.clone()) - .await - .unwrap(); - let (server_reader, server_writer) = split(server_tcp); - let mut ss_reader = CustomSsAeadReader::new(server_reader, kind, key.clone()); - let mut ss_writer = CustomSsAeadWriter::new(server_writer, kind, key) - .await - .unwrap(); - - let mut received = Vec::new(); - ss_reader.read_chunk(&mut received).await.unwrap(); - assert_eq!(received, socks_domain_addr("example.com", 443).unwrap()); - - plain.write_all(b"ping").await.unwrap(); - received.clear(); - ss_reader.read_chunk(&mut received).await.unwrap(); - assert_eq!(received, b"ping"); - - ss_writer.write_chunk(b"pong").await.unwrap(); - let mut response = [0u8; 4]; - plain.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"pong"); - } - - #[tokio::test] - async fn custom_stream_plain_tcp_stream_exposes_plaintext_for_sing_mux() { - let kind = CustomSsStreamKind::XChacha20; - let key: Arc<[u8]> = Arc::from(vec![9u8; kind.key_len()]); - let (client_tcp, server_tcp) = tokio::io::duplex(4096); - let target = ProxyTcpTarget { - address: "198.18.64.1:443".parse().unwrap(), - hostname: Some("example.com.".to_owned()), - }; - let mut plain = custom_stream_plain_tcp_stream(client_tcp, &target, kind, key.clone()) - .await - .unwrap(); - let (server_reader, server_writer) = split(server_tcp); - let mut ss_reader = CustomSsStreamReader::new(server_reader, kind, key.clone()); - let mut ss_writer = CustomSsStreamWriter::new(server_writer, kind, key) - .await - .unwrap(); - - let mut received = vec![0u8; 64]; - let n = ss_reader.read_decrypted(&mut received).await.unwrap(); - assert_eq!( - &received[..n], - socks_domain_addr("example.com", 443).unwrap().as_slice() - ); - - plain.write_all(b"ping").await.unwrap(); - let n = ss_reader.read_decrypted(&mut received).await.unwrap(); - assert_eq!(&received[..n], b"ping"); - - ss_writer.write_all_encrypted(b"pong").await.unwrap(); - let mut response = [0u8; 4]; - plain.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"pong"); - } - - #[tokio::test] - async fn aead2022_aes_ccm_plain_tcp_stream_exposes_plaintext_for_sing_mux() { - let kind = Aead2022AesCcmKind::Aes128; - let user_key: Arc<[u8]> = Arc::from(vec![11u8; kind.key_len()]); - let identity_keys: Arc<[Arc<[u8]>]> = Arc::from(Vec::>::new()); - let (client_tcp, mut server_tcp) = tokio::io::duplex(4096); - let target = ProxyTcpTarget { - address: "198.18.64.1:443".parse().unwrap(), - hostname: Some("example.com.".to_owned()), - }; - let mut plain = aead2022_aes_ccm_plain_tcp_stream( - client_tcp, - &target, - kind, - user_key.clone(), - identity_keys, - ) - .await - .unwrap(); - - let (request_salt, mut request_cipher, variable) = - read_aead2022_tcp_request_for_test(&mut server_tcp, kind, &user_key) - .await - .unwrap(); - let expected_target = socks_domain_addr("example.com", 443).unwrap(); - assert!(variable.starts_with(&expected_target)); - let padding_len_offset = expected_target.len(); - let padding_len = u16::from_be_bytes([ - variable[padding_len_offset], - variable[padding_len_offset + 1], - ]) as usize; - assert_eq!(padding_len, 1); - - plain.write_all(b"ping").await.unwrap(); - let payload = read_aead2022_tcp_chunk_for_test(&mut server_tcp, kind, &mut request_cipher) - .await - .unwrap(); - assert_eq!(payload, b"ping"); - - let mut response_cipher = write_aead2022_tcp_response_for_test( - &mut server_tcp, - kind, - &user_key, - request_salt.as_ref(), - ) - .await - .unwrap(); - write_aead2022_tcp_chunk_for_test(&mut server_tcp, &mut response_cipher, b"pong") - .await - .unwrap(); - - let mut response = [0u8; 4]; - plain.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"pong"); - } - - async fn read_aead2022_tcp_request_for_test( - reader: &mut R, - kind: Aead2022AesCcmKind, - user_key: &[u8], - ) -> Result<(Arc<[u8]>, Aead2022TcpCipher, Vec)> - where - R: AsyncRead + Unpin, - { - let mut salt = vec![0u8; kind.key_len()]; - reader.read_exact(&mut salt).await?; - let mut cipher = Aead2022TcpCipher::new(kind, user_key, &salt)?; - let mut encrypted_fixed = vec![0u8; 1 + 8 + 2 + kind.tag_len()]; - reader.read_exact(&mut encrypted_fixed).await?; - let fixed = cipher.open_packet(&mut encrypted_fixed)?; - assert_eq!(fixed[0], AEAD2022_HEADER_TYPE_CLIENT); - aead2022_validate_timestamp(u64::from_be_bytes( - fixed[1..9].try_into().expect("fixed timestamp"), - ))?; - let variable_len = u16::from_be_bytes([fixed[9], fixed[10]]) as usize; - let mut encrypted_variable = vec![0u8; variable_len + kind.tag_len()]; - reader.read_exact(&mut encrypted_variable).await?; - let variable = cipher.open_packet(&mut encrypted_variable)?.to_vec(); - Ok((Arc::from(salt), cipher, variable)) - } - - async fn read_aead2022_tcp_chunk_for_test( - reader: &mut R, - kind: Aead2022AesCcmKind, - cipher: &mut Aead2022TcpCipher, - ) -> Result> - where - R: AsyncRead + Unpin, - { - let mut encrypted_len = vec![0u8; 2 + kind.tag_len()]; - reader.read_exact(&mut encrypted_len).await?; - let len = cipher.open_packet(&mut encrypted_len)?; - let payload_len = u16::from_be_bytes([len[0], len[1]]) as usize; - let mut encrypted_payload = vec![0u8; payload_len + kind.tag_len()]; - reader.read_exact(&mut encrypted_payload).await?; - Ok(cipher.open_packet(&mut encrypted_payload)?.to_vec()) - } - - async fn write_aead2022_tcp_response_for_test( - writer: &mut W, - kind: Aead2022AesCcmKind, - user_key: &[u8], - request_salt: &[u8], - ) -> Result - where - W: AsyncWrite + Unpin, - { - let response_salt = vec![13u8; kind.key_len()]; - let mut cipher = Aead2022TcpCipher::new(kind, user_key, &response_salt)?; - let mut fixed = Vec::with_capacity(1 + 8 + kind.key_len() + 2); - fixed.push(AEAD2022_HEADER_TYPE_SERVER); - fixed.extend_from_slice(&aead2022_now_timestamp()?.to_be_bytes()); - fixed.extend_from_slice(request_salt); - fixed.extend_from_slice(&0u16.to_be_bytes()); - writer.write_all(&response_salt).await?; - writer.write_all(&cipher.seal_packet(&fixed)?).await?; - writer.flush().await?; - Ok(cipher) - } - - async fn write_aead2022_tcp_chunk_for_test( - writer: &mut W, - cipher: &mut Aead2022TcpCipher, - payload: &[u8], - ) -> Result<()> - where - W: AsyncWrite + Unpin, - { - writer - .write_all(&cipher.seal_packet(&(payload.len() as u16).to_be_bytes())?) - .await?; - writer.write_all(&cipher.seal_packet(payload)?).await?; - writer.flush().await?; - Ok(()) - } - - #[test] - fn shadowsocks_udp_over_tcp_enables_udp_session_path() { - fn shadowsocks_node( - name: &str, - udp: bool, - udp_over_tcp: bool, - plugin: Option, - ) -> ProxyNode { - ProxyNode { - ordinal: 0, - name: name.to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "pass".to_owned(), - udp, - udp_over_tcp, - udp_over_tcp_version: UOT_LEGACY_VERSION, - client_fingerprint: None, - plugin, - smux: None, - }), - } - } - - let uot_node = shadowsocks_node("ss-uot-without-native-udp", false, true, None); - let ProxyNodeConfig::Shadowsocks(config) = &uot_node.config else { - panic!("expected Shadowsocks config"); - }; - let outbound = ShadowsocksOutbound::from_node(&uot_node, config).unwrap(); - assert!(outbound.udp_enabled); - assert!(outbound.udp_over_tcp); - - let native_udp_node = shadowsocks_node("ss-native-udp-disabled", false, false, None); - let ProxyNodeConfig::Shadowsocks(config) = &native_udp_node.config else { - panic!("expected Shadowsocks config"); - }; - let outbound = ShadowsocksOutbound::from_node(&native_udp_node, config).unwrap(); - assert!(!outbound.udp_enabled); - assert!(!outbound.udp_over_tcp); - - let kcptun_node = shadowsocks_node( - "ss-kcptun-forced-uot", - false, - false, - Some(ShadowsocksPlugin { - name: "kcptun".to_owned(), - opts: HashMap::new(), - }), - ); - let ProxyNodeConfig::Shadowsocks(config) = &kcptun_node.config else { - panic!("expected Shadowsocks config"); - }; - let outbound = ShadowsocksOutbound::from_node(&kcptun_node, config).unwrap(); - assert!(outbound.udp_enabled); - assert!(outbound.udp_over_tcp); - } - - #[test] - fn runtime_support_rejects_shadowsocks_tls_client_fingerprint() { - fn tls_plugin_node(plugin: ShadowsocksPlugin) -> ProxyNode { - ProxyNode { - ordinal: 0, - name: "ss-tls-fingerprint".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "proxy.example".to_owned(), - port: 443, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher: "aes-128-gcm".to_owned(), - password: "ss-secret".to_owned(), - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: Some("firefox".to_owned()), - plugin: Some(plugin), - smux: None, - }), - } - } - - let shadow_tls = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ("version".to_owned(), "2".to_owned()), - ]), - }; - - #[cfg(not(feature = "boring-browser-fingerprints"))] - let rejected_nodes = [tls_plugin_node(shadow_tls.clone())]; - #[cfg(feature = "boring-browser-fingerprints")] - let rejected_nodes: [ProxyNode; 0] = []; - - for node in rejected_nodes { - let err = runtime_support_error(&node) - .expect("uTLS client-fingerprint is not runtime-supported yet"); - assert!(err.contains("client-fingerprint firefox"), "{err}"); - assert!(err.contains("browser TLS profile"), "{err}"); - } - - #[cfg(feature = "boring-browser-fingerprints")] - assert!(runtime_support_error(&tls_plugin_node(shadow_tls)).is_none()); - - let shadow_tls_v1 = tls_plugin_node(ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("version".to_owned(), "1".to_owned()), - ]), - }); - assert!(runtime_support_error(&shadow_tls_v1).is_none()); - - let mut none_node = tls_plugin_node(ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ("version".to_owned(), "2".to_owned()), - ]), - }); - if let ProxyNodeConfig::Shadowsocks(shadowsocks) = &mut none_node.config { - shadowsocks.client_fingerprint = Some("none".to_owned()); - } - assert!(runtime_support_error(&none_node).is_none()); - - let mut unknown_node = none_node; - if let ProxyNodeConfig::Shadowsocks(shadowsocks) = &mut unknown_node.config { - shadowsocks.client_fingerprint = Some("unknown-browser".to_owned()); - } - assert!(runtime_support_error(&unknown_node).is_none()); - } - - #[cfg(feature = "boring-browser-fingerprints")] - const TEST_CLIENT_CERTIFICATE_PEM: &str = r#"-----BEGIN CERTIFICATE----- -MIIDDTCCAfWgAwIBAgIUcQjiH+9SQYcKZJBHGs3/ymabzqYwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLYnVycm93LXRlc3QwHhcNMjYwNjAxMTUwMjA5WhcNMjYw -NjAyMTUwMjA5WjAWMRQwEgYDVQQDDAtidXJyb3ctdGVzdDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAOahzwpBhDKatLPvsnSjIQGdb9jq34dGt5Yu2znj -PAL4aTI9mFsid5L7RlHJTV1MSRsYddrLTCchDZl46hEArRQCQRf/kGiHIqJWcYDt -7gJaQHZXNTasQ9CWEoC0X+Jqv89lf9q9caMYye9ZXICeRbDxhCrQSEfY1rxXtN3i -iqJFcxmaUdzMXjgfIyMlFCHdcYtGiITZQEVizI1EelFr0RyxDuE+9IEvokpgbI2y -PnMirWVoeOKyaWFbJyPR3qSKuDF1jYrtsluW7IkTHF0B8jvbg6IJNSV7r2JaiQhD -RrgPxvXXHvfPjoZbCqAMN6JarRnWkw58uuNCr+3OxZygUkECAwEAAaNTMFEwHQYD -VR0OBBYEFI2Pv9COWSRHYwScAy17EP8MUr6uMB8GA1UdIwQYMBaAFI2Pv9COWSRH -YwScAy17EP8MUr6uMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -AHgOvphtyX9CO/ibtA8KKcH9puWeV7CJLoqHW9XU5ZvdawI01IEdn8oIn5IAzurj -X7DAqaBUvzc58EB9cmnGnGhZZJMHBE2PVlxea1EcB5geWV+y6lIM7peqP+XGsAJo -32pUr2W+Sl3IMf6FkXy4VneZCkGlOgnxrEB9ULm1foLGQtTkXTT3dqT098Gqs3ip -HC37IQ6Bqg3/TseRUpzAJ8RVV8qvyJlYvrPvYqYRMl8xHRS2Gj5q2FvxuKlCWP/t -3oXNpvfjecQFMpghL3BcMoQUq2kMLwOz5sAUFxjP6pQf6K1kB98u1NgkzF8VXgUU -0dEYKIH7JFYVNeMzscI+9cM= ------END CERTIFICATE-----"#; - - #[cfg(feature = "boring-browser-fingerprints")] - const TEST_CLIENT_PRIVATE_KEY_PEM: &str = r#"-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDmoc8KQYQymrSz -77J0oyEBnW/Y6t+HRreWLts54zwC+GkyPZhbIneS+0ZRyU1dTEkbGHXay0wnIQ2Z -eOoRAK0UAkEX/5BohyKiVnGA7e4CWkB2VzU2rEPQlhKAtF/iar/PZX/avXGjGMnv -WVyAnkWw8YQq0EhH2Na8V7Td4oqiRXMZmlHczF44HyMjJRQh3XGLRoiE2UBFYsyN -RHpRa9EcsQ7hPvSBL6JKYGyNsj5zIq1laHjismlhWycj0d6kirgxdY2K7bJbluyJ -ExxdAfI724OiCTUle69iWokIQ0a4D8b11x73z46GWwqgDDeiWq0Z1pMOfLrjQq/t -zsWcoFJBAgMBAAECggEADce+8wTCzTKWN9F2JGb3hWeunlORAz2z9tAaPuViGWar -WRpPZ1PyDhZfzL+OP6lbuDfoGX3Kho9V3ITaLiWQBzaH732l12u0+OXZBqmbbjON -+iQwJJylY/9biqonDS8bV9003FEzytl5KLPPsEsErKkO9cSX1Qcn9Cf6FsSUS6ll -SIq5iKPYtFIKOV7U8Bu1MqiMx9eKIpCcHVKNlP4uConpsq/TPc0pouSIkT8okdt0 -JxAAvNo+YvqkqbK7+t5rCvaG8csT+Uw6osa0GcupKmXWbVbIeMWjdHJNqBfinmRF -U62sGDzkVDdj1HroBNf9Ns9sovET29UgX5z6oj9WuwKBgQD1OTsDgrfkoWaXQuJR -8Vm5SILE+vKWlAXAyJrN6Dp+jbUNaV98kYWziTg4ngftjBPjUf0PYyZUmYo55+Os -LALCEjc7axgFSAjIvlH+WlfLChAGOAcOyLA1QzSn1G6f31OlMUFu9GRmGsZf5Ufc -wMn/MKQbAxTrOtmYyUmxztYnRwKBgQDwxGx2odDVD2NnE03427wQfnKETkS31DQb -7mp7uZDVg21Dr+5V0tmuxRywQFtSVDNo8sxB4REozbhCibDJcvWD0hAd/8FsxAuU -AkeXjfwaneSrw8b0JoEK/9aEkm0rin+XN/CmmCCPWNjiIguG/klL2YfXkSv71fpw -if4PgcgONwKBgQCJQVFAs8fOFnDftTYL+3Tm+ikHrBZgJdXag+3x1kv3TcXLDfG+ -PY2CYgmv1vRFB6SSFe/4ztxDefUeWCbc1X1ttthnT5gQTLNt+OjX3yVIpgc2E+IP -alEGXul4DrUkktG0oo8nVW9knxPt1N2WN+pYBZe07tKknznwBKpU9Zp0PQKBgC1w -1Qu61KAxrFAa659pUWBHjTN9VijfywnugHhjeHtjt66LuM7H4b/Dgfud2d5698z5 -7iUM5mEuGnWsaQpMQRwk/Fe9GnN9uLWxjHOFH6yiWjM02wrfbYF28bTJsgMCu7v9 -mdTHZ3XGjgB37ncG7Sx8nM/JnWSFaSPuV13z358XAoGAIuigV+9koumwqNd2ueIQ -uCWixSkOVwRHPiWG4iyWku9RpspR+Sw23gUbQ45J7u326Aw0P3oHDme2bxgsvME6 -ld+z0BdOLxyLEw55UtQrJkFNCbGsNFHRs774PuZdfExfzcd9d+56vYcVb49PSBb6 -OW4mfN8jmXq6wTtOv85mBGE= ------END PRIVATE KEY-----"#; - - #[cfg(feature = "boring-browser-fingerprints")] - #[test] - fn boring_client_identity_loads_inline_pem() { - let auth = load_boring_client_identity(&TlsClientIdentity { - certificate: TEST_CLIENT_CERTIFICATE_PEM.to_owned(), - private_key: TEST_CLIENT_PRIVATE_KEY_PEM.to_owned(), - }) - .expect("inline PEM client identity should load for BoringSSL"); - - assert_eq!(auth.cert_chain.len(), 1); - } - - #[cfg(feature = "boring-browser-fingerprints")] - #[test] - fn shadow_tls_random_fingerprint_is_mihomo_style_initial_choice() { - let first = mihomo_initial_random_fingerprint(); - assert!(matches!( - first, - MihomoClientFingerprint::Chrome - | MihomoClientFingerprint::Safari - | MihomoClientFingerprint::Ios - | MihomoClientFingerprint::Firefox - )); - for _ in 0..16 { - assert_eq!(mihomo_initial_random_fingerprint(), first); - } - } - - #[cfg(feature = "boring-browser-fingerprints")] - #[test] - fn shadow_tls_v2_browser_profile_removes_x25519_mlkem768() { - let mut config = rama_net::tls::client::ClientConfig { - extensions: Some(vec![RamaClientHelloExtension::SupportedGroups(vec![ - RamaSupportedGroup::X25519MLKEM768, - RamaSupportedGroup::X25519, - ])]), - ..Default::default() - }; - - strip_x25519_mlkem768_from_client_config(&mut config); - - let extensions = config.extensions.expect("extensions should remain present"); - let RamaClientHelloExtension::SupportedGroups(groups) = &extensions[0] else { - panic!("expected supported groups extension"); - }; - assert_eq!(groups, &[RamaSupportedGroup::X25519]); - } - - #[cfg(feature = "boring-browser-fingerprints")] - #[test] - fn shadow_tls_browser_profiles_cover_mihomo_safari16_alias() { - let profile = mihomo_browser_user_agent_profile(MihomoClientFingerprint::Safari16) - .expect("safari16 should map to an embedded browser TLS profile"); - - assert_eq!(profile.ua_kind, UserAgentKind::Safari); - assert_eq!(profile.platform, Some(PlatformKind::MacOS)); - assert!(profile - .ua_str() - .is_some_and(|ua| ua.contains("Version/16."))); - assert!(MihomoClientFingerprint::Safari16.shadow_tls_v2_boring_supported()); - } - - #[test] - fn shadowsocks_runtime_accepts_simple_obfs_plugins() { - let plugin = ShadowsocksPlugin { - name: "obfs".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "http".to_owned()), - ("host".to_owned(), "front.example".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&plugin), None).unwrap(), - Some(ShadowsocksPluginTransport::SimpleObfs { - mode: SimpleObfsMode::Http, - host: "front.example".to_owned() - }) - ); - - let uri_normalized_plugin = ShadowsocksPlugin { - name: "obfs".to_owned(), - opts: HashMap::from([ - ("obfs".to_owned(), "tls".to_owned()), - ("obfs-host".to_owned(), "cdn.example".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&uri_normalized_plugin), None).unwrap(), - Some(ShadowsocksPluginTransport::SimpleObfs { - mode: SimpleObfsMode::Tls, - host: "cdn.example".to_owned() - }) - ); - - let yaml_simple_obfs_alias = ShadowsocksPlugin { - name: "obfs-local".to_owned(), - opts: HashMap::from([ - ("obfs".to_owned(), "tls".to_owned()), - ("obfs-host".to_owned(), "cdn.example".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&yaml_simple_obfs_alias), None).unwrap(), - None - ); - - let v2ray = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("path".to_owned(), "/ws".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&v2ray), None).unwrap(), - Some(ShadowsocksPluginTransport::WebSocket( - ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "bing.com".to_owned(), - path: "/ws".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::V2ray, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - } - )) - ); - - let v2ray_uri_aliases = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::from([ - ("obfs".to_owned(), "websocket".to_owned()), - ("obfs-host".to_owned(), "cdn.example".to_owned()), - ("path".to_owned(), "/ws".to_owned()), - ("tls".to_owned(), "true".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&v2ray_uri_aliases), None).unwrap(), - Some(ShadowsocksPluginTransport::WebSocket( - ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "cdn.example".to_owned(), - path: "/ws".to_owned(), - headers: Vec::new(), - tls: true, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::V2ray, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - } - )) - ); - - let v2ray_fast_open = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("v2ray-http-upgrade".to_owned(), "true".to_owned()), - ("v2ray-http-upgrade-fast-open".to_owned(), "true".to_owned()), - ]), - }; - let Some(ShadowsocksPluginTransport::WebSocket(v2ray_fast_open)) = - shadowsocks_plugin_transport(Some(&v2ray_fast_open), None).unwrap() - else { - panic!("expected websocket plugin transport"); - }; - assert!(v2ray_fast_open.v2ray_http_upgrade); - assert!(v2ray_fast_open.v2ray_http_upgrade_fast_open); - - let v2ray_mtls = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("tls".to_owned(), "true".to_owned()), - ("certificate".to_owned(), "/tmp/client.crt".to_owned()), - ("private-key".to_owned(), "/tmp/client.key".to_owned()), - ]), - }; - let Some(ShadowsocksPluginTransport::WebSocket(v2ray_mtls)) = - shadowsocks_plugin_transport(Some(&v2ray_mtls), None).unwrap() - else { - panic!("expected websocket plugin transport"); - }; - assert_eq!( - v2ray_mtls.client_identity, - Some(TlsClientIdentity { - certificate: "/tmp/client.crt".to_owned(), - private_key: "/tmp/client.key".to_owned(), - }) - ); - - let v2ray_ech = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("tls".to_owned(), "true".to_owned()), - ("ech-opts.enable".to_owned(), "true".to_owned()), - ("ech-opts.config".to_owned(), "AQIDBA==".to_owned()), - ( - "ech-opts.query-server-name".to_owned(), - "public.example".to_owned(), - ), - ]), - }; - let Some(ShadowsocksPluginTransport::WebSocket(v2ray_ech)) = - shadowsocks_plugin_transport(Some(&v2ray_ech), None).unwrap() - else { - panic!("expected websocket plugin transport"); - }; - assert_eq!( - v2ray_ech.ech, - Some(ShadowsocksEchConfig { - config_list: Some(vec![1, 2, 3, 4]), - query_server_name: Some("public.example".to_owned()), - }) - ); - - let gost_without_mux = ShadowsocksPlugin { - name: "gost-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("mux".to_owned(), "false".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&gost_without_mux), None).unwrap(), - Some(ShadowsocksPluginTransport::WebSocket( - ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::Gost, - host: "bing.com".to_owned(), - path: "/".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - } - )) - ); - - let gost_default_mux = ShadowsocksPlugin { - name: "gost-plugin".to_owned(), - opts: HashMap::from([("mode".to_owned(), "websocket".to_owned())]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&gost_default_mux), None).unwrap(), - Some(ShadowsocksPluginTransport::WebSocket( - ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::Gost, - host: "bing.com".to_owned(), - path: "/".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::Gost, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - } - )) - ); - - let missing_obfs_mode = ShadowsocksPlugin { - name: "obfs".to_owned(), - opts: HashMap::new(), - }; - assert!(shadowsocks_plugin_transport(Some(&missing_obfs_mode), None).is_err()); - - let uppercase_obfs_plugin = ShadowsocksPlugin { - name: "Obfs".to_owned(), - opts: HashMap::from([("mode".to_owned(), "http".to_owned())]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&uppercase_obfs_plugin), None).unwrap(), - None - ); - - let missing_websocket_mode = ShadowsocksPlugin { - name: "v2ray-plugin".to_owned(), - opts: HashMap::new(), - }; - assert!(shadowsocks_plugin_transport(Some(&missing_websocket_mode), None).is_err()); - - let shadow_tls_alias = ShadowsocksPlugin { - name: "shadowtls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ]), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&shadow_tls_alias), None).unwrap(), - None - ); - - let unknown_plugin = ShadowsocksPlugin { - name: "unknown-plugin".to_owned(), - opts: HashMap::new(), - }; - assert_eq!( - shadowsocks_plugin_transport(Some(&unknown_plugin), None).unwrap(), - None - ); - - let shadow_tls = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ("version".to_owned(), "1".to_owned()), - ("alpn".to_owned(), "http/1.1".to_owned()), - ("skip-cert-verify".to_owned(), "true".to_owned()), - ( - "fingerprint".to_owned(), - "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff" - .to_owned(), - ), - ]), - }; - let shadow_tls_fingerprint = parse_certificate_fingerprint( - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", - ) - .unwrap(); - assert_eq!( - shadowsocks_plugin_transport(Some(&shadow_tls), None).unwrap(), - Some(ShadowsocksPluginTransport::ShadowTls( - ShadowsocksShadowTlsTransport { - host: "cover.example".to_owned(), - password: "secret".to_owned(), - version: 1, - alpn: vec!["http/1.1".to_owned()], - skip_cert_verify: true, - certificate_fingerprint: Some(shadow_tls_fingerprint), - client_identity: None, - client_fingerprint: None, - } - )) - ); - - let shadow_tls_v1_without_password_plugin = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("version".to_owned(), "1".to_owned()), - ]), - }; - let Some(ShadowsocksPluginTransport::ShadowTls(shadow_tls_v1_without_password)) = - shadowsocks_plugin_transport(Some(&shadow_tls_v1_without_password_plugin), None) - .unwrap() - else { - panic!("expected shadow-tls plugin transport"); - }; - assert_eq!(shadow_tls_v1_without_password.version, 1); - assert_eq!(shadow_tls_v1_without_password.password, ""); - assert_eq!( - shadow_tls_v1_without_password.alpn, - vec!["h2".to_owned(), "http/1.1".to_owned()] - ); - - let shadow_tls_explicit_empty_alpn = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("version".to_owned(), "1".to_owned()), - ("alpn".to_owned(), "".to_owned()), - ]), - }; - let Some(ShadowsocksPluginTransport::ShadowTls(shadow_tls_empty_alpn)) = - shadowsocks_plugin_transport(Some(&shadow_tls_explicit_empty_alpn), None).unwrap() - else { - panic!("expected shadow-tls plugin transport"); - }; - assert!(shadow_tls_empty_alpn.alpn.is_empty()); - - let Some(ShadowsocksPluginTransport::ShadowTls(shadow_tls_v1_with_fingerprint)) = - shadowsocks_plugin_transport( - Some(&shadow_tls_v1_without_password_plugin), - Some("firefox"), - ) - .unwrap() - else { - panic!("expected shadow-tls plugin transport"); - }; - assert_eq!(shadow_tls_v1_with_fingerprint.version, 1); - assert_eq!(shadow_tls_v1_with_fingerprint.client_fingerprint, None); - - let shadow_tls_v2_with_fingerprint_plugin = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ("version".to_owned(), "2".to_owned()), - ]), - }; - #[cfg(feature = "boring-browser-fingerprints")] - { - let Some(ShadowsocksPluginTransport::ShadowTls(shadow_tls_v2_with_fingerprint)) = - shadowsocks_plugin_transport( - Some(&shadow_tls_v2_with_fingerprint_plugin), - Some("firefox"), - ) - .unwrap() - else { - panic!("expected shadow-tls plugin transport"); - }; - assert_eq!(shadow_tls_v2_with_fingerprint.version, 2); - assert_eq!( - shadow_tls_v2_with_fingerprint.client_fingerprint, - Some(MihomoClientFingerprint::Firefox) - ); - } - #[cfg(not(feature = "boring-browser-fingerprints"))] - assert!(shadowsocks_plugin_transport( - Some(&shadow_tls_v2_with_fingerprint_plugin), - Some("firefox"), - ) - .is_err()); - - let shadow_tls_v2_without_password = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("version".to_owned(), "2".to_owned()), - ]), - }; - assert!(shadowsocks_plugin_transport(Some(&shadow_tls_v2_without_password), None).is_err()); - - let incomplete_mtls = ShadowsocksPlugin { - name: "gost-plugin".to_owned(), - opts: HashMap::from([ - ("mode".to_owned(), "websocket".to_owned()), - ("certificate".to_owned(), "/tmp/client.crt".to_owned()), - ]), - }; - assert!(shadowsocks_plugin_transport(Some(&incomplete_mtls), None).is_err()); - - let shadow_tls_v3_plugin = ShadowsocksPlugin { - name: "shadow-tls".to_owned(), - opts: HashMap::from([ - ("host".to_owned(), "cover.example".to_owned()), - ("password".to_owned(), "secret".to_owned()), - ("version".to_owned(), "3".to_owned()), - ]), - }; - assert!(shadowsocks_plugin_transport(Some(&shadow_tls_v3_plugin), None).is_err()); - - let kcptun = ShadowsocksPlugin { - name: "kcptun".to_owned(), - opts: HashMap::from([ - ("key".to_owned(), "secret".to_owned()), - ("crypt".to_owned(), "tea".to_owned()), - ("mode".to_owned(), "fast3".to_owned()), - ("conn".to_owned(), "2".to_owned()), - ("nocomp".to_owned(), "true".to_owned()), - ("ratelimit".to_owned(), "2048".to_owned()), - ("dscp".to_owned(), "46".to_owned()), - ("sockbuf".to_owned(), "8388608".to_owned()), - ("smuxver".to_owned(), "3".to_owned()), - ("framesize".to_owned(), "4096".to_owned()), - ]), - }; - let Some(ShadowsocksPluginTransport::Kcptun(kcptun)) = - shadowsocks_plugin_transport(Some(&kcptun), None).unwrap() - else { - panic!("expected kcptun plugin transport"); - }; - assert_eq!(kcptun.key, "secret"); - assert_eq!(kcptun.crypt, "tea"); - assert_eq!(kcptun.mode, "fast3"); - assert_eq!(kcptun.conn, 2); - assert_eq!(kcptun.no_delay, 1); - assert_eq!(kcptun.interval, 10); - assert_eq!(kcptun.resend, 2); - assert!(kcptun.no_congestion); - assert!(kcptun.no_comp); - assert_eq!(kcptun.rate_limit, 2048); - assert_eq!(kcptun.dscp, 46); - assert_eq!(kcptun.sockbuf, 8_388_608); - assert_eq!(kcptun.smux_ver, 2); - assert_eq!(kcptun.frame_size, 4096); - assert_eq!(kcptun_ipv4_dscp_tos(kcptun.dscp), 184); - - } - - #[test] - fn restls_application_records_authenticate_and_mask_headers() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x42; 32]; - let script = vec![RestlsScriptLine { - target_len: RestlsTargetLength { base: 8, random_range: 0 }, - command: RestlsCommand::Response(1), - }]; - let mut writer = RestlsApplicationCodec::new(secret, server_random, false); - let result = writer.build_to_server_record(b"hello", &script).unwrap(); - - assert_eq!(result.consumed, 5); - assert_eq!(result.command, RestlsCommand::Response(1)); - assert_eq!(result.record[0], TLS_APPLICATION_DATA_RECORD_TYPE); - assert_eq!(&result.record[1..3], &TLS12_RECORD_VERSION); - assert_eq!( - usize::from(u16::from_be_bytes([result.record[3], result.record[4]])), - RESTLS_APP_DATA_AUTH_HEADER_LEN + 8 - ); - assert_ne!( - &result.record[TLS_RECORD_HEADER_LEN + RESTLS_APP_DATA_LEN_OFFSET - ..TLS_RECORD_HEADER_LEN + RESTLS_APP_DATA_OFFSET], - &[0, 5, 1, 1], - "Restls masks the length/command bytes with the BLAKE3 auth header hash" - ); - - let mut reader = RestlsApplicationCodec::new(secret, server_random, false); - let frame = reader.decode_to_server_record(&result.record).unwrap(); - assert_eq!(frame.data, b"hello"); - assert_eq!(frame.command, RestlsCommand::Response(1)); - - let mut tampered = result.record.clone(); - let last = tampered.last_mut().unwrap(); - *last ^= 0x01; - let mut reader = RestlsApplicationCodec::new(secret, server_random, false); - assert!(reader.decode_to_server_record(&tampered).is_err()); - } - - #[test] - fn restls_first_application_record_binds_client_finished() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x11; 32]; - let client_finished = b"encrypted-client-finished".to_vec(); - let mut writer = RestlsApplicationCodec::new(secret, server_random, false); - writer.set_client_finished_auth(client_finished.clone()); - let first = writer.build_to_server_record(b"first", &[]).unwrap(); - let second = writer.build_to_server_record(b"second", &[]).unwrap(); - - let mut missing_finished = RestlsApplicationCodec::new(secret, server_random, false); - assert!(missing_finished - .decode_to_server_record(&first.record) - .is_err()); - - let mut reader = RestlsApplicationCodec::new(secret, server_random, false); - reader.set_client_finished_auth(client_finished); - let first_frame = reader.decode_to_server_record(&first.record).unwrap(); - assert_eq!(first_frame.data, b"first"); - assert_eq!(first_frame.command, RestlsCommand::Noop); - - let second_frame = reader.decode_to_server_record(&second.record).unwrap(); - assert_eq!(second_frame.data, b"second"); - assert_eq!(second_frame.command, RestlsCommand::Noop); - } - - #[test] - fn restls_tls12_gcm_application_records_carry_explicit_counters() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x24; 32]; - let mut writer = RestlsApplicationCodec::new(secret, server_random, true); - let result = writer.build_to_server_record(b"abc", &[]).unwrap(); - assert_eq!( - u64::from_be_bytes( - result.record[TLS_RECORD_HEADER_LEN..TLS_RECORD_HEADER_LEN + 8] - .try_into() - .unwrap() - ), - 1 - ); - - let mut reader = RestlsApplicationCodec::new(secret, server_random, true); - let frame = reader.decode_to_server_record(&result.record).unwrap(); - assert_eq!(frame.data, b"abc"); - assert_eq!(frame.command, RestlsCommand::Noop); - - let mut replay_reader = RestlsApplicationCodec::new(secret, server_random, true); - replay_reader.to_server_counter = 1; - assert!(replay_reader - .decode_to_server_record(&result.record) - .is_err()); - } - - #[test] - fn restls_tls12_gcm_to_client_can_disable_explicit_counter() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x25; 32]; - let mut server_writer = RestlsApplicationCodec::new(secret, server_random, false); - let record = server_writer - .build_to_client_test_record(b"abc", RestlsCommand::Noop) - .unwrap(); - - let mut strict_reader = RestlsApplicationCodec::new(secret, server_random, true); - assert!(strict_reader.decode_to_client_record(&record).is_err()); - - let mut reader = RestlsApplicationCodec::new(secret, server_random, true); - reader.set_tls12_gcm_to_client_disable_counter(true); - let frame = reader.decode_to_client_record(&record).unwrap(); - assert_eq!(frame.data, b"abc"); - assert_eq!(frame.command, RestlsCommand::Noop); - } - - #[test] - fn restls_response_count_matches_mihomo_signed_byte_shape() { - assert_eq!(restls_response_repeat_count(2, false), 2); - assert_eq!(restls_response_repeat_count(2, true), 1); - assert_eq!(restls_response_repeat_count(0, true), 0); - assert_eq!(restls_response_repeat_count(200, false), 0); - assert_eq!(restls_response_repeat_count(128, true), 127); - assert_eq!(restls_response_repeat_count(300, false), 44); - } - - #[test] - fn restls_stream_state_buffers_and_resumes_scripted_interrupts() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x51; 32]; - let script = vec![ - RestlsScriptLine { - target_len: RestlsTargetLength { base: 3, random_range: 0 }, - command: RestlsCommand::Response(2), - }, - RestlsScriptLine { - target_len: RestlsTargetLength { base: 6, random_range: 0 }, - command: RestlsCommand::Noop, - }, - ]; - let mut writer = RestlsStreamState::new( - RestlsApplicationCodec::new(secret, server_random, false), - script, - ); - let mut reader = RestlsApplicationCodec::new(secret, server_random, false); - - let first = writer.write(b"abcdef").unwrap(); - assert_eq!(first.accepted, 6); - assert_eq!(first.records.len(), 1); - assert!(writer.write_pending); - assert_eq!(writer.send_buffer, b"def"); - let frame = reader.decode_to_server_record(&first.records[0]).unwrap(); - assert_eq!(frame.data, b"abc"); - assert_eq!(frame.command, RestlsCommand::Response(2)); - - let buffered = writer.write(b"ghi").unwrap(); - assert_eq!(buffered.accepted, 3); - assert!(buffered.records.is_empty()); - assert_eq!(writer.send_buffer, b"defghi"); - - let resumed = writer.resume_pending_write().unwrap(); - assert_eq!(resumed.records.len(), 1); - assert!(!writer.write_pending); - assert!(writer.send_buffer.is_empty()); - let frame = reader.decode_to_server_record(&resumed.records[0]).unwrap(); - assert_eq!(frame.data, b"defghi"); - assert_eq!(frame.command, RestlsCommand::Noop); - - let response_records = writer - .receive_command(RestlsCommand::Response(2), true) - .unwrap(); - assert_eq!(response_records.len(), 1); - let frame = reader - .decode_to_server_record(&response_records[0]) - .unwrap(); - assert!(frame.data.is_empty()); - assert_eq!(frame.command, RestlsCommand::Noop); - } - - #[test] - fn restls_stream_state_decodes_server_records_and_unblocks_upload() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x52; 32]; - let script = vec![ - RestlsScriptLine { - target_len: RestlsTargetLength { base: 2, random_range: 0 }, - command: RestlsCommand::Response(2), - }, - RestlsScriptLine { - target_len: RestlsTargetLength { base: 2, random_range: 0 }, - command: RestlsCommand::Noop, - }, - RestlsScriptLine { - target_len: RestlsTargetLength { base: 0, random_range: 0 }, - command: RestlsCommand::Noop, - }, - ]; - let mut state = RestlsStreamState::new( - RestlsApplicationCodec::new(secret, server_random, false), - script, - ); - let mut peer_reader = RestlsApplicationCodec::new(secret, server_random, false); - let mut server_writer = RestlsApplicationCodec::new(secret, server_random, false); - - let upload = state.write(b"abcd").unwrap(); - assert_eq!(upload.records.len(), 1); - assert!(state.write_pending); - assert_eq!(state.send_buffer, b"cd"); - assert_eq!( - peer_reader - .decode_to_server_record(&upload.records[0]) - .unwrap() - .data, - b"ab" - ); - - let server_record = server_writer - .build_to_client_test_record(b"server-data", RestlsCommand::Response(2)) - .unwrap(); - let read = state.read_to_client_record(&server_record).unwrap(); - assert_eq!(read.data, b"server-data"); - assert!(read.resumed_pending_write); - assert_eq!(read.records.len(), 2); - assert!(!state.write_pending); - assert!(state.send_buffer.is_empty()); - - let resumed = peer_reader - .decode_to_server_record(&read.records[0]) - .unwrap(); - assert_eq!(resumed.data, b"cd"); - assert_eq!(resumed.command, RestlsCommand::Noop); - let response = peer_reader - .decode_to_server_record(&read.records[1]) - .unwrap(); - assert!(response.data.is_empty()); - assert_eq!(response.command, RestlsCommand::Noop); - } - - #[tokio::test] - async fn restls_stream_wraps_application_read_and_write() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x61; 32]; - let state = RestlsStreamState::new( - RestlsApplicationCodec::new(secret, server_random, false), - Vec::new(), - ); - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = RestlsStream::new(client, state); - - stream.write_all(b"upload").await.unwrap(); - stream.flush().await.unwrap(); - let record = read_test_tls_record(&mut server).await; - let mut peer_reader = RestlsApplicationCodec::new(secret, server_random, false); - let frame = peer_reader.decode_to_server_record(&record).unwrap(); - assert_eq!(frame.data, b"upload"); - assert_eq!(frame.command, RestlsCommand::Noop); - - let mut server_writer = RestlsApplicationCodec::new(secret, server_random, false); - let record = server_writer - .build_to_client_test_record(b"download", RestlsCommand::Noop) - .unwrap(); - server.write_all(&record).await.unwrap(); - - let mut body = [0u8; 8]; - stream.read_exact(&mut body).await.unwrap(); - assert_eq!(&body, b"download"); - } - - #[tokio::test] - async fn restls_stream_resumes_pending_write_from_server_response() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x62; 32]; - let script = vec![ - RestlsScriptLine { - target_len: RestlsTargetLength { base: 2, random_range: 0 }, - command: RestlsCommand::Response(2), - }, - RestlsScriptLine { - target_len: RestlsTargetLength { base: 2, random_range: 0 }, - command: RestlsCommand::Noop, - }, - RestlsScriptLine { - target_len: RestlsTargetLength { base: 0, random_range: 0 }, - command: RestlsCommand::Noop, - }, - ]; - let state = RestlsStreamState::new( - RestlsApplicationCodec::new(secret, server_random, false), - script, - ); - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = RestlsStream::new(client, state); - let mut peer_reader = RestlsApplicationCodec::new(secret, server_random, false); - - stream.write_all(b"abcd").await.unwrap(); - stream.flush().await.unwrap(); - let first = read_test_tls_record(&mut server).await; - let frame = peer_reader.decode_to_server_record(&first).unwrap(); - assert_eq!(frame.data, b"ab"); - assert_eq!(frame.command, RestlsCommand::Response(2)); - - let mut server_writer = RestlsApplicationCodec::new(secret, server_random, false); - let response = server_writer - .build_to_client_test_record(b"ok", RestlsCommand::Response(2)) - .unwrap(); - server.write_all(&response).await.unwrap(); - - let mut body = [0u8; 2]; - stream.read_exact(&mut body).await.unwrap(); - assert_eq!(&body, b"ok"); - - let resumed = read_test_tls_record(&mut server).await; - let frame = peer_reader.decode_to_server_record(&resumed).unwrap(); - assert_eq!(frame.data, b"cd"); - assert_eq!(frame.command, RestlsCommand::Noop); - - let fake_response = read_test_tls_record(&mut server).await; - let frame = peer_reader.decode_to_server_record(&fake_response).unwrap(); - assert!(frame.data.is_empty()); - assert_eq!(frame.command, RestlsCommand::Noop); - } - - #[test] - fn restls_client_hello_auth_material_matches_mihomo_shape() { - fn extension(extension_type: u16, payload: Vec) -> Vec { - let mut encoded = Vec::with_capacity(4 + payload.len()); - encoded.extend_from_slice(&extension_type.to_be_bytes()); - encoded.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - encoded.extend_from_slice(&payload); - encoded - } - - fn test_client_hello(key_shares: &[(u16, Vec)], psk_labels: &[Vec]) -> Vec { - let mut key_share_entries = Vec::new(); - for (group, key) in key_shares { - key_share_entries.extend_from_slice(&group.to_be_bytes()); - key_share_entries.extend_from_slice(&(key.len() as u16).to_be_bytes()); - key_share_entries.extend_from_slice(key); - } - let mut key_share_payload = Vec::new(); - key_share_payload.extend_from_slice(&(key_share_entries.len() as u16).to_be_bytes()); - key_share_payload.extend_from_slice(&key_share_entries); - - let mut identities = Vec::new(); - for label in psk_labels { - identities.extend_from_slice(&(label.len() as u16).to_be_bytes()); - identities.extend_from_slice(label); - identities.extend_from_slice(&0u32.to_be_bytes()); - } - let mut psk_payload = Vec::new(); - psk_payload.extend_from_slice(&(identities.len() as u16).to_be_bytes()); - psk_payload.extend_from_slice(&identities); - psk_payload.extend_from_slice(&2u16.to_be_bytes()); - psk_payload.extend_from_slice(&[0xaa, 0xbb]); - - let mut extensions = Vec::new(); - extensions.extend_from_slice(&extension(0x0033, key_share_payload)); - extensions.extend_from_slice(&extension(0x0029, psk_payload)); - - let mut body = Vec::new(); - body.extend_from_slice(&[0x03, 0x03]); - body.extend_from_slice(&[0x44; 32]); - body.push(32); - body.extend_from_slice(&[0; 32]); - body.extend_from_slice(&2u16.to_be_bytes()); - body.extend_from_slice(&0x1301u16.to_be_bytes()); - body.push(1); - body.push(0); - body.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); - body.extend_from_slice(&extensions); - - let mut hello = Vec::new(); - hello.push(0x01); - hello.extend_from_slice(&[ - ((body.len() >> 16) & 0xff) as u8, - ((body.len() >> 8) & 0xff) as u8, - (body.len() & 0xff) as u8, - ]); - hello.extend_from_slice(&body); - hello - } - - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let key_shares = vec![(0x001d, vec![1, 2, 3, 4]), (0x0017, vec![5, 6, 7, 8])]; - let psk_labels = vec![b"ticket-a".to_vec(), b"ticket-b".to_vec()]; - let seed_session_id = [0x7a; 32]; - let session_id = - restls_tls13_session_id(&secret, &key_shares, &psk_labels, seed_session_id); - - let mut expected_hmac = blake3::Hasher::new_keyed(&secret); - expected_hmac.update(&0x001du16.to_be_bytes()); - expected_hmac.update(&[1, 2, 3, 4]); - expected_hmac.update(&0x0017u16.to_be_bytes()); - expected_hmac.update(&[5, 6, 7, 8]); - expected_hmac.update(b"ticket-a"); - expected_hmac.update(b"ticket-b"); - let expected = expected_hmac.finalize(); - assert_eq!( - &session_id[..RESTLS_HANDSHAKE_MAC_LEN], - &expected.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN] - ); - assert_eq!(&session_id[RESTLS_HANDSHAKE_MAC_LEN..], &[0x7a; 16]); - - let client_hello = test_client_hello(&key_shares, &psk_labels); - let material = restls_tls13_client_hello_auth_material(&client_hello).unwrap(); - assert_eq!(material.key_shares, key_shares); - assert_eq!(material.psk_labels, psk_labels); - assert_eq!( - restls_tls13_session_id_from_client_hello(&secret, &client_hello, seed_session_id) - .unwrap(), - session_id - ); - let mut truncated = client_hello; - truncated.pop(); - assert!(restls_tls13_client_hello_auth_material(&truncated).is_err()); - - let error = StdMutex::new(None); - let fallback_session_id = restls_tls13_session_id_for_client_hello(&secret, &[], &error); - assert_eq!( - &fallback_session_id[..RESTLS_HANDSHAKE_MAC_LEN], - &[0; RESTLS_HANDSHAKE_MAC_LEN] - ); - let err = take_restls_session_id_error(&error).expect("session id error was recorded"); - assert!(err.contains("failed to derive Restls TLS 1.3 session id from ClientHello")); - assert!(err.contains("missing handshake type")); - - let materials = vec![ - vec![0x11; 32], - vec![0x22; 65], - vec![0x33; 97], - b"session-ticket".to_vec(), - ]; - let session_id = restls_tls12_session_id(&secret, &materials).unwrap(); - for (index, window) in RESTLS_TLS12_CLIENT_AUTH_LAYOUT_4.windows(2).enumerate() { - let mut hmac = blake3::Hasher::new_keyed(&secret); - hmac.update(&materials[index]); - let digest = hmac.finalize(); - assert_eq!( - &session_id[window[0]..window[1]], - &digest.as_bytes()[..window[1] - window[0]] - ); - } - assert!(restls_tls12_session_id(&secret, &materials[..2]).is_err()); - } - - #[test] - fn restls_tls13_client_hello_record_signer_preserves_random_tail() { - fn extension(extension_type: u16, payload: Vec) -> Vec { - let mut encoded = Vec::with_capacity(4 + payload.len()); - encoded.extend_from_slice(&extension_type.to_be_bytes()); - encoded.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - encoded.extend_from_slice(&payload); - encoded - } - - let mut key_share_entries = Vec::new(); - key_share_entries.extend_from_slice(&0x001du16.to_be_bytes()); - key_share_entries.extend_from_slice(&4u16.to_be_bytes()); - key_share_entries.extend_from_slice(&[1, 2, 3, 4]); - let mut key_share_payload = Vec::new(); - key_share_payload.extend_from_slice(&(key_share_entries.len() as u16).to_be_bytes()); - key_share_payload.extend_from_slice(&key_share_entries); - - let mut extensions = Vec::new(); - extensions.extend_from_slice(&extension(0x0033, key_share_payload)); - - let mut body = Vec::new(); - body.extend_from_slice(&[0x03, 0x03]); - body.extend_from_slice(&[0x44; 32]); - body.push(32); - body.extend_from_slice(&[0x7a; 32]); - body.extend_from_slice(&2u16.to_be_bytes()); - body.extend_from_slice(&0x1301u16.to_be_bytes()); - body.push(1); - body.push(0); - body.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); - body.extend_from_slice(&extensions); - - let mut payload = vec![0x01]; - payload.extend_from_slice(&[ - ((body.len() >> 16) & 0xff) as u8, - ((body.len() >> 8) & 0xff) as u8, - (body.len() & 0xff) as u8, - ]); - payload.extend_from_slice(&body); - - let mut record = vec![0x16, 0x03, 0x03]; - record.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - record.extend_from_slice(&payload); - record.extend_from_slice(b"next-record"); - - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - assert!(restls_tls13_sign_client_hello_record(&secret, &record[..4]) - .unwrap() - .is_none()); - let signed = restls_tls13_sign_client_hello_record(&secret, &record) - .unwrap() - .expect("complete ClientHello record is signed"); - assert_eq!( - &signed[signed.len() - b"next-record".len()..], - b"next-record" - ); - - let session_id_start = TLS_RECORD_HEADER_LEN + SHADOW_TLS_V3_SESSION_ID_START; - let session_id = &signed[session_id_start..session_id_start + 32]; - let mut expected_hmac = blake3::Hasher::new_keyed(&secret); - expected_hmac.update(&0x001du16.to_be_bytes()); - expected_hmac.update(&[1, 2, 3, 4]); - let expected = expected_hmac.finalize(); - assert_eq!( - &session_id[..RESTLS_HANDSHAKE_MAC_LEN], - &expected.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN] - ); - assert_eq!(&session_id[RESTLS_HANDSHAKE_MAC_LEN..], &[0x7a; 16]); - } - - #[test] - fn restls_server_auth_record_unmasking_matches_mihomo_offsets() { - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x99; 32]; - let mut hmac = blake3::Hasher::new_keyed(&secret); - hmac.update(&server_random); - let digest = hmac.finalize(); - let mask = &digest.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN]; - - let mut plain = vec![TLS_APPLICATION_DATA_RECORD_TYPE, 3, 3, 0, 18]; - plain.extend_from_slice(b"server-auth-record"); - let mut masked = plain.clone(); - xor_with_restls_mask(&mut masked[TLS_RECORD_HEADER_LEN..], mask); - let unmasked = - restls_unmask_server_auth_record(&secret, &server_random, &masked, false).unwrap(); - assert_eq!(unmasked.record, plain); - assert!(!unmasked.tls12_gcm_server_disable_counter); - - let mut plain_gcm = vec![TLS_APPLICATION_DATA_RECORD_TYPE, 3, 3, 0, 23]; - plain_gcm.extend_from_slice(&[0; 8]); - plain_gcm.extend_from_slice(b"gcm-server-auth"); - let mut masked_gcm = plain_gcm.clone(); - xor_with_restls_mask(&mut masked_gcm[TLS_RECORD_HEADER_LEN + 8..], mask); - let unmasked = - restls_unmask_server_auth_record(&secret, &server_random, &masked_gcm, true).unwrap(); - assert_eq!(unmasked.record, plain_gcm); - assert!(!unmasked.tls12_gcm_server_disable_counter); - - let mut plain_gcm_no_counter = vec![TLS_APPLICATION_DATA_RECORD_TYPE, 3, 3, 0, 23]; - plain_gcm_no_counter.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 1]); - plain_gcm_no_counter.extend_from_slice(b"gcm-server-auth"); - let mut masked_gcm_no_counter = plain_gcm_no_counter.clone(); - xor_with_restls_mask(&mut masked_gcm_no_counter[TLS_RECORD_HEADER_LEN..], mask); - let unmasked = - restls_unmask_server_auth_record(&secret, &server_random, &masked_gcm_no_counter, true) - .unwrap(); - assert_eq!(unmasked.record, plain_gcm_no_counter); - assert!(unmasked.tls12_gcm_server_disable_counter); - } - - #[test] - fn restls_handshake_stream_captures_auth_state() { - fn server_hello_record(server_random: [u8; 32]) -> Vec { - let mut body = Vec::new(); - body.extend_from_slice(&[0x03, 0x03]); - body.extend_from_slice(&server_random); - body.push(0); - - let mut payload = Vec::new(); - payload.push(0x02); - payload.extend_from_slice(&[ - ((body.len() >> 16) & 0xff) as u8, - ((body.len() >> 8) & 0xff) as u8, - (body.len() & 0xff) as u8, - ]); - payload.extend_from_slice(&body); - - let mut record = vec![0x16, 0x03, 0x03]; - record.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - record.extend_from_slice(&payload); - record - } - - let secret = blake3::derive_key("restls-traffic-key", b"secret"); - let server_random = [0x71; 32]; - let mut stream = RestlsHandshakeStream::new((), secret, false); - let server_hello = server_hello_record(server_random); - stream.process_read_frame(server_hello.clone()).unwrap(); - assert_eq!(stream.server_random, Some(server_random)); - assert_eq!(stream.read_buf, server_hello); - - let mut hmac = blake3::Hasher::new_keyed(&secret); - hmac.update(&server_random); - let digest = hmac.finalize(); - let mask = &digest.as_bytes()[..RESTLS_HANDSHAKE_MAC_LEN]; - let mut plain_auth = vec![TLS_APPLICATION_DATA_RECORD_TYPE, 0x03, 0x03, 0, 11]; - plain_auth.extend_from_slice(b"server-auth"); - let mut masked_auth = plain_auth.clone(); - xor_with_restls_mask(&mut masked_auth[TLS_RECORD_HEADER_LEN..], mask); - stream.process_read_frame(masked_auth).unwrap(); - assert_eq!(stream.read_buf, plain_auth); - assert!(stream.server_auth_unmasked); - - let ccs = [0x14, 0x03, 0x03, 0, 1, 1]; - let client_finished = [TLS_APPLICATION_DATA_RECORD_TYPE, 0x03, 0x03, 0, 3, 1, 2, 3]; - let mut write = Vec::new(); - write.extend_from_slice(&ccs); - stream.process_write_bytes(&write); - stream.process_write_bytes(&client_finished[..2]); - assert_eq!(stream.client_finished_auth, None); - stream.process_write_bytes(&client_finished[2..]); - - let (_, state) = stream.into_inner(); - assert_eq!(state.server_random, Some(server_random)); - assert_eq!(state.client_finished_auth, Some(client_finished.to_vec())); - assert!(state.server_auth_unmasked); - assert!(!state.tls12_gcm_server_disable_counter); - } - - #[test] - fn kcptun_rate_limiter_uses_mihomo_burst_shape() { - let (client, _server) = tokio::io::duplex(1); - let stream = RateLimitedStream::new(client, 2048); - assert_eq!(stream.bytes_per_second, 2048); - assert_eq!(stream.burst_capacity, KCPTUN_RATE_LIMIT_BURST_BYTES); - assert_eq!(stream.available, KCPTUN_RATE_LIMIT_BURST_BYTES); - } - - #[test] - fn kcptun_smux_keep_alive_timeout_matches_mihomo_default_shape() { - assert_eq!( - kcptun_smux_keep_alive_timeout(Duration::from_secs(5)), - Duration::from_secs(30) - ); - assert_eq!( - kcptun_smux_keep_alive_timeout(Duration::from_secs(10)), - Duration::from_secs(30) - ); - assert_eq!( - kcptun_smux_keep_alive_timeout(Duration::from_secs(30)), - Duration::from_secs(90) - ); - } - - #[test] - fn kcptun_block_crypt_accepts_mihomo_names() { - assert!(kcptun_block_crypt("secret", "null").unwrap().is_none()); - for crypt in [ - "aes", - "aes-128", - "aes-192", - "aes-256", - "tea", - "xor", - "none", - "blowfish", - "cast5", - "3des", - "twofish", - "xtea", - "salsa20", - "sm4", - "aes-128-gcm", - "unknown-falls-back-to-aes", - ] { - assert!( - kcptun_block_crypt("secret", crypt).unwrap().is_some(), - "crypt {crypt}" - ); - } - } - - #[tokio::test] - async fn uot_frame_round_trips_ipv4_and_ipv6() { - for addr in [ - "1.2.3.4:53".parse::().unwrap(), - "[2001:db8::1]:443".parse::().unwrap(), - ] { - let (mut writer, mut reader) = tokio::io::duplex(128); - let write = - tokio::spawn(async move { write_uot_frame(&mut writer, addr, b"hello").await }); - - let (decoded_addr, payload) = read_uot_frame(&mut reader).await.unwrap(); - write.await.unwrap().unwrap(); - assert_eq!(decoded_addr, addr); - assert_eq!(payload, b"hello"); - } - } - - #[tokio::test] - async fn simple_obfs_http_wraps_first_write_and_strips_response() { - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = SimpleObfsStream::new( - client, - SimpleObfsMode::Http, - "front.example".to_owned(), - 8080, - ); - - let write = tokio::spawn(async move { - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut request = vec![0u8; 2048]; - let len = server.read(&mut request).await.unwrap(); - let request = &request[..len]; - assert!(request.starts_with(b"GET http://front.example/ HTTP/1.1\r\n")); - assert!(request - .windows(b"Host: front.example:8080\r\n".len()) - .any(|window| window == b"Host: front.example:8080\r\n")); - assert!(request.ends_with(b"\r\n\r\nhello")); - - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\n\r\nworld") - .await - .unwrap(); - assert_eq!(write.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn simple_obfs_tls_wraps_writes_and_strips_records() { - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = - SimpleObfsStream::new(client, SimpleObfsMode::Tls, "front.example".to_owned(), 443); - - let write = tokio::spawn(async move { - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - stream.write_all(b"again").await?; - stream.flush().await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut client_hello = vec![0u8; 4096]; - let len = server.read(&mut client_hello).await.unwrap(); - let client_hello = &client_hello[..len]; - assert_eq!(client_hello[0], 22); - assert!(client_hello - .windows("front.example".len()) - .any(|window| window == b"front.example")); - assert!(client_hello.windows(5).any(|window| window == b"hello")); - - let mut response = vec![0u8; 105]; - response.extend_from_slice(&5u16.to_be_bytes()); - response.extend_from_slice(b"world"); - server.write_all(&response).await.unwrap(); - - let mut record = [0u8; 10]; - server.read_exact(&mut record).await.unwrap(); - assert_eq!(&record[..3], &[0x17, 0x03, 0x03]); - assert_eq!(u16::from_be_bytes([record[3], record[4]]), 5); - assert_eq!(&record[5..], b"again"); - assert_eq!(write.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn shadow_tls_v2_wraps_records_and_strips_headers() { - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = ShadowTlsV2Stream::new(client, [1, 2, 3, 4, 5, 6, 7, 8]); - - let write = tokio::spawn(async move { - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - stream.write_all(b"again").await?; - stream.flush().await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut first_record = [0u8; 18]; - server.read_exact(&mut first_record).await.unwrap(); - assert_eq!(&first_record[..3], &[0x17, 0x03, 0x03]); - assert_eq!(u16::from_be_bytes([first_record[3], first_record[4]]), 13); - assert_eq!(&first_record[5..13], &[1, 2, 3, 4, 5, 6, 7, 8]); - assert_eq!(&first_record[13..], b"hello"); - - server - .write_all(&[0x17, 0x03, 0x03, 0x00, 0x05]) - .await - .unwrap(); - server.write_all(b"world").await.unwrap(); - - let mut second_record = [0u8; 10]; - server.read_exact(&mut second_record).await.unwrap(); - assert_eq!(&second_record[..3], &[0x17, 0x03, 0x03]); - assert_eq!(u16::from_be_bytes([second_record[3], second_record[4]]), 5); - assert_eq!(&second_record[5..], b"again"); - assert_eq!(write.await.unwrap().unwrap(), *b"world"); - } - - #[test] - fn shadow_tls_v3_session_id_carries_password_hmac() { - let mut client_hello = - vec![0u8; SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE + 8]; - client_hello[0] = 0x01; - client_hello[SHADOW_TLS_V3_SESSION_ID_START - 1] = SHADOW_TLS_V3_SESSION_ID_SIZE as u8; - let session_id = shadow_tls_v3_generate_session_id("secret", &client_hello); - - let mut context = shadow_tls_v3_hmac("secret", b"", b""); - let mut signed_session_id = session_id; - signed_session_id[SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE..].fill(0); - context.update(&client_hello[..SHADOW_TLS_V3_SESSION_ID_START]); - context.update(&signed_session_id); - context.update( - &client_hello[SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE..], - ); - let expected = shadow_tls_v3_hmac_prefix(&context, SHADOW_TLS_V3_HMAC_SIZE); - assert_eq!( - &session_id[SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE..], - expected.as_slice() - ); - } - - #[test] - fn shadow_tls_v3_client_hello_record_signer_rewrites_session_id() { - let mut payload = - vec![0u8; SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE + 8]; - payload[0] = 0x01; - payload[SHADOW_TLS_V3_SESSION_ID_START - 1] = SHADOW_TLS_V3_SESSION_ID_SIZE as u8; - payload[SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE..].fill(0x7a); - - let mut record = vec![0x16, 0x03, 0x03]; - record.extend_from_slice(&(payload.len() as u16).to_be_bytes()); - record.extend_from_slice(&payload); - - assert!( - shadow_tls_v3_sign_client_hello_record("secret", &record[..4]) - .unwrap() - .is_none() - ); - let signed = shadow_tls_v3_sign_client_hello_record("secret", &record) - .unwrap() - .expect("complete ClientHello record is signed"); - assert_eq!(signed.len(), record.len()); - let session_id_start = SHADOW_TLS_V3_TLS_HEADER_SIZE + SHADOW_TLS_V3_SESSION_ID_START; - let session_id = - &signed[session_id_start..session_id_start + SHADOW_TLS_V3_SESSION_ID_SIZE]; - assert_ne!(session_id, &[0u8; SHADOW_TLS_V3_SESSION_ID_SIZE]); - - let mut expected_context = shadow_tls_v3_hmac("secret", b"", b""); - let mut signed_session_id = [0u8; SHADOW_TLS_V3_SESSION_ID_SIZE]; - signed_session_id.copy_from_slice(session_id); - signed_session_id[SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE..].fill(0); - expected_context.update(&payload[..SHADOW_TLS_V3_SESSION_ID_START]); - expected_context.update(&signed_session_id); - expected_context - .update(&payload[SHADOW_TLS_V3_SESSION_ID_START + SHADOW_TLS_V3_SESSION_ID_SIZE..]); - let expected = shadow_tls_v3_hmac_prefix(&expected_context, SHADOW_TLS_V3_HMAC_SIZE); - assert_eq!( - &session_id[SHADOW_TLS_V3_SESSION_ID_SIZE - SHADOW_TLS_V3_HMAC_SIZE..], - expected.as_slice() - ); - } - - #[tokio::test] - async fn shadow_tls_v3_wraps_and_verifies_application_data() { - let (client, mut server) = tokio::io::duplex(4096); - let server_random = [9u8; SHADOW_TLS_V3_TLS_RANDOM_SIZE]; - let mut stream = ShadowTlsV3Stream::new(client, "secret", server_random, None); - - let task = tokio::spawn(async move { - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut first_record = [0u8; SHADOW_TLS_V3_HMAC_HEADER_SIZE + 5]; - server.read_exact(&mut first_record).await.unwrap(); - assert_eq!(&first_record[..3], &[0x17, 0x03, 0x03]); - assert_eq!( - u16::from_be_bytes([first_record[3], first_record[4]]), - (SHADOW_TLS_V3_HMAC_SIZE + 5) as u16 - ); - let mut client_hmac = shadow_tls_v3_hmac("secret", &server_random, b"C"); - client_hmac.update(b"hello"); - assert_eq!( - &first_record[SHADOW_TLS_V3_TLS_HEADER_SIZE..SHADOW_TLS_V3_HMAC_HEADER_SIZE], - shadow_tls_v3_hmac_prefix(&client_hmac, SHADOW_TLS_V3_HMAC_SIZE).as_slice() - ); - assert_eq!(&first_record[SHADOW_TLS_V3_HMAC_HEADER_SIZE..], b"hello"); - - let mut server_hmac = shadow_tls_v3_hmac("secret", &server_random, b"S"); - let response = shadow_tls_v3_record(b"world", 5, &mut server_hmac).unwrap(); - server.write_all(&response).await.unwrap(); - - assert_eq!(task.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn websocket_plugin_wraps_frames_after_upgrade() { - let (client, mut server) = tokio::io::duplex(4096); - let config = ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "front.example".to_owned(), - path: "ws?ed=0".to_owned(), - headers: vec![("User-Agent".to_owned(), "BurrowTest".to_owned())], - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - }; - - let client_task = tokio::spawn(async move { - let mut stream = websocket_plugin_connect(Box::new(client), &config, 443).await?; - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let request = read_http_headers_for_test(&mut server).await; - assert!(request.starts_with("GET /ws HTTP/1.1\r\n")); - assert!(request.contains("Host: front.example\r\n")); - assert!(request.contains("Upgrade: websocket\r\n")); - assert!(request.contains("Sec-WebSocket-Key: ")); - assert!(request.contains("User-Agent: BurrowTest\r\n")); - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n") - .await - .unwrap(); - - assert_eq!( - read_client_websocket_frame_for_test(&mut server).await, - b"hello" - ); - server - .write_all(&server_websocket_frame_for_test(b"world")) - .await - .unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn websocket_early_data_moves_first_payload_to_protocol_header() { - let (client, mut server) = tokio::io::duplex(4096); - let config = ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "front.example".to_owned(), - path: "/ws?ed=8&x=1".to_owned(), - headers: vec![("Sec-WebSocket-Protocol".to_owned(), "stale".to_owned())], - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - }; - - let client_task = tokio::spawn(async move { - let mut stream = websocket_plugin_connect(Box::new(client), &config, 443).await?; - stream.write_all(b"hello-world").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let request = read_http_headers_for_test(&mut server).await; - assert!(request.starts_with("GET /ws?x=1 HTTP/1.1\r\n")); - assert!(request.contains("Sec-WebSocket-Protocol: aGVsbG8td28\r\n")); - assert!(!request.contains("Sec-WebSocket-Protocol: stale\r\n")); - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n") - .await - .unwrap(); - assert_eq!( - read_client_websocket_frame_for_test(&mut server).await, - b"rld" - ); - server - .write_all(&server_websocket_frame_for_test(b"world")) - .await - .unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn websocket_early_data_without_payload_omits_protocol_header() { - let (client, mut server) = tokio::io::duplex(4096); - let config = ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "front.example".to_owned(), - path: "/ws?ed=8".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: false, - v2ray_http_upgrade_fast_open: false, - }; - - let client_task = tokio::spawn(async move { - let mut stream = websocket_plugin_connect(Box::new(client), &config, 443).await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let request = read_http_headers_for_test(&mut server).await; - assert!(request.starts_with("GET /ws HTTP/1.1\r\n")); - assert!(!request.contains("Sec-WebSocket-Protocol:")); - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n") - .await - .unwrap(); - server - .write_all(&server_websocket_frame_for_test(b"world")) - .await - .unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn websocket_http_upgrade_leaves_stream_raw() { - let (client, mut server) = tokio::io::duplex(4096); - let config = ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "front.example".to_owned(), - path: "/upgrade".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: true, - v2ray_http_upgrade_fast_open: false, - }; - - let client_task = tokio::spawn(async move { - let mut stream = websocket_plugin_connect(Box::new(client), &config, 443).await?; - stream.write_all(b"raw").await?; - stream.flush().await?; - let mut body = [0u8; 4]; - stream.read_exact(&mut body).await?; - Result::<[u8; 4]>::Ok(body) - }); - - let request = read_http_headers_for_test(&mut server).await; - assert!(request.starts_with("GET /upgrade HTTP/1.1\r\n")); - assert!(!request.contains("Sec-WebSocket-Key")); - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n") - .await - .unwrap(); - let mut raw = [0u8; 3]; - server.read_exact(&mut raw).await.unwrap(); - assert_eq!(&raw, b"raw"); - server.write_all(b"pong").await.unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"pong"); - } - - #[tokio::test] - async fn websocket_http_upgrade_fast_open_writes_before_response() { - let (client, mut server) = tokio::io::duplex(4096); - let config = ShadowsocksWebSocketTransport { - kind: ShadowsocksWebSocketKind::V2ray, - host: "front.example".to_owned(), - path: "/upgrade".to_owned(), - headers: Vec::new(), - tls: false, - ech: None, - skip_cert_verify: false, - certificate_fingerprint: None, - client_identity: None, - mux: ShadowsocksWebSocketMux::None, - v2ray_http_upgrade: true, - v2ray_http_upgrade_fast_open: true, - }; - - let client_task = tokio::spawn(async move { - let mut stream = websocket_plugin_connect(Box::new(client), &config, 443).await?; - stream.write_all(b"raw").await?; - stream.flush().await?; - let mut body = [0u8; 4]; - stream.read_exact(&mut body).await?; - Result::<[u8; 4]>::Ok(body) - }); - - let request = read_http_headers_for_test(&mut server).await; - assert!(request.starts_with("GET /upgrade HTTP/1.1\r\n")); - let mut raw = [0u8; 3]; - server.read_exact(&mut raw).await.unwrap(); - assert_eq!(&raw, b"raw"); - server - .write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\npong") - .await - .unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"pong"); - } - - #[tokio::test] - async fn v2ray_mux_wraps_open_and_data_frames() { - let (client, mut server) = tokio::io::duplex(4096); - let mut stream = V2rayMuxStream::new(client); - - let client_task = tokio::spawn(async move { - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut open_len = [0u8; 2]; - server.read_exact(&mut open_len).await.unwrap(); - let open_len = u16::from_be_bytes(open_len) as usize; - let mut open = vec![0u8; open_len]; - server.read_exact(&mut open).await.unwrap(); - assert_eq!(&open[..4], &[0, 0, 0x01, 0x00]); - assert!(open - .windows("127.0.0.1".len()) - .any(|window| window == b"127.0.0.1")); - - let mut data_prefix = [0u8; 8]; - server.read_exact(&mut data_prefix).await.unwrap(); - assert_eq!(&data_prefix[..6], &[0, 4, 0, 0, 0x02, 0x01]); - assert_eq!(u16::from_be_bytes([data_prefix[6], data_prefix[7]]), 5); - let mut payload = [0u8; 5]; - server.read_exact(&mut payload).await.unwrap(); - assert_eq!(&payload, b"hello"); - - server - .write_all(&[0, 4, 0, 0, 0x02, 0x01, 0, 5]) - .await - .unwrap(); - server.write_all(b"world").await.unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"world"); - } - - #[tokio::test] - async fn gost_smux_stream_opens_client_stream() { - let (client, server) = tokio::io::duplex(4096); - let client_task = tokio::spawn(async move { - let mut stream = gost_smux_stream(Box::new(client)).await?; - stream.write_all(b"hello").await?; - stream.flush().await?; - let mut body = [0u8; 5]; - stream.read_exact(&mut body).await?; - Result::<[u8; 5]>::Ok(body) - }); - - let mut config = tokio_smux::SmuxConfig::default(); - config.keep_alive_disable = true; - let mut session = tokio_smux::Session::server(server, config).unwrap(); - let mut stream = session.accept_stream().await.unwrap(); - assert_eq!(stream.recv_message().await.unwrap().unwrap(), b"hello"); - stream.send_message(b"world".to_vec()).await.unwrap(); - assert_eq!(client_task.await.unwrap().unwrap(), *b"world"); - } - - async fn read_http_headers_for_test(stream: &mut S) -> String - where - S: AsyncRead + Unpin, - { - let mut bytes = Vec::new(); - let mut byte = [0u8; 1]; - loop { - stream.read_exact(&mut byte).await.unwrap(); - bytes.push(byte[0]); - if bytes.ends_with(b"\r\n\r\n") { - return String::from_utf8(bytes).unwrap(); - } - } - } - - async fn read_client_websocket_frame_for_test(stream: &mut S) -> Vec - where - S: AsyncRead + Unpin, - { - let mut header = [0u8; 2]; - stream.read_exact(&mut header).await.unwrap(); - assert_eq!(header[0] & 0x0f, 0x02); - assert_ne!(header[1] & 0x80, 0); - let len = usize::from(header[1] & 0x7f); - let mut mask = [0u8; 4]; - stream.read_exact(&mut mask).await.unwrap(); - let mut payload = vec![0u8; len]; - stream.read_exact(&mut payload).await.unwrap(); - apply_websocket_mask(&mut payload, mask); - payload - } - - fn server_websocket_frame_for_test(payload: &[u8]) -> Vec { - let mut frame = Vec::new(); - frame.push(0x82); - frame.push(payload.len() as u8); - frame.extend_from_slice(payload); - frame - } - - #[tokio::test] - async fn custom_aead_byte_reader_reassembles_uot_frame_chunks() { - let kind = CustomSsAeadKind::Aes192Gcm; - let key = Arc::<[u8]>::from(evp_bytes_to_key(b"secret", kind.key_len())); - let target = "8.8.8.8:53".parse::().unwrap(); - let frame = uot_frame(target, b"hello").unwrap(); - let split_at = 3; - let (client, server) = tokio::io::duplex(256); - let mut writer = CustomSsAeadWriter::new(client, kind, key.clone()) - .await - .unwrap(); - let mut reader = - CustomSsAeadByteReader::new(CustomSsAeadReader::new(server, kind, key.clone())); - - let write = tokio::spawn(async move { - writer.write_chunk(&frame[..split_at]).await?; - writer.write_chunk(&frame[split_at..]).await?; - Result::<()>::Ok(()) - }); - - let (decoded_addr, payload) = reader.read_frame().await.unwrap().unwrap(); - write.await.unwrap().unwrap(); - assert_eq!(decoded_addr, target); - assert_eq!(payload, b"hello"); - } - - #[test] - fn aead2022_aes_ccm_keys_follow_mihomo_base64_rules() { - let kind = Aead2022AesCcmKind::Aes128; - let identity = vec![1u8; kind.key_len()]; - let user = vec![2u8; kind.key_len()]; - let password = format!( - "{}:{}", - BASE64_STANDARD.encode(&identity), - BASE64_STANDARD.encode(&user) - ); - let (parsed_user, parsed_identities) = parse_aead2022_keys(kind, &password).unwrap(); - assert_eq!(parsed_user.as_ref(), user.as_slice()); - assert_eq!(parsed_identities.len(), 1); - assert_eq!(parsed_identities[0].as_ref(), identity.as_slice()); - - assert!(parse_aead2022_keys(kind, &BASE64_STANDARD.encode([3u8; 64])).is_err()); - assert!(parse_aead2022_keys(kind, &BASE64_STANDARD.encode([1u8; 8])).is_err()); - } - - #[test] - fn standard_aead2022_keys_follow_mihomo_base64_rules() { - let identity = vec![1u8; 16]; - let user = vec![2u8; 16]; - let password = format!( - "{}:{}", - BASE64_STANDARD.encode(&identity), - BASE64_STANDARD.encode(&user) - ); - let config = ShadowsocksNode { - cipher: "2022-blake3-aes-128-gcm".to_owned(), - password, - udp: true, - udp_over_tcp: false, - udp_over_tcp_version: UOT_VERSION, - client_fingerprint: None, - plugin: None, - smux: None, - }; - let node = ProxyNode { - ordinal: 0, - name: "ss-2022-gcm".to_owned(), - protocol: crate::proxy_subscription::ProxyProtocol::Shadowsocks, - server: "127.0.0.1".to_owned(), - port: 8388, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(config.clone()), - }; - - let protocol = shadowsocks_protocol_for_node(&node, &config).unwrap(); - let ShadowsocksProtocol::Standard { server_config, .. } = protocol else { - panic!("expected delegated standard Shadowsocks protocol"); - }; - assert_eq!(server_config.key(), user.as_slice()); - assert_eq!(server_config.identity_keys().len(), 1); - assert_eq!( - server_config.identity_keys()[0].as_ref(), - identity.as_slice() - ); - - let unpadded_user = BASE64_STANDARD - .encode(&user) - .trim_end_matches('=') - .to_owned(); - assert!( - validate_standard_aead2022_mihomo_password( - SsCipherKind::AEAD2022_BLAKE3_AES_128_GCM, - &unpadded_user - ) - .is_err(), - "Mihomo's base64.StdEncoding requires key padding for 16-byte keys" - ); - assert!( - validate_standard_aead2022_mihomo_password( - SsCipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305, - &format!( - "{}:{}", - BASE64_STANDARD.encode([3u8; 32]), - BASE64_STANDARD.encode([4u8; 32]) - ) - ) - .is_err(), - "Mihomo rejects EIH identity chains on Shadowsocks 2022 chacha methods" - ); - validate_standard_aead2022_mihomo_password( - SsCipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305, - &BASE64_STANDARD.encode([5u8; 32]), - ) - .unwrap(); - } - - #[test] - fn aead2022_aes_ccm_tcp_cipher_round_trips_chunks() { - for kind in [Aead2022AesCcmKind::Aes128, Aead2022AesCcmKind::Aes256] { - let key = vec![7u8; kind.key_len()]; - let salt = vec![9u8; kind.key_len()]; - let mut writer = Aead2022TcpCipher::new(kind, &key, &salt).unwrap(); - let mut reader = Aead2022TcpCipher::new(kind, &key, &salt).unwrap(); - - let mut encrypted_len = writer.seal_packet(&5u16.to_be_bytes()).unwrap(); - let len = reader.open_packet(&mut encrypted_len).unwrap(); - assert_eq!(u16::from_be_bytes([len[0], len[1]]), 5); - - let mut encrypted_payload = writer.seal_packet(b"hello").unwrap(); - let payload = reader.open_packet(&mut encrypted_payload).unwrap(); - assert_eq!(payload, b"hello"); - } - } - - #[test] - fn aead2022_aes_ccm_udp_packets_round_trip_client_and_server_shapes() { - for kind in [Aead2022AesCcmKind::Aes128, Aead2022AesCcmKind::Aes256] { - let user_key = Arc::<[u8]>::from(vec![5u8; kind.key_len()]); - let mut session = - Aead2022AesCcmUdpSession::new(kind, user_key.clone(), Arc::from(Vec::new())) - .unwrap(); - let target = "8.8.8.8:443".parse::().unwrap(); - let packet = session.encrypt_client_packet(target, b"hello").unwrap(); - let (decoded_target, decoded_payload) = - decrypt_aead2022_client_packet_for_test(kind, &user_key, &packet).unwrap(); - assert_eq!(decoded_target, target); - assert_eq!(decoded_payload, b"hello"); - - let source = "1.1.1.1:53".parse::().unwrap(); - let response = encrypt_aead2022_server_packet_for_test( - kind, - &user_key, - 99, - 1, - session.client_session_id, - source, - b"world", - ) - .unwrap(); - let (decoded_source, decoded_payload) = - session.decrypt_server_packet(&response).unwrap(); - assert_eq!(decoded_source, source); - assert_eq!(decoded_payload, b"world"); - } - } - - #[test] - fn custom_shadowsocks_aead_udp_packet_round_trips_mihomo_extra_methods() { - for cipher in [ - "aes-192-gcm", - "aes-192-ccm", - "chacha8-ietf-poly1305", - "xchacha8-ietf-poly1305", - "rabbit128-poly1305", - "aegis-128l", - "aegis-256", - "aez-384", - "deoxys-ii-256-128", - "ascon128", - "ascon128a", - "lea-128-gcm", - "lea-192-gcm", - "lea-256-gcm", - ] { - let kind = CustomSsAeadKind::from_name(cipher).unwrap(); - let key = evp_bytes_to_key(b"secret", kind.key_len()); - let target = "8.8.8.8:53".parse().unwrap(); - let packet = encrypt_custom_ss_udp_packet(kind, &key, target, b"hello").unwrap(); - let (decoded_target, payload) = - decrypt_custom_ss_udp_packet(kind, &key, &packet).unwrap(); - assert_eq!(decoded_target, target); - assert_eq!(payload, b"hello"); - } - } - - #[test] - fn custom_shadowsocks_stream_udp_packet_round_trips_mihomo_methods() { - for cipher in ["chacha20", "xchacha20"] { - let kind = CustomSsStreamKind::from_name(cipher).unwrap(); - let key = evp_bytes_to_key(b"secret", kind.key_len()); - let target = "8.8.8.8:53".parse().unwrap(); - let packet = encrypt_custom_ss_stream_udp_packet(kind, &key, target, b"hello").unwrap(); - let (decoded_target, payload) = - decrypt_custom_ss_stream_udp_packet(kind, &key, &packet).unwrap(); - assert_eq!(decoded_target, target); - assert_eq!(payload, b"hello"); - } - } - - #[tokio::test] - async fn custom_shadowsocks_stream_tcp_cipher_keeps_state_across_writes() { - for cipher in ["chacha20", "xchacha20"] { - let kind = CustomSsStreamKind::from_name(cipher).unwrap(); - let key = Arc::<[u8]>::from(evp_bytes_to_key(b"secret", kind.key_len())); - let (client, mut server) = tokio::io::duplex(1024); - let mut writer = CustomSsStreamWriter::new(client, kind, key.clone()) - .await - .unwrap(); - writer.write_all_encrypted(b"hello").await.unwrap(); - writer.write_all_encrypted(b"world").await.unwrap(); - writer.shutdown().await.unwrap(); - - let mut raw = Vec::new(); - server.read_to_end(&mut raw).await.unwrap(); - let (salt, encrypted) = raw.split_at(kind.salt_len()); - let mut decrypted = encrypted.to_vec(); - let mut cipher = CustomSsStreamCipher::new(kind, &key, salt).unwrap(); - cipher.apply_keystream(&mut decrypted); - assert_eq!(decrypted, b"helloworld"); - - let (mut server, client) = tokio::io::duplex(1024); - let salt = vec![7u8; kind.salt_len()]; - let mut encrypted = b"response".to_vec(); - let mut cipher = CustomSsStreamCipher::new(kind, &key, &salt).unwrap(); - cipher.apply_keystream(&mut encrypted[..3]); - cipher.apply_keystream(&mut encrypted[3..]); - server.write_all(&salt).await.unwrap(); - server.write_all(&encrypted).await.unwrap(); - drop(server); - - let mut reader = CustomSsStreamReader::new(client, kind, key); - let mut response = Vec::new(); - let mut output = [0u8; 4]; - loop { - let n = reader.read_decrypted(&mut output).await.unwrap(); - if n == 0 { - break; - } - response.extend_from_slice(&output[..n]); - } - assert_eq!(response, b"response"); - } - } - - #[test] - fn custom_ascon128_matches_v12_reference_vector() { - let key: [u8; 16] = core::array::from_fn(|index| index as u8); - let nonce: [u8; 16] = core::array::from_fn(|index| index as u8); - let mut plaintext = Vec::new(); - let tag = ascon_seal_in_place(&key, &nonce, AsconMode::Ascon128, &mut plaintext); - assert_eq!(hex_lower(&tag), "e355159f292911f794cb1432a0103a8a"); - - let mut plaintext = Vec::new(); - let tag = ascon_seal_in_place(&key, &nonce, AsconMode::Ascon128A, &mut plaintext); - assert_eq!(hex_lower(&tag), "7a834e6f09210957067b10fd831f0078"); - } - - #[test] - fn custom_lea_gcm_matches_mihomo_vectors() { - for (cipher, expected) in [ - ("lea-128-gcm", "b644a1f5ad436a94f4eac45f12010eed1396e49e9b"), - ("lea-192-gcm", "9ed6792982070248de7ddcb355da658774ab781ea0"), - ("lea-256-gcm", "223060f9a91775199a3f9331cd8d410e5935b0be50"), - ] { - let kind = CustomSsAeadKind::from_name(cipher).unwrap(); - let key: Vec = (0..kind.key_len()).map(|index| index as u8).collect(); - let cipher = CustomSsAeadCipher::new(kind, &key).unwrap(); - let mut nonce: Vec = (0..kind.nonce_len()).map(|index| index as u8).collect(); - let mut message = b"hello".to_vec(); - cipher.seal_in_place(&mut nonce, &mut message).unwrap(); - assert_eq!(hex_lower(&message), expected); - } - } - - fn decrypt_aead2022_client_packet_for_test( - kind: Aead2022AesCcmKind, - user_key: &[u8], - packet: &[u8], - ) -> Result<(SocketAddr, Vec)> { - let mut buf = packet.to_vec(); - aead2022_aes_decrypt_block(kind, user_key, &mut buf[..16])?; - let session_id_bytes: [u8; 8] = buf[..8].try_into().expect("fixed session id"); - let cipher = Aead2022AesCcmCipher::new( - kind, - &aead2022_session_key(kind, user_key, &session_id_bytes)?, - )?; - let nonce: [u8; 12] = buf[4..16].try_into().expect("fixed nonce"); - let plaintext = cipher.open_in_place(&nonce, &mut buf[16..])?; - if plaintext[0] != AEAD2022_HEADER_TYPE_CLIENT { - bail!("unexpected test client packet type"); - } - let padding_len = u16::from_be_bytes([plaintext[9], plaintext[10]]) as usize; - let address_offset = 11 + padding_len; - let (target, used) = parse_socks_addr(&plaintext[address_offset..])?; - Ok((target, plaintext[address_offset + used..].to_vec())) - } - - fn encrypt_aead2022_server_packet_for_test( - kind: Aead2022AesCcmKind, - user_key: &[u8], - session_id: u64, - packet_id: u64, - client_session_id: u64, - source: SocketAddr, - payload: &[u8], - ) -> Result> { - let session_id_bytes = session_id.to_be_bytes(); - let cipher = Aead2022AesCcmCipher::new( - kind, - &aead2022_session_key(kind, user_key, &session_id_bytes)?, - )?; - let mut packet = Vec::new(); - packet.extend_from_slice(&session_id.to_be_bytes()); - packet.extend_from_slice(&packet_id.to_be_bytes()); - let data_index = packet.len(); - packet.push(AEAD2022_HEADER_TYPE_SERVER); - packet.extend_from_slice(&aead2022_now_timestamp()?.to_be_bytes()); - packet.extend_from_slice(&client_session_id.to_be_bytes()); - packet.extend_from_slice(&0u16.to_be_bytes()); - packet.extend_from_slice(&socks_addr(source)?); - packet.extend_from_slice(payload); - let nonce: [u8; 12] = packet[4..16].try_into().expect("fixed nonce"); - let mut encrypted_body = packet[data_index..].to_vec(); - cipher.seal_in_place(&nonce, &mut encrypted_body)?; - packet.truncate(data_index); - packet.extend_from_slice(&encrypted_body); - aead2022_aes_encrypt_block(kind, user_key, &mut packet[..16])?; - Ok(packet) - } - - #[test] - fn shadowsocks_address_preserves_fake_dns_hostname() { - let target = ProxyTcpTarget { - address: "198.18.64.1:443".parse().unwrap(), - hostname: Some("example.com.".to_owned()), - }; - - assert_eq!( - shadowsocks_addr_for_target(&target).unwrap(), - SsAddress::DomainNameAddress("example.com".to_owned(), 443) - ); - } - - #[test] - fn trojan_runtime_uses_mihomo_default_alpn_when_absent() { - let mut node = TrojanNode { - password: "secret".to_owned(), - sni: None, - alpn: Vec::new(), - alpn_present: false, - skip_cert_verify: false, - network: TrojanNetwork::Tcp, - udp: true, - client_fingerprint: None, - fingerprint: None, - }; - assert_eq!(trojan_alpn(&node), vec!["h2", "http/1.1"]); - - node.alpn_present = true; - assert!(trojan_alpn(&node).is_empty()); - - node.alpn = vec!["h3".to_owned()]; - assert_eq!(trojan_alpn(&node), vec!["h3"]); - } - - #[test] - fn parses_certificate_fingerprint_hex() { - let fp = parse_certificate_fingerprint( - "00:112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", - ) - .unwrap(); - assert_eq!(fp.0[0], 0x00); - assert_eq!(fp.0[1], 0x11); - assert_eq!(fp.0[31], 0xff); - assert!(parse_certificate_fingerprint("chrome").is_err()); - assert!(parse_certificate_fingerprint("abcd").is_err()); - } - - #[tokio::test] - async fn trojan_udp_writer_fragments_oversized_payloads() { - let target = socks_addr("1.2.3.4:53".parse().unwrap()).unwrap(); - let payload = vec![7u8; TROJAN_MAX_UDP_PAYLOAD + 3]; - let (mut writer, mut reader) = tokio::io::duplex(20_000); - - let write = - tokio::spawn( - async move { write_trojan_udp_packet(&mut writer, &target, &payload).await }, - ); - - let mut encoded = Vec::new(); - reader.read_to_end(&mut encoded).await.unwrap(); - write.await.unwrap().unwrap(); - - let first_payload_offset = 7 + 2 + 2; - assert_eq!( - u16::from_be_bytes([encoded[7], encoded[8]]) as usize, - TROJAN_MAX_UDP_PAYLOAD - ); - assert_eq!(&encoded[9..11], b"\r\n"); - assert_eq!(encoded[first_payload_offset], 7); - let second = 7 + 2 + 2 + TROJAN_MAX_UDP_PAYLOAD; - assert_eq!( - u16::from_be_bytes([encoded[second + 7], encoded[second + 8]]) as usize, - 3 - ); - assert_eq!(&encoded[second + 9..second + 11], b"\r\n"); - assert_eq!(encoded.len(), second + 11 + 3); - } - - #[test] - fn fake_ip_detection_flags_benchmark_range() { - assert!(is_fake_or_benchmark_ip("198.18.0.1".parse().unwrap())); - assert!(is_fake_or_benchmark_ip("198.19.255.254".parse().unwrap())); - assert!(!is_fake_or_benchmark_ip("198.20.0.1".parse().unwrap())); - assert!(!is_fake_or_benchmark_ip("2001:db8::1".parse().unwrap())); - } - - #[test] - fn proxy_outbound_socket_binding_skips_loopback() { - assert!(!should_bind_proxy_outbound_socket( - "127.0.0.1".parse().unwrap() - )); - assert!(!should_bind_proxy_outbound_socket("::1".parse().unwrap())); - assert!(should_bind_proxy_outbound_socket( - "192.0.2.10".parse().unwrap() - )); - } - - #[test] - fn dns_query_encodes_host_labels() { - let query = build_dns_query("example.com", 1).unwrap(); - assert_eq!(&query[12..25], b"\x07example\x03com\0"); - assert_eq!(u16::from_be_bytes([query[25], query[26]]), 1); - assert_eq!(u16::from_be_bytes([query[27], query[28]]), 1); - } - - #[test] - fn dns_response_parser_reads_compressed_a_and_aaaa_answers() { - let mut response = Vec::new(); - response.extend_from_slice(&0x1234u16.to_be_bytes()); - response.extend_from_slice(&0x8180u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&2u16.to_be_bytes()); - response.extend_from_slice(&0u16.to_be_bytes()); - response.extend_from_slice(&0u16.to_be_bytes()); - response.extend_from_slice(b"\x07example\x03com\0"); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&[0xc0, 0x0c]); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&60u32.to_be_bytes()); - response.extend_from_slice(&4u16.to_be_bytes()); - response.extend_from_slice(&[93, 184, 216, 34]); - response.extend_from_slice(&[0xc0, 0x0c]); - response.extend_from_slice(&28u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&60u32.to_be_bytes()); - response.extend_from_slice(&16u16.to_be_bytes()); - response.extend_from_slice(&[ - 0x26, 0x06, 0x28, 0x00, 0x02, 0x20, 0x00, 0x01, 0x02, 0x48, 0x18, 0x93, 0x25, 0xc8, - 0x19, 0x46, - ]); - - let addresses = parse_dns_response(&response, 0x1234).unwrap(); - assert_eq!( - addresses, - vec![ - "93.184.216.34".parse::().unwrap(), - "2606:2800:220:1:248:1893:25c8:1946" - .parse::() - .unwrap(), - ] - ); - } - - #[test] - fn dns_https_parser_extracts_ech_service_parameter() { - let mut response = build_dns_query("example.com", DNS_TYPE_HTTPS).unwrap(); - response[0..2].copy_from_slice(&0x1234u16.to_be_bytes()); - response[2..4].copy_from_slice(&0x8180u16.to_be_bytes()); - response[6..8].copy_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&[0xc0, 0x0c]); - response.extend_from_slice(&DNS_TYPE_HTTPS.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.extend_from_slice(&60u32.to_be_bytes()); - response.extend_from_slice(&10u16.to_be_bytes()); - response.extend_from_slice(&1u16.to_be_bytes()); - response.push(0); - response.extend_from_slice(&HTTPS_SVC_PARAM_ECH.to_be_bytes()); - response.extend_from_slice(&3u16.to_be_bytes()); - response.extend_from_slice(&[0xaa, 0xbb, 0xcc]); - - assert_eq!( - parse_dns_https_ech_config_list(&response, 0x1234).unwrap(), - Some(vec![0xaa, 0xbb, 0xcc]) - ); - } - - #[tokio::test] - async fn fake_dns_response_returns_stable_hostname_mapping() { - let query = build_dns_query("example.com", 1).unwrap(); - let cache = Arc::new(RwLock::new(DnsHostnameCacheState::new())); - let response = build_fake_dns_response(&cache, &query).await.unwrap(); - let addresses = parse_dns_response(&response, 0).unwrap(); - assert_eq!(addresses, vec!["198.18.64.1".parse::().unwrap()]); - assert_eq!( - cache - .read() - .await - .lookup_hostname("198.18.64.1".parse().unwrap()), - Some("example.com".to_owned()) - ); - } - - #[test] - fn runtime_selection_skips_unsupported_transports_when_unselected() { - let preview = crate::proxy_subscription::parse_subscription_body( - r#" -proxies: - - name: trojan-grpc - type: trojan - server: example.com - port: 443 - password: secret - network: grpc - - name: ss-aead - type: ss - server: example.net - port: 8388 - cipher: aes-128-gcm - password: secret -"#, - Some("https://example.com/sub"), - ); - let payload = crate::proxy_subscription::build_payload( - preview, - "https://example.com/sub", - Some("Proxy Subscription".to_owned()), - None, - ); - assert_eq!(selected_node(&payload).unwrap().name, "ss-aead"); - } - - #[test] - fn runtime_selection_prefers_selected_name_over_stale_ordinal() { - let preview = crate::proxy_subscription::parse_subscription_body( - r#" -proxies: - - name: first - type: ss - server: first.example.net - port: 8388 - cipher: aes-128-gcm - password: secret - - name: selected - type: ss - server: selected.example.net - port: 8388 - cipher: aes-128-gcm - password: secret -"#, - Some("https://example.com/sub"), - ); - let mut payload = crate::proxy_subscription::build_payload( - preview, - "https://example.com/sub", - Some("Proxy Subscription".to_owned()), - Some(0), - ); - payload.selected_name = Some("selected".to_owned()); - - assert_eq!(selected_node(&payload).unwrap().name, "selected"); - } - - #[test] - fn proxy_server_bypass_routes_use_host_prefixes() { - let routes = excluded_routes_for_server("127.0.0.1", 443).unwrap(); - assert_eq!(routes, vec!["127.0.0.1/32"]); - } - - #[test] - fn parses_physical_dns_from_scutil_output() { - let servers = parse_apple_physical_dns_servers( - r#" -DNS configuration - -resolver #1 - nameserver[0] : 2606:4700:4700::1111 - nameserver[1] : 1.1.1.1 - if_index : 35 (utun8) - -DNS configuration (for scoped queries) - -resolver #1 - search domain[0] : lan - nameserver[0] : 192.168.2.1 - if_index : 17 (en0) - -resolver #2 - nameserver[0] : 198.18.0.1 - if_index : 35 (utun8) -"#, - ); - assert_eq!(servers, vec!["192.168.2.1"]); - } - - #[test] - fn parses_resolv_conf_dns_servers() { - let servers = parse_resolv_conf_dns_servers( - r#" -# macOS generated resolver file -nameserver 192.168.2.1 -nameserver 198.18.0.1 -nameserver 192.168.2.1 -nameserver ::1 -"#, - ); - assert_eq!(servers, vec!["192.168.2.1"]); - } - - #[test] - fn proxy_dns_excluded_routes_use_host_prefixes() { - let routes = proxy_dns_excluded_routes(&[ - "100.64.0.2".to_owned(), - "192.168.2.1".to_owned(), - "2001:db8::53".to_owned(), - "not-an-ip".to_owned(), - ]); - assert_eq!(routes, vec!["192.168.2.1/32", "2001:db8::53/128"]); - } - - #[test] - fn proxy_dns_servers_use_daemon_resolver() { - assert_eq!(proxy_dns_servers(), vec!["100.64.0.2"]); - } -} diff --git a/burrow/src/proxy_subscription.rs b/burrow/src/proxy_subscription.rs deleted file mode 100644 index 428f74d..0000000 --- a/burrow/src/proxy_subscription.rs +++ /dev/null @@ -1,1455 +0,0 @@ -use std::{collections::HashMap, time::Duration}; - -use anyhow::{anyhow, bail, Context, Result}; -use base64::{ - engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD}, - Engine as _, -}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use serde_yaml::Value; - -const MAX_SUBSCRIPTION_BYTES: usize = 1024 * 1024; -const SUBSCRIPTION_USER_AGENT: &str = "mihomo/1.18.3"; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ProxySubscriptionPayload { - pub name: String, - pub source: ProxySubscriptionSource, - pub detected_format: ProxySubscriptionFormat, - pub selected_ordinal: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub selected_name: Option, - pub nodes: Vec, - pub warnings: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ProxySubscriptionSource { - pub url: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum ProxySubscriptionFormat { - ClashYaml, - UriList, -} - -impl ProxySubscriptionFormat { - pub fn as_str(self) -> &'static str { - match self { - Self::ClashYaml => "clash-yaml", - Self::UriList => "uri-list", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ProxyNode { - pub ordinal: usize, - pub name: String, - pub protocol: ProxyProtocol, - pub server: String, - pub port: u16, - pub warnings: Vec, - pub config: ProxyNodeConfig, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum ProxyProtocol { - Trojan, - Shadowsocks, -} - -impl ProxyProtocol { - pub fn as_str(self) -> &'static str { - match self { - Self::Trojan => "trojan", - Self::Shadowsocks => "ss", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "type", rename_all = "kebab-case")] -pub enum ProxyNodeConfig { - Trojan(TrojanNode), - Shadowsocks(ShadowsocksNode), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TrojanNode { - pub password: String, - pub sni: Option, - pub alpn: Vec, - #[serde(default)] - pub alpn_present: bool, - pub skip_cert_verify: bool, - pub network: TrojanNetwork, - #[serde(default = "default_true")] - pub udp: bool, - pub client_fingerprint: Option, - pub fingerprint: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum TrojanNetwork { - Tcp, - Ws { - path: Option, - host: Option, - }, - Grpc { - service_name: Option, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ShadowsocksNode { - pub cipher: String, - pub password: String, - pub udp: bool, - pub udp_over_tcp: bool, - #[serde(default = "default_uot_legacy_version")] - pub udp_over_tcp_version: u8, - pub client_fingerprint: Option, - pub plugin: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub smux: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ShadowsocksPlugin { - pub name: String, - pub opts: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ShadowsocksSmux { - #[serde(default)] - pub enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub protocol: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_connections: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub min_streams: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_streams: Option, - #[serde(default)] - pub padding: bool, - #[serde(default)] - pub statistic: bool, - #[serde(default)] - pub only_tcp: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub brutal: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ShadowsocksSmuxBrutal { - #[serde(default)] - pub enabled: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub up: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub down: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxySubscriptionPreview { - pub suggested_name: String, - pub detected_format: ProxySubscriptionFormat, - pub nodes: Vec, - pub rejected: Vec, - pub warnings: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxyRejectedEntry { - pub ordinal: usize, - pub scheme: Option, - pub reason: String, -} - -#[derive(Debug, Deserialize)] -struct ProxySchema { - proxies: Option>>, -} - -pub async fn fetch_subscription(url: &str) -> Result { - let redacted_url = redacted_subscription_url(url); - let client = reqwest::Client::builder() - .user_agent(SUBSCRIPTION_USER_AGENT) - .redirect(reqwest::redirect::Policy::limited(5)) - .timeout(Duration::from_secs(20)) - .build()?; - let response = client - .get(url) - .send() - .await - .with_context(|| format!("failed to fetch {redacted_url}"))?; - let status = response.status(); - let body = response - .bytes() - .await - .with_context(|| format!("failed to read subscription body from {redacted_url}"))?; - if !status.is_success() { - bail!( - "subscription request failed for {}: HTTP {}{}", - redacted_url, - status.as_u16(), - subscription_error_body(&body) - ); - } - if body.len() > MAX_SUBSCRIPTION_BYTES { - bail!("subscription body exceeds {} bytes", MAX_SUBSCRIPTION_BYTES); - } - Ok(String::from_utf8_lossy(&body).into_owned()) -} - -fn redacted_subscription_url(raw: &str) -> String { - let Ok(mut url) = Url::parse(raw) else { - return "".to_owned(); - }; - - if !url.username().is_empty() { - let _ = url.set_username("REDACTED"); - } - if url.password().is_some() { - let _ = url.set_password(Some("REDACTED")); - } - if url.query().is_some() { - url.set_query(Some("REDACTED")); - } - if url.fragment().is_some() { - url.set_fragment(Some("REDACTED")); - } - url.to_string() -} - -fn subscription_error_body(body: &[u8]) -> String { - let excerpt = String::from_utf8_lossy(&body[..body.len().min(160)]) - .chars() - .map(|ch| if ch.is_control() { ' ' } else { ch }) - .collect::() - .trim() - .to_owned(); - if excerpt.is_empty() { - String::new() - } else { - format!(": {excerpt}") - } -} - -pub fn build_payload( - preview: ProxySubscriptionPreview, - source_url: &str, - name: Option, - selected_ordinal: Option, -) -> ProxySubscriptionPayload { - let selected_name = selected_ordinal.and_then(|ordinal| { - preview - .nodes - .iter() - .find(|node| node.ordinal == ordinal) - .map(|node| node.name.clone()) - }); - ProxySubscriptionPayload { - name: name - .and_then(|value| non_empty(value.trim()).map(ToOwned::to_owned)) - .unwrap_or_else(|| preview.suggested_name.clone()), - source: ProxySubscriptionSource { url: source_url.to_owned() }, - detected_format: preview.detected_format, - selected_ordinal, - selected_name, - nodes: preview.nodes, - warnings: preview.warnings, - } -} - -pub fn validate_payload(payload: &ProxySubscriptionPayload) -> Result<()> { - if payload.name.trim().is_empty() { - bail!("proxy subscription name must not be empty"); - } - if payload.nodes.is_empty() { - bail!("proxy subscription must include at least one compatible node"); - } - if let Some(selected) = payload.selected_ordinal { - if !payload.nodes.iter().any(|node| node.ordinal == selected) { - bail!("selected proxy node ordinal {selected} is not present in subscription"); - } - } - if let Some(selected) = &payload.selected_name { - if !payload.nodes.iter().any(|node| node.name == *selected) { - bail!("selected proxy node {selected} is not present in subscription"); - } - } - for node in &payload.nodes { - validate_node(node)?; - } - Ok(()) -} - -pub fn parse_subscription_body(body: &str, source_url: Option<&str>) -> ProxySubscriptionPreview { - match parse_yaml_subscription(body, source_url) { - Ok(preview) => preview, - Err(yaml_error) => { - let mut preview = parse_uri_subscription(body, source_url); - if preview.nodes.is_empty() && preview.rejected.is_empty() { - preview.warnings.push(format!( - "subscription is not Mihomo YAML and did not contain proxy URIs: {yaml_error}" - )); - } - preview - } - } -} - -pub fn summarize_payload(payload: &ProxySubscriptionPayload) -> String { - let selected = payload - .selected_name - .as_deref() - .and_then(|name| payload.nodes.iter().find(|node| node.name == name)) - .or_else(|| { - payload - .selected_ordinal - .and_then(|ordinal| payload.nodes.iter().find(|node| node.ordinal == ordinal)) - }) - .or_else(|| payload.nodes.first()); - match selected { - Some(node) => format!( - "{} - {} {}:{} ({} nodes)", - payload.name, - node.protocol.as_str(), - node.server, - node.port, - payload.nodes.len() - ), - None => format!("{} - no compatible nodes", payload.name), - } -} - -fn parse_yaml_subscription( - body: &str, - source_url: Option<&str>, -) -> Result { - let value: Value = serde_yaml::from_str(body).context("subscription is not proxy YAML")?; - let mapping = value - .as_mapping() - .ok_or_else(|| anyhow!("YAML subscription must be a mapping"))?; - if !mapping.contains_key(Value::String("proxies".to_owned())) { - bail!("YAML subscription must include a `proxies` field"); - } - - let schema: ProxySchema = - serde_yaml::from_str(body).context("subscription is not proxy YAML")?; - let proxies = schema - .proxies - .ok_or_else(|| anyhow!("YAML subscription must include a `proxies` field"))?; - let mut nodes = Vec::new(); - let mut rejected = Vec::new(); - let mut warnings = Vec::new(); - - for (ordinal, mapping) in proxies.into_iter().enumerate() { - let scheme = yaml_string(&mapping, "type").map(str::to_owned); - match parse_yaml_proxy(ordinal, mapping) { - Ok(node) => nodes.push(node), - Err(error) => rejected.push(ProxyRejectedEntry { - ordinal, - scheme, - reason: error.to_string(), - }), - } - } - - if nodes.is_empty() { - warnings.push("no compatible Trojan or Shadowsocks nodes found in YAML".to_owned()); - } - - Ok(ProxySubscriptionPreview { - suggested_name: suggested_name(source_url), - detected_format: ProxySubscriptionFormat::ClashYaml, - nodes, - rejected, - warnings, - }) -} - -fn parse_uri_subscription(body: &str, source_url: Option<&str>) -> ProxySubscriptionPreview { - let decoded = decode_subscription_body(body).unwrap_or_else(|| body.to_owned()); - let mut nodes = Vec::new(); - let mut rejected = Vec::new(); - - for (ordinal, raw) in decoded - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .enumerate() - { - let scheme = raw - .split_once("://") - .map(|(scheme, _)| scheme.to_ascii_lowercase()); - let parsed = match scheme.as_deref() { - Some("trojan") => parse_trojan_uri(raw, ordinal), - Some("ss") => parse_shadowsocks_uri(raw, ordinal), - Some(other) => Err(anyhow!("unsupported proxy URI scheme {other}")), - None => Err(anyhow!("missing proxy URI scheme")), - }; - match parsed { - Ok(node) => nodes.push(node), - Err(error) => rejected.push(ProxyRejectedEntry { - ordinal, - scheme, - reason: error.to_string(), - }), - } - } - - ProxySubscriptionPreview { - suggested_name: suggested_name(source_url), - detected_format: ProxySubscriptionFormat::UriList, - nodes, - rejected, - warnings: Vec::new(), - } -} - -fn parse_yaml_proxy(ordinal: usize, mapping: HashMap) -> Result { - let proxy_type = required_yaml_string(&mapping, "type")?.to_ascii_lowercase(); - match proxy_type.as_str() { - "trojan" => parse_yaml_trojan(ordinal, &mapping), - "ss" | "shadowsocks" => parse_yaml_shadowsocks(ordinal, &mapping), - _ => bail!("unsupported proxy type {proxy_type}"), - } -} - -fn parse_yaml_trojan(ordinal: usize, mapping: &HashMap) -> Result { - let name = required_yaml_string(mapping, "name")?.to_owned(); - let server = required_yaml_string(mapping, "server")?.to_owned(); - let port = required_yaml_u16(mapping, "port")?; - let password = required_yaml_string(mapping, "password")?.to_owned(); - let alpn = yaml_string(mapping, "alpn") - .map(split_csv) - .or_else(|| yaml_string_vec(mapping, "alpn")) - .unwrap_or_default(); - let alpn_present = mapping.contains_key("alpn"); - let skip_cert_verify = yaml_bool(mapping, "skip-cert-verify").unwrap_or(false); - let network = match yaml_string(mapping, "network") - .unwrap_or("tcp") - .to_ascii_lowercase() - .as_str() - { - "tcp" | "" => TrojanNetwork::Tcp, - "ws" => { - let ws_opts = yaml_mapping(mapping, "ws-opts"); - let host = ws_opts - .as_ref() - .and_then(|opts| yaml_mapping(opts, "headers")) - .and_then(|headers| { - yaml_string(&headers, "Host") - .or_else(|| yaml_string(&headers, "host")) - .map(ToOwned::to_owned) - }); - TrojanNetwork::Ws { - path: ws_opts - .as_ref() - .and_then(|opts| yaml_string(opts, "path").map(ToOwned::to_owned)), - host, - } - } - "grpc" => { - let grpc_opts = yaml_mapping(mapping, "grpc-opts"); - TrojanNetwork::Grpc { - service_name: grpc_opts - .as_ref() - .and_then(|opts| yaml_string(opts, "grpc-service-name")) - .map(ToOwned::to_owned), - } - } - other => bail!("unsupported Trojan network {other}"), - }; - Ok(ProxyNode { - ordinal, - name, - protocol: ProxyProtocol::Trojan, - server, - port, - warnings: Vec::new(), - config: ProxyNodeConfig::Trojan(TrojanNode { - password, - sni: yaml_string(mapping, "sni").map(ToOwned::to_owned), - alpn, - alpn_present, - skip_cert_verify, - network, - udp: yaml_bool(mapping, "udp").unwrap_or(false), - client_fingerprint: yaml_string(mapping, "client-fingerprint").map(ToOwned::to_owned), - fingerprint: yaml_string(mapping, "fingerprint").map(ToOwned::to_owned), - }), - }) -} - -fn parse_yaml_shadowsocks(ordinal: usize, mapping: &HashMap) -> Result { - let name = required_yaml_scalar_string(mapping, "name")?; - let server = required_yaml_scalar_string(mapping, "server")?; - let port = required_yaml_u16(mapping, "port")?; - let cipher = required_yaml_scalar_string(mapping, "cipher")?; - let password = required_yaml_scalar_string(mapping, "password")?; - Ok(ProxyNode { - ordinal, - name, - protocol: ProxyProtocol::Shadowsocks, - server, - port, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher, - password, - udp: yaml_bool(mapping, "udp").unwrap_or(false), - udp_over_tcp: yaml_bool(mapping, "udp-over-tcp").unwrap_or(false), - udp_over_tcp_version: yaml_u8(mapping, "udp-over-tcp-version") - .unwrap_or(UOT_LEGACY_VERSION), - client_fingerprint: yaml_scalar_string(mapping, "client-fingerprint"), - plugin: yaml_plugin(mapping), - smux: yaml_smux(mapping), - }), - }) -} - -fn parse_trojan_uri(uri: &str, ordinal: usize) -> Result { - let url = Url::parse(uri).context("invalid Trojan URI")?; - let server = url - .host_str() - .map(ToOwned::to_owned) - .ok_or_else(|| anyhow!("missing Trojan server host"))?; - let port = url - .port() - .ok_or_else(|| anyhow!("missing Trojan server port"))?; - let password = percent_decode(url.username())?; - if password.is_empty() { - bail!("missing Trojan password"); - } - - let query = url.query_pairs().collect::>(); - let skip_cert_verify = query - .get("allowInsecure") - .or_else(|| query.get("skip-cert-verify")) - .or_else(|| query.get("skipCertVerify")) - .map(|value| is_truthy(value)) - .unwrap_or(false); - let network = query - .get("type") - .or_else(|| query.get("network")) - .map(|value| value.to_ascii_lowercase()) - .unwrap_or_else(|| "tcp".to_owned()); - let trojan_network = match network.as_str() { - "" | "tcp" => TrojanNetwork::Tcp, - "ws" => TrojanNetwork::Ws { - path: query - .get("path") - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - host: query - .get("host") - .or_else(|| query.get("Host")) - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - }, - "grpc" => TrojanNetwork::Grpc { - service_name: query - .get("serviceName") - .or_else(|| query.get("grpc-service-name")) - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - }, - other => bail!("unsupported Trojan network {other}"), - }; - let fingerprint = query - .get("fp") - .and_then(|value| non_empty(value).map(ToOwned::to_owned)) - .or_else(|| Some("chrome".to_owned())); - let alpn = query - .get("alpn") - .and_then(|value| non_empty(value).map(split_csv)) - .unwrap_or_default(); - let alpn_present = !alpn.is_empty(); - - Ok(ProxyNode { - ordinal, - name: uri_fragment_name(&url, "Proxy Node", ordinal)?, - protocol: ProxyProtocol::Trojan, - server, - port, - warnings: Vec::new(), - config: ProxyNodeConfig::Trojan(TrojanNode { - password, - sni: query - .get("sni") - .or_else(|| query.get("peer")) - .or_else(|| query.get("host")) - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - alpn, - alpn_present, - skip_cert_verify, - network: trojan_network, - udp: true, - client_fingerprint: fingerprint, - fingerprint: query - .get("pcs") - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - }), - }) -} - -fn parse_shadowsocks_uri(uri: &str, ordinal: usize) -> Result { - let mut url = Url::parse(uri).context("invalid Shadowsocks URI")?; - if url.port().is_none() { - let decoded_host = decode_base64_segment(url.host_str().unwrap_or_default()) - .context("invalid legacy Shadowsocks host encoding")?; - url = Url::parse(&format!("ss://{decoded_host}")) - .context("invalid decoded legacy Shadowsocks URI")?; - } - let server = url - .host_str() - .map(ToOwned::to_owned) - .ok_or_else(|| anyhow!("missing Shadowsocks server host"))?; - let port = url - .port() - .ok_or_else(|| anyhow!("missing Shadowsocks server port"))?; - let user = url.username(); - let (cipher, password) = match url.password() { - Some(password) => (user.to_owned(), password.to_owned()), - None => decode_base64_segment(user) - .ok() - .and_then(|decoded| { - decoded - .split_once(':') - .map(|(a, b)| (a.to_owned(), b.to_owned())) - }) - .ok_or_else(|| anyhow!("missing Shadowsocks cipher/password"))?, - }; - let query = url.query_pairs().collect::>(); - let udp_over_tcp = query - .get("udp-over-tcp") - .map(|value| value == "true") - .unwrap_or(false) - || query.get("uot").map(|value| value == "1").unwrap_or(false); - let udp_over_tcp_version = query - .get("udp-over-tcp-version") - .and_then(|value| value.parse::().ok()) - .or_else(|| { - query - .get("uot-version") - .and_then(|value| value.parse::().ok()) - }) - .unwrap_or(UOT_LEGACY_VERSION); - - Ok(ProxyNode { - ordinal, - name: uri_fragment_name(&url, "Proxy Node", ordinal)?, - protocol: ProxyProtocol::Shadowsocks, - server, - port, - warnings: Vec::new(), - config: ProxyNodeConfig::Shadowsocks(ShadowsocksNode { - cipher, - password, - udp: true, - udp_over_tcp, - udp_over_tcp_version, - client_fingerprint: query - .get("client-fingerprint") - .or_else(|| query.get("fp")) - .and_then(|value| non_empty(value).map(ToOwned::to_owned)), - plugin: query.get("plugin").and_then(|value| parse_ss_plugin(value)), - smux: None, - }), - }) -} - -fn validate_node(node: &ProxyNode) -> Result<()> { - if node.name.trim().is_empty() { - bail!("proxy node name must not be empty"); - } - if node.server.trim().is_empty() { - bail!("proxy node server must not be empty"); - } - match &node.config { - ProxyNodeConfig::Trojan(config) if config.password.is_empty() => { - bail!("Trojan node password must not be empty") - } - ProxyNodeConfig::Shadowsocks(config) - if config.cipher.is_empty() || config.password.is_empty() => - { - bail!("Shadowsocks node cipher and password must not be empty") - } - _ => Ok(()), - } -} - -fn yaml_string<'a>(mapping: &'a HashMap, key: &str) -> Option<&'a str> { - mapping.get(key).and_then(Value::as_str) -} - -fn yaml_scalar_string(mapping: &HashMap, key: &str) -> Option { - yaml_value_to_scalar_string(mapping.get(key)?) -} - -fn yaml_value_to_scalar_string(value: &Value) -> Option { - value - .as_str() - .map(ToOwned::to_owned) - .or_else(|| value.as_bool().map(|value| value.to_string())) - .or_else(|| value.as_i64().map(|value| value.to_string())) - .or_else(|| value.as_u64().map(|value| value.to_string())) - .or_else(|| value.as_f64().map(|value| value.to_string())) -} - -fn yaml_string_vec(mapping: &HashMap, key: &str) -> Option> { - let values = mapping.get(key)?.as_sequence()?; - Some( - values - .iter() - .filter_map(Value::as_str) - .map(ToOwned::to_owned) - .collect(), - ) -} - -fn yaml_mapping<'a>( - mapping: &'a HashMap, - key: &str, -) -> Option> { - mapping.get(key)?.as_mapping().and_then(|map| { - map.iter() - .map(|(key, value)| Some((key.as_str()?.to_owned(), value.clone()))) - .collect::>>() - }) -} - -fn required_yaml_string<'a>(mapping: &'a HashMap, key: &str) -> Result<&'a str> { - yaml_string(mapping, key).ok_or_else(|| anyhow!("missing `{key}`")) -} - -fn required_yaml_scalar_string(mapping: &HashMap, key: &str) -> Result { - yaml_scalar_string(mapping, key).ok_or_else(|| anyhow!("missing `{key}`")) -} - -fn required_yaml_u16(mapping: &HashMap, key: &str) -> Result { - if let Some(value) = mapping.get(key).and_then(Value::as_u64) { - return u16::try_from(value).with_context(|| format!("invalid `{key}`")); - } - let value = required_yaml_string(mapping, key)?; - value - .parse::() - .with_context(|| format!("invalid `{key}`")) -} - -fn yaml_bool(mapping: &HashMap, key: &str) -> Option { - mapping - .get(key) - .and_then(Value::as_bool) - .or_else(|| { - mapping - .get(key) - .and_then(Value::as_i64) - .map(|value| value != 0) - }) - .or_else(|| { - mapping - .get(key) - .and_then(Value::as_u64) - .map(|value| value != 0) - }) - .or_else(|| yaml_string(mapping, key).map(is_truthy)) -} - -fn yaml_u8(mapping: &HashMap, key: &str) -> Option { - mapping - .get(key) - .and_then(Value::as_u64) - .and_then(|value| u8::try_from(value).ok()) - .or_else(|| yaml_string(mapping, key).and_then(|value| value.parse::().ok())) -} - -fn yaml_u32(mapping: &HashMap, key: &str) -> Option { - mapping - .get(key) - .and_then(Value::as_u64) - .and_then(|value| u32::try_from(value).ok()) - .or_else(|| yaml_string(mapping, key).and_then(|value| value.parse::().ok())) -} - -fn yaml_smux(mapping: &HashMap) -> Option { - let smux = yaml_mapping(mapping, "smux")?; - let brutal = yaml_mapping(&smux, "brutal-opts").map(|brutal| ShadowsocksSmuxBrutal { - enabled: yaml_bool(&brutal, "enabled").unwrap_or(false), - up: yaml_string(&brutal, "up").map(ToOwned::to_owned), - down: yaml_string(&brutal, "down").map(ToOwned::to_owned), - }); - - Some(ShadowsocksSmux { - enabled: yaml_bool(&smux, "enabled").unwrap_or(false), - protocol: yaml_string(&smux, "protocol").map(ToOwned::to_owned), - max_connections: yaml_u32(&smux, "max-connections"), - min_streams: yaml_u32(&smux, "min-streams"), - max_streams: yaml_u32(&smux, "max-streams"), - padding: yaml_bool(&smux, "padding").unwrap_or(false), - statistic: yaml_bool(&smux, "statistic").unwrap_or(false), - only_tcp: yaml_bool(&smux, "only-tcp").unwrap_or(false), - brutal, - }) -} - -fn yaml_plugin(mapping: &HashMap) -> Option { - let name = yaml_scalar_string(mapping, "plugin")?; - let mut opts: HashMap = yaml_mapping(mapping, "plugin-opts") - .map(|opts| { - opts.iter() - .filter_map(|(key, value)| { - yaml_plugin_value(value).map(|value| (key.clone(), value)) - }) - .collect() - }) - .unwrap_or_default(); - if let Some(headers) = - yaml_mapping(mapping, "plugin-opts").and_then(|opts| yaml_mapping(&opts, "headers")) - { - for (key, value) in headers { - if let Some(value) = yaml_plugin_value(&value) { - opts.insert(format!("headers.{key}"), value.to_owned()); - } - } - } - if let Some(ech_opts) = - yaml_mapping(mapping, "plugin-opts").and_then(|opts| yaml_mapping(&opts, "ech-opts")) - { - for (key, value) in ech_opts { - if let Some(value) = yaml_plugin_value(&value) { - opts.insert(format!("ech-opts.{key}"), value.to_owned()); - } - } - } - Some(ShadowsocksPlugin { name, opts }) -} - -fn yaml_plugin_value(value: &Value) -> Option { - yaml_value_to_scalar_string(value).or_else(|| { - value.as_sequence().map(|values| { - values - .iter() - .filter_map(yaml_value_to_scalar_string) - .collect::>() - .join(",") - }) - }) -} - -fn parse_ss_plugin(plugin: &str) -> Option { - if !plugin.contains(';') { - return None; - } - let (name, rest) = plugin.split_once(';').unwrap_or((plugin, "")); - let mut opts = HashMap::new(); - for part in rest.split(';').filter(|part| !part.is_empty()) { - if let Some((key, value)) = part.split_once('=') { - let key = decode_ss_plugin_component(key); - if let Some(key) = non_empty(&key) { - opts.insert(key.to_owned(), decode_ss_plugin_component(value)); - } - } else if let Some(key) = non_empty(part) { - let key = decode_ss_plugin_component(key); - if let Some(key) = non_empty(&key) { - opts.insert(key.to_owned(), "true".to_owned()); - } - } - } - let name = non_empty(name)?; - let normalized_name = normalize_ss_uri_plugin_name(name)?; - normalize_ss_uri_plugin_opts(plugin, normalized_name, &mut opts); - Some(ShadowsocksPlugin { - name: normalized_name.to_owned(), - opts, - }) -} - -fn normalize_ss_uri_plugin_name(name: &str) -> Option<&str> { - let normalized = name.trim().to_ascii_lowercase(); - if normalized.contains("obfs") { - Some("obfs") - } else if normalized.contains("v2ray-plugin") { - Some("v2ray-plugin") - } else { - None - } -} - -fn normalize_ss_uri_plugin_opts( - raw_plugin: &str, - normalized_name: &str, - opts: &mut HashMap, -) { - if normalized_name == "v2ray-plugin" { - if !opts.contains_key("mode") { - if let Some(mode) = opts.get("obfs").cloned() { - opts.insert("mode".to_owned(), mode); - } - } - if !opts.contains_key("host") { - if let Some(host) = opts.get("obfs-host").cloned() { - opts.insert("host".to_owned(), host); - } - } - if raw_plugin.contains("tls") { - opts.insert("tls".to_owned(), "true".to_owned()); - } - } -} - -fn decode_ss_plugin_component(value: &str) -> String { - let query_value = value.replace('+', " "); - percent_decode(&query_value).unwrap_or_else(|_| value.to_owned()) -} - -fn decode_subscription_body(body: &str) -> Option { - let compact: String = body.chars().filter(|ch| !ch.is_whitespace()).collect(); - decode_base64_segment(&compact).ok() -} - -fn decode_base64_segment(input: &str) -> Result { - if let Ok(decoded) = STANDARD_NO_PAD.decode(input.as_bytes()) { - return String::from_utf8(decoded).context("base64 decoded text is not valid UTF-8"); - } - if let Ok(decoded) = STANDARD.decode(input.as_bytes()) { - return String::from_utf8(decoded).context("base64 decoded text is not valid UTF-8"); - } - if let Ok(decoded) = URL_SAFE_NO_PAD.decode(input.as_bytes()) { - return String::from_utf8(decoded).context("base64 decoded text is not valid UTF-8"); - } - if let Ok(decoded) = URL_SAFE.decode(input.as_bytes()) { - return String::from_utf8(decoded).context("base64 decoded text is not valid UTF-8"); - } - bail!("invalid base64 text") -} - -fn uri_fragment_name(url: &Url, fallback: &str, ordinal: usize) -> Result { - Ok(url - .fragment() - .map(percent_decode) - .transpose()? - .and_then(|fragment| non_empty(&fragment).map(ToOwned::to_owned)) - .or_else(|| non_empty(url.host_str().unwrap_or_default()).map(ToOwned::to_owned)) - .unwrap_or_else(|| format!("{fallback} {}", ordinal + 1))) -} - -fn suggested_name(source_url: Option<&str>) -> String { - source_url - .and_then(|url| Url::parse(url).ok()) - .and_then(|url| url.host_str().map(ToOwned::to_owned)) - .unwrap_or_else(|| "Proxy Subscription".to_owned()) -} - -fn split_csv(value: &str) -> Vec { - value - .split(',') - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .collect() -} - -fn is_truthy(value: impl AsRef) -> bool { - matches!( - value.as_ref().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "y" - ) -} - -fn default_true() -> bool { - true -} - -const UOT_LEGACY_VERSION: u8 = 1; - -fn default_uot_legacy_version() -> u8 { - UOT_LEGACY_VERSION -} - -fn non_empty(value: &str) -> Option<&str> { - let value = value.trim(); - (!value.is_empty()).then_some(value) -} - -fn percent_decode(input: &str) -> Result { - let mut out = Vec::with_capacity(input.len()); - let bytes = input.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if bytes[i] == b'%' { - if i + 2 >= bytes.len() { - bail!("invalid percent encoding"); - } - let hi = hex_value(bytes[i + 1]).ok_or_else(|| anyhow!("invalid percent encoding"))?; - let lo = hex_value(bytes[i + 2]).ok_or_else(|| anyhow!("invalid percent encoding"))?; - out.push((hi << 4) | lo); - i += 3; - } else { - out.push(bytes[i]); - i += 1; - } - } - String::from_utf8(out).context("percent-decoded text is not valid UTF-8") -} - -fn hex_value(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_base64_uri_list_with_trojan_and_shadowsocks() { - let body = STANDARD.encode( - "trojan://secret@example.com:443?security=tls&sni=front.example&type=grpc&serviceName=edge&allowInsecure=1&fp=random#US%20Trojan\nss://YWVzLTEyOC1nY206cGFzcw@example.net:8388?uot=1&uot-version=2#SS%20Node\n", - ); - let preview = parse_subscription_body(&body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.nodes.len(), 2); - assert_eq!(preview.rejected.len(), 0); - assert_eq!(preview.nodes[0].name, "US Trojan"); - assert!(preview.nodes[0].warnings.is_empty()); - let ProxyNodeConfig::Trojan(trojan) = &preview.nodes[0].config else { - panic!("expected Trojan node"); - }; - assert!(!trojan.alpn_present); - assert_eq!(trojan_alpn_for_test(trojan), Vec::::new()); - assert!(trojan.udp); - assert_eq!(preview.nodes[1].protocol, ProxyProtocol::Shadowsocks); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[1].config else { - panic!("expected Shadowsocks node"); - }; - assert!(shadowsocks.udp); - assert!(shadowsocks.udp_over_tcp); - assert_eq!(shadowsocks.udp_over_tcp_version, 2); - } - - #[test] - fn parses_shadowsocks_v2ray_plugin_uri_aliases() { - let body = "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388?plugin=v2ray-plugin%3Bobfs%3Dwebsocket%3Bobfs-host%3Dcdn.example%3Bpath%3D%252Fws%3Btls#SS%20V2Ray"; - let preview = parse_subscription_body(body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.nodes.len(), 1); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - let plugin = shadowsocks.plugin.as_ref().unwrap(); - assert_eq!(plugin.name, "v2ray-plugin"); - assert_eq!(plugin.opts.get("mode"), Some(&"websocket".to_owned())); - assert_eq!(plugin.opts.get("host"), Some(&"cdn.example".to_owned())); - assert_eq!(plugin.opts.get("obfs"), Some(&"websocket".to_owned())); - assert_eq!( - plugin.opts.get("obfs-host"), - Some(&"cdn.example".to_owned()) - ); - assert_eq!(plugin.opts.get("path"), Some(&"/ws".to_owned())); - assert_eq!(plugin.opts.get("tls"), Some(&"true".to_owned())); - } - - #[test] - fn parses_base64_uri_list_even_when_yaml_accepts_scalar_text() { - let body = STANDARD - .encode("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwYXNz@example.net:1080#SS%20Node\n"); - let preview = parse_subscription_body(&body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.rejected.len(), 0); - assert_eq!(preview.nodes.len(), 1); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - assert_eq!(shadowsocks.cipher, "chacha20-ietf-poly1305"); - assert_eq!(shadowsocks.password, "pass"); - } - - #[test] - fn parses_shadowsocks_simple_obfs_uri_like_mihomo_converter() { - let body = "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388?plugin=obfs-local%3Bobfs%3Dhttp%3Bobfs-host%3Dcdn.example#SS%20Obfs"; - let preview = parse_subscription_body(body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.nodes.len(), 1); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - let plugin = shadowsocks.plugin.as_ref().unwrap(); - assert_eq!(plugin.name, "obfs"); - assert_eq!(plugin.opts.get("obfs"), Some(&"http".to_owned())); - assert_eq!( - plugin.opts.get("obfs-host"), - Some(&"cdn.example".to_owned()) - ); - } - - #[test] - fn ignores_bare_shadowsocks_uri_plugin_like_mihomo_converter() { - let body = - "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388?plugin=v2ray-plugin#SS%20Bare%20Plugin"; - let preview = parse_subscription_body(body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.nodes.len(), 1); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - assert!(shadowsocks.plugin.is_none()); - } - - #[test] - fn ignores_unknown_shadowsocks_uri_plugin_like_mihomo_converter() { - let body = "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388?plugin=gost-plugin%3Bmode%3Dwebsocket#SS%20Gost%20URI"; - let preview = parse_subscription_body(body, Some("https://sub.example/path?token=secret")); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::UriList); - assert_eq!(preview.nodes.len(), 1); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - assert!(shadowsocks.plugin.is_none()); - } - - #[test] - fn parses_mihomo_yaml_proxies() { - let preview = parse_subscription_body( - r#" -proxies: - - name: trojan-ws - type: trojan - server: example.com - port: 443 - password: secret - sni: edge.example.com - network: ws - ws-opts: - path: /ws - headers: - Host: edge.example.com - - name: ss - type: ss - server: example.net - port: 8388 - cipher: aes-128-gcm - password: pass - plugin: v2ray-plugin - plugin-opts: - mode: websocket - path: /ws - v2ray-http-upgrade: true - v2ray-http-upgrade-fast-open: true - headers: - Host: edge.example.net - ech-opts: - enable: true - config: AQIDBA== - query-server-name: public.example.net - udp-over-tcp: true - udp-over-tcp-version: 2 - smux: - enabled: true - protocol: smux - max-connections: 4 - min-streams: 2 - padding: true - statistic: true - only-tcp: false - brutal-opts: - enabled: true - up: 10 Mbps - down: 20 Mbps - - name: ss-shadowtls - type: ss - server: shadow.example.net - port: 443 - cipher: aes-128-gcm - password: pass - client-fingerprint: chrome - plugin: shadow-tls - plugin-opts: - host: cover.example.com - password: shadow-secret - version: 1 - fingerprint: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" - alpn: - - h2 - - http/1.1 - - name: ss-kcptun - type: ss - server: kcp.example.net - port: 29900 - cipher: aes-128-gcm - password: pass - plugin: kcptun - plugin-opts: - key: secret - crypt: aes - mode: fast2 - conn: 2 - nocomp: true - smuxver: 2 -"#, - None, - ); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::ClashYaml); - assert_eq!(preview.nodes.len(), 4); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Trojan(trojan) = &preview.nodes[0].config else { - panic!("expected Trojan node"); - }; - assert!(!trojan.alpn_present); - assert!(!trojan.udp); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[1].config else { - panic!("expected Shadowsocks node"); - }; - assert!(!shadowsocks.udp); - assert!(shadowsocks.udp_over_tcp); - assert_eq!(shadowsocks.udp_over_tcp_version, 2); - assert_eq!( - shadowsocks - .plugin - .as_ref() - .unwrap() - .opts - .get("headers.Host"), - Some(&"edge.example.net".to_owned()) - ); - assert_eq!( - shadowsocks - .plugin - .as_ref() - .unwrap() - .opts - .get("v2ray-http-upgrade-fast-open"), - Some(&"true".to_owned()) - ); - assert_eq!( - shadowsocks - .plugin - .as_ref() - .unwrap() - .opts - .get("ech-opts.enable"), - Some(&"true".to_owned()) - ); - assert_eq!( - shadowsocks - .plugin - .as_ref() - .unwrap() - .opts - .get("ech-opts.config"), - Some(&"AQIDBA==".to_owned()) - ); - assert_eq!( - shadowsocks - .plugin - .as_ref() - .unwrap() - .opts - .get("ech-opts.query-server-name"), - Some(&"public.example.net".to_owned()) - ); - let smux = shadowsocks - .smux - .as_ref() - .expect("expected Shadowsocks smux"); - assert!(smux.enabled); - assert_eq!(smux.protocol.as_deref(), Some("smux")); - assert_eq!(smux.max_connections, Some(4)); - assert_eq!(smux.min_streams, Some(2)); - assert!(smux.padding); - assert!(smux.statistic); - assert!(!smux.only_tcp); - let brutal = smux.brutal.as_ref().expect("expected brutal opts"); - assert!(brutal.enabled); - assert_eq!(brutal.up.as_deref(), Some("10 Mbps")); - assert_eq!(brutal.down.as_deref(), Some("20 Mbps")); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[2].config else { - panic!("expected Shadowsocks node"); - }; - let plugin = shadowsocks.plugin.as_ref().unwrap(); - assert_eq!(plugin.name, "shadow-tls"); - assert_eq!(shadowsocks.client_fingerprint.as_deref(), Some("chrome")); - assert_eq!(plugin.opts.get("version"), Some(&"1".to_owned())); - assert_eq!( - plugin.opts.get("fingerprint"), - Some(&"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff".to_owned()) - ); - assert_eq!(plugin.opts.get("alpn"), Some(&"h2,http/1.1".to_owned())); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[3].config else { - panic!("expected Shadowsocks node"); - }; - let plugin = shadowsocks.plugin.as_ref().unwrap(); - assert_eq!(plugin.name, "kcptun"); - assert_eq!(plugin.opts.get("conn"), Some(&"2".to_owned())); - assert_eq!(plugin.opts.get("nocomp"), Some(&"true".to_owned())); - assert_eq!(plugin.opts.get("smuxver"), Some(&"2".to_owned())); - } - - #[test] - fn shadowsocks_yaml_udp_defaults_match_mihomo() { - let preview = parse_subscription_body( - r#" -proxies: - - name: ss-default - type: ss - server: example.net - port: 8388 - cipher: aes-128-gcm - password: pass - - name: ss-udp - type: ss - server: example.org - port: 8388 - cipher: aes-128-gcm - password: pass - udp: true -"#, - None, - ); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::ClashYaml); - assert_eq!(preview.rejected.len(), 0); - let ProxyNodeConfig::Shadowsocks(default_node) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - assert!(!default_node.udp); - let ProxyNodeConfig::Shadowsocks(udp_node) = &preview.nodes[1].config else { - panic!("expected Shadowsocks node"); - }; - assert!(udp_node.udp); - } - - #[test] - fn shadowsocks_yaml_accepts_weakly_typed_scalars_like_mihomo() { - let preview = parse_subscription_body( - r#" -proxies: - - name: 12345 - type: ss - server: 127.0.0.1 - port: "8388" - cipher: aes-128-gcm - password: 123456 - udp: 1 - udp-over-tcp: 0 - client-fingerprint: false - plugin: v2ray-plugin - plugin-opts: - mode: websocket - path: /ws - mux: true - alpn: - - h2 - - 123 - smux: - enabled: 1 - padding: 0 -"#, - None, - ); - assert_eq!(preview.detected_format, ProxySubscriptionFormat::ClashYaml); - assert_eq!(preview.rejected.len(), 0); - assert_eq!(preview.nodes.len(), 1); - assert_eq!(preview.nodes[0].name, "12345"); - let ProxyNodeConfig::Shadowsocks(shadowsocks) = &preview.nodes[0].config else { - panic!("expected Shadowsocks node"); - }; - assert_eq!(shadowsocks.password, "123456"); - assert!(shadowsocks.udp); - assert!(!shadowsocks.udp_over_tcp); - assert_eq!(shadowsocks.client_fingerprint.as_deref(), Some("false")); - let smux = shadowsocks.smux.as_ref().expect("expected smux"); - assert!(smux.enabled); - assert!(!smux.padding); - let plugin = shadowsocks.plugin.as_ref().expect("expected plugin"); - assert_eq!(plugin.name, "v2ray-plugin"); - assert_eq!(plugin.opts.get("mux"), Some(&"true".to_owned())); - assert_eq!(plugin.opts.get("alpn"), Some(&"h2,123".to_owned())); - } - - #[test] - fn tracks_explicit_trojan_alpn_presence() { - let preview = parse_subscription_body( - r#" -proxies: - - name: trojan-empty-alpn - type: trojan - server: example.com - port: 443 - password: secret - alpn: [] - - name: trojan-custom-alpn - type: trojan - server: example.net - port: 443 - password: secret - alpn: - - h3 -"#, - None, - ); - let ProxyNodeConfig::Trojan(empty_alpn) = &preview.nodes[0].config else { - panic!("expected Trojan node"); - }; - assert!(empty_alpn.alpn_present); - assert!(empty_alpn.alpn.is_empty()); - - let ProxyNodeConfig::Trojan(custom_alpn) = &preview.nodes[1].config else { - panic!("expected Trojan node"); - }; - assert!(custom_alpn.alpn_present); - assert_eq!(custom_alpn.alpn, vec!["h3"]); - } - - #[test] - fn build_payload_records_selected_node_name() { - let preview = parse_subscription_body( - "ss://YWVzLTEyOC1nY206cGFzcw@example.net:8388#SS%20Node\n", - Some("https://sub.example/path"), - ); - let payload = build_payload( - preview, - "https://sub.example/path", - Some("sub".to_owned()), - Some(0), - ); - - assert_eq!(payload.selected_ordinal, Some(0)); - assert_eq!(payload.selected_name.as_deref(), Some("SS Node")); - } - - #[test] - fn redacts_subscription_url_secrets_for_errors() { - let redacted = redacted_subscription_url( - "https://user:password@sub.example/path?token=secret&user=alice#fragment", - ); - - assert_eq!( - redacted, - "https://REDACTED:REDACTED@sub.example/path?REDACTED#REDACTED" - ); - assert!(!redacted.contains("password")); - assert!(!redacted.contains("token=secret")); - assert!(!redacted.contains("fragment")); - } - - fn trojan_alpn_for_test(node: &TrojanNode) -> Vec { - node.alpn.clone() - } -} diff --git a/burrow/src/tracing.rs b/burrow/src/tracing.rs index fb88100..8a245ef 100644 --- a/burrow/src/tracing.rs +++ b/burrow/src/tracing.rs @@ -1,13 +1,7 @@ -use std::{ - fs::{File, OpenOptions}, - io::{self, Write}, - path::PathBuf, - sync::{Arc, Mutex, Once}, -}; +use std::sync::Once; use tracing::{error, info}; use tracing_subscriber::{ - fmt::writer::MakeWriter, layer::{Layer, SubscriberExt}, EnvFilter, Registry, }; @@ -26,30 +20,14 @@ pub fn initialize() { .with_writer(std::io::stderr) .with_line_number(true) .compact() - .with_filter(env_filter()) - }; - - let make_file = || { - tracing_log_file().map(|writer| { - tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_level(true) - .with_line_number(true) - .compact() - .with_writer(writer) - .with_filter(env_filter()) - }) + .with_filter(EnvFilter::from_default_env()) }; #[cfg(target_os = "windows")] let subscriber = { let system_log = Some(tracing_subscriber::fmt::layer()); - let stderr = - (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); - Registry::default() - .with(stderr) - .with(system_log) - .with(make_file()) + let stderr = (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); + Registry::default().with(stderr).with(system_log) }; #[cfg(target_os = "linux")] @@ -63,12 +41,8 @@ pub fn initialize() { None } }; - let stderr = - (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); - Registry::default() - .with(stderr) - .with(system_log) - .with(make_file()) + let stderr = (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); + Registry::default().with(stderr).with(system_log) }; #[cfg(target_os = "macos")] @@ -80,20 +54,15 @@ pub fn initialize() { std::env::var("BURROW_ENABLE_OSLOG").as_deref(), Ok("1" | "true" | "TRUE" | "yes" | "YES") ); - let system_log = enable_oslog - .then(|| tracing_oslog::OsLogger::new("com.hackclub.burrow", "tracing")); - let stderr = - (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); - Registry::default() - .with(stderr) - .with(system_log) - .with(make_file()) + let system_log = enable_oslog.then(|| { + tracing_oslog::OsLogger::new("com.hackclub.burrow", "tracing") + }); + let stderr = (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); + Registry::default().with(stderr).with(system_log) }; #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] - let subscriber = Registry::default() - .with(Some(make_stderr())) - .with(make_file()); + let subscriber = Registry::default().with(Some(make_stderr())); #[cfg(feature = "tokio-console")] let subscriber = subscriber.with( @@ -111,50 +80,3 @@ pub fn initialize() { info!("Initialized logging") }); } - -fn env_filter() -> EnvFilter { - EnvFilter::try_from_env("BURROW_LOG") - .or_else(|_| EnvFilter::try_from_default_env()) - .unwrap_or_else(|_| EnvFilter::new("info")) -} - -fn tracing_log_file() -> Option { - let path = std::env::var_os("BURROW_LOG_FILE").map(PathBuf::from)?; - let file = match OpenOptions::new().create(true).append(true).open(&path) { - Ok(file) => file, - Err(err) => { - error!(path = %path.display(), error = %err, "failed to open Burrow log file"); - return None; - } - }; - Some(SharedLogWriter { - file: Arc::new(Mutex::new(file)), - }) -} - -#[derive(Clone)] -struct SharedLogWriter { - file: Arc>, -} - -struct SharedLogGuard { - file: Arc>, -} - -impl<'a> MakeWriter<'a> for SharedLogWriter { - type Writer = SharedLogGuard; - - fn make_writer(&'a self) -> Self::Writer { - SharedLogGuard { file: self.file.clone() } - } -} - -impl Write for SharedLogGuard { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.file.lock().unwrap().write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.file.lock().unwrap().flush() - } -} diff --git a/deploy/railway-sing-box/Dockerfile b/deploy/railway-sing-box/Dockerfile deleted file mode 100644 index 2b2c4cf..0000000 --- a/deploy/railway-sing-box/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM ghcr.io/sagernet/sing-box:v1.13.12 - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/railway-sing-box/README.md b/deploy/railway-sing-box/README.md deleted file mode 100644 index 64d4c87..0000000 --- a/deploy/railway-sing-box/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Railway sing-box Shadowsocks Test Node - -This is a minimal sing-box Shadowsocks server for testing Burrow against a real -internet-hosted node on Railway. - -Railway public networking exposes raw TCP through TCP Proxy. That is enough for -plain Shadowsocks TCP testing. It is not a full UDP validation environment, so -use Burrow's local self-tests or a VPS for native UDP testing. - -## Deploy - -1. Create a new Railway service from this repo. -2. Set the service root directory to: - - ```text - deploy/railway-sing-box - ``` - -3. Add Railway variables: - - ```text - SS_PASSWORD= - SS_METHOD=chacha20-ietf-poly1305 - SS_LISTEN_HOST=0.0.0.0 - SS_LISTEN_PORT=8388 - ``` - - `SS_METHOD`, `SS_LISTEN_HOST`, and `SS_LISTEN_PORT` are optional; those are - the defaults. - -4. Deploy the service. -5. In the service settings, create a TCP Proxy with internal port `8388`. -6. Railway will show an external proxy host and port, for example: - - ```text - shuttle.proxy.rlwy.net:15140 - ``` - -## Burrow Test URI - -Build the Shadowsocks URI with: - -```sh -uv run python - <<'PY' -import base64 -import urllib.parse - -method = "chacha20-ietf-poly1305" -password = "" -host = "" -port = "" -name = "railway-sing-box-ss" - -userinfo = base64.urlsafe_b64encode(f"{method}:{password}".encode()).decode().rstrip("=") -print(f"ss://{userinfo}@{host}:{port}#{urllib.parse.quote(name)}") -PY -``` - -Use the generated `ss://` URI in a Burrow proxy subscription or local test -payload. - -## Notes - -- Railway's TCP Proxy external port is assigned by Railway. Use that external - port in the client URI, not `8388`. -- This is intentionally a plain Shadowsocks node. Once plain TCP works, add - extra protocol features in separate test deployments so failures stay easy to - isolate. diff --git a/deploy/railway-sing-box/entrypoint.sh b/deploy/railway-sing-box/entrypoint.sh deleted file mode 100644 index 1ba2800..0000000 --- a/deploy/railway-sing-box/entrypoint.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -set -eu - -: "${SS_PASSWORD:?Set SS_PASSWORD to a strong test password in Railway variables}" - -SS_METHOD="${SS_METHOD:-chacha20-ietf-poly1305}" -SS_LISTEN_HOST="${SS_LISTEN_HOST:-0.0.0.0}" -SS_LISTEN_PORT="${SS_LISTEN_PORT:-8388}" -LOG_LEVEL="${LOG_LEVEL:-info}" - -json_escape() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -SS_METHOD_JSON="$(json_escape "$SS_METHOD")" -SS_PASSWORD_JSON="$(json_escape "$SS_PASSWORD")" -LOG_LEVEL_JSON="$(json_escape "$LOG_LEVEL")" -SS_LISTEN_HOST_JSON="$(json_escape "$SS_LISTEN_HOST")" - -mkdir -p /etc/sing-box -cat >/etc/sing-box/config.json <