ios: PCA Phobos VPN for iPhone (source)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Бобырев Андрей
2026-10-23 03:37:06 +03:00
parent dff6af2c8e
commit 071c1dc7f1
63 changed files with 7726 additions and 0 deletions

BIN
docs/img/ios-app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

3
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
build/
*.xcuserstate
xcuserdata/

View File

@@ -0,0 +1 @@
{"images":[{"filename":"icon-1024.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1 @@
{"info":{"author":"xcode","version":1}}

97
ios/App/ContentView.swift Normal file
View File

@@ -0,0 +1,97 @@
import NetworkExtension
import SwiftUI
struct ContentView: View {
@EnvironmentObject var vpn: VPNController
@State private var pasteText = ""
@State private var showImport = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 18) {
profileCard
powerButton
statusLine
optionsCard
Button { showImport = true } label: {
Label("Импорт ссылки phobos://", systemImage: "square.and.arrow.down")
.frame(maxWidth: .infinity).padding(.vertical, 12)
}
.buttonStyle(.borderedProminent).tint(.indigo)
Link(destination: URL(string: "https://speed.cloudflare.com/")!) {
Label("Проверить скорость", systemImage: "speedometer").frame(maxWidth: .infinity).padding(.vertical, 12)
}
.buttonStyle(.bordered)
if !vpn.message.isEmpty {
Text(vpn.message).font(.footnote).foregroundStyle(.secondary).multilineTextAlignment(.center)
}
}
.padding()
}
.navigationTitle("PCA Phobos VPN")
.alert("Ссылка из панели", isPresented: $showImport) {
TextField("phobos://…", text: $pasteText)
Button("Импорт") { vpn.importLink(pasteText); pasteText = "" }
Button("Из буфера") { vpn.importLink(UIPasteboard.general.string ?? "") }
Button("Отмена", role: .cancel) {}
} message: {
Text("В панели: клиент → 📱 → ссылка phobos://. Можно просто открыть ссылку на iPhone — профиль импортируется сам.")
}
}
}
private var profileCard: some View {
VStack(alignment: .leading, spacing: 4) {
if let p = vpn.profile {
Text(vpn.name).font(.title3.bold())
Text("Сервер: \(p.target ?? "?")").font(.subheadline).foregroundStyle(.secondary)
if let tcp = p.tcpPort { Text("Резервный TCP: порт \(tcp)").font(.caption).foregroundStyle(.secondary) }
} else {
Text("Профиль не импортирован").font(.headline)
Text("Откройте на iPhone ссылку phobos:// из панели PCA Phobos или вставьте её кнопкой ниже.")
.font(.subheadline).foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding().background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
private var powerButton: some View {
Button(action: vpn.toggle) {
Text(vpn.isOn ? "Отключить" : "Подключить")
.font(.title2.bold()).foregroundStyle(.white)
.frame(width: 180, height: 180)
.background(Circle().fill(vpn.isOn ? Color.green : Color.indigo))
}
.padding(.top, 8)
}
private var statusLine: some View {
let text: String = {
switch vpn.status {
case .connected: return "🟢 VPN включён" + (vpn.transport.isEmpty ? "" : " · \(vpn.transport)")
case .connecting, .reasserting: return "🟡 Подключение…"
case .disconnecting: return "Отключение…"
default: return "⚪ VPN выключен"
}
}()
return Text(text).font(.headline)
}
private var optionsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Российские сайты — напрямую, без VPN", isOn: $vpn.ruDirect)
VStack(alignment: .leading, spacing: 6) {
Text("Канал до сервера").font(.subheadline.bold())
Picker("Канал", selection: $vpn.transportMode) {
Text("Авто").tag("auto"); Text("UDP").tag("udp"); Text("TCP").tag("tcp")
}
.pickerStyle(.segmented)
Text("Авто: если UDP режут, через 15 секунд переходит на TCP. Изменения применяются сразу.")
.font(.caption).foregroundStyle(.secondary)
}
}
.padding().background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
}

41
ios/App/Info.plist Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PCA Phobos</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.bobyrev.pcaphobos.link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>phobos</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
</dict>
</plist>

103
ios/App/PCAPhobosApp.swift Normal file
View File

@@ -0,0 +1,103 @@
import NetworkExtension
import SwiftUI
@main
struct PCAPhobosApp: App {
@StateObject private var vpn = VPNController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vpn)
.onOpenURL { vpn.importLink($0.absoluteString) } // phobos:// from the panel
}
}
}
/// Owns the NETunnelProviderManager: profile, options, connect/disconnect, live status.
@MainActor
final class VPNController: ObservableObject {
@Published var name = UserDefaults.standard.string(forKey: "name") ?? ""
@Published var conf = UserDefaults.standard.string(forKey: "conf") ?? ""
@Published var ruDirect = UserDefaults.standard.object(forKey: "ruDirect") as? Bool ?? true { didSet { saveOptions() } }
@Published var transportMode = UserDefaults.standard.string(forKey: "transport") ?? "auto" { didSet { saveOptions() } }
@Published var status: NEVPNStatus = .invalid
@Published var transport = ""
@Published var message = ""
private var manager: NETunnelProviderManager?
private var timer: Timer?
init() {
Task { await load() }
NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: .main) { [weak self] _ in
Task { @MainActor in self?.status = self?.manager?.connection.status ?? .invalid }
}
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
Task { @MainActor in self?.pollTransport() }
}
}
var profile: PhobosProfile? { conf.isEmpty ? nil : PhobosProfile(conf) }
var isOn: Bool { status == .connected || status == .connecting || status == .reasserting }
func load() async {
let all = (try? await NETunnelProviderManager.loadAllFromPreferences()) ?? []
manager = all.first
status = manager?.connection.status ?? .invalid
}
func importLink(_ link: String) {
guard let parsed = PhobosProfile.fromLink(link) else { message = "Это не ссылка phobos:// из панели"; return }
conf = parsed.profile.raw
name = parsed.name
UserDefaults.standard.set(conf, forKey: "conf")
UserDefaults.standard.set(name, forKey: "name")
message = "Профиль «\(name)» импортирован"
Task { await save(reconnect: isOn) }
}
private func saveOptions() {
UserDefaults.standard.set(ruDirect, forKey: "ruDirect")
UserDefaults.standard.set(transportMode, forKey: "transport")
Task { await save(reconnect: isOn) }
}
/// Write the VPN configuration to iOS settings (asks the user once).
func save(reconnect: Bool = false) async {
guard let profile else { return }
let m = manager ?? NETunnelProviderManager()
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = "com.bobyrev.pcaphobos.tunnel"
proto.serverAddress = profile.targetHost ?? "Phobos"
proto.providerConfiguration = ["conf": conf, "ruDirect": ruDirect, "transport": transportMode]
m.protocolConfiguration = proto
m.localizedDescription = "PCA Phobos"
m.isEnabled = true
do {
try await m.saveToPreferences()
try await m.loadFromPreferences()
manager = m
if reconnect { m.connection.stopVPNTunnel(); try await Task.sleep(nanoseconds: 1_000_000_000); try m.connection.startVPNTunnel() }
} catch {
message = "Не удалось сохранить VPN: \(error.localizedDescription)"
}
}
func toggle() {
Task {
if isOn { manager?.connection.stopVPNTunnel(); return }
guard profile != nil else { message = "Сначала импортируйте ссылку phobos:// из панели"; return }
await save()
do { try manager?.connection.startVPNTunnel() } catch { message = "Ошибка: \(error.localizedDescription)" }
}
}
private func pollTransport() {
guard status == .connected, let session = manager?.connection as? NETunnelProviderSession else { transport = ""; return }
try? session.sendProviderMessage(Data()) { [weak self] data in
guard let data, let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
Task { @MainActor in self?.transport = obj["transport"] as? String ?? "" }
}
}
}

View File

@@ -0,0 +1,572 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
008DA398D56DFBDB16D97CE1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF14952A4263FF0E004A3F04 /* ContentView.swift */; };
08C4842C3185E04B2154C971 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA829997FAB330F6EFA104C9 /* WireGuardKit */; };
11F2A6A8D281AB36B45D4603 /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */; };
1C05D29ACA95548374AD6FD9 /* logging.c in Sources */ = {isa = PBXBuildFile; fileRef = 9FC04650634F03E6DB09AB6B /* logging.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
38870650A7C2B1F0927A1230 /* wg-obfuscator.c in Sources */ = {isa = PBXBuildFile; fileRef = 1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
4E4255B9E0F8626F6E209B0F /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */; };
56F35C68C6A387E499703CFF /* masking.c in Sources */ = {isa = PBXBuildFile; fileRef = CCE5C98CD905BDE16A8D83D4 /* masking.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
5F5D376513C0B27C45A3E7B6 /* ru_bypass.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2572D9A6F822B038D0652619 /* ru_bypass.txt */; };
78B74F81E0042D9AD94B7A6B /* config.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E5C1673464AAC3EF81DD2FE /* config.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
7D8AB66421409CBFCCF2769A /* PhobosProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */; };
99F5DB52833E3B5505EEA727 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */; };
B3BB8BE3409D63FEB59B5449 /* obfuscation.c in Sources */ = {isa = PBXBuildFile; fileRef = EF9115CA0D5EC52C8679A12A /* obfuscation.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
C3C41D3C419E8D17F20A38B5 /* PCAPhobosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */; };
C6116E8B9CDFEE363BCBD2D6 /* PhobosTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
CBC338B91B5FCE1E207B36AC /* PhobosProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */; };
D8CCC1E6CC8164EDD4E34D25 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6359B8954E69147DA50EDB46 /* Assets.xcassets */; };
DF6D62C526E908CA5CDDFCAE /* pcarelay.c in Sources */ = {isa = PBXBuildFile; fileRef = 211950F8776D3D14D7B9C49A /* pcarelay.c */; };
E88105F9325A93041D617213 /* masking_stun.c in Sources */ = {isa = PBXBuildFile; fileRef = 05DD44245829A52D32B42AF8 /* masking_stun.c */; settings = {COMPILER_FLAGS = "-Dmain=wgobf_main -w"; }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B9875EF5DEB6362919B43F22 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = E59D1112D2B128DF6FE7A1E9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0FD066BC2C51A1E092914FE8;
remoteInfo = PhobosTunnel;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9FC25420ED5EF973CE140584 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
C6116E8B9CDFEE363BCBD2D6 /* PhobosTunnel.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
027878C5E0654DB27FAE88A3 /* PCAPhobos.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = PCAPhobos.app; sourceTree = BUILT_PRODUCTS_DIR; };
05DD44245829A52D32B42AF8 /* masking_stun.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = masking_stun.c; sourceTree = "<group>"; };
0E5C1673464AAC3EF81DD2FE /* config.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = config.c; sourceTree = "<group>"; };
1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "wg-obfuscator.c"; sourceTree = "<group>"; };
211950F8776D3D14D7B9C49A /* pcarelay.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = pcarelay.c; sourceTree = "<group>"; };
2572D9A6F822B038D0652619 /* ru_bypass.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ru_bypass.txt; sourceTree = "<group>"; };
2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
6359B8954E69147DA50EDB46 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
722DE8EFC35A88BC68637FB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
7E66B3AD4BB22D2AB5676E10 /* PCAPhobos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PCAPhobos.entitlements; sourceTree = "<group>"; };
8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhobosProfile.swift; sourceTree = "<group>"; };
9FC04650634F03E6DB09AB6B /* logging.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = logging.c; sourceTree = "<group>"; };
A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = PhobosTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PCAPhobosApp.swift; sourceTree = "<group>"; };
CA6480536DA633DB94B6930F /* WireGuardKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WireGuardKit; path = WireGuardKit; sourceTree = SOURCE_ROOT; };
CCE5C98CD905BDE16A8D83D4 /* masking.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = masking.c; sourceTree = "<group>"; };
EF9115CA0D5EC52C8679A12A /* obfuscation.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = obfuscation.c; sourceTree = "<group>"; };
FF14952A4263FF0E004A3F04 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D366EA9C7C0EC88CAE5EA47E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
08C4842C3185E04B2154C971 /* WireGuardKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1EBBB94A186F7080FE3609C8 /* Products */ = {
isa = PBXGroup;
children = (
027878C5E0654DB27FAE88A3 /* PCAPhobos.app */,
A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */,
);
name = Products;
sourceTree = "<group>";
};
2500D202DC9E96E7BBCE29B1 /* Tunnel */ = {
isa = PBXGroup;
children = (
8E36BA501FDCB2034F09415A /* PacketTunnelProvider.swift */,
211950F8776D3D14D7B9C49A /* pcarelay.c */,
2572D9A6F822B038D0652619 /* ru_bypass.txt */,
7500F0B31797D49B5BBEC5CC /* String+ArrayConversion.swift */,
2C1AA6DED9B45949864A9B7D /* TunnelConfiguration+WgQuickConfig.swift */,
);
path = Tunnel;
sourceTree = "<group>";
};
527268DF962A8B7EC120C536 /* obfuscator */ = {
isa = PBXGroup;
children = (
0E5C1673464AAC3EF81DD2FE /* config.c */,
9FC04650634F03E6DB09AB6B /* logging.c */,
05DD44245829A52D32B42AF8 /* masking_stun.c */,
CCE5C98CD905BDE16A8D83D4 /* masking.c */,
EF9115CA0D5EC52C8679A12A /* obfuscation.c */,
1FADBB2A65AF77D4ECCAFE78 /* wg-obfuscator.c */,
);
name = obfuscator;
path = Tunnel/obfuscator;
sourceTree = "<group>";
};
D4CB33FA621F53C8F82ED115 /* Packages */ = {
isa = PBXGroup;
children = (
CA6480536DA633DB94B6930F /* WireGuardKit */,
);
name = Packages;
sourceTree = "<group>";
};
DD1FD95C0B2DE48623872799 /* App */ = {
isa = PBXGroup;
children = (
6359B8954E69147DA50EDB46 /* Assets.xcassets */,
FF14952A4263FF0E004A3F04 /* ContentView.swift */,
722DE8EFC35A88BC68637FB4 /* Info.plist */,
7E66B3AD4BB22D2AB5676E10 /* PCAPhobos.entitlements */,
B3E470E13EF603CB26991683 /* PCAPhobosApp.swift */,
);
path = App;
sourceTree = "<group>";
};
E31449B836D353F74991D905 = {
isa = PBXGroup;
children = (
DD1FD95C0B2DE48623872799 /* App */,
527268DF962A8B7EC120C536 /* obfuscator */,
D4CB33FA621F53C8F82ED115 /* Packages */,
F6FC482145D1EFAD5A15F63B /* Shared */,
2500D202DC9E96E7BBCE29B1 /* Tunnel */,
1EBBB94A186F7080FE3609C8 /* Products */,
);
sourceTree = "<group>";
};
F6FC482145D1EFAD5A15F63B /* Shared */ = {
isa = PBXGroup;
children = (
98DC1765DEC5F326E2B5F9C4 /* PhobosProfile.swift */,
);
path = Shared;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0FD066BC2C51A1E092914FE8 /* PhobosTunnel */ = {
isa = PBXNativeTarget;
buildConfigurationList = 329F6A5A13B95B1525F94841 /* Build configuration list for PBXNativeTarget "PhobosTunnel" */;
buildPhases = (
173BA4C39EE769A34EBB19CB /* Sources */,
A5352C1E8C48FFB2CCEC38F3 /* Resources */,
D366EA9C7C0EC88CAE5EA47E /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = PhobosTunnel;
packageProductDependencies = (
FA829997FAB330F6EFA104C9 /* WireGuardKit */,
);
productName = PhobosTunnel;
productReference = A81F96BF4131FDBB816623BE /* PhobosTunnel.appex */;
productType = "com.apple.product-type.app-extension";
};
E1F7E69029753CF102C6CE2E /* PCAPhobos */ = {
isa = PBXNativeTarget;
buildConfigurationList = 61EF055D39FD21A29FA039D5 /* Build configuration list for PBXNativeTarget "PCAPhobos" */;
buildPhases = (
F83DEE91B85335727CA6B39E /* Sources */,
D18596DD6D613F6BC9647644 /* Resources */,
9FC25420ED5EF973CE140584 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
0B334C04E5AD898D8736FFAA /* PBXTargetDependency */,
);
name = PCAPhobos;
packageProductDependencies = (
);
productName = PCAPhobos;
productReference = 027878C5E0654DB27FAE88A3 /* PCAPhobos.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
E59D1112D2B128DF6FE7A1E9 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
0FD066BC2C51A1E092914FE8 = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic;
};
E1F7E69029753CF102C6CE2E = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = F73680B923D76C65D03B442C /* Build configuration list for PBXProject "PCAPhobos" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = E31449B836D353F74991D905;
minimizedProjectReferenceProxies = 1;
packageReferences = (
9F8EC62EA6724F5FA6B8BAE0 /* XCLocalSwiftPackageReference "WireGuardKit" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 1EBBB94A186F7080FE3609C8 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
E1F7E69029753CF102C6CE2E /* PCAPhobos */,
0FD066BC2C51A1E092914FE8 /* PhobosTunnel */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
A5352C1E8C48FFB2CCEC38F3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5F5D376513C0B27C45A3E7B6 /* ru_bypass.txt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D18596DD6D613F6BC9647644 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D8CCC1E6CC8164EDD4E34D25 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
173BA4C39EE769A34EBB19CB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
99F5DB52833E3B5505EEA727 /* PacketTunnelProvider.swift in Sources */,
7D8AB66421409CBFCCF2769A /* PhobosProfile.swift in Sources */,
4E4255B9E0F8626F6E209B0F /* String+ArrayConversion.swift in Sources */,
11F2A6A8D281AB36B45D4603 /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
78B74F81E0042D9AD94B7A6B /* config.c in Sources */,
1C05D29ACA95548374AD6FD9 /* logging.c in Sources */,
56F35C68C6A387E499703CFF /* masking.c in Sources */,
E88105F9325A93041D617213 /* masking_stun.c in Sources */,
B3BB8BE3409D63FEB59B5449 /* obfuscation.c in Sources */,
DF6D62C526E908CA5CDDFCAE /* pcarelay.c in Sources */,
38870650A7C2B1F0927A1230 /* wg-obfuscator.c in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F83DEE91B85335727CA6B39E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
008DA398D56DFBDB16D97CE1 /* ContentView.swift in Sources */,
C3C41D3C419E8D17F20A38B5 /* PCAPhobosApp.swift in Sources */,
CBC338B91B5FCE1E207B36AC /* PhobosProfile.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0B334C04E5AD898D8736FFAA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0FD066BC2C51A1E092914FE8 /* PhobosTunnel */;
targetProxy = B9875EF5DEB6362919B43F22 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
35473CF1EA2AAAC350F346D0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/PCAPhobos.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = App/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
62739003B63EE4EE3B3FF1FB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Tunnel/PhobosTunnel.entitlements;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Tunnel",
"$(PROJECT_DIR)/Tunnel/obfuscator",
);
INFOPLIST_FILE = Tunnel/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(PROJECT_DIR)/WireGuardGo/iphoneos",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(PROJECT_DIR)/WireGuardGo/iphonesimulator",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos.tunnel;
SDKROOT = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "Tunnel/Tunnel-Bridging-Header.h";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
7ABA8220043DEF78154F9F38 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 141;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 1.4.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
};
name = Release;
};
B5D2F9B70DD37D3A57737EED /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 141;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 1.4.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
D0AEA32C3C7BFB88918AB206 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Tunnel/PhobosTunnel.entitlements;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Tunnel",
"$(PROJECT_DIR)/Tunnel/obfuscator",
);
INFOPLIST_FILE = Tunnel/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(PROJECT_DIR)/WireGuardGo/iphoneos",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(PROJECT_DIR)/WireGuardGo/iphonesimulator",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos.tunnel;
SDKROOT = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "Tunnel/Tunnel-Bridging-Header.h";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D42BF07983FD43EBB6D84762 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/PCAPhobos.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = App/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bobyrev.pcaphobos;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
329F6A5A13B95B1525F94841 /* Build configuration list for PBXNativeTarget "PhobosTunnel" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D0AEA32C3C7BFB88918AB206 /* Debug */,
62739003B63EE4EE3B3FF1FB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
61EF055D39FD21A29FA039D5 /* Build configuration list for PBXNativeTarget "PCAPhobos" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D42BF07983FD43EBB6D84762 /* Debug */,
35473CF1EA2AAAC350F346D0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
F73680B923D76C65D03B442C /* Build configuration list for PBXProject "PCAPhobos" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B5D2F9B70DD37D3A57737EED /* Debug */,
7ABA8220043DEF78154F9F38 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
9F8EC62EA6724F5FA6B8BAE0 /* XCLocalSwiftPackageReference "WireGuardKit" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = WireGuardKit;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
FA829997FAB330F6EFA104C9 /* WireGuardKit */ = {
isa = XCSwiftPackageProductDependency;
productName = WireGuardKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = E59D1112D2B128DF6FE7A1E9 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

26
ios/README.md Normal file
View File

@@ -0,0 +1,26 @@
# PCA Phobos VPN — iPhone
То же, что Android-клиент: импорт `phobos://` из панели, обфускация (wg-obfuscator внутри
расширения), **российские сайты напрямую**, канал **Авто/UDP/TCP** (при блокировке UDP — сам на TCP).
Выбор отдельных приложений на iOS невозможен (Apple даёт это только через MDM) — делим по адресам.
Устройство:
- `App/` — SwiftUI-приложение (профиль, кнопка, настройки), `Shared/PhobosProfile.swift` — разбор ссылки.
- `Tunnel/` — Packet Tunnel: WireGuardKit → 127.0.0.1:13255 wg-obfuscator (`-Dmain=wgobf_main`, один
экземпляр: iOS запрещает fork) → 127.0.0.1:13258 `pcarelay.c` → сервер по UDP или TCP (формат udptcp).
- `WireGuardKit/` — wireguard-apple (MIT), `WireGuardGo/<sdk>/libwg-go.a` — собран `make` (iphoneos) и
`go build -buildmode c-archive` с `-target arm64-apple-ios15.0-simulator` (симулятор).
Сборка: `xcodegen generate` → `PCAPhobos.xcodeproj`. Проверка без телефона: `Tests/ProfileCheck`.
## Установка на iPhone (нужен ПЛАТНЫЙ Apple Developer, $99/год)
Network Extension (packet-tunnel-provider) Apple выдаёт только платным аккаунтам — с бесплатным
Xcode не подпишет даже одно приложение. Когда аккаунт есть:
1. В `project.yml` вписать `DEVELOPMENT_TEAM: <Team ID>`, `xcodegen generate`.
2. Xcode → Signing: включить capability Network Extensions (Packet Tunnel) для обоих таргетов.
3. iPhone по кабелю → Run, либо Product → Archive → TestFlight (тестировщики по e-mail, 90 дней).
4. В панели клиента → 📱 → открыть ссылку `phobos://` на iPhone → «Открыть в PCA Phobos» → Подключить
→ разрешить добавление VPN.
Проверено 23.10.2026: сборка под iphoneos и симулятор, UI и импорт реальной ссылки в симуляторе.
Сам туннель в симуляторе iOS не работает — проверяется только на телефоне.

View File

@@ -0,0 +1,84 @@
import Foundation
/// Parsed phobos:// payload from the PCA Phobos panel:
/// [Interface] + [Peer] (WireGuard) + [instance] (wg-obfuscator) + "# pca-tcp = <port>".
struct PhobosProfile {
let raw: String
init(_ raw: String) { self.raw = raw }
/// phobos://<urlsafe base64>#name
static func fromLink(_ link: String) -> (profile: PhobosProfile, name: String)? {
var s = link.trimmingCharacters(in: .whitespacesAndNewlines)
guard s.hasPrefix("phobos://") else { return nil }
s.removeFirst("phobos://".count)
var name = "Phobos"
if let hash = s.firstIndex(of: "#") {
name = String(s[s.index(after: hash)...]).removingPercentEncoding ?? name
s = String(s[..<hash])
}
var b64 = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
while b64.count % 4 != 0 { b64 += "=" }
guard let data = Data(base64Encoded: b64), let text = String(data: data, encoding: .utf8),
text.contains("[Interface]"), text.contains("[instance]") else { return nil }
return (PhobosProfile(text), name)
}
/// Lines of a section, without "= none" placeholders and comments.
func lines(_ header: String) -> [(key: String, line: String)] {
var inSection = false
var out: [(String, String)] = []
for rawLine in raw.split(separator: "\n") {
let line = rawLine.trimmingCharacters(in: .whitespaces)
if line.hasPrefix("[") && line.hasSuffix("]") { inSection = line.lowercased() == header.lowercased(); continue }
guard inSection, !line.hasPrefix("#"), let eq = line.firstIndex(of: "=") else { continue }
let value = line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces)
if value.lowercased() == "none" { continue }
out.append((line[..<eq].trimmingCharacters(in: .whitespaces).lowercased(), line))
}
return out
}
private func value(_ header: String, _ key: String) -> String? {
lines(header).first { $0.key == key }.map { line in
line.line[line.line.index(after: line.line.firstIndex(of: "=")!)...].trimmingCharacters(in: .whitespaces)
}
}
var target: String? { value("[instance]", "target") }
var targetHost: String? { target.map { String($0[..<($0.lastIndex(of: ":") ?? $0.endIndex)]) } }
var targetPort: Int? { target.flatMap { $0.split(separator: ":").last.flatMap { Int($0) } } }
/// "# pca-tcp = 443" written by the panel when the server has the TCP fallback.
var tcpPort: Int? {
for rawLine in raw.split(separator: "\n") {
let line = rawLine.trimmingCharacters(in: .whitespaces)
if line.hasPrefix("#"), line.contains("pca-tcp"), let eq = line.firstIndex(of: "=") {
return Int(line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces))
}
}
return nil
}
func obfuscatorConfig(listen: Int, target: String) -> String {
var out = "[instance]\nsource-if = 127.0.0.1\nsource-lport = \(listen)\ntarget = \(target)\n"
for item in lines("[instance]") where !["source-if", "source-lport", "target", "verbose"].contains(item.key) {
out += item.line + "\n"
}
return out + "verbose = INFO\n"
}
func wireGuardConfig(endpoint: String, allowedIPs: String?) -> String {
var out = "[Interface]\n"
for item in lines("[Interface]") where !item.key.hasSuffix("applications") { out += item.line + "\n" }
out += "\n[Peer]\n"
for item in lines("[Peer]") {
switch item.key {
case "endpoint": out += "Endpoint = \(endpoint)\n"
case "allowedips" where allowedIPs != nil: out += "AllowedIPs = \(allowedIPs!)\n"
default: out += item.line + "\n"
}
}
return out
}
}

View File

@@ -0,0 +1,38 @@
// Self-check of phobos:// parsing (no device needed):
// cd ios/Tests/ProfileCheck && xcrun swiftc -o /tmp/pc ../../Shared/PhobosProfile.swift main.swift && /tmp/pc
import Foundation
let conf = """
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.25.0.9/32
DNS = 1.1.1.1
MTU = 1420
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
PresharedKey = none
Endpoint = 127.0.0.1:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
[instance]
source-if = 127.0.0.1
source-lport = 51820
target = 203.0.113.10:2083
key = testkey
masking = STUN
verbose = INFO
idle-timeout = 300
max-dummy = 4
# pca-tcp = 443
"""
let link = "phobos://" + Data(conf.utf8).base64EncodedString()
.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "") + "#demo"
let (p, name) = PhobosProfile.fromLink(link)!
precondition(name == "demo" && p.targetHost == "203.0.113.10" && p.targetPort == 2083 && p.tcpPort == 443)
let obf = p.obfuscatorConfig(listen: 13255, target: "127.0.0.1:13258")
precondition(obf.contains("target = 127.0.0.1:13258") && obf.contains("key = testkey") && !obf.contains("203.0.113.10"))
let wg = p.wireGuardConfig(endpoint: "127.0.0.1:13255", allowedIPs: "1.0.0.0/8")
precondition(wg.contains("Endpoint = 127.0.0.1:13255") && wg.contains("AllowedIPs = 1.0.0.0/8") && !wg.contains("= none"))
print("profile_check ok")

31
ios/Tunnel/Info.plist Normal file
View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PhobosTunnel</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,92 @@
import Foundation
import NetworkExtension
import WireGuardKit
/// PCA Phobos packet tunnel:
/// WireGuard (userspace) -> 127.0.0.1:13255 wg-obfuscator -> 127.0.0.1:13258 pcarelay -> server (UDP or TCP).
/// providerConfiguration: "conf" (phobos:// payload), "ruDirect" (Bool), "transport" ("auto"|"udp"|"tcp").
final class PacketTunnelProvider: NEPacketTunnelProvider {
private static let wgPort = 13255, relayPort = 13258
private static var obfuscatorStarted = false // one wg-obfuscator per process (it forks for more)
private lazy var adapter = WireGuardAdapter(with: self) { _, message in NSLog("wg: %@", message) }
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
let pc = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration ?? [:]
guard let conf = pc["conf"] as? String, !conf.isEmpty else {
completionHandler(NSError(domain: "PCAPhobos", code: 1, userInfo: [NSLocalizedDescriptionKey: "нет профиля"]))
return
}
let ruDirect = pc["ruDirect"] as? Bool ?? true
let transport = pc["transport"] as? String ?? "auto"
let profile = PhobosProfile(conf)
guard let host = profile.targetHost, let udpPort = profile.targetPort else {
completionHandler(NSError(domain: "PCAPhobos", code: 2, userInfo: [NSLocalizedDescriptionKey: "в профиле нет адреса сервера"]))
return
}
let tcpPort = profile.tcpPort ?? 0
if transport == "tcp" && tcpPort == 0 {
completionHandler(NSError(domain: "PCAPhobos", code: 3, userInfo: [NSLocalizedDescriptionKey: "на сервере нет TCP-канала"]))
return
}
pcarelay_use_tcp(transport == "tcp" ? 1 : 0)
if !Self.obfuscatorStarted {
Self.obfuscatorStarted = true
pcarelay_start(Int32(Self.relayPort), host, Int32(udpPort), Int32(tcpPort))
let path = NSTemporaryDirectory() + "obf.conf"
try? profile.obfuscatorConfig(listen: Self.wgPort, target: "127.0.0.1:\(Self.relayPort)")
.write(toFile: path, atomically: true, encoding: .utf8)
Thread.detachNewThread {
var args: [UnsafeMutablePointer<CChar>?] = ["wg-obfuscator", "-c", path].map { strdup($0) }
_ = wgobf_main(Int32(args.count), &args)
}
}
let allowed = ruDirect ? Self.ruBypassAllowedIPs() : nil
let wgText = profile.wireGuardConfig(endpoint: "127.0.0.1:\(Self.wgPort)", allowedIPs: allowed)
let tunnelConfig: TunnelConfiguration
do {
tunnelConfig = try TunnelConfiguration(fromWgQuickConfig: wgText, called: "phobos")
} catch {
completionHandler(error)
return
}
adapter.start(tunnelConfiguration: tunnelConfig) { [weak self] error in
completionHandler(error)
if error == nil && transport == "auto" && tcpPort != 0 { self?.watchUdp() }
}
}
/// Auto: nothing came back over UDP within 15 s -> UDP is cut, move the relay to TCP.
private func watchUdp() {
DispatchQueue.global().asyncAfter(deadline: .now() + 15) {
if pcarelay_is_tcp() == 0 && pcarelay_rx_bytes() == 0 {
NSLog("PCAPhobos: no UDP answer in 15 s -> TCP")
pcarelay_use_tcp(1)
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
adapter.stop { _ in
completionHandler()
// the obfuscator thread cannot be stopped cleanly; a fresh process starts next time
exit(0)
}
}
/// App asks for status: {"transport": "UDP"|"TCP", "rx": bytes}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
let status: [String: Any] = ["transport": pcarelay_is_tcp() != 0 ? "TCP" : "UDP",
"rx": pcarelay_rx_bytes()]
completionHandler?(try? JSONSerialization.data(withJSONObject: status))
}
private static func ruBypassAllowedIPs() -> String? {
guard let url = Bundle.main.url(forResource: "ru_bypass", withExtension: "txt"),
let text = try? String(contentsOf: url, encoding: .utf8) else { return nil }
return text.split(separator: "\n").first { !$0.hasPrefix("#") && $0.contains("/") }.map(String.init)
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
extension String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
return split(separator: separator)
.map {
if let charSet = trimmingCharacters {
return $0.trimmingCharacters(in: charSet)
} else {
return String($0)
}
}
}
}
extension Optional where Wrapped == String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
switch self {
case .none:
return []
case .some(let wrapped):
return wrapped.splitToArray(separator: separator, trimmingCharacters: trimmingCharacters)
}
}
}

View File

@@ -0,0 +1 @@
#include "pcarelay.h"

View File

@@ -0,0 +1,253 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import WireGuardKit
extension TunnelConfiguration {
enum ParserState {
case inInterfaceSection
case inPeerSection
case notInASection
}
enum ParseError: Error {
case invalidLine(String.SubSequence)
case noInterface
case multipleInterfaces
case interfaceHasNoPrivateKey
case interfaceHasInvalidPrivateKey(String)
case interfaceHasInvalidListenPort(String)
case interfaceHasInvalidAddress(String)
case interfaceHasInvalidDNS(String)
case interfaceHasInvalidMTU(String)
case interfaceHasUnrecognizedKey(String)
case peerHasNoPublicKey
case peerHasInvalidPublicKey(String)
case peerHasInvalidPreSharedKey(String)
case peerHasInvalidAllowedIP(String)
case peerHasInvalidEndpoint(String)
case peerHasInvalidPersistentKeepAlive(String)
case peerHasInvalidTransferBytes(String)
case peerHasInvalidLastHandshakeTime(String)
case peerHasUnrecognizedKey(String)
case multiplePeersWithSamePublicKey
case multipleEntriesForKey(String)
}
convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]()
let lines = wgQuickConfig.split { $0.isNewline }
var parserState = ParserState.notInASection
var attributes = [String: String]()
for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String
if let commentRange = line.range(of: "#") {
trimmedLine = String(line[..<commentRange.lowerBound])
} else {
trimmedLine = String(line)
}
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespacesAndNewlines)
let lowercasedLine = trimmedLine.lowercased()
if !trimmedLine.isEmpty {
if let equalsIndex = trimmedLine.firstIndex(of: "=") {
// Line contains an attribute
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
let key = keyWithCase.lowercased()
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
if let presentValue = attributes[key] {
if keysWithMultipleEntriesAllowed.contains(key) {
attributes[key] = presentValue + "," + value
} else {
throw ParseError.multipleEntriesForKey(keyWithCase)
}
} else {
attributes[key] = value
}
let interfaceSectionKeys: Set<String> = ["privatekey", "listenport", "address", "dns", "mtu"]
let peerSectionKeys: Set<String> = ["publickey", "presharedkey", "allowedips", "endpoint", "persistentkeepalive"]
if parserState == .inInterfaceSection {
guard interfaceSectionKeys.contains(key) else {
throw ParseError.interfaceHasUnrecognizedKey(keyWithCase)
}
} else if parserState == .inPeerSection {
guard peerSectionKeys.contains(key) else {
throw ParseError.peerHasUnrecognizedKey(keyWithCase)
}
}
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
throw ParseError.invalidLine(line)
}
}
let isLastLine = lineIndex == lines.count - 1
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
// Previous section has ended; process the attributes collected so far
if parserState == .inInterfaceSection {
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
} else if parserState == .inPeerSection {
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
peerConfigurations.append(peer)
}
}
if lowercasedLine == "[interface]" {
parserState = .inInterfaceSection
attributes.removeAll()
} else if lowercasedLine == "[peer]" {
parserState = .inPeerSection
attributes.removeAll()
}
}
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey
}
if let interfaceConfiguration = interfaceConfiguration {
self.init(name: name, interface: interfaceConfiguration, peers: peerConfigurations)
} else {
throw ParseError.noInterface
}
}
func asWgQuickConfig() -> String {
var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64Key)\n")
if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n")
}
if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
var dnsLine = interface.dns.map { $0.stringRepresentation }
dnsLine.append(contentsOf: interface.dnsSearch)
let dnsString = dnsLine.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
if let mtu = interface.mtu {
output.append("MTU = \(mtu)\n")
}
for peer in peers {
output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64Key)\n")
if let preSharedKey = peer.preSharedKey?.base64Key {
output.append("PresharedKey = \(preSharedKey)\n")
}
if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
if let endpoint = peer.endpoint {
output.append("Endpoint = \(endpoint.stringRepresentation)\n")
}
if let persistentKeepAlive = peer.persistentKeepAlive {
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
}
}
return output
}
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
guard let privateKeyString = attributes["privatekey"] else {
throw ParseError.interfaceHasNoPrivateKey
}
guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
}
var interface = InterfaceConfiguration(privateKey: privateKey)
if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else {
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
}
interface.listenPort = listenPort
}
if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]()
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let address = IPAddressRange(from: addressString) else {
throw ParseError.interfaceHasInvalidAddress(addressString)
}
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
var dnsSearch = [String]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let dnsServer = DNSServer(from: dnsServerString) {
dnsServers.append(dnsServer)
} else {
dnsSearch.append(dnsServerString)
}
}
interface.dns = dnsServers
interface.dnsSearch = dnsSearch
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else {
throw ParseError.interfaceHasInvalidMTU(mtuString)
}
interface.mtu = mtu
}
return interface
}
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
guard let publicKeyString = attributes["publickey"] else {
throw ParseError.peerHasNoPublicKey
}
guard let publicKey = PublicKey(base64Key: publicKeyString) else {
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
}
var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) else {
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
}
peer.preSharedKey = preSharedKey
}
if let allowedIPsString = attributes["allowedips"] {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
}
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else {
throw ParseError.peerHasInvalidEndpoint(endpointString)
}
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
}
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
}

View File

@@ -0,0 +1,535 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include "config.h"
#include "wg-obfuscator.h"
#include "mini_argp.h"
#include "masking.h"
// Executable name
static const char *arg0;
/* The options we understand. */
static const mini_argp_opt options[] = {
{ "help", '?', 0 },
{ "config", 'c', 1 },
{ "source-if", 'i', 1 },
{ "source", 's', 1 },
{ "source-lport", 'p', 1 },
{ "target-if", 'o', 1 },
{ "target", 't', 1 },
{ "target-lport", 'r', 1 },
{ "key", 'k', 1 },
{ "masking", 'a', 1 },
{ "static-bindings", 'b', 1 },
{ "max-clients" , 'm', 1 },
{ "idle-timeout", 'l', 1 },
{ "in-timeout", 'n', 1 },
{ "max-dummy", 'd', 1 },
{ "fwmark", 'f', 1 },
{ "allow-clean", 'e', 0 },
{ "verbose", 'v', 1 },
{ "log-file", 'L', 1 },
{ "log-timestamps", 'T', 1 },
{ "resolve-interval", 'R', 1 },
{ 0 }
};
static void show_usage(void)
{
printf("Usage: %s [options]\n%s", arg0,
" -?, --help Give this help list\n"
"\n"
"Main settings:\n"
" -c, --config=<config_file> Read configuration from file\n"
" (can be used instead of the rest arguments)\n"
" -i, --source-if=<ip> Source interface to listen on\n"
" (optional, default - 0.0.0.0, e.g. all)\n"
" -p, --source-lport=<port> Source port to listen\n"
" -t, --target=<ip>:<port> Target IP and port\n"
" -k, --key=<key> Obfuscation key \n"
" (required, must be 1-255 characters long)\n"
" -a, --masking=<type> Masking type (optional, default - AUTO)\n"
" Supported values: STUN, AUTO, NONE\n"
" -b, --static-bindings=<ip>:<port>:<port>,...\n"
" Comma-separated static bindings for two-way mode\n"
" as <client_ip>:<client_port>:<forward_port>\n"
" You can also repeat this option.\n"
" -f, --fwmark=<mark> Firewall mark to set on all packets\n"
" (optional, default - 0, e.g. disabled)\n"
" -v, --verbose=<level> Verbosity level (optional, default - INFO)\n"
" ERRORS (critical errors only)\n"
" WARNINGS (important messages)\n"
" 2 - INFO (informational messages: status messages,\n"
" connection established, etc.)\n"
" 3 - DEBUG (detailed debug messages)\n"
" 4 - TRACE (very detailed debug messages, including\n"
" packet dumps)\n"
" -L, --log-file=<path> Write the log to this file instead of stderr\n"
" (optional, the file is reopened on SIGHUP)\n"
" -T, --log-timestamps=<val> Prefix log lines with a timestamp\n"
" (optional, default - AUTO, e.g. only when\n"
" writing to a log file)\n"
" Supported values: AUTO, TRUE, FALSE\n"
"\n"
"Additional options:\n"
" -m, --max-clients=<number> Maximum number of clients (default: 1024)\n"
" -l, --idle-timeout=<sec> Idle timeout in seconds (default: 300)\n"
" -n, --in-timeout=<sec> Incoming timeout in seconds (default: 0 - disabled)\n"
" -d, --max-dummy=<bytes> Maximum length of dummy bytes for data packets\n"
" (default: 4)\n"
" -e, --allow-clean For servers, allow non-obfuscated incoming connections\n"
" -R, --resolve-interval=<sec>\n"
" Re-resolve target and static-binding hostnames\n"
" every N seconds (default: 0 - disabled).\n"
" SIGHUP always triggers a refresh.\n"
" If non-zero, a failed resolve at startup is retried\n"
" instead of exiting.\n");
}
static int parse_opt(const char *lname, char sname, const char *val, void *ctx);
/**
* @brief Resets the configuration structure to its default values.
*
* This function clears the configuration structure by setting all fields to zero
* and resetting the verbosity level to the default value.
*
* @param config Pointer to the obfuscator_config structure to be reset.
*/
static void reset_config(obfuscator_config_t *config)
{
if (config->static_bindings) {
free(config->static_bindings);
}
memset(config, 0, sizeof(*config));
config->max_clients = MAX_CLIENTS_DEFAULT;
config->idle_timeout = IDLE_TIMEOUT_DEFAULT;
config->in_timeout = IN_TIMEOUT_DEFAULT;
config->max_dummy_length_data = MAX_DUMMY_LENGTH_DATA_DEFAULT;
config->log_timestamps = -1; // auto
verbose = LL_DEFAULT;
}
/**
* Checks if the given string represents a valid integer.
*
* @param str Pointer to the null-terminated string to check.
* @return Non-zero value if the string is a valid integer, 0 otherwise.
*/
static uint8_t is_integer(const char *str)
{
if (!str || !*str) {
return 0; // Empty string is not an integer
}
while (*str) {
if (!isdigit((unsigned char)*str)) {
return 0; // Non-digit character found
}
str++;
}
return 1; // All characters are digits
}
/**
* Parses a boolean value from a string.
*
* @param str Pointer to the null-terminated string to parse.
* @return 1 for true values ("true", "yes", "on", "1"),
* 0 for false values ("false", "no", "off", "0"),
* -1 if the string is not a valid boolean value.
*/
static int parse_bool(const char *str)
{
char lower[16];
if (!str || !*str || strlen(str) >= sizeof(lower)) {
return -1;
}
strcpy(lower, str);
for (char *p = lower; *p; ++p) *p = tolower((unsigned char)*p);
if (!strcmp(lower, "true") || !strcmp(lower, "yes") || !strcmp(lower, "on") || !strcmp(lower, "1")) {
return 1;
}
if (!strcmp(lower, "false") || !strcmp(lower, "no") || !strcmp(lower, "off") || !strcmp(lower, "0")) {
return 0;
}
return -1;
}
/**
* @brief Reads and processes the configuration file.
*
* This function opens the specified configuration file and parses its contents
* to initialize or update the application's configuration settings.
*
* @param filename The path to the configuration file to be read.
* @param config Pointer to the obfuscator_config structure where the parsed settings will be stored.
*/
static void read_config_file(const char *filename, obfuscator_config_t *config)
{
// Read configuration from the file
uint8_t first_section = 1; // Flag to indicate if this is the first section being processed
char line[10 * 1024]; // Buffer to hold each line from the config file
FILE *config_file = fopen(filename, "r");
if (config_file == NULL) {
perror("Can't open config file");
exit(EXIT_FAILURE);
}
while (fgets(line, sizeof(line), config_file)) {
// Remove trailing newlines, carriage returns, spaces and tabs
while (strlen(line) && (line[strlen(line) - 1] == '\n' || line[strlen(line) - 1] == '\r'
|| line[strlen(line) - 1] == ' ' || line[strlen(line) - 1] == '\t')) {
line[strlen(line) - 1] = 0;
}
// Remove leading spaces and tabs
while (strlen(line) && (line[0] == ' ' || line[0] == '\t')) {
memmove(line, line + 1, strlen(line));
}
// Ignore comments
char *comment_index = strstr(line, "#");
if (comment_index != NULL) {
*comment_index = 0;
}
// Skip empty lines or with spaces only
size_t line_len = strlen(line);
if (strspn(line, " \t\r\n") == line_len) {
continue;
}
// It can be new section
if (line_len > 0 && line[0] == '[' && line[line_len - 1] == ']') {
if (!first_section) {
// new config, need to fork the process
pid_t pid = fork();
if (pid < 0) {
log(LL_ERROR, "Can't fork a new instance - %s (%d)", strerror(errno), errno);
exit(EXIT_FAILURE);
}
if (pid == 0) {
// Close in the child process
fclose(config_file);
// Stop config file processing for this instance
return;
}
// Remember the instance to be able to forward signals to it
register_child_instance(pid);
}
size_t len = strlen(line) - 2;
if (len > sizeof(section_name) - 1) {
len = sizeof(section_name) - 1;
}
strncpy(section_name, line + 1, len);
section_name[len] = 0;
// Reset all the parameters
reset_config(config);
first_section = 0; // We have processed the first section
continue;
}
// Parse key-value pairs
char *key = strtok(line, "=");
key = trim(key);
while (strlen(key) && (key[strlen(key) - 1] == ' ' || key[strlen(key) - 1] == '\t' || key[strlen(key) - 1] == '\r' || key[strlen(key) - 1] == '\n')) {
key[strlen(key) - 1] = 0;
}
char *value = strtok(NULL, "=");
if (value == NULL) {
log(LL_ERROR, "Invalid configuration line: %s", line);
exit(EXIT_FAILURE);
}
value = trim(value);
if (!*value) {
log(LL_ERROR, "Invalid configuration line: %s", line);
exit(EXIT_FAILURE);
}
const mini_argp_opt *o = margp_find(options, key, 0);
if (o == NULL) {
log(LL_ERROR, "Unknown configuration key: %s", key);
exit(EXIT_FAILURE);
}
if (!o->has_arg) {
// Flag options are represented as booleans in the config file
int b = parse_bool(value);
if (b < 0) {
log(LL_ERROR, "Configuration key '%s' accepts only boolean values (true/yes/on/1 or false/no/off/0)", key);
exit(EXIT_FAILURE);
}
if (!b) {
continue;
}
value = NULL;
}
parse_opt(o->long_name, o->short_name, value, config);
}
fclose(config_file);
}
/* Parse a single option. */
static int parse_opt(const char *lname, char sname, const char *val, void *ctx)
{
obfuscator_config_t *config = (obfuscator_config_t *)ctx;
char val_lower[16];
switch (sname)
{
case '?':
// Show usage and exit
show_usage();
exit(EXIT_SUCCESS);
case 'c':
read_config_file(val, config);
break;
case 'i':
strncpy(config->client_interface, val, sizeof(config->client_interface) - 1);
config->client_interface[sizeof(config->client_interface) - 1] = 0; // Ensure null-termination
config->client_interface_set = 1;
break;
case 'p':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid source port: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->listen_port = atoi(val);
if (config->listen_port <= 0 || config->listen_port > 65535) {
log(LL_ERROR, "Invalid listen port: %s (must be between 1 and 65535)", val);
exit(EXIT_FAILURE);
}
config->listen_port_set = 1;
break;
case 't':
strncpy(config->forward_host_port, val, sizeof(config->forward_host_port) - 1);
config->forward_host_port[sizeof(config->forward_host_port) - 1] = 0; // Ensure null-termination
config->forward_host_port_set = 1;
break;
case 'b':
{
char *new_bindings;
if (!config->static_bindings) {
new_bindings = strdup(val);
} else {
size_t old_len = strlen(config->static_bindings);
new_bindings = realloc(config->static_bindings, old_len + strlen(val) + 2);
if (new_bindings) {
new_bindings[old_len] = ',';
strcpy(new_bindings + old_len + 1, val);
}
}
if (!new_bindings) {
log(LL_ERROR, "Out of memory while parsing static-bindings");
exit(EXIT_FAILURE);
}
config->static_bindings = new_bindings;
config->static_bindings_set = 1;
}
break;
case 'k':
strncpy(config->xor_key, val, sizeof(config->xor_key));
config->xor_key[sizeof(config->xor_key) - 1] = 0; // Ensure null-termination
if (strlen(config->xor_key) == 0) {
log(LL_ERROR, "XOR key cannot be empty");
exit(EXIT_FAILURE);
}
config->xor_key_set = 1;
break;
case 'm':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid maximum number of clients: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->max_clients = atoi(val);
if (config->max_clients <= 0) {
log(LL_ERROR, "Invalid maximum number of clients: %s (must be greater than 0)", val);
exit(EXIT_FAILURE);
}
break;
case 'l':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid idle timeout: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->idle_timeout = atol(val);
if (config->idle_timeout <= 0) {
log(LL_ERROR, "Invalid idle timeout: %s (must be greater than 0)", val);
exit(EXIT_FAILURE);
}
config->idle_timeout *= 1000; // Convert to milliseconds
break;
case 'n':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid incoming timeout: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->in_timeout = atol(val);
if (config->in_timeout <= 0) {
log(LL_ERROR, "Invalid incoming timeout: %s (must be greater than 0)", val);
exit(EXIT_FAILURE);
}
config->in_timeout *= 1000; // Convert to milliseconds
break;
case 'd':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid maximum dummy length for data packets: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->max_dummy_length_data = atoi(val);
if (config->max_dummy_length_data < 0 || config->max_dummy_length_data > MAX_DUMMY_LENGTH_TOTAL) {
log(LL_ERROR, "Invalid maximum dummy length for data packets: %s (must be between 0 and %d)", val, MAX_DUMMY_LENGTH_TOTAL);
exit(EXIT_FAILURE);
}
break;
case 'e':
config->allow_clean = 1;
break;
case 'f':
// parse string with decimal and hexadecimal support
#ifdef __linux__
{
long int v = strtol(val, NULL, 0);
if (v <= 0 || v > UINT16_MAX) {
log(LL_ERROR, "Invalid firewall mark: %s", val);
exit(EXIT_FAILURE);
}
config->fwmark = (uint16_t)v;
}
#else
log(LL_WARN, "Firewall mark is not supported on this platform");
#endif
break;
case 'a':
{
strncpy(val_lower, val, sizeof(val_lower) - 1);
val_lower[sizeof(val_lower) - 1] = 0;
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
if (strcmp(val_lower, "none") == 0) {
config->masking_handler = NULL;
config->masking_handler_set = 1;
break;
}
if (strcmp(val_lower, "auto") == 0) {
config->masking_handler = NULL;
config->masking_handler_set = 0;
break;
}
masking_handler_t *handler = get_masking_handler_by_name(val_lower);
if (handler == NULL) {
log(LL_ERROR, "Unknown masking type: %s", val);
exit(EXIT_FAILURE);
}
config->masking_handler = handler;
config->masking_handler_set = 1;
}
break;
case 'v':
strncpy(val_lower, val, sizeof(val_lower) - 1);
val_lower[sizeof(val_lower) - 1] = 0;
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
if (strcmp(val_lower, "error") == 0) {
verbose = LL_ERROR;
} else if (strcmp(val_lower, "warn") == 0) {
verbose = LL_WARN;
} else if (strcmp(val_lower, "info") == 0) {
verbose = LL_INFO;
} else if (strcmp(val_lower, "debug") == 0) {
verbose = LL_DEBUG;
} else if (strcmp(val_lower, "trace") == 0) {
verbose = LL_TRACE;
} else {
// check if it's a number
if (is_integer(val)) {
verbose = atoi(val);
if (verbose < 0 || verbose > 4) {
log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
exit(EXIT_FAILURE);
}
} else {
log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
exit(EXIT_FAILURE);
}
}
break;
case 'L':
strncpy(config->log_file, val, sizeof(config->log_file) - 1);
config->log_file[sizeof(config->log_file) - 1] = 0; // Ensure null-termination
if (strlen(config->log_file) == 0) {
log(LL_ERROR, "Log file path cannot be empty");
exit(EXIT_FAILURE);
}
config->log_file_set = 1;
break;
case 'T':
{
strncpy(val_lower, val, sizeof(val_lower) - 1);
val_lower[sizeof(val_lower) - 1] = 0;
for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
if (strcmp(val_lower, "auto") == 0) {
config->log_timestamps = -1;
break;
}
int b = parse_bool(val_lower);
if (b < 0) {
log(LL_ERROR, "Invalid log timestamps mode: %s (must be one of 'AUTO', 'TRUE', 'FALSE')", val);
exit(EXIT_FAILURE);
}
config->log_timestamps = b;
}
break;
case 'R':
if (!is_integer(val)) {
log(LL_ERROR, "Invalid resolve interval: %s (must be an integer)", val);
exit(EXIT_FAILURE);
}
config->resolve_interval = atol(val);
if (config->resolve_interval < 0) {
log(LL_ERROR, "Invalid resolve interval: %s (must be 0 or greater)", val);
exit(EXIT_FAILURE);
}
config->resolve_interval *= 1000; // Convert to milliseconds
break;
default:
// should never happen
return -1;
}
return 0;
}
int parse_config(int argc, char **argv, obfuscator_config_t *config)
{
/* Parse command line arguments */
reset_config(config);
arg0 = argv[0]; // Save the executable name
if (argc == 1) {
fprintf(stderr, "No arguments provided, use \"%s --help\" command for usage information\n", argv[0]);
return -1;
}
if (mini_argp_parse(argc, argv, options, config, parse_opt) != 0) {
fprintf(stderr, "Failed to parse command line arguments\n");
return -1;
}
return 0;
}
/**
* @brief Removes leading and trailing whitespace characters from the input string.
*
* This function modifies the input string in place by trimming any whitespace
* characters (such as spaces, tabs, or newlines) from both the beginning and end.
*
* @param s Pointer to the null-terminated string to be trimmed.
* @return Pointer to the trimmed string.
*/
char *trim(char *s) {
char *end;
// Trim leading spaces, tabs, carriage returns and newlines
while (*s && (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')) s++;
if (!*s) return s;
// Trim trailing spaces, tabs, carriage returns and newlines
end = s + strlen(s) - 1;
while (end > s && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = 0;
return s;
}

View File

@@ -0,0 +1,9 @@
#ifndef _CONFIG_H_
#define _CONFIG_H_
#include "wg-obfuscator.h"
char *trim(char *s);
int parse_config(int argc, char **argv, obfuscator_config_t *config);
#endif // _CONFIG_H_

View File

@@ -0,0 +1,258 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include "wg-obfuscator.h"
#ifndef O_BINARY
#define O_BINARY 0
#endif
#ifndef STDERR_FILENO
#define STDERR_FILENO 2
#endif
// Log files may contain client addresses, so they are not world-readable
#define LOG_FILE_MODE 0640
// File descriptor of the log file, -1 when logging to stderr
static int log_fd = -1;
// Path of the log file, kept to be able to reopen it on SIGHUP
static char *log_path = NULL;
// 1 if every line must be prefixed with a timestamp
static int log_timestamps = 0;
/**
* @brief Returns the single character representing a logging level.
*/
static char level_char(int level)
{
switch (level) {
case LL_ERROR: return 'E';
case LL_WARN: return 'W';
case LL_INFO: return 'I';
case LL_DEBUG: return 'D';
case LL_TRACE: return 'T';
default: return '?';
}
}
/**
* @brief Writes the whole buffer to the descriptor, retrying on partial writes.
*
* @param fd Descriptor to write to.
* @param buf Data to write.
* @param len Number of bytes to write.
*/
static void write_all(int fd, const char *buf, size_t len)
{
while (len) {
ssize_t written = write(fd, buf, len);
if (written <= 0) {
if (written < 0 && errno == EINTR) {
continue;
}
// Nothing sensible can be done about a broken log, drop the rest of the line
return;
}
buf += written;
len -= (size_t)written;
}
}
/**
* @brief Appends formatted text to the buffer without ever overflowing it.
*
* @param buf Buffer to append to.
* @param size Usable size of the buffer, including the terminating zero.
* @param len Current length of the text in the buffer.
* @param fmt Format string.
* @param args Arguments for the format string.
* @return New length of the text in the buffer.
*/
static size_t append_vfmt(char *buf, size_t size, size_t len, const char *fmt, va_list args)
{
int n;
if (len + 1 >= size) {
return len;
}
n = vsnprintf(buf + len, size - len, fmt, args);
if (n < 0) {
return len;
}
if ((size_t)n >= size - len) {
return size - 1; // truncated
}
return len + (size_t)n;
}
/**
* @brief Appends formatted text to the buffer without ever overflowing it.
*
* @param buf Buffer to append to.
* @param size Usable size of the buffer, including the terminating zero.
* @param len Current length of the text in the buffer.
* @param fmt Format string.
* @return New length of the text in the buffer.
*/
static size_t append_fmt(char *buf, size_t size, size_t len, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
len = append_vfmt(buf, size, len, fmt, args);
va_end(args);
return len;
}
/**
* @brief Writes the common line prefix (timestamp and instance name) to the buffer.
*
* @param buf Buffer to write to.
* @param size Usable size of the buffer, including the terminating zero.
* @param level Logging level of the line.
* @return Length of the prefix.
*/
static size_t log_prefix(char *buf, size_t size, int level)
{
size_t len = 0;
if (log_timestamps) {
struct timespec ts;
struct tm *tm;
if (clock_gettime(CLOCK_REALTIME, &ts) == 0 && (tm = localtime(&ts.tv_sec)) != NULL) {
len = strftime(buf, size, "%Y-%m-%d %H:%M:%S", tm);
len = append_fmt(buf, size, len, ".%03ld ", (long)(ts.tv_nsec / 1000000));
}
}
return append_fmt(buf, size, len, "[%s][%c] ", section_name, level_char(level));
}
/**
* @brief Terminates the line and writes it out with a single write() call.
*
* A single write() to a descriptor opened with O_APPEND keeps lines of different
* instances from interleaving when they share the same log file.
*
* @param buf Buffer holding the line, must have one spare byte for the newline.
* @param len Length of the line.
*/
static void log_emit(char *buf, size_t len)
{
buf[len++] = '\n';
write_all(log_fd >= 0 ? log_fd : STDERR_FILENO, buf, len);
}
/**
* @brief Writes a single log line.
*
* This is the implementation behind the log() macro, which is the one that
* checks the verbosity level, so the line written here is unconditional.
*
* @param level Logging level of the line.
* @param fmt Format string.
*/
void log_printf(int level, const char *fmt, ...)
{
char buf[LOG_LINE_MAX];
va_list args;
size_t len = log_prefix(buf, sizeof(buf) - 1, level);
va_start(args, fmt);
len = append_vfmt(buf, sizeof(buf) - 1, len, fmt, args);
va_end(args);
log_emit(buf, len);
}
/**
* @brief Writes a hexadecimal dump of a buffer, one or more prefixed lines.
*
* @param level Logging level of the dump.
* @param prefix Text to put right after the line prefix, e.g. a direction marker.
* @param data Data to dump.
* @param length Number of bytes to dump.
*/
void log_hexdump(int level, const char *prefix, const uint8_t *data, int length)
{
char buf[LOG_LINE_MAX];
int i = 0;
if (verbose < level) {
return;
}
// Long dumps are split into several lines, each one with its own prefix
do {
size_t len = log_prefix(buf, sizeof(buf) - 1, level);
len = append_fmt(buf, sizeof(buf) - 1, len, "%s", prefix);
while ((i < length) && (len + 3 < sizeof(buf) - 1)) {
len = append_fmt(buf, sizeof(buf) - 1, len, "%02X ", data[i++]);
}
log_emit(buf, len);
} while (i < length);
}
/**
* @brief Initializes logging.
*
* @param path Path of the log file or NULL to keep logging to stderr.
* @param timestamps_mode 1 to force timestamps on, 0 to force them off,
* negative to enable them only when logging to a file.
*/
void log_init(const char *path, int8_t timestamps_mode)
{
log_timestamps = (timestamps_mode >= 0) ? (timestamps_mode != 0) : (path != NULL);
if (!path) {
return;
}
log_path = strdup(path);
if (!log_path) {
fprintf(stderr, "Out of memory while initializing the log file\n");
exit(EXIT_FAILURE);
}
log_fd = open(path, O_WRONLY | O_CREAT | O_APPEND | O_BINARY, LOG_FILE_MODE);
if (log_fd < 0) {
// Logging into nowhere is worse than not starting at all
fprintf(stderr, "Can't open log file '%s' - %s (%d)\n", path, strerror(errno), errno);
exit(EXIT_FAILURE);
}
// Make every log file start with the version, no matter the verbosity level
log_printf(LL_INFO, "Starting %s", version_string());
}
/**
* @brief Reopens the log file, to be called after the log has been rotated.
*/
void log_reopen(void)
{
int fd;
if (!log_path) {
return;
}
fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_BINARY, LOG_FILE_MODE);
if (fd < 0) {
// Keep the old descriptor, losing the log completely would be worse
fprintf(stderr, "Can't reopen log file '%s' - %s (%d)\n", log_path, strerror(errno), errno);
return;
}
if (log_fd >= 0) {
close(log_fd);
}
log_fd = fd;
log_printf(LL_INFO, "Log file reopened, %s", version_string());
}

View File

@@ -0,0 +1,170 @@
#include <stdint.h>
#include <stdio.h>
#include <ctype.h>
#include "wg-obfuscator.h"
#include "masking.h"
#include "masking_handlers.h"
static _Thread_local struct {
int listen_sock;
struct sockaddr_in *sender_addr;
int server_sock;
} g_send_ctx;
static ssize_t send_to_client_cb(uint8_t *buffer, int length) {
return sendto(g_send_ctx.listen_sock, buffer, length, 0, (struct sockaddr *)g_send_ctx.sender_addr, sizeof(*g_send_ctx.sender_addr));
}
static ssize_t send_to_server_cb(uint8_t *buffer, int length) {
return send(g_send_ctx.server_sock, buffer, length, 0);
}
masking_handler_t * get_masking_handler_by_name(const char *name) {
if (!name) return NULL;
for (int i = 0; masking_handlers[i]; ++i) {
char handler_name_lower[MASKING_HANDLER_NAME_LEN];
strncpy(handler_name_lower, masking_handlers[i]->name, sizeof(handler_name_lower) - 1);
handler_name_lower[sizeof(handler_name_lower) - 1] = 0;
for (char *p = handler_name_lower; *p; ++p) *p = tolower((unsigned char)*p);
if (strcmp(name, handler_name_lower) == 0) {
return masking_handlers[i];
}
}
return NULL;
}
void masking_on_handshake_req_from_client(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_handshake_req) {
return;
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
client->masking_handler->on_handshake_req(config, client, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, send_to_server_cb);
}
void masking_on_handshake_req_from_server(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_handshake_req) {
return;
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
client->masking_handler->on_handshake_req(config, client, DIR_SERVER_TO_CLIENT, server_addr, client_addr, send_to_server_cb, send_to_client_cb);
}
int masking_unwrap_from_client(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client, // can be NULL!
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr,
masking_handler_t **masking_handler_out) {
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = client_addr;
g_send_ctx.server_sock = client ? client->server_sock : 0;
if (!client && !config->masking_handler_set) {
// Brueteforce detection of masking type if no client entry and no default masking handler
for (int i = 0; masking_handlers[i]; ++i) {
// A handler may move *buffer_ptr while unwrapping. If it does not match
// (returns < 0), restore the pointer so the next handler sees the original data.
uint8_t *saved_buffer = *buffer_ptr;
int r = masking_handlers[i]->on_data_unwrap(buffer_ptr, length, config, NULL, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, send_to_server_cb);
if (r < 0) {
*buffer_ptr = saved_buffer;
continue;
}
// Found a matching masking handler
log(LL_TRACE, "Autodetected masking handler for packet from %s:%d: %s", inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), masking_handlers[i]->name);
if (masking_handler_out) {
*masking_handler_out = masking_handlers[i];
}
return r;
}
// No matching masking handler found
if (masking_handler_out) {
*masking_handler_out = NULL;
}
return length;
}
masking_handler_t *handler = client ? client->masking_handler : config->masking_handler;
if (!handler || !handler->on_data_unwrap) {
return length; // no masking handler, nothing to do
}
return handler->on_data_unwrap(buffer_ptr, length, config, client, DIR_CLIENT_TO_SERVER, client_addr, server_addr, send_to_client_cb, client ? send_to_server_cb : NULL);
}
int masking_unwrap_from_server(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_data_unwrap) {
return length; // no masking handler, nothing to do
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
return client->masking_handler->on_data_unwrap(buffer_ptr, length, config, client, DIR_SERVER_TO_CLIENT, server_addr, &client->client_addr, send_to_server_cb, send_to_client_cb);
}
int masking_data_wrap_to_client(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_data_wrap) {
return length; // no masking handler, nothing to do
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
return client->masking_handler->on_data_wrap(buffer_ptr, length, config, client, DIR_SERVER_TO_CLIENT, server_addr, &client->client_addr, send_to_server_cb, send_to_client_cb);
}
int masking_data_wrap_to_server(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_data_wrap) {
return length; // no masking handler, nothing to do
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
return client->masking_handler->on_data_wrap(buffer_ptr, length, config, client, DIR_CLIENT_TO_SERVER, &client->client_addr, server_addr, send_to_client_cb, send_to_server_cb);
}
void masking_on_timer(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr) {
if (!client->masking_handler || !client->masking_handler->on_timer) {
return;
}
g_send_ctx.listen_sock = listen_sock;
g_send_ctx.sender_addr = &client->client_addr;
g_send_ctx.server_sock = client->server_sock;
client->masking_handler->on_timer(config, client, &client->client_addr, server_addr, send_to_client_cb, send_to_server_cb);
}

View File

@@ -0,0 +1,91 @@
#ifndef _MASKING_H_
#define _MASKING_H_
#include <stdint.h>
#include <netinet/in.h>
#include "wg-obfuscator.h"
#define MASKING_HANDLER_NAME_LEN 32
typedef ssize_t (*send_data_callback_t)(uint8_t *buffer, int length);
typedef void (*masking_event_handler_t)(obfuscator_config_t *config,
client_entry_t *client,
direction_t direction,
const struct sockaddr_in *src_addr,
const struct sockaddr_in *dest_addr,
send_data_callback_t send_back_callback,
send_data_callback_t send_forward_callback);
typedef int (*masking_data_handler_t)(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
direction_t direction,
const struct sockaddr_in *src_addr,
const struct sockaddr_in *dest_addr,
send_data_callback_t send_back_callback,
send_data_callback_t send_forward_callback);
typedef void (*masking_timer_handler_t)(obfuscator_config_t *config,
client_entry_t *client,
const struct sockaddr_in *client_addr,
const struct sockaddr_in *server_addr,
send_data_callback_t send_to_client_callback,
send_data_callback_t send_to_server_callback);
struct masking_handler {
char name[MASKING_HANDLER_NAME_LEN];
masking_event_handler_t on_handshake_req;
masking_data_handler_t on_data_wrap;
masking_data_handler_t on_data_unwrap;
masking_timer_handler_t on_timer;
uint32_t timer_interval_s;
};
typedef struct masking_handler masking_handler_t;
masking_handler_t * get_masking_handler_by_name(const char *name);
void masking_on_handshake_req_from_client(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr);
void masking_on_handshake_req_from_server(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr);
int masking_data_wrap_to_client(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr);
int masking_data_wrap_to_server(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr);
int masking_unwrap_from_client(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *client_addr,
struct sockaddr_in *server_addr,
masking_handler_t **masking_handler_out);
int masking_unwrap_from_server(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr);
void masking_on_timer(obfuscator_config_t *config,
client_entry_t *client,
int listen_sock,
struct sockaddr_in *server_addr);
#endif // _MASKING_H_

View File

@@ -0,0 +1,14 @@
#ifndef _MASKING_HANDLERS_H_
#define _MASKING_HANDLERS_H_
#include "masking.h"
/* List of available masking handlers */
#include "masking_stun.h"
static masking_handler_t * const masking_handlers[] = {
&stun_masking_handler,
NULL
};
#endif

View File

@@ -0,0 +1,298 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include "wg-obfuscator.h"
#include "masking.h"
#include "masking_stun.h"
// Bytes prepended in front of the payload when wrapping data in a STUN Data Indication
// (20-byte STUN header + 4-byte attribute header). Must fit into PREBUFFER_SIZE so that
// wrapping can be done by shifting the buffer pointer instead of moving the payload.
#define STUN_WRAP_OVERHEAD 24
_Static_assert(STUN_WRAP_OVERHEAD <= PREBUFFER_SIZE,
"PREBUFFER_SIZE is too small to hold the STUN wrap header");
static void rand_bytes(uint8_t* p, size_t n){ for(size_t i=0;i<n;i++) p[i]=rand()&0xFF; }
int stun_check_magic(const uint8_t *buf, size_t len) {
if (!buf || len < 8) return 0;
return !memcmp(buf + 4, COOKIE_BE, 4);
}
uint16_t stun_peek_type(const uint8_t *buf){
return (buf[0] << 8) | buf[1];
}
static int stun_write_header(uint8_t *b, uint16_t type, uint16_t mlen, const uint8_t txid[12]) {
b[0] = type >> 8;
b[1] = type & 0xFF;
b[2] = mlen >> 8;
b[3] = mlen & 0xFF;
memcpy(b + 4, COOKIE_BE, 4);
memcpy(b + 8, txid, 12);
return 20;
}
static int stun_attr_xor_mapped_addr(uint8_t *b, const struct sockaddr_in *src) {
// type,len
b[0] = STUN_ATTR_XORMAPPED >> 8;
b[1] = STUN_ATTR_XORMAPPED & 0xFF;
b[2] = 0;
b[3] = 8; // len=8 (family + port + addr)
b[4] = 0;
b[5] = 0x01; // family IPv4
memcpy(b + 6, &src->sin_port, 2); // network-order bytes
((uint8_t*)b)[6] ^= COOKIE_BE[0];
((uint8_t*)b)[7] ^= COOKIE_BE[1];
memcpy(b + 8, &src->sin_addr.s_addr, 4); // network-order bytes
((uint8_t*)b)[8] ^= COOKIE_BE[0];
((uint8_t*)b)[9] ^= COOKIE_BE[1];
((uint8_t*)b)[10] ^= COOKIE_BE[2];
((uint8_t*)b)[11] ^= COOKIE_BE[3];
return 12; // 4 hdr + 8 val
}
/*
static int stun_attr_software(uint8_t *b, const char *s) {
uint16_t n = (uint16_t)strlen(s);
uint16_t pad = (4 - (n & 3)) & 3;
if (b) {
b[0] = STUN_ATTR_SOFTWARE >> 8;
b[1] = STUN_ATTR_SOFTWARE&0xFF;
b[2] = n>>8;
b[3] = n&0xFF;
memcpy(b+4,s,n);
if (pad) memset(b+4+n,0,pad);
}
return 4 + n + pad;
}
*/
static uint32_t crc32(const uint8_t *p, size_t n) {
uint32_t crc = ~0u;
for(size_t i = 0; i < n; i++){
crc ^= p[i];
for(int k = 0; k < 8; k++)
crc = (crc >> 1) ^ (0xEDB88320u & (-(int)(crc & 1)));
}
return ~crc;
}
static int stun_attr_fingerprint(uint8_t *pkt, size_t cur_len) {
uint8_t *b = pkt + cur_len;
b[0] = STUN_ATTR_FINGERPR >> 8;
b[1] = STUN_ATTR_FINGERPR & 0xFF;
b[2] = 0;
b[3] = 4;
uint32_t fp = htonl(crc32(pkt, cur_len) ^ 0x5354554eu);
memcpy(b + 4, &fp, 4);
return 8; // 4 hdr + 4 val
}
static int stun_build_binding_request(uint8_t *out) {
uint8_t txid[12];
rand_bytes(txid, 12);
stun_write_header(out, STUN_BINDING_REQ, 0, txid);
size_t mlen = 0;
// optional SOFTWARE
//mlen += stun_attr_software(out+20+mlen, "wgo/1.0");
mlen += stun_attr_fingerprint(out, 20 + mlen);
out[2] = (mlen >> 8) & 0xFF;
out[3] = mlen & 0xFF;
return (int)(20 + mlen);
}
static int stun_build_binding_success(uint8_t *out,
const uint8_t txid[12],
const struct sockaddr_in *src) {
if (BUFFER_SIZE < 20 + 12 + 8) return -1; // header + XOR-MAPPED-ADDRESS + fingerprint
stun_write_header(out, STUN_BINDING_RESP, 0, (uint8_t*)txid);
size_t mlen = 0;
mlen += stun_attr_xor_mapped_addr(out + 20 + mlen, src);
// optional SOFTWARE
//mlen += stun_attr_software(out+20+mlen, "wgo/1.0");
mlen += stun_attr_fingerprint(out, 20 + mlen);
out[2] = (mlen >> 8) & 0xFF;
out[3] = mlen & 0xFF;
return (int)(20 + mlen);
}
static int stun_wrap(uint8_t **buf_ptr, size_t data_len) {
size_t mlen = 0;
// The caller guarantees the payload already fits into the main buffer (it was
// received/encoded within BUFFER_SIZE), so the wrapped packet cannot overflow
// the tail of full_buffer. The attribute length field is also 16-bit only.
// Note: BUFFER_SIZE (65535) equals the maximum value of a 16-bit unsigned
// integer (0xFFFF), so this single check covers both the buffer limit and
// the STUN attribute length field limit.
if (data_len > BUFFER_SIZE) {
log(LL_WARN, "Can't wrap data in STUN, data too large (%zu bytes)", data_len);
return -ENOMEM;
}
// Header is prepended into the prebuffer (checked at compile time via _Static_assert).
*buf_ptr -= STUN_WRAP_OVERHEAD;
uint8_t *buf = *buf_ptr;
uint8_t txid[12];
rand_bytes(txid,12);
mlen += stun_write_header(buf, STUN_TYPE_DATA_IND, 0, txid);
buf[mlen] = STUN_ATTR_DATA >> 8;
buf[mlen + 1] = STUN_ATTR_DATA & 0xFF;
buf[mlen + 2] = data_len >> 8;
buf[mlen + 3] = data_len & 0xFF;
return STUN_WRAP_OVERHEAD + data_len;
}
static int stun_unwrap(uint8_t **buf_ptr, size_t len) {
uint8_t *buf = *buf_ptr;
if (len < 20 + 4) return -1; // header + attr
uint16_t msg_type = (buf[0] << 8) | buf[1];
if (msg_type != STUN_TYPE_DATA_IND) return -1;
uint16_t msg_len = (buf[2] << 8) | buf[3];
if (msg_len + 20 > len) return -1;
uint16_t attr_type = (buf[20] << 8) | buf[21];
if (attr_type != STUN_ATTR_DATA) return -1;
uint16_t data_len = (buf[22] << 8) | buf[23];
if (data_len + 24 > len) return -1;
// Move the buffer pointer to avoid moving the data
*buf_ptr += 24;
return data_len;
}
static void stun_on_handshake_req(obfuscator_config_t *config,
client_entry_t *client,
direction_t direction,
const struct sockaddr_in *src_addr,
const struct sockaddr_in *dest_addr,
send_data_callback_t send_back_callback,
send_data_callback_t send_forward_callback) {
uint8_t buffer[128];
int len = stun_build_binding_request(buffer);
if (len < 0) return;
int sent = send_forward_callback(buffer, len);
if (sent < 0) {
serror("Can't send STUN binding request to %s:%d", inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port));
} else if (sent != len) {
log(LL_WARN, "Partial send of STUN binding request to %s:%d (%d of %d bytes)", inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port), sent, len);
} else {
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(dest_addr->sin_addr), ntohs(dest_addr->sin_port));
}
}
static int stun_on_data_unwrap(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
direction_t direction,
const struct sockaddr_in *src_addr,
const struct sockaddr_in *dest_addr,
send_data_callback_t send_back_callback,
send_data_callback_t send_forward_callback) {
if (!stun_check_magic(*buffer_ptr, length)) {
return -EINVAL;
}
uint16_t stun_type = stun_peek_type(*buffer_ptr);
switch (stun_type) {
case STUN_BINDING_REQ: {
// Received STUN Binding Request from client, send Binding Success Response
log(LL_TRACE, "Received STUN Binding Request from %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
uint8_t txid[12];
memcpy(txid, (*buffer_ptr) + 8, 12);
int resp_len = stun_build_binding_success(*buffer_ptr, txid, src_addr);
if (resp_len > 0) {
int sent = send_back_callback(*buffer_ptr, resp_len);
if (sent < 0) {
serror("sendto STUN response to %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
} else if (sent != resp_len) {
log(LL_WARN, "Partial send of STUN Binding Success Response to %s:%d (%d of %d bytes)", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port), sent, resp_len);
} else {
log(LL_TRACE, "Sent STUN Binding Success Response (%d bytes) to %s:%d", resp_len, inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
}
} else {
log(LL_ERROR, "Failed to build STUN Binding Success Response");
}
return 0;
}
case STUN_BINDING_RESP:
log(LL_TRACE, "Received STUN Binding Success Response from %s:%d, ignoring", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
return 0;
case STUN_TYPE_DATA_IND:
length = stun_unwrap(buffer_ptr, length);
if (length < 0) {
log(LL_DEBUG, "Failed to unwrap STUN Data Indication from %s:%d", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
return length;
}
log(LL_TRACE, "Unwrapped STUN Data Indication from %s:%d (%d bytes)", inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port), length);
return length;
default:
log(LL_DEBUG, "Received unknown STUN type %04X from %s:%d, ignoring", stun_type, inet_ntoa(src_addr->sin_addr), ntohs(src_addr->sin_port));
return 0;
}
}
static int stun_on_data_wrap(uint8_t **buffer_ptr, int length,
obfuscator_config_t *config,
client_entry_t *client,
direction_t direction,
const struct sockaddr_in *src_addr,
const struct sockaddr_in *dest_addr,
send_data_callback_t send_back_callback,
send_data_callback_t send_forward_callback) {
return stun_wrap(buffer_ptr, length);
}
static void stun_on_timer(obfuscator_config_t *config,
client_entry_t *client,
const struct sockaddr_in *client_addr,
const struct sockaddr_in *server_addr,
send_data_callback_t send_to_client_callback,
send_data_callback_t send_to_server_callback) {
uint8_t buffer[128];
int len = stun_build_binding_request(buffer);
if (len < 0) return;
if (client->client_obfuscated) {
int sent = send_to_client_callback(buffer, len);
if (sent < 0) {
serror("STUN binding request to client");
} else if (sent != len) {
log(LL_WARN, "Partial send of STUN binding request to client (%d of %d bytes)", sent, len);
} else {
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port));
}
}
if (client->server_obfuscated) {
int sent = send_to_server_callback(buffer, len);
if (sent < 0) {
serror("STUN binding request to server");
} else if (sent != len) {
log(LL_WARN, "Partial send of STUN binding request to server (%d of %d bytes)", sent, len);
} else {
log(LL_TRACE, "Sent STUN binding request (%d bytes) to %s:%d", len, inet_ntoa(server_addr->sin_addr), ntohs(server_addr->sin_port));
}
}
}
masking_handler_t stun_masking_handler = {
.name = "STUN",
.on_handshake_req = stun_on_handshake_req,
.on_data_wrap = stun_on_data_wrap,
.on_data_unwrap = stun_on_data_unwrap,
.on_timer = stun_on_timer,
.timer_interval_s = 10, // 10 seconds
};

View File

@@ -0,0 +1,19 @@
#ifndef _MASKING_STUN_H_
#define _MASKING_STUN_H_
#include <stdint.h>
#include <netinet/in.h>
#include "wg-obfuscator.h"
static const uint8_t COOKIE_BE[4] = {0x21,0x12,0xA4,0x42};
#define STUN_TYPE_DATA_IND 0x0115
#define STUN_BINDING_REQ 0x0001
#define STUN_BINDING_RESP 0x0101
#define STUN_ATTR_XORMAPPED 0x0020
#define STUN_ATTR_SOFTWARE 0x8022
#define STUN_ATTR_FINGERPR 0x8028
#define STUN_ATTR_DATA 0x0013
extern masking_handler_t stun_masking_handler;
#endif // _MASKING_STUN_H_

View File

@@ -0,0 +1,82 @@
/* mini_argp.h
* Tiny callback-based CLI parser */
#ifndef _MINI_ARGP_H_
#define _MINI_ARGP_H_
#include <stdio.h>
#include <string.h>
typedef int (*mini_argp_cb)(const char *lname, char sname,
const char *val, void *ctx);
typedef struct {
const char *long_name; /* "--target" or NULL */
char short_name; /* 't' or 0 */
int has_arg; /* 0 flag, 1 needs val */
mini_argp_cb cb; /* your handler */
} mini_argp_opt;
/* internal: find desc by name */
static const mini_argp_opt *
margp_find(const mini_argp_opt *o, char *lname, char sname)
{
for (; o->long_name || o->short_name; o++)
if ((lname && o->long_name && !strcmp(lname,o->long_name)) ||
(sname && o->short_name==sname))
return o;
return NULL;
}
static int mini_argp_parse(int argc, char **argv,
const mini_argp_opt *opts,
void *ctx,
mini_argp_cb cb)
{
for (int i = 1; i < argc; ++i) {
char *arg = argv[i];
/* long option: --foo or --foo=bar */
if (!strncmp(arg, "--", 2)) {
char *name = arg + 2, *val = NULL;
if ((val = strchr(name, '='))) { *val = 0; ++val; }
const mini_argp_opt *o = margp_find(opts, name, 0);
if (!o) { fprintf(stderr,"unknown --%s\n", name); return -1; }
if (o->has_arg && !val) {
if (++i == argc) { fprintf(stderr,"--%s needs value\n",name);return -1; }
val = argv[i];
}
if (!o->has_arg) val = NULL;
mini_argp_cb to_call = o->cb ? o->cb : cb;
if (to_call(o->long_name, o->short_name, val, ctx)) return -1;
}
/* short(s): -a, -fVAL, -xzvf */
else if (arg[0]=='-' && arg[1]) {
for (size_t pos=1; arg[pos]; ++pos) {
char c = arg[pos];
const mini_argp_opt *o = margp_find(opts,NULL,c);
if (!o){ fprintf(stderr,"unknown -%c\n",c); return -1;}
const char *val = NULL;
if (o->has_arg) {
if (arg[pos+1]) val = &arg[pos+1]; /* -fVAL */
else if (++i < argc) val = argv[i]; /* -f VAL */
else { fprintf(stderr,"-%c needs value\n",c); return -1;}
/* Stop further char-scanning: the rest of the arg is the value */
pos = strlen(arg)-1;
}
mini_argp_cb to_call = o->cb ? o->cb : cb;
if (to_call(o->long_name, o->short_name, val, ctx)) return -1;
}
}
/* positional not allowed */
else {
fprintf(stderr,"unexpected arg: %s\n",arg); return -1;
}
}
return 0;
}
#endif/* _MINI_ARGP_H_ */

View File

@@ -0,0 +1,130 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "wg-obfuscator.h"
#include "obfuscation.h"
static uint8_t crc_a[256]; // step(crc, 0): contribution of the current CRC state
static uint8_t crc_b[256]; // step(0, inbyte): contribution of the input byte
static uint8_t tables_ready = 0;
typedef struct {
uint8_t *data; // cached keystream bytes, NULL until first allocation
int valid; // number of valid bytes in data
int cap; // allocated capacity of data
uint8_t crc; // CRC state right after the 'valid' bytes
} keystream_row_t;
static keystream_row_t rows[256];
// Single step of the original bit-by-bit CRC8 update, kept only to build the tables.
static uint8_t crc8_step(uint8_t crc, uint8_t inbyte) {
for (uint8_t j = 0; j < 8; j++) {
uint8_t mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix) {
crc ^= 0x8C;
}
inbyte >>= 1;
}
return crc;
}
static void ensure_tables(void) {
if (tables_ready) {
return;
}
for (int i = 0; i < 256; i++) {
crc_a[i] = crc8_step((uint8_t)i, 0);
crc_b[i] = crc8_step(0, (uint8_t)i);
}
tables_ready = 1;
}
// Ensure the given row holds at least 'need' valid keystream bytes (capped at
// KEYSTREAM_ROW_MAX). On allocation failure the row is left as large as it could
// grow; the caller then computes the missing tail on the fly.
static void grow_row(keystream_row_t *row, int cls, int need, const char *key, int key_length) {
if (need > KEYSTREAM_ROW_MAX) {
need = KEYSTREAM_ROW_MAX;
}
if (row->valid >= need) {
return;
}
if (row->cap < need) {
int newcap = (need + 255) & ~255; // round up to a 256-byte chunk
if (newcap > KEYSTREAM_ROW_MAX) {
newcap = KEYSTREAM_ROW_MAX;
}
uint8_t *p = realloc(row->data, newcap);
if (!p) {
return; // keep whatever we already have; tail handled by caller
}
row->data = p;
row->cap = newcap;
}
uint8_t crc = row->crc;
int ki = row->valid % key_length;
for (int i = row->valid; i < need; i++) {
uint8_t inbyte = (uint8_t)(key[ki] + cls + key_length);
crc = crc_a[crc] ^ crc_b[inbyte];
row->data[i] = crc;
if (++ki == key_length) {
ki = 0;
}
}
row->valid = need;
row->crc = crc;
}
// XOR 'n' keystream bytes into dst, 8 bytes at a time. memcpy avoids both
// strict-aliasing violations and unaligned-access assumptions.
static void xor_block(uint8_t *dst, const uint8_t *ks, int n) {
int i = 0;
for (; i + 8 <= n; i += 8) {
uint64_t a, b;
memcpy(&a, dst + i, 8);
memcpy(&b, ks + i, 8);
a ^= b;
memcpy(dst + i, &a, 8);
}
for (; i < n; i++) {
dst[i] ^= ks[i];
}
}
void xor_data(uint8_t *buffer, int length, char *key, int key_length) {
if (length <= 0) {
return;
}
ensure_tables();
int cls = length & 0xFF;
keystream_row_t *row = &rows[cls];
int cached = (length <= KEYSTREAM_ROW_MAX) ? length : KEYSTREAM_ROW_MAX;
if (row->valid < cached) {
grow_row(row, cls, cached, key, key_length);
}
int n = (length < row->valid) ? length : row->valid;
if (n > 0) {
xor_block(buffer, row->data, n);
}
// Tail beyond the cache limit (or beyond what we could allocate): compute on
// the fly, continuing the CRC chain from the last cached state.
if (n < length) {
uint8_t crc = row->crc;
int ki = n % key_length;
for (int i = n; i < length; i++) {
uint8_t inbyte = (uint8_t)(key[ki] + cls + key_length);
crc = crc_a[crc] ^ crc_b[inbyte];
buffer[i] ^= crc;
if (++ki == key_length) {
ki = 0;
}
}
}
}

View File

@@ -0,0 +1,139 @@
#ifndef _OBFUSCATION_H_
#define _OBFUSCATION_H_
#include <stdint.h>
// Current obfuscation version
#define OBFUSCATION_VERSION 1
// Maximum length (in bytes) of a single cached keystream row.
// The keystream depends only on (key, length mod 256), so it is cached in 256
// lazily-grown rows. Packets longer than this limit are still handled correctly:
// the part beyond the limit is computed on the fly. Worst-case cache memory is
// 256 * KEYSTREAM_ROW_MAX bytes. 2048 covers a typical MTU plus masking overhead.
#define KEYSTREAM_ROW_MAX 2048
// WireGuard packet types
#define WG_TYPE_HANDSHAKE 0x01
#define WG_TYPE_HANDSHAKE_RESP 0x02
#define WG_TYPE_COOKIE 0x03
#define WG_TYPE_DATA 0x04
#define WG_TYPE(data) ((uint32_t)(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24)))
#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
/**
* Checks if the given data is obfuscated.
*
* @param data Pointer to the data buffer to check.
* @return uint8_t Returns a non-zero value if the data is obfuscated, 0 otherwise.
*/
static inline uint8_t is_obfuscated(uint8_t *data) {
uint32_t packet_type = WG_TYPE(data);
return !(packet_type >= 1 && packet_type <= 4);
}
/**
* @brief XORs the data in the given buffer with the key-derived keystream.
*
* The keystream is a per-byte CRC8 value derived from the key, the byte position
* and the packet length; it depends only on (key, length mod 256), so it is
* cached (see obfuscation.c). The on-the-wire result is byte-for-byte identical
* to the original bit-by-bit implementation.
*
* @param buffer Pointer to the data buffer to be XORed.
* @param length Length of the data buffer in bytes.
* @param key Pointer to the key used for XOR operation.
* @param key_length Length of the key in bytes.
*/
void xor_data(uint8_t *buffer, int length, char *key, int key_length);
/**
* @brief Encodes the given buffer using the specified key and version.
*
* This function applies an encoding algorithm to the input buffer using the provided key and version.
* WARNING: buffer must be at least 4 bytes long and aligned to 4 bytes.
*
* @param buffer Pointer to the data buffer to encode.
* @param length Length of the data buffer in bytes.
* @param key Pointer to the key used for encoding.
* @param key_length Length of the key in bytes.
* @param version Encoding version to use.
* @param max_dummy_length_data Maximum length of dummy data for data packets.
* @return 0 on success, or a negative value on error.
*/
static inline int encode(uint8_t *buffer, int length, char *key, int key_length, uint8_t version, int max_dummy_length_data) {
if (version >= 1) {
uint32_t packet_type = WG_TYPE(buffer);
// Add some randomness to the packet
uint8_t rnd = 1 + (rand() % 255);
buffer[0] ^= rnd; // Xor the first byte to a random value
buffer[1] = rnd; // Set the second byte to a random value
// Add dummy data to the packet
if (length < MAX_DUMMY_LENGTH_TOTAL) {
uint16_t dummy_length = 0;
uint16_t max_dummy_length = MAX_DUMMY_LENGTH_TOTAL - length;
switch (packet_type) {
case WG_TYPE_HANDSHAKE:
case WG_TYPE_HANDSHAKE_RESP:
// length to MAX_DUMMY_LENGTH_HANDSHAKE
dummy_length = rand() % MIN(max_dummy_length, MAX_DUMMY_LENGTH_HANDSHAKE);
break;
case WG_TYPE_COOKIE:
case WG_TYPE_DATA:
// length to MAX_DUMMY_LENGTH_HANDSHAKE
if (max_dummy_length_data) {
dummy_length = rand() % MIN(max_dummy_length, max_dummy_length_data);
}
break;
default:
//assert(0);
break;
}
buffer[2] = dummy_length & 0xFF; // Set the dummy length in the packet
buffer[3] = dummy_length >> 8; // Set the dummy length in
if (dummy_length > 0) {
int i = length;
length += dummy_length;
for (; i < length; ++i) {
buffer[i] = 0xFF; // Fill with FFs, random data is not needed
}
}
}
}
xor_data(buffer, length, key, key_length);
return length;
}
/**
* Decodes the given buffer using the provided key.
*
* WARNING: buffer must be at least 4 bytes long and aligned to 4 bytes.
*
* @param buffer Pointer to the input buffer to decode.
* @param length Length of the input buffer.
* @param key Pointer to the key used for decoding.
* @param key_length Length of the key.
* @param version_out Pointer to a variable where the decoded version will be stored.
* @return Length of the decoded data (smaller than or equal to the input length).
*/
static inline int decode(uint8_t *buffer, int length, char *key, int key_length, uint8_t *version_out) {
xor_data(buffer, length, key, key_length);
if (!is_obfuscated(buffer)) {
// Looks like an old version
*version_out = 0;
return length;
}
buffer[0] ^= buffer[1]; // Restore the first byte by XORing it with the second byte
length -= (uint16_t)(buffer[2] | (buffer[3] << 8)); // Remove dummy data length from the packet
buffer[1] = buffer[2] = buffer[3] = 0; // Reset the dummy length field to 0
return length;
}
#endif // _OBFUSCATION_H_

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,135 @@
#ifndef _WG_OBFUSCATOR_H_
#define _WG_OBFUSCATOR_H_
#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <sys/types.h>
#include "uthash.h"
// on Linux, use epoll for better performance
#ifdef __linux__
#define USE_EPOLL
#endif
#ifdef USE_EPOLL
#include <sys/epoll.h>
#define MAX_EVENTS 1024
#else
#include <poll.h>
#endif
#define WG_OBFUSCATOR_VERSION "1.6"
#define WG_OBFUSCATOR_GIT_REPO "https://github.com/ClusterM/wg-obfuscator"
#define LL_DEFAULT LL_INFO // default logging level
// Main parameters
// TODO: make these configurable via command line arguments or config file
#define BUFFER_SIZE 65535 // size of the buffer for receiving data from the clients and server
#define PREBUFFER_SIZE 1024 // size of the additional buffer size before the main buffer
#define POLL_TIMEOUT 5000 // in milliseconds
#define HANDSHAKE_TIMEOUT 5000 // in milliseconds
#define ITERATE_INTERVAL 1000 // in milliseconds
#define MAX_DUMMY_LENGTH_TOTAL 1024 // maximum length of a packet after dummy data extension
#define MAX_DUMMY_LENGTH_HANDSHAKE 512 // maximum length of dummy data for handshake packets
#define MAX_CLIENTS_DEFAULT 1024 // maximum number of clients
#define IDLE_TIMEOUT_DEFAULT 300000 // in milliseconds
#define IN_TIMEOUT_DEFAULT 0 // in milliseconds
#define MAX_DUMMY_LENGTH_DATA_DEFAULT 4 // maximum length of dummy data for data packets
// Default instance name
#define DEFAULT_INSTANCE_NAME "main"
// Logging levels
#define LL_ERROR 0
#define LL_WARN 1
#define LL_INFO 2
#define LL_DEBUG 3
#define LL_TRACE 4
// Maximum length of a single log line, longer hex dumps are split into several lines
#define LOG_LINE_MAX 16384
#define log(level, fmt, ...) do { if (verbose >= (level)) log_printf((level), fmt, ##__VA_ARGS__); } while (0)
#define serror_level(level, fmt, ...) log(level, fmt " - %s (%d)", ##__VA_ARGS__, strerror(errno), errno)
#define serror(fmt, ...) serror_level(LL_ERROR, fmt, ##__VA_ARGS__)
// Direction of... something
typedef enum {
DIR_CLIENT_TO_SERVER = 0,
DIR_SERVER_TO_CLIENT = 1,
} direction_t;
struct masking_handler; // forward declaration
typedef struct masking_handler masking_handler_t;
// Structure to hold obfuscator configuration
typedef struct {
int listen_port; // Listening port for the obfuscator
char forward_host_port[256]; // Host and port to forward the data to
char xor_key[256]; // Key for obfuscation
char client_interface[256]; // Client interface as a string
char *static_bindings; // Static bindings as a string
int max_clients; // Maximum number of clients
long idle_timeout; // Idle timeout in milliseconds
long in_timeout; // Incoming timeout in milliseconds
int max_dummy_length_data; // Maximum length of dummy data for data packets
uint32_t fwmark; // Firewall mark
masking_handler_t *masking_handler; // Masking handler to use
uint8_t allow_clean; // 1 if non-obfuscated (clean) clients are allowed, 0 otherwise
char log_file[512]; // Path of the log file
int8_t log_timestamps; // 1 to force timestamps on, 0 to force them off, -1 for auto
long resolve_interval; // Hostname re-resolve interval in milliseconds, 0 to disable periodic refresh
uint8_t log_file_set; // 1 if the log file is set, 0 otherwise
uint8_t listen_port_set; // 1 if the listen port is set, 0 otherwise
uint8_t forward_host_port_set; // 1 if the forward host and port are set, 0 otherwise
uint8_t xor_key_set; // 1 if the XOR key is set, 0 otherwise
uint8_t client_interface_set; // 1 if the client interface is set, 0 otherwise
uint8_t static_bindings_set; // 1 if the static bindings are set, 0 otherwise
uint8_t masking_handler_set; // 1 if the masking handler is set, 0 otherwise
} obfuscator_config_t;
// Structure to hold client connection information
typedef struct {
struct sockaddr_in client_addr; // client address and port (key for the hash table)
struct sockaddr_in our_addr; // our address and port on the server connection
long last_activity_time; // last time we received data from/to this client
long last_incoming_time; // last time we received data from the remote server
long last_handshake_request_time; // last time we received a handshake request from/to this client
long last_handshake_time; // last time we received a handshake response from/to this client
long last_masking_timer_time; // last time we called the masking timer handler for this client
int server_sock; // socket for the connection to the server
uint8_t version; // obfuscation version
masking_handler_t *masking_handler; // masking handler in use
uint8_t handshaked : 1; // 1 if the handshake is complete, 0 otherwise
uint8_t handshake_direction : 1; // 1 if the handshake is from client to server, 0 if from server to client
uint8_t client_obfuscated : 1; // 1 if the client is obfuscated, 0 otherwise
uint8_t server_obfuscated : 1; // 1 if the server is obfuscated, 0 otherwise
uint8_t client_clean : 1; // 1 if the client speaks plain (non-obfuscated) WireGuard, traffic is passed through as is (allow-clean mode)
uint8_t is_static : 1; // 1 if this is a static binding entry, 0 otherwise
char bind_host[256]; // Original hostname of a static binding, empty if the address is a literal or the entry is dynamic
UT_hash_handle hh;
} client_entry_t;
// Verbosity level
extern int verbose;
// Section name (for multiple instances)
extern char section_name[256];
const char *version_string(void);
void print_version(void);
void register_child_instance(pid_t pid);
void log_init(const char *path, int8_t timestamps_mode);
void log_reopen(void);
#ifdef __GNUC__
void log_printf(int level, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
#else
void log_printf(int level, const char *fmt, ...);
#endif
void log_hexdump(int level, const char *prefix, const uint8_t *data, int length);
#endif

118
ios/Tunnel/pcarelay.c Normal file
View File

@@ -0,0 +1,118 @@
/*
* pcarelay — in-process transport switch for the iOS packet tunnel.
* wg-obfuscator (one instance, iOS forbids fork) sends to 127.0.0.1:<local>;
* we forward either over UDP to the server's obfuscator port or over TCP to
* the server's udptcp (frames: 2-byte length + packet). Switch with pcarelay_use_tcp().
*/
#include "pcarelay.h"
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
static atomic_int use_tcp = 0;
static atomic_ullong rx_bytes = 0;
static char g_host[256], g_udp_port[8], g_tcp_port[8];
static int g_local_port;
static int dial(int socktype, const char *port) {
struct addrinfo hints = {0}, *res, *r;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;
if (getaddrinfo(g_host, port, &hints, &res) != 0) return -1;
int s = -1;
for (r = res; r; r = r->ai_next) {
s = socket(r->ai_family, r->ai_socktype, 0);
if (s < 0) continue;
if (connect(s, r->ai_addr, r->ai_addrlen) == 0) break;
close(s); s = -1;
}
freeaddrinfo(res);
if (s >= 0 && socktype == SOCK_STREAM) {
int one = 1;
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);
setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
}
return s;
}
static int read_full(int fd, unsigned char *b, size_t n) {
size_t got = 0;
while (got < n) { ssize_t r = read(fd, b + got, n - got); if (r <= 0) return -1; got += (size_t)r; }
return 0;
}
static void *loop(void *arg) {
(void)arg;
struct sockaddr_in la = {0};
la.sin_family = AF_INET;
la.sin_port = htons((uint16_t)g_local_port);
la.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int local = socket(AF_INET, SOCK_DGRAM, 0);
if (local < 0 || bind(local, (struct sockaddr *)&la, sizeof la) < 0) return NULL;
int udp = -1, tcp = -1;
struct sockaddr_storage peer; socklen_t pl = 0;
unsigned char buf[65537];
time_t last_dial = 0;
for (;;) {
int want_tcp = atomic_load(&use_tcp);
if (!want_tcp && tcp >= 0) { close(tcp); tcp = -1; }
if (want_tcp && udp >= 0) { close(udp); udp = -1; }
if (!want_tcp && udp < 0 && time(NULL) != last_dial) { last_dial = time(NULL); udp = dial(SOCK_DGRAM, g_udp_port); }
int up = want_tcp ? tcp : udp;
struct pollfd p[2] = {{local, POLLIN, 0}, {up, POLLIN, 0}};
poll(p, up >= 0 ? 2 : 1, 1000);
if (p[0].revents & POLLIN) {
pl = sizeof peer;
ssize_t n = recvfrom(local, buf, 65535, 0, (struct sockaddr *)&peer, &pl);
if (n <= 0) continue;
if (want_tcp) {
if (tcp < 0 && time(NULL) != last_dial) { last_dial = time(NULL); tcp = dial(SOCK_STREAM, g_tcp_port); }
if (tcp >= 0) {
unsigned char h[2] = {(unsigned char)(n >> 8), (unsigned char)n};
if (send(tcp, h, 2, 0) != 2 || send(tcp, buf, (size_t)n, 0) != n) { close(tcp); tcp = -1; }
}
} else if (udp >= 0) {
send(udp, buf, (size_t)n, 0);
}
}
if (up >= 0 && (p[1].revents & (POLLIN | POLLHUP | POLLERR))) {
ssize_t n;
if (want_tcp) {
unsigned char h[2];
if (read_full(tcp, h, 2) < 0) { close(tcp); tcp = -1; continue; }
n = (h[0] << 8) | h[1];
if (n == 0 || read_full(tcp, buf, (size_t)n) < 0) { close(tcp); tcp = -1; continue; }
} else {
n = recv(udp, buf, 65535, 0);
if (n <= 0) { close(udp); udp = -1; continue; }
}
atomic_fetch_add(&rx_bytes, (unsigned long long)n);
if (pl) sendto(local, buf, (size_t)n, 0, (struct sockaddr *)&peer, pl);
}
}
}
int pcarelay_start(int local_port, const char *host, int udp_port, int tcp_port) {
g_local_port = local_port;
snprintf(g_host, sizeof g_host, "%s", host);
snprintf(g_udp_port, sizeof g_udp_port, "%d", udp_port);
snprintf(g_tcp_port, sizeof g_tcp_port, "%d", tcp_port);
pthread_t t;
if (pthread_create(&t, NULL, loop, NULL) != 0) return -1;
pthread_detach(t);
return 0;
}
void pcarelay_use_tcp(int on) { atomic_store(&use_tcp, on ? 1 : 0); }
int pcarelay_is_tcp(void) { return atomic_load(&use_tcp); }
unsigned long long pcarelay_rx_bytes(void) { return atomic_load(&rx_bytes); }

14
ios/Tunnel/pcarelay.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef PCARELAY_H
#define PCARELAY_H
/* Start the transport switch thread: obfuscator -> 127.0.0.1:local_port -> host via UDP or TCP. */
int pcarelay_start(int local_port, const char *host, int udp_port, int tcp_port);
void pcarelay_use_tcp(int on);
int pcarelay_is_tcp(void);
/* Bytes received back from the server on any path (auto-detects a dead UDP). */
unsigned long long pcarelay_rx_bytes(void);
/* wg-obfuscator compiled in with -Dmain=wgobf_main (one instance per process). */
int wgobf_main(int argc, char **argv);
#endif

2
ios/Tunnel/ru_bypass.txt Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

19
ios/WireGuardKit/COPYING Normal file
View File

@@ -0,0 +1,19 @@
Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,15 @@
// swift-tools-version:5.5
// Vendored WireGuardKit (github.com/WireGuard/wireguard-apple, MIT). libwg-go.a is
// prebuilt in ../WireGuardGo/<sdk>/ — the app target adds it to LIBRARY_SEARCH_PATHS.
import PackageDescription
let package = Package(
name: "WireGuardKit",
platforms: [.iOS(.v15)],
products: [.library(name: "WireGuardKit", targets: ["WireGuardKit"])],
targets: [
.target(name: "WireGuardKit", dependencies: ["WireGuardKitGo", "WireGuardKitC"]),
.target(name: "WireGuardKitC", publicHeadersPath: "."),
.target(name: "WireGuardKitGo", publicHeadersPath: ".", linkerSettings: [.linkedLibrary("wg-go")]),
]
)

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
extension Array {
/// Returns an array containing the results of mapping the given closure over the sequence’s
/// elements concurrently.
///
/// - Parameters:
/// - queue: The queue for performing concurrent computations.
/// If the given queue is serial, the values are mapped in a serial fashion.
/// Pass `nil` to perform computations on the current queue.
/// - transform: the block to perform concurrent computations over the given element.
/// - Returns: an array of concurrently computed values.
func concurrentMap<U>(queue: DispatchQueue?, _ transform: (Element) -> U) -> [U] {
var result = [U?](repeating: nil, count: self.count)
let resultQueue = DispatchQueue(label: "ConcurrentMapQueue")
let execute = queue?.sync ?? { $0() }
execute {
DispatchQueue.concurrentPerform(iterations: self.count) { index in
let value = transform(self[index])
resultQueue.sync {
result[index] = value
}
}
}
return result.map { $0! }
}
}

View File

@@ -0,0 +1,153 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Network
import Foundation
enum DNSResolver {}
extension DNSResolver {
/// Concurrent queue used for DNS resolutions
private static let resolverQueue = DispatchQueue(label: "DNSResolverQueue", qos: .default, attributes: .concurrent)
static func resolveSync(endpoints: [Endpoint?]) -> [Result<Endpoint, DNSResolutionError>?] {
let isAllEndpointsAlreadyResolved = endpoints.allSatisfy { maybeEndpoint -> Bool in
return maybeEndpoint?.hasHostAsIPAddress() ?? true
}
if isAllEndpointsAlreadyResolved {
return endpoints.map { endpoint in
return endpoint.map { .success($0) }
}
}
return endpoints.concurrentMap(queue: resolverQueue) { endpoint -> Result<Endpoint, DNSResolutionError>? in
guard let endpoint = endpoint else { return nil }
if endpoint.hasHostAsIPAddress() {
return .success(endpoint)
} else {
return Result { try DNSResolver.resolveSync(endpoint: endpoint) }
.mapError { error -> DNSResolutionError in
// swiftlint:disable:next force_cast
return error as! DNSResolutionError
}
}
}
}
private static func resolveSync(endpoint: Endpoint) throws -> Endpoint {
guard case .name(let name, _) = endpoint.host else {
return endpoint
}
var hints = addrinfo()
hints.ai_flags = AI_ALL // We set this to ALL so that we get v4 addresses even on DNS64 networks
hints.ai_family = AF_UNSPEC
hints.ai_socktype = SOCK_DGRAM
hints.ai_protocol = IPPROTO_UDP
var resultPointer: UnsafeMutablePointer<addrinfo>?
defer {
resultPointer.flatMap { freeaddrinfo($0) }
}
let errorCode = getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer)
if errorCode != 0 {
throw DNSResolutionError(errorCode: errorCode, address: name)
}
var ipv4Address: IPv4Address?
var ipv6Address: IPv6Address?
var next: UnsafeMutablePointer<addrinfo>? = resultPointer
let iterator = AnyIterator { () -> addrinfo? in
let result = next?.pointee
next = result?.ai_next
return result
}
for addrInfo in iterator {
if let maybeIpv4Address = IPv4Address(addrInfo: addrInfo) {
ipv4Address = maybeIpv4Address
break // If we found an IPv4 address, we can stop
} else if let maybeIpv6Address = IPv6Address(addrInfo: addrInfo) {
ipv6Address = maybeIpv6Address
continue // If we already have an IPv6 address, we can skip this one
}
}
// We prefer an IPv4 address over an IPv6 address
if let ipv4Address = ipv4Address {
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6Address = ipv6Address {
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
// Must never happen
fatalError()
}
}
}
extension Endpoint {
func withReresolvedIP() throws -> Endpoint {
#if os(iOS)
let hostname: String
switch host {
case .name(let name, _):
hostname = name
case .ipv4(let address):
hostname = "\(address)"
case .ipv6(let address):
hostname = "\(address)"
@unknown default:
fatalError()
}
var hints = addrinfo()
hints.ai_family = AF_UNSPEC
hints.ai_socktype = SOCK_DGRAM
hints.ai_protocol = IPPROTO_UDP
hints.ai_flags = 0 // We set this to zero so that we actually resolve this using DNS64
var result: UnsafeMutablePointer<addrinfo>?
defer {
result.flatMap { freeaddrinfo($0) }
}
let errorCode = getaddrinfo(hostname, "\(self.port)", &hints, &result)
if errorCode != 0 {
throw DNSResolutionError(errorCode: errorCode, address: hostname)
}
let addrInfo = result!.pointee
if let ipv4Address = IPv4Address(addrInfo: addrInfo) {
return Endpoint(host: .ipv4(ipv4Address), port: port)
} else if let ipv6Address = IPv6Address(addrInfo: addrInfo) {
return Endpoint(host: .ipv6(ipv6Address), port: port)
} else {
fatalError()
}
#elseif os(macOS)
return self
#else
#error("Unimplemented")
#endif
}
}
/// An error type describing DNS resolution error
public struct DNSResolutionError: LocalizedError {
public let errorCode: Int32
public let address: String
init(errorCode: Int32, address: String) {
self.errorCode = errorCode
self.address = address
}
public var errorDescription: String? {
return String(cString: gai_strerror(errorCode))
}
}

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
public struct DNSServer {
public let address: IPAddress
public init(address: IPAddress) {
self.address = address
}
}
extension DNSServer: Equatable {
public static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue
}
}
extension DNSServer {
public var stringRepresentation: String {
return "\(address)"
}
public init?(from addressString: String) {
if let addr = IPv4Address(addressString) {
address = addr
} else if let addr = IPv6Address(addressString) {
address = addr
} else {
return nil
}
}
}

View File

@@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
public struct Endpoint {
public let host: NWEndpoint.Host
public let port: NWEndpoint.Port
public init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
self.host = host
self.port = port
}
}
extension Endpoint: Equatable {
public static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
return lhs.host == rhs.host && lhs.port == rhs.port
}
}
extension Endpoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(host)
hasher.combine(port)
}
}
extension Endpoint {
public var stringRepresentation: String {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
@unknown default:
fatalError()
}
}
public init?(from string: String) {
// Separation of host and port is based on 'parse_endpoint' function in
// https://git.zx2c4.com/wireguard-tools/tree/src/config.c
guard !string.isEmpty else { return nil }
let startOfPort: String.Index
let hostString: String
if string.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = string.index(after: string.startIndex)
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
let afterEndOfHost = string.index(after: endOfHost)
if afterEndOfHost == string.endIndex { return nil }
guard string[afterEndOfHost] == ":" else { return nil }
startOfPort = string.index(after: afterEndOfHost)
hostString = String(string[startOfHost ..< endOfHost])
} else {
// Look for an IPv4-style endpoint, like 127.0.0.1:80
guard let endOfHost = string.firstIndex(of: ":") else { return nil }
startOfPort = string.index(after: endOfHost)
hostString = String(string[string.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard invalidCharacterIndex == nil else { return nil }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
}
extension Endpoint {
public func hasHostAsIPAddress() -> Bool {
switch host {
case .name:
return false
case .ipv4:
return true
case .ipv6:
return true
@unknown default:
fatalError()
}
}
public func hostname() -> String? {
switch host {
case .name(let hostname, _):
return hostname
case .ipv4:
return nil
case .ipv6:
return nil
@unknown default:
fatalError()
}
}
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
extension IPv4Address {
init?(addrInfo: addrinfo) {
guard addrInfo.ai_family == AF_INET else { return nil }
let addressData = addrInfo.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { ptr -> Data in
return Data(bytes: &ptr.pointee.sin_addr, count: MemoryLayout<in_addr>.size)
}
self.init(addressData)
}
}
extension IPv6Address {
init?(addrInfo: addrinfo) {
guard addrInfo.ai_family == AF_INET6 else { return nil }
let addressData = addrInfo.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { ptr -> Data in
return Data(bytes: &ptr.pointee.sin6_addr, count: MemoryLayout<in6_addr>.size)
}
self.init(addressData)
}
}

View File

@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
public struct IPAddressRange {
public let address: IPAddress
public let networkPrefixLength: UInt8
init(address: IPAddress, networkPrefixLength: UInt8) {
self.address = address
self.networkPrefixLength = networkPrefixLength
}
}
extension IPAddressRange: Equatable {
public static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
}
}
extension IPAddressRange: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(address.rawValue)
hasher.combine(networkPrefixLength)
}
}
extension IPAddressRange {
public var stringRepresentation: String {
return "\(address)/\(networkPrefixLength)"
}
public init?(from string: String) {
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
address = parsed.0
networkPrefixLength = parsed.1
}
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
let addressString = String(string[string.startIndex ..< endOfIPAddress])
let address: IPAddress
if let addr = IPv4Address(addressString) {
address = addr
} else if let addr = IPv6Address(addressString) {
address = addr
} else {
return nil
}
let maxNetworkPrefixLength: UInt8 = address is IPv4Address ? 32 : 128
var networkPrefixLength: UInt8
if endOfIPAddress < string.endIndex { // "/" was located
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
networkPrefixLength = min(npl, maxNetworkPrefixLength)
} else {
networkPrefixLength = maxNetworkPrefixLength
}
return (address, networkPrefixLength)
}
public func subnetMask() -> IPAddress {
if address is IPv4Address {
let mask = networkPrefixLength > 0 ? ~UInt32(0) << (32 - networkPrefixLength) : UInt32(0)
let bytes = Data([
UInt8(truncatingIfNeeded: mask >> 24),
UInt8(truncatingIfNeeded: mask >> 16),
UInt8(truncatingIfNeeded: mask >> 8),
UInt8(truncatingIfNeeded: mask >> 0)
])
return IPv4Address(bytes)!
}
if address is IPv6Address {
var bytes = Data(repeating: 0, count: 16)
for i in 0..<Int(networkPrefixLength/8) {
bytes[i] = 0xff
}
let nibble = networkPrefixLength % 32
if nibble != 0 {
let mask = ~UInt32(0) << (32 - nibble)
let i = Int(networkPrefixLength / 32 * 4)
bytes[i + 0] = UInt8(truncatingIfNeeded: mask >> 24)
bytes[i + 1] = UInt8(truncatingIfNeeded: mask >> 16)
bytes[i + 2] = UInt8(truncatingIfNeeded: mask >> 8)
bytes[i + 3] = UInt8(truncatingIfNeeded: mask >> 0)
}
return IPv6Address(bytes)!
}
fatalError()
}
public func maskedAddress() -> IPAddress {
let subnet = subnetMask().rawValue
var masked = Data(address.rawValue)
if subnet.count != masked.count {
fatalError()
}
for i in 0..<subnet.count {
masked[i] &= subnet[i]
}
if subnet.count == 4 {
return IPv4Address(masked)!
}
if subnet.count == 16 {
return IPv6Address(masked)!
}
fatalError()
}
}

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
public struct InterfaceConfiguration {
public var privateKey: PrivateKey
public var addresses = [IPAddressRange]()
public var listenPort: UInt16?
public var mtu: UInt16?
public var dns = [DNSServer]()
public var dnsSearch = [String]()
public init(privateKey: PrivateKey) {
self.privateKey = privateKey
}
}
extension InterfaceConfiguration: Equatable {
public static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
return lhs.privateKey == rhs.privateKey &&
lhsAddresses == rhsAddresses &&
lhs.listenPort == rhs.listenPort &&
lhs.mtu == rhs.mtu &&
lhs.dns == rhs.dns &&
lhs.dnsSearch == rhs.dnsSearch
}
}

View File

@@ -0,0 +1,183 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
#if SWIFT_PACKAGE
import WireGuardKitC
#endif
/// A type alias for `Result` type that holds a tuple with source and resolved endpoint.
typealias EndpointResolutionResult = Result<(Endpoint, Endpoint), DNSResolutionError>
class PacketTunnelSettingsGenerator {
let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?]
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
self.tunnelConfiguration = tunnelConfiguration
self.resolvedEndpoints = resolvedEndpoints
}
func endpointUapiConfiguration() -> (String, [EndpointResolutionResult?]) {
var resolutionResults = [EndpointResolutionResult?]()
var wgSettings = ""
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (peer, resolvedEndpoint) in zip(self.tunnelConfiguration.peers, self.resolvedEndpoints) {
wgSettings.append("public_key=\(peer.publicKey.hexKey)\n")
let result = resolvedEndpoint.map(Self.reresolveEndpoint)
if case .success((_, let resolvedEndpoint)) = result {
if case .name = resolvedEndpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(resolvedEndpoint.stringRepresentation)\n")
}
resolutionResults.append(result)
}
return (wgSettings, resolutionResults)
}
func uapiConfiguration() -> (String, [EndpointResolutionResult?]) {
var resolutionResults = [EndpointResolutionResult?]()
var wgSettings = ""
wgSettings.append("private_key=\(tunnelConfiguration.interface.privateKey.hexKey)\n")
if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n")
}
if !tunnelConfiguration.peers.isEmpty {
wgSettings.append("replace_peers=true\n")
}
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (peer, resolvedEndpoint) in zip(self.tunnelConfiguration.peers, self.resolvedEndpoints) {
wgSettings.append("public_key=\(peer.publicKey.hexKey)\n")
if let preSharedKey = peer.preSharedKey?.hexKey {
wgSettings.append("preshared_key=\(preSharedKey)\n")
}
let result = resolvedEndpoint.map(Self.reresolveEndpoint)
if case .success((_, let resolvedEndpoint)) = result {
if case .name = resolvedEndpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(resolvedEndpoint.stringRepresentation)\n")
}
resolutionResults.append(result)
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n")
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation)\n") }
}
}
return (wgSettings, resolutionResults)
}
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
if !tunnelConfiguration.interface.dnsSearch.isEmpty || !tunnelConfiguration.interface.dns.isEmpty {
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation }
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
dnsSettings.searchDomains = tunnelConfiguration.interface.dnsSearch
if !tunnelConfiguration.interface.dns.isEmpty {
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
}
networkSettings.dnsSettings = dnsSettings
}
let mtu = tunnelConfiguration.interface.mtu ?? 0
/* 0 means automatic MTU. In theory, we should just do
* `networkSettings.tunnelOverheadBytes = 80` but in
* practice there are too many broken networks out there.
* Instead set it to 1280. Boohoo. Maybe someday we'll
* add a nob, maybe, or iOS will do probing for us.
*/
if mtu == 0 {
#if os(iOS)
networkSettings.mtu = NSNumber(value: 1280)
#elseif os(macOS)
networkSettings.tunnelOverheadBytes = 80
#else
#error("Unimplemented")
#endif
} else {
networkSettings.mtu = NSNumber(value: mtu)
}
let (ipv4Addresses, ipv6Addresses) = addresses()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses.map { $0.destinationAddress }, subnetMasks: ipv4Addresses.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes
networkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses.map { $0.destinationAddress }, networkPrefixLengths: ipv6Addresses.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
networkSettings.ipv6Settings = ipv6Settings
return networkSettings
}
private func addresses() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4Routes = [NEIPv4Route]()
var ipv6Routes = [NEIPv6Route]()
for addressRange in tunnelConfiguration.interface.addresses {
if addressRange.address is IPv4Address {
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: "\(addressRange.subnetMask())"))
} else if addressRange.address is IPv6Address {
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
* very bad, if various network parameters were actually relying on that subnet being
* intentionally small. TODO: talk about this with upstream iOS devs.
*/
ipv6Routes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: min(120, addressRange.networkPrefixLength))))
}
}
return (ipv4Routes, ipv6Routes)
}
private func includedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for addressRange in tunnelConfiguration.interface.addresses {
if addressRange.address is IPv4Address {
let route = NEIPv4Route(destinationAddress: "\(addressRange.maskedAddress())", subnetMask: "\(addressRange.subnetMask())")
route.gatewayAddress = "\(addressRange.address)"
ipv4IncludedRoutes.append(route)
} else if addressRange.address is IPv6Address {
let route = NEIPv6Route(destinationAddress: "\(addressRange.maskedAddress())", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength))
route.gatewayAddress = "\(addressRange.address)"
ipv6IncludedRoutes.append(route)
}
}
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if addressRange.address is IPv4Address {
ipv4IncludedRoutes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: "\(addressRange.subnetMask())"))
} else if addressRange.address is IPv6Address {
ipv6IncludedRoutes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength)))
}
}
}
return (ipv4IncludedRoutes, ipv6IncludedRoutes)
}
private class func reresolveEndpoint(endpoint: Endpoint) -> EndpointResolutionResult {
return Result { (endpoint, try endpoint.withReresolvedIP()) }
.mapError { error -> DNSResolutionError in
// swiftlint:disable:next force_cast
return error as! DNSResolutionError
}
}
}

View File

@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
public struct PeerConfiguration {
public var publicKey: PublicKey
public var preSharedKey: PreSharedKey?
public var allowedIPs = [IPAddressRange]()
public var endpoint: Endpoint?
public var persistentKeepAlive: UInt16?
public var rxBytes: UInt64?
public var txBytes: UInt64?
public var lastHandshakeTime: Date?
public init(publicKey: PublicKey) {
self.publicKey = publicKey
}
}
extension PeerConfiguration: Equatable {
public static func == (lhs: PeerConfiguration, rhs: PeerConfiguration) -> Bool {
return lhs.publicKey == rhs.publicKey &&
lhs.preSharedKey == rhs.preSharedKey &&
Set(lhs.allowedIPs) == Set(rhs.allowedIPs) &&
lhs.endpoint == rhs.endpoint &&
lhs.persistentKeepAlive == rhs.persistentKeepAlive
}
}
extension PeerConfiguration: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(publicKey)
hasher.combine(preSharedKey)
hasher.combine(Set(allowedIPs))
hasher.combine(endpoint)
hasher.combine(persistentKeepAlive)
}
}

View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
#if SWIFT_PACKAGE
import WireGuardKitC
#endif
/// The class describing a private key used by WireGuard.
public class PrivateKey: BaseKey {
/// Derived public key
public var publicKey: PublicKey {
return rawValue.withUnsafeBytes { (privateKeyBufferPointer: UnsafeRawBufferPointer) -> PublicKey in
var publicKeyData = Data(repeating: 0, count: Int(WG_KEY_LEN))
let privateKeyBytes = privateKeyBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
publicKeyData.withUnsafeMutableBytes { (publicKeyBufferPointer: UnsafeMutableRawBufferPointer) in
let publicKeyBytes = publicKeyBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
curve25519_derive_public_key(publicKeyBytes, privateKeyBytes)
}
return PublicKey(rawValue: publicKeyData)!
}
}
/// Initialize new private key
convenience public init() {
var privateKeyData = Data(repeating: 0, count: Int(WG_KEY_LEN))
privateKeyData.withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) in
let privateKeyBytes = rawBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
curve25519_generate_private_key(privateKeyBytes)
}
self.init(rawValue: privateKeyData)!
}
}
/// The class describing a public key used by WireGuard.
public class PublicKey: BaseKey {}
/// The class describing a pre-shared key used by WireGuard.
public class PreSharedKey: BaseKey {}
/// The base key implementation. Should not be used directly.
public class BaseKey: RawRepresentable, Equatable, Hashable {
/// Raw key representation
public let rawValue: Data
/// Hex encoded representation
public var hexKey: String {
return rawValue.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> String in
let inBytes = rawBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
var outBytes = [CChar](repeating: 0, count: Int(WG_KEY_LEN_HEX))
key_to_hex(&outBytes, inBytes)
return String(cString: outBytes, encoding: .ascii)!
}
}
/// Base64 encoded representation
public var base64Key: String {
return rawValue.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> String in
let inBytes = rawBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
var outBytes = [CChar](repeating: 0, count: Int(WG_KEY_LEN_BASE64))
key_to_base64(&outBytes, inBytes)
return String(cString: outBytes, encoding: .ascii)!
}
}
/// Initialize the key with existing raw representation
required public init?(rawValue: Data) {
if rawValue.count == WG_KEY_LEN {
self.rawValue = rawValue
} else {
return nil
}
}
/// Initialize the key with hex representation
public convenience init?(hexKey: String) {
var bytes = Data(repeating: 0, count: Int(WG_KEY_LEN))
let success = bytes.withUnsafeMutableBytes { (bufferPointer: UnsafeMutableRawBufferPointer) -> Bool in
return key_from_hex(bufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self), hexKey)
}
if success {
self.init(rawValue: bytes)
} else {
return nil
}
}
/// Initialize the key with base64 representation
public convenience init?(base64Key: String) {
var bytes = Data(repeating: 0, count: Int(WG_KEY_LEN))
let success = bytes.withUnsafeMutableBytes { (bufferPointer: UnsafeMutableRawBufferPointer) -> Bool in
return key_from_base64(bufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self), base64Key)
}
if success {
self.init(rawValue: bytes)
} else {
return nil
}
}
public static func == (lhs: BaseKey, rhs: BaseKey) -> Bool {
return lhs.rawValue.withUnsafeBytes { (lhsBytes: UnsafeRawBufferPointer) -> Bool in
return rhs.rawValue.withUnsafeBytes { (rhsBytes: UnsafeRawBufferPointer) -> Bool in
return key_eq(
lhsBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
rhsBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
)
}
}
}
}

View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
public final class TunnelConfiguration {
public var name: String?
public var interface: InterfaceConfiguration
public let peers: [PeerConfiguration]
public init(name: String?, interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
self.name = name
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
}
extension TunnelConfiguration: Equatable {
public static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
return lhs.name == rhs.name &&
lhs.interface == rhs.interface &&
Set(lhs.peers) == Set(rhs.peers)
}
}

View File

@@ -0,0 +1,487 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
#if SWIFT_PACKAGE
import WireGuardKitGo
import WireGuardKitC
#endif
public enum WireGuardAdapterError: Error {
/// Failure to locate tunnel file descriptor.
case cannotLocateTunnelFileDescriptor
/// Failure to perform an operation in such state.
case invalidState
/// Failure to resolve endpoints.
case dnsResolution([DNSResolutionError])
/// Failure to set network settings.
case setNetworkSettings(Error)
/// Failure to start WireGuard backend.
case startWireGuardBackend(Int32)
}
/// Enum representing internal state of the `WireGuardAdapter`
private enum State {
/// The tunnel is stopped
case stopped
/// The tunnel is up and running
case started(_ handle: Int32, _ settingsGenerator: PacketTunnelSettingsGenerator)
/// The tunnel is temporarily shutdown due to device going offline
case temporaryShutdown(_ settingsGenerator: PacketTunnelSettingsGenerator)
}
public class WireGuardAdapter {
public typealias LogHandler = (WireGuardLogLevel, String) -> Void
/// Network routes monitor.
private var networkMonitor: NWPathMonitor?
/// Packet tunnel provider.
private weak var packetTunnelProvider: NEPacketTunnelProvider?
/// Log handler closure.
private let logHandler: LogHandler
/// Private queue used to synchronize access to `WireGuardAdapter` members.
private let workQueue = DispatchQueue(label: "WireGuardAdapterWorkQueue")
/// Adapter state.
private var state: State = .stopped
/// Tunnel device file descriptor.
private var tunnelFileDescriptor: Int32? {
var ctlInfo = ctl_info()
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
_ = strcpy($0, "com.apple.net.utun_control")
}
}
for fd: Int32 in 0...1024 {
var addr = sockaddr_ctl()
var ret: Int32 = -1
var len = socklen_t(MemoryLayout.size(ofValue: addr))
withUnsafeMutablePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
ret = getpeername(fd, $0, &len)
}
}
if ret != 0 || addr.sc_family != AF_SYSTEM {
continue
}
if ctlInfo.ctl_id == 0 {
ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
if ret != 0 {
continue
}
}
if addr.sc_id == ctlInfo.ctl_id {
return fd
}
}
return nil
}
/// Returns a WireGuard version.
class var backendVersion: String {
guard let ver = wgVersion() else { return "unknown" }
let str = String(cString: ver)
free(UnsafeMutableRawPointer(mutating: ver))
return str
}
/// Returns the tunnel device interface name, or nil on error.
/// - Returns: String.
public var interfaceName: String? {
guard let tunnelFileDescriptor = self.tunnelFileDescriptor else { return nil }
var buffer = [UInt8](repeating: 0, count: Int(IFNAMSIZ))
return buffer.withUnsafeMutableBufferPointer { mutableBufferPointer in
guard let baseAddress = mutableBufferPointer.baseAddress else { return nil }
var ifnameSize = socklen_t(IFNAMSIZ)
let result = getsockopt(
tunnelFileDescriptor,
2 /* SYSPROTO_CONTROL */,
2 /* UTUN_OPT_IFNAME */,
baseAddress,
&ifnameSize)
if result == 0 {
return String(cString: baseAddress)
} else {
return nil
}
}
}
// MARK: - Initialization
/// Designated initializer.
/// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored
/// as a weak reference.
/// - Parameter logHandler: a log handler closure.
public init(with packetTunnelProvider: NEPacketTunnelProvider, logHandler: @escaping LogHandler) {
self.packetTunnelProvider = packetTunnelProvider
self.logHandler = logHandler
setupLogHandler()
}
deinit {
// Force remove logger to make sure that no further calls to the instance of this class
// can happen after deallocation.
wgSetLogger(nil, nil)
// Cancel network monitor
networkMonitor?.cancel()
// Shutdown the tunnel
if case .started(let handle, _) = self.state {
wgTurnOff(handle)
}
}
// MARK: - Public methods
/// Returns a runtime configuration from WireGuard.
/// - Parameter completionHandler: completion handler.
public func getRuntimeConfiguration(completionHandler: @escaping (String?) -> Void) {
workQueue.async {
guard case .started(let handle, _) = self.state else {
completionHandler(nil)
return
}
if let settings = wgGetConfig(handle) {
completionHandler(String(cString: settings))
free(settings)
} else {
completionHandler(nil)
}
}
}
/// Start the tunnel tunnel.
/// - Parameters:
/// - tunnelConfiguration: tunnel configuration.
/// - completionHandler: completion handler.
public func start(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
workQueue.async {
guard case .stopped = self.state else {
completionHandler(.invalidState)
return
}
let networkMonitor = NWPathMonitor()
networkMonitor.pathUpdateHandler = { [weak self] path in
self?.didReceivePathUpdate(path: path)
}
networkMonitor.start(queue: self.workQueue)
do {
let settingsGenerator = try self.makeSettingsGenerator(with: tunnelConfiguration)
try self.setNetworkSettings(settingsGenerator.generateNetworkSettings())
let (wgConfig, resolutionResults) = settingsGenerator.uapiConfiguration()
self.logEndpointResolutionResults(resolutionResults)
self.state = .started(
try self.startWireGuardBackend(wgConfig: wgConfig),
settingsGenerator
)
self.networkMonitor = networkMonitor
completionHandler(nil)
} catch let error as WireGuardAdapterError {
networkMonitor.cancel()
completionHandler(error)
} catch {
fatalError()
}
}
}
/// Stop the tunnel.
/// - Parameter completionHandler: completion handler.
public func stop(completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
workQueue.async {
switch self.state {
case .started(let handle, _):
wgTurnOff(handle)
case .temporaryShutdown:
break
case .stopped:
completionHandler(.invalidState)
return
}
self.networkMonitor?.cancel()
self.networkMonitor = nil
self.state = .stopped
completionHandler(nil)
}
}
/// Update runtime configuration.
/// - Parameters:
/// - tunnelConfiguration: tunnel configuration.
/// - completionHandler: completion handler.
public func update(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
workQueue.async {
if case .stopped = self.state {
completionHandler(.invalidState)
return
}
// Tell the system that the tunnel is going to reconnect using new WireGuard
// configuration.
// This will broadcast the `NEVPNStatusDidChange` notification to the GUI process.
self.packetTunnelProvider?.reasserting = true
defer {
self.packetTunnelProvider?.reasserting = false
}
do {
let settingsGenerator = try self.makeSettingsGenerator(with: tunnelConfiguration)
try self.setNetworkSettings(settingsGenerator.generateNetworkSettings())
switch self.state {
case .started(let handle, _):
let (wgConfig, resolutionResults) = settingsGenerator.uapiConfiguration()
self.logEndpointResolutionResults(resolutionResults)
wgSetConfig(handle, wgConfig)
#if os(iOS)
wgDisableSomeRoamingForBrokenMobileSemantics(handle)
#endif
self.state = .started(handle, settingsGenerator)
case .temporaryShutdown:
self.state = .temporaryShutdown(settingsGenerator)
case .stopped:
fatalError()
}
completionHandler(nil)
} catch let error as WireGuardAdapterError {
completionHandler(error)
} catch {
fatalError()
}
}
}
// MARK: - Private methods
/// Setup WireGuard log handler.
private func setupLogHandler() {
let context = Unmanaged.passUnretained(self).toOpaque()
wgSetLogger(context) { context, logLevel, message in
guard let context = context, let message = message else { return }
let unretainedSelf = Unmanaged<WireGuardAdapter>.fromOpaque(context)
.takeUnretainedValue()
let swiftString = String(cString: message).trimmingCharacters(in: .newlines)
let tunnelLogLevel = WireGuardLogLevel(rawValue: logLevel) ?? .verbose
unretainedSelf.logHandler(tunnelLogLevel, swiftString)
}
}
/// Set network tunnel configuration.
/// This method ensures that the call to `setTunnelNetworkSettings` does not time out, as in
/// certain scenarios the completion handler given to it may not be invoked by the system.
///
/// - Parameters:
/// - networkSettings: an instance of type `NEPacketTunnelNetworkSettings`.
/// - Throws: an error of type `WireGuardAdapterError`.
/// - Returns: `PacketTunnelSettingsGenerator`.
private func setNetworkSettings(_ networkSettings: NEPacketTunnelNetworkSettings) throws {
var systemError: Error?
let condition = NSCondition()
// Activate the condition
condition.lock()
defer { condition.unlock() }
self.packetTunnelProvider?.setTunnelNetworkSettings(networkSettings) { error in
systemError = error
condition.signal()
}
// Packet tunnel's `setTunnelNetworkSettings` times out in certain
// scenarios & never calls the given callback.
let setTunnelNetworkSettingsTimeout: TimeInterval = 5 // seconds
if condition.wait(until: Date().addingTimeInterval(setTunnelNetworkSettingsTimeout)) {
if let systemError = systemError {
throw WireGuardAdapterError.setNetworkSettings(systemError)
}
} else {
self.logHandler(.error, "setTunnelNetworkSettings timed out after 5 seconds; proceeding anyway")
}
}
/// Resolve peers of the given tunnel configuration.
/// - Parameter tunnelConfiguration: tunnel configuration.
/// - Throws: an error of type `WireGuardAdapterError`.
/// - Returns: The list of resolved endpoints.
private func resolvePeers(for tunnelConfiguration: TunnelConfiguration) throws -> [Endpoint?] {
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
let resolutionResults = DNSResolver.resolveSync(endpoints: endpoints)
let resolutionErrors = resolutionResults.compactMap { result -> DNSResolutionError? in
if case .failure(let error) = result {
return error
} else {
return nil
}
}
assert(endpoints.count == resolutionResults.count)
guard resolutionErrors.isEmpty else {
throw WireGuardAdapterError.dnsResolution(resolutionErrors)
}
let resolvedEndpoints = resolutionResults.map { result -> Endpoint? in
// swiftlint:disable:next force_try
return try! result?.get()
}
return resolvedEndpoints
}
/// Start WireGuard backend.
/// - Parameter wgConfig: WireGuard configuration
/// - Throws: an error of type `WireGuardAdapterError`
/// - Returns: tunnel handle
private func startWireGuardBackend(wgConfig: String) throws -> Int32 {
guard let tunnelFileDescriptor = self.tunnelFileDescriptor else {
throw WireGuardAdapterError.cannotLocateTunnelFileDescriptor
}
let handle = wgTurnOn(wgConfig, tunnelFileDescriptor)
if handle < 0 {
throw WireGuardAdapterError.startWireGuardBackend(handle)
}
#if os(iOS)
wgDisableSomeRoamingForBrokenMobileSemantics(handle)
#endif
return handle
}
/// Resolves the hostnames in the given tunnel configuration and return settings generator.
/// - Parameter tunnelConfiguration: an instance of type `TunnelConfiguration`.
/// - Throws: an error of type `WireGuardAdapterError`.
/// - Returns: an instance of type `PacketTunnelSettingsGenerator`.
private func makeSettingsGenerator(with tunnelConfiguration: TunnelConfiguration) throws -> PacketTunnelSettingsGenerator {
return PacketTunnelSettingsGenerator(
tunnelConfiguration: tunnelConfiguration,
resolvedEndpoints: try self.resolvePeers(for: tunnelConfiguration)
)
}
/// Log DNS resolution results.
/// - Parameter resolutionErrors: an array of type `[DNSResolutionError]`.
private func logEndpointResolutionResults(_ resolutionResults: [EndpointResolutionResult?]) {
for case .some(let result) in resolutionResults {
switch result {
case .success((let sourceEndpoint, let resolvedEndpoint)):
if sourceEndpoint.host == resolvedEndpoint.host {
self.logHandler(.verbose, "DNS64: mapped \(sourceEndpoint.host) to itself.")
} else {
self.logHandler(.verbose, "DNS64: mapped \(sourceEndpoint.host) to \(resolvedEndpoint.host)")
}
case .failure(let resolutionError):
self.logHandler(.error, "Failed to resolve endpoint \(resolutionError.address): \(resolutionError.errorDescription ?? "(nil)")")
}
}
}
/// Helper method used by network path monitor.
/// - Parameter path: new network path
private func didReceivePathUpdate(path: Network.NWPath) {
self.logHandler(.verbose, "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
#if os(macOS)
if case .started(let handle, _) = self.state {
wgBumpSockets(handle)
}
#elseif os(iOS)
switch self.state {
case .started(let handle, let settingsGenerator):
if path.status.isSatisfiable {
let (wgConfig, resolutionResults) = settingsGenerator.endpointUapiConfiguration()
self.logEndpointResolutionResults(resolutionResults)
wgSetConfig(handle, wgConfig)
wgDisableSomeRoamingForBrokenMobileSemantics(handle)
wgBumpSockets(handle)
} else {
self.logHandler(.verbose, "Connectivity offline, pausing backend.")
self.state = .temporaryShutdown(settingsGenerator)
wgTurnOff(handle)
}
case .temporaryShutdown(let settingsGenerator):
guard path.status.isSatisfiable else { return }
self.logHandler(.verbose, "Connectivity online, resuming backend.")
do {
try self.setNetworkSettings(settingsGenerator.generateNetworkSettings())
let (wgConfig, resolutionResults) = settingsGenerator.uapiConfiguration()
self.logEndpointResolutionResults(resolutionResults)
self.state = .started(
try self.startWireGuardBackend(wgConfig: wgConfig),
settingsGenerator
)
} catch {
self.logHandler(.error, "Failed to restart backend: \(error.localizedDescription)")
}
case .stopped:
// no-op
break
}
#else
#error("Unsupported")
#endif
}
}
/// A enum describing WireGuard log levels defined in `api-apple.go`.
public enum WireGuardLogLevel: Int32 {
case verbose = 0
case error = 1
}
private extension Network.NWPath.Status {
/// Returns `true` if the path is potentially satisfiable.
var isSatisfiable: Bool {
switch self {
case .requiresConnection, .satisfied:
return true
case .unsatisfied:
return false
@unknown default:
return true
}
}
}

View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
#include <sys/types.h>
#include "key.h"
#include "x25519.h"
/* From <sys/kern_control.h> */
#define CTLIOCGINFO 0xc0644e03UL
struct ctl_info {
u_int32_t ctl_id;
char ctl_name[96];
};
struct sockaddr_ctl {
u_char sc_len;
u_char sc_family;
u_int16_t ss_sysaddr;
u_int32_t sc_id;
u_int32_t sc_unit;
u_int32_t sc_reserved[5];
};

View File

@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
/*
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
* This is a specialized constant-time base64/hex implementation that resists side-channel attacks.
*/
#include <string.h>
#include "key.h"
static inline void encode_base64(char dest[static 4], const uint8_t src[static 3])
{
const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 };
for (unsigned int i = 0; i < 4; ++i)
dest[i] = input[i] + 'A'
+ (((25 - input[i]) >> 8) & 6)
- (((51 - input[i]) >> 8) & 75)
- (((61 - input[i]) >> 8) & 15)
+ (((62 - input[i]) >> 8) & 3);
}
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN])
{
unsigned int i;
for (i = 0; i < WG_KEY_LEN / 3; ++i)
encode_base64(&base64[i * 4], &key[i * 3]);
encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 });
base64[WG_KEY_LEN_BASE64 - 2] = '=';
base64[WG_KEY_LEN_BASE64 - 1] = '\0';
}
static inline int decode_base64(const char src[static 4])
{
int val = 0;
for (unsigned int i = 0; i < 4; ++i)
val |= (-1
+ ((((('A' - 1) - src[i]) & (src[i] - ('Z' + 1))) >> 8) & (src[i] - 64))
+ ((((('a' - 1) - src[i]) & (src[i] - ('z' + 1))) >> 8) & (src[i] - 70))
+ ((((('0' - 1) - src[i]) & (src[i] - ('9' + 1))) >> 8) & (src[i] + 5))
+ ((((('+' - 1) - src[i]) & (src[i] - ('+' + 1))) >> 8) & 63)
+ ((((('/' - 1) - src[i]) & (src[i] - ('/' + 1))) >> 8) & 64)
) << (18 - 6 * i);
return val;
}
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64)
{
unsigned int i;
volatile uint8_t ret = 0;
int val;
if (strlen(base64) != WG_KEY_LEN_BASE64 - 1 || base64[WG_KEY_LEN_BASE64 - 2] != '=')
return false;
for (i = 0; i < WG_KEY_LEN / 3; ++i) {
val = decode_base64(&base64[i * 4]);
ret |= (uint32_t)val >> 31;
key[i * 3 + 0] = (val >> 16) & 0xff;
key[i * 3 + 1] = (val >> 8) & 0xff;
key[i * 3 + 2] = val & 0xff;
}
val = decode_base64((const char[]){ base64[i * 4 + 0], base64[i * 4 + 1], base64[i * 4 + 2], 'A' });
ret |= ((uint32_t)val >> 31) | (val & 0xff);
key[i * 3 + 0] = (val >> 16) & 0xff;
key[i * 3 + 1] = (val >> 8) & 0xff;
return 1 & ((ret - 1) >> 8);
}
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN])
{
unsigned int i;
for (i = 0; i < WG_KEY_LEN; ++i) {
hex[i * 2] = 87U + (key[i] >> 4) + ((((key[i] >> 4) - 10U) >> 8) & ~38U);
hex[i * 2 + 1] = 87U + (key[i] & 0xf) + ((((key[i] & 0xf) - 10U) >> 8) & ~38U);
}
hex[i * 2] = '\0';
}
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex)
{
uint8_t c, c_acc, c_alpha0, c_alpha, c_num0, c_num, c_val;
volatile uint8_t ret = 0;
if (strlen(hex) != WG_KEY_LEN_HEX - 1)
return false;
for (unsigned int i = 0; i < WG_KEY_LEN_HEX - 1; i += 2) {
c = (uint8_t)hex[i];
c_num = c ^ 48U;
c_num0 = (c_num - 10U) >> 8;
c_alpha = (c & ~32U) - 55U;
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
c_acc = c_val * 16U;
c = (uint8_t)hex[i + 1];
c_num = c ^ 48U;
c_num0 = (c_num - 10U) >> 8;
c_alpha = (c & ~32U) - 55U;
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
key[i / 2] = c_acc | c_val;
}
return 1 & ((ret - 1) >> 8);
}
bool key_eq(const uint8_t key1[static WG_KEY_LEN], const uint8_t key2[static WG_KEY_LEN])
{
volatile uint8_t acc = 0;
for (unsigned int i = 0; i < WG_KEY_LEN; ++i) {
acc |= key1[i] ^ key2[i];
asm volatile("" : "=r"(acc) : "0"(acc));
}
return 1 & ((acc - 1) >> 8);
}

View File

@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: MIT */
/*
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#ifndef KEY_H
#define KEY_H
#include <stdbool.h>
#include <stdint.h>
#define WG_KEY_LEN (32)
#define WG_KEY_LEN_BASE64 (45)
#define WG_KEY_LEN_HEX (65)
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN]);
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64);
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN]);
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex);
bool key_eq(const uint8_t key1[static WG_KEY_LEN], const uint8_t key2[static WG_KEY_LEN]);
#endif

View File

@@ -0,0 +1,4 @@
module WireGuardKitC {
umbrella header "WireGuardKitC.h"
export *
}

View File

@@ -0,0 +1,178 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
* Curve25519 ECDH functions, based on TweetNaCl but cleaned up.
*/
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <CommonCrypto/CommonRandom.h>
#include "x25519.h"
typedef int64_t fe[16];
static inline void carry(fe o)
{
int i;
for (i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i == 15 ? 38 : 1) * (o[i] >> 16);
o[i] &= 0xffff;
}
}
static inline void cswap(fe p, fe q, int b)
{
int i;
int64_t t, c = ~(b - 1);
for (i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
static inline void pack(uint8_t *o, const fe n)
{
int i, j, b;
fe m, t;
memcpy(t, n, sizeof(t));
carry(t);
carry(t);
carry(t);
for (j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed;
for (i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
cswap(t, m, 1 - b);
}
for (i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
static inline void unpack(fe o, const uint8_t *n)
{
int i;
for (i = 0; i < 16; ++i)
o[i] = n[2 * i] + ((int64_t)n[2 * i + 1] << 8);
o[15] &= 0x7fff;
}
static inline void add(fe o, const fe a, const fe b)
{
int i;
for (i = 0; i < 16; ++i)
o[i] = a[i] + b[i];
}
static inline void subtract(fe o, const fe a, const fe b)
{
int i;
for (i = 0; i < 16; ++i)
o[i] = a[i] - b[i];
}
static inline void multmod(fe o, const fe a, const fe b)
{
int i, j;
int64_t t[31] = { 0 };
for (i = 0; i < 16; ++i) {
for (j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j];
}
for (i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16];
memcpy(o, t, sizeof(fe));
carry(o);
carry(o);
}
static inline void invert(fe o, const fe i)
{
fe c;
int a;
memcpy(c, i, sizeof(c));
for (a = 253; a >= 0; --a) {
multmod(c, c, c);
if (a != 2 && a != 4)
multmod(c, c, i);
}
memcpy(o, c, sizeof(fe));
}
static void curve25519_shared_secret(uint8_t shared_secret[32], const uint8_t private_key[32], const uint8_t public_key[32])
{
static const fe a24 = { 0xdb41, 1 };
uint8_t z[32];
int64_t r;
int i;
fe a = { 1 }, b, c = { 0 }, d = { 1 }, e, f, x;
memcpy(z, private_key, sizeof(z));
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
unpack(x, public_key);
memcpy(b, x, sizeof(b));
for (i = 254; i >= 0; --i) {
r = (z[i >> 3] >> (i & 7)) & 1;
cswap(a, b, (int)r);
cswap(c, d, (int)r);
add(e, a, c);
subtract(a, a, c);
add(c, b, d);
subtract(b, b, d);
multmod(d, e, e);
multmod(f, a, a);
multmod(a, c, a);
multmod(c, b, e);
add(e, a, c);
subtract(a, a, c);
multmod(b, a, a);
subtract(c, d, f);
multmod(a, c, a24);
add(a, a, d);
multmod(c, c, a);
multmod(a, d, f);
multmod(d, b, x);
multmod(b, e, e);
cswap(a, b, (int)r);
cswap(c, d, (int)r);
}
invert(c, c);
multmod(a, a, c);
pack(shared_secret, a);
}
void curve25519_derive_public_key(uint8_t public_key[32], const uint8_t private_key[32])
{
static const uint8_t basepoint[32] = { 9 };
curve25519_shared_secret(public_key, private_key, basepoint);
}
void curve25519_generate_private_key(uint8_t private_key[32])
{
assert(CCRandomGenerateBytes(private_key, 32) == kCCSuccess);
private_key[31] = (private_key[31] & 127) | 64;
private_key[0] &= 248;
}

View File

@@ -0,0 +1,7 @@
#ifndef X25519_H
#define X25519_H
void curve25519_derive_public_key(unsigned char public_key[32], const unsigned char private_key[32]);
void curve25519_generate_private_key(unsigned char private_key[32]);
#endif

View File

@@ -0,0 +1 @@
// Empty

View File

@@ -0,0 +1,5 @@
module WireGuardKitGo {
umbrella header "wireguard.h"
link "wg-go"
export *
}

View File

@@ -0,0 +1,23 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2018-2023 WireGuard LLC. All Rights Reserved.
*/
#ifndef WIREGUARD_H
#define WIREGUARD_H
#include <sys/types.h>
#include <stdint.h>
#include <stdbool.h>
typedef void(*logger_fn_t)(void *context, int level, const char *msg);
extern void wgSetLogger(void *context, logger_fn_t logger_fn);
extern int wgTurnOn(const char *settings, int32_t tun_fd);
extern void wgTurnOff(int handle);
extern int64_t wgSetConfig(int handle, const char *settings);
extern char *wgGetConfig(int handle);
extern void wgBumpSockets(int handle);
extern void wgDisableSomeRoamingForBrokenMobileSemantics(int handle);
extern const char *wgVersion();
#endif

79
ios/project.yml Normal file
View File

@@ -0,0 +1,79 @@
# xcodegen spec: `xcodegen generate` -> PCAPhobos.xcodeproj
# Signing: set DEVELOPMENT_TEAM (paid Apple Developer account — Network Extension needs it).
name: PCAPhobos
options:
bundleIdPrefix: com.bobyrev
deploymentTarget:
iOS: "16.0"
packages:
WireGuardKit:
path: WireGuardKit
settings:
base:
MARKETING_VERSION: "1.4.1"
CURRENT_PROJECT_VERSION: "141"
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
targets:
PCAPhobos:
type: application
platform: iOS
sources:
- App
- Shared
resources:
- App/Assets.xcassets
dependencies:
- target: PhobosTunnel
entitlements:
path: App/PCAPhobos.entitlements
properties:
com.apple.developer.networking.networkextension: [packet-tunnel-provider]
info:
path: App/Info.plist
properties:
CFBundleDisplayName: PCA Phobos
UILaunchScreen: {}
UISupportedInterfaceOrientations: [UIInterfaceOrientationPortrait]
CFBundleURLTypes:
- CFBundleURLName: com.bobyrev.pcaphobos.link
CFBundleURLSchemes: [phobos]
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.bobyrev.pcaphobos
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
PhobosTunnel:
type: app-extension
platform: iOS
sources:
- Tunnel/PacketTunnelProvider.swift
- Tunnel/TunnelConfiguration+WgQuickConfig.swift
- Tunnel/String+ArrayConversion.swift
- Tunnel/pcarelay.c
- Shared
- path: Tunnel/ru_bypass.txt
buildPhase: resources
- path: Tunnel/obfuscator
compilerFlags: ["-Dmain=wgobf_main", "-w"]
excludes: ["*.h"]
dependencies:
- package: WireGuardKit
entitlements:
path: Tunnel/PhobosTunnel.entitlements
properties:
com.apple.developer.networking.networkextension: [packet-tunnel-provider]
info:
path: Tunnel/Info.plist
properties:
CFBundleDisplayName: PhobosTunnel
NSExtension:
NSExtensionPointIdentifier: com.apple.networkextension.packet-tunnel
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).PacketTunnelProvider
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.bobyrev.pcaphobos.tunnel
SWIFT_OBJC_BRIDGING_HEADER: Tunnel/Tunnel-Bridging-Header.h
HEADER_SEARCH_PATHS: ["$(PROJECT_DIR)/Tunnel", "$(PROJECT_DIR)/Tunnel/obfuscator"]
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]": ["$(PROJECT_DIR)/WireGuardGo/iphoneos"]
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]": ["$(PROJECT_DIR)/WireGuardGo/iphonesimulator"]
"EXCLUDED_ARCHS[sdk=iphonesimulator*]": x86_64