summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2023-07-11 17:27:36 +0200
committerEmīls <emils@mullvad.net>2023-07-11 17:27:36 +0200
commit2d4f28072a976c513ac8a7e2f5657bdb347e6817 (patch)
treee3e8b603b781a2ae70f98b41183566e93b8ebdcc
parent473093688bb8f80d407fe7ce3de6d3533c064e57 (diff)
parentc75db498ecbe738c8ebf435f4176fe057d57689a (diff)
downloadmullvadvpn-2d4f28072a976c513ac8a7e2f5657bdb347e6817.tar.xz
mullvadvpn-2d4f28072a976c513ac8a7e2f5657bdb347e6817.zip
Merge branch 'add-udp2tcp-bindings-ios'
-rw-r--r--Cargo.lock12
-rw-r--r--Cargo.toml1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj337
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme17
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/TunnelObfuscationTests.xcscheme54
-rw-r--r--ios/TunnelObfuscation/Info.plist5
-rw-r--r--ios/TunnelObfuscation/TunnelObfuscation.h19
-rw-r--r--ios/TunnelObfuscation/TunnelObfuscator.swift72
-rw-r--r--ios/TunnelObfuscation/module.private.modulemap5
-rw-r--r--ios/TunnelObfuscation/tunnel-obfuscator-proxy/Cargo.toml20
-rw-r--r--ios/TunnelObfuscation/tunnel-obfuscator-proxy/build.rs14
-rw-r--r--ios/TunnelObfuscation/tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h16
-rw-r--r--ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/ffi.rs90
-rw-r--r--ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/lib.rs93
-rw-r--r--ios/TunnelObfuscationTests/TCPConnection.swift73
-rw-r--r--ios/TunnelObfuscationTests/TCPListener.swift75
-rw-r--r--ios/TunnelObfuscationTests/TCPUnsafeListener.swift81
-rw-r--r--ios/TunnelObfuscationTests/TunnelObfuscationTests.swift49
-rw-r--r--ios/TunnelObfuscationTests/UDPConnection.swift72
19 files changed, 1104 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b237808e43..7f8d7a7007 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4362,6 +4362,18 @@ dependencies = [
]
[[package]]
+name = "tunnel-obfuscator-proxy"
+version = "0.0.0"
+dependencies = [
+ "cbindgen",
+ "libc",
+ "log",
+ "oslog",
+ "tokio",
+ "tunnel-obfuscation",
+]
+
+[[package]]
name = "typenum"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 246e2a1e02..7e674b734a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ resolver = "2"
members = [
"android/translations-converter",
"ios/MullvadTransport/shadowsocks-proxy",
+ "ios/TunnelObfuscation/tunnel-obfuscator-proxy",
"mullvad-daemon",
"mullvad-cli",
"mullvad-fs",
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index e7dea2516a..0c93ba9efe 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -98,6 +98,8 @@
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; };
583FE01229C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */; };
583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
+ 584023222A406BF5007B27AC /* TunnelObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* TunnelObfuscator.swift */; };
+ 584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */; };
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58435AC129CB2A350099C71B /* LocationCellFactory.swift */; };
@@ -113,6 +115,9 @@
584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
5859A55329CD9B1300F66591 /* ChangeLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5859A55229CD9B1300F66591 /* ChangeLog.swift */; };
5859A55529CD9DD900F66591 /* changes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5859A55429CD9DD800F66591 /* changes.txt */; };
+ 585A02E92A4B283000C6CAFF /* TCPUnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */; };
+ 585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; };
+ 585A02ED2A4B28F300C6CAFF /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */; };
585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; };
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585CA70E25F8C44600B47C62 /* UIMetrics.swift */; };
585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; };
@@ -132,6 +137,8 @@
5867771629097C5B006F721F /* ProductState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771529097C5B006F721F /* ProductState.swift */; };
5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; };
586891CD29D452E4002A8278 /* SafariCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586891CC29D452E4002A8278 /* SafariCoordinator.swift */; };
+ 58695AA02A4ADA9200328DB3 /* TunnelObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */; };
+ 58695AA72A4B109F00328DB3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
586A0DCB2A20E359006C731C /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
586A0DD12A20E371006C731C /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 586A0DD02A20E371006C731C /* WireGuardKitTypes */; };
586A0DD42A20E4A9006C731C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
@@ -222,6 +229,9 @@
589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; };
589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; };
589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; };
+ 589C6A782A45AAB700DAD3EF /* tunnel_obfuscator_proxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 584023272A407679007B27AC /* tunnel_obfuscator_proxy.h */; settings = {ATTRIBUTES = (Private, ); }; };
+ 589C6A7C2A45AE0100DAD3EF /* TunnelObfuscation.h in Headers */ = {isa = PBXBuildFile; fileRef = 589C6A7B2A45AE0100DAD3EF /* TunnelObfuscation.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 589C6A7D2A45B06800DAD3EF /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; };
58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; };
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */; };
@@ -407,6 +417,8 @@
A9D99BA52A1F808900DE27D3 /* RelayCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; };
A9D99BA62A1F809C00DE27D3 /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
A9D99BA92A1F81B700DE27D3 /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; };
+ A9EC20EF2A5D79ED0040D56E /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
+ A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -467,6 +479,13 @@
remoteGlobalIDString = 06799ABB28F98E1D00ACD94E;
remoteInfo = MullvadREST;
};
+ 58695AA22A4ADA9200328DB3 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 5840231E2A406BF5007B27AC;
+ remoteInfo = TunnelObfuscation;
+ };
586A0DCD2A20E359006C731C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 58CE5E58224146200008646E /* Project object */;
@@ -691,6 +710,13 @@
remoteGlobalIDString = A97F1F402A1F4E1A00ECEFDE;
remoteInfo = MullvadTransport;
};
+ A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 5840231E2A406BF5007B27AC;
+ remoteInfo = TunnelObfuscation;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -702,6 +728,7 @@
files = (
58D223E7294C8F120029F5F8 /* MullvadTypes.framework in Embed Frameworks */,
58D223FA294C8FF10029F5F8 /* MullvadLogging.framework in Embed Frameworks */,
+ A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */,
06799AD228F98E1D00ACD94E /* MullvadREST.framework in Embed Frameworks */,
58D223CD294C8BCB0029F5F8 /* Operations.framework in Embed Frameworks */,
A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */,
@@ -711,6 +738,15 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ 5840231D2A406BF5007B27AC /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/$(PRODUCT_NAME)";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
586A0DD32A20E371006C731C /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -877,6 +913,10 @@
583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeCoordinator.swift; sourceTree = "<group>"; };
583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; };
583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = "<group>"; };
+ 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TunnelObfuscation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 584023212A406BF5007B27AC /* TunnelObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscator.swift; sourceTree = "<group>"; };
+ 584023272A407679007B27AC /* tunnel_obfuscator_proxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tunnel_obfuscator_proxy.h; path = "tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h"; sourceTree = "<group>"; };
+ 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtunnel_obfuscator_proxy.a; path = "../target/x86_64-apple-ios/debug/libtunnel_obfuscator_proxy.a"; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
5840BE34279EDB16002836BA /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
@@ -899,6 +939,9 @@
58561C98239A5D1500BD6B5E /* IPv4Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4Endpoint.swift; sourceTree = "<group>"; };
5859A55229CD9B1300F66591 /* ChangeLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLog.swift; sourceTree = "<group>"; };
5859A55429CD9DD800F66591 /* changes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = changes.txt; sourceTree = "<group>"; };
+ 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = "<group>"; };
+ 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
+ 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = "<group>"; };
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = "<group>"; };
@@ -921,6 +964,8 @@
5867771529097C5B006F721F /* ProductState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductState.swift; sourceTree = "<group>"; };
5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
586891CC29D452E4002A8278 /* SafariCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariCoordinator.swift; sourceTree = "<group>"; };
+ 58695A9D2A4ADA9100328DB3 /* TunnelObfuscationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunnelObfuscationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = "<group>"; };
586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = "<group>"; };
586A951329013235007BAF2B /* AnyIPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPEndpoint.swift; sourceTree = "<group>"; };
586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTunnelProviderMessageOperation.swift; sourceTree = "<group>"; };
@@ -994,6 +1039,8 @@
5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraint.swift; sourceTree = "<group>"; };
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelRelay.swift; sourceTree = "<group>"; };
589A455228E094B300565204 /* OperationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OperationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 589C6A7A2A45ACCA00DAD3EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 589C6A7B2A45AE0100DAD3EF /* TunnelObfuscation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TunnelObfuscation.h; sourceTree = "<group>"; };
589D28772846250500F9A7B3 /* OperationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationCondition.swift; sourceTree = "<group>"; };
589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncOperationQueue.swift; sourceTree = "<group>"; };
589D28792846250500F9A7B3 /* OperationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = "<group>"; };
@@ -1172,6 +1219,22 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 5840231C2A406BF5007B27AC /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 58695A9A2A4ADA9100328DB3 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 58695AA72A4B109F00328DB3 /* TunnelObfuscation.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
5898D28629017BD300EB5EBA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -1210,6 +1273,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ A9EC20EF2A5D79ED0040D56E /* TunnelObfuscation.framework in Frameworks */,
58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */,
5898D2A92901844E00EB5EBA /* libRelaySelector.a in Frameworks */,
58D223F9294C8FF00029F5F8 /* MullvadLogging.framework in Frameworks */,
@@ -1226,6 +1290,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 589C6A7D2A45B06800DAD3EF /* TunnelObfuscation.framework in Frameworks */,
A9D99BA92A1F81B700DE27D3 /* MullvadTransport.framework in Frameworks */,
5898D2AB2901845400EB5EBA /* libRelaySelector.a in Frameworks */,
5898D2AC2901845400EB5EBA /* libTunnelProviderMessaging.a in Frameworks */,
@@ -1715,6 +1780,17 @@
path = "Supporting Files";
sourceTree = "<group>";
};
+ 584023202A406BF5007B27AC /* TunnelObfuscation */ = {
+ isa = PBXGroup;
+ children = (
+ 589C6A7A2A45ACCA00DAD3EF /* Info.plist */,
+ 584023272A407679007B27AC /* tunnel_obfuscator_proxy.h */,
+ 589C6A7B2A45AE0100DAD3EF /* TunnelObfuscation.h */,
+ 584023212A406BF5007B27AC /* TunnelObfuscator.swift */,
+ );
+ path = TunnelObfuscation;
+ sourceTree = "<group>";
+ };
5846226F26E229CD0035F7C2 /* StorePaymentManager */ = {
isa = PBXGroup;
children = (
@@ -1743,6 +1819,7 @@
584F991F2902CBDD001F858D /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */,
01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */,
);
name = Frameworks;
@@ -1775,6 +1852,17 @@
path = Protocols;
sourceTree = "<group>";
};
+ 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */ = {
+ isa = PBXGroup;
+ children = (
+ 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */,
+ 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */,
+ 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */,
+ 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */,
+ );
+ path = TunnelObfuscationTests;
+ sourceTree = "<group>";
+ };
586A950B2901250A007BAF2B /* Operations */ = {
isa = PBXGroup;
children = (
@@ -1975,6 +2063,8 @@
589A455328E094B300565204 /* OperationsTests */,
58CE5E7A224146470008646E /* PacketTunnel */,
A97F1F422A1F4E1A00ECEFDE /* MullvadTransport */,
+ 584023202A406BF5007B27AC /* TunnelObfuscation */,
+ 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */,
58CE5E61224146200008646E /* Products */,
584F991F2902CBDD001F858D /* Frameworks */,
);
@@ -1997,6 +2087,8 @@
58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */,
58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */,
A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */,
+ 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */,
+ 58695A9D2A4ADA9100328DB3 /* TunnelObfuscationTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -2206,6 +2298,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 5840232A2A4081BF007B27AC /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 589C6A7C2A45AE0100DAD3EF /* TunnelObfuscation.h in Headers */,
+ 589C6A782A45AAB700DAD3EF /* tunnel_obfuscator_proxy.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
58D223A0294C8A480029F5F8 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -2308,6 +2409,43 @@
productReference = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */;
productType = "com.apple.product-type.framework";
};
+ 5840231E2A406BF5007B27AC /* TunnelObfuscation */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 584023232A406BF5007B27AC /* Build configuration list for PBXNativeTarget "TunnelObfuscation" */;
+ buildPhases = (
+ 584023262A406C01007B27AC /* ShellScript */,
+ 5840232A2A4081BF007B27AC /* Headers */,
+ 5840231B2A406BF5007B27AC /* Sources */,
+ 5840231C2A406BF5007B27AC /* Frameworks */,
+ 5840231D2A406BF5007B27AC /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = TunnelObfuscation;
+ productName = TunnelObfuscator;
+ productReference = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ 58695A9C2A4ADA9100328DB3 /* TunnelObfuscationTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 58695AA62A4ADA9200328DB3 /* Build configuration list for PBXNativeTarget "TunnelObfuscationTests" */;
+ buildPhases = (
+ 58695A992A4ADA9100328DB3 /* Sources */,
+ 58695A9A2A4ADA9100328DB3 /* Frameworks */,
+ 58695A9B2A4ADA9100328DB3 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 58695AA32A4ADA9200328DB3 /* PBXTargetDependency */,
+ );
+ name = TunnelObfuscationTests;
+ productName = TunnelObfuscationTests;
+ productReference = 58695A9D2A4ADA9100328DB3 /* TunnelObfuscationTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
5898D28829017BD300EB5EBA /* TunnelProviderMessaging */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5898D28F29017BD400EB5EBA /* Build configuration list for PBXNativeTarget "TunnelProviderMessaging" */;
@@ -2408,6 +2546,7 @@
063F02782902B63F001FA09F /* PBXTargetDependency */,
58CE5E80224146470008646E /* PBXTargetDependency */,
A97F1F462A1F4E1A00ECEFDE /* PBXTargetDependency */,
+ A9EC20F22A5D79ED0040D56E /* PBXTargetDependency */,
);
name = MullvadVPN;
packageProductDependencies = (
@@ -2576,7 +2715,7 @@
58CE5E58224146200008646E /* Project object */ = {
isa = PBXProject;
attributes = {
- LastSwiftUpdateCheck = 1410;
+ LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1420;
ORGANIZATIONNAME = "Mullvad VPN AB";
TargetAttributes = {
@@ -2586,6 +2725,13 @@
06799ABB28F98E1D00ACD94E = {
CreatedOnToolsVersion = 14.0.1;
};
+ 5840231E2A406BF5007B27AC = {
+ CreatedOnToolsVersion = 14.3.1;
+ LastSwiftMigration = 1430;
+ };
+ 58695A9C2A4ADA9100328DB3 = {
+ CreatedOnToolsVersion = 14.3.1;
+ };
5898D28829017BD300EB5EBA = {
CreatedOnToolsVersion = 14.1;
};
@@ -2675,6 +2821,8 @@
58D223D4294C8E5E0029F5F8 /* MullvadTypes */,
58D223F2294C8FF00029F5F8 /* MullvadLogging */,
A97F1F402A1F4E1A00ECEFDE /* MullvadTransport */,
+ 5840231E2A406BF5007B27AC /* TunnelObfuscation */,
+ 58695A9C2A4ADA9100328DB3 /* TunnelObfuscationTests */,
);
};
/* End PBXProject section */
@@ -2696,6 +2844,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 58695A9B2A4ADA9100328DB3 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
589A455028E094B300565204 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -2793,6 +2948,24 @@
shellPath = /bin/sh;
shellScript = "exec > $PROJECT_DIR/relays-prebuild.log 2>&1\n\n$PROJECT_DIR/relays-prebuild.sh\n";
};
+ 584023262A406C01007B27AC /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 12;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "CARGO_TARGET_DIR=${PROJECT_DIR}/../target bash ${PROJECT_DIR}/build-rust-library.sh tunnel-obfuscator-proxy\n";
+ };
A95F86B92A1F54F800245DAC /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -2857,6 +3030,25 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 5840231B2A406BF5007B27AC /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 584023222A406BF5007B27AC /* TunnelObfuscator.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 58695A992A4ADA9100328DB3 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 58695AA02A4ADA9200328DB3 /* TunnelObfuscationTests.swift in Sources */,
+ 585A02ED2A4B28F300C6CAFF /* TCPConnection.swift in Sources */,
+ 585A02E92A4B283000C6CAFF /* TCPUnsafeListener.swift in Sources */,
+ 585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
5898D28529017BD300EB5EBA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -3317,6 +3509,11 @@
target = 06799ABB28F98E1D00ACD94E /* MullvadREST */;
targetProxy = 58153073294CBE8B00D1702E /* PBXContainerItemProxy */;
};
+ 58695AA32A4ADA9200328DB3 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */;
+ targetProxy = 58695AA22A4ADA9200328DB3 /* PBXContainerItemProxy */;
+ };
586A0DCE2A20E359006C731C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */;
@@ -3471,6 +3668,11 @@
target = A97F1F402A1F4E1A00ECEFDE /* MullvadTransport */;
targetProxy = A9D99BA72A1F81B100DE27D3 /* PBXContainerItemProxy */;
};
+ A9EC20F22A5D79ED0040D56E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */;
+ targetProxy = A9EC20F12A5D79ED0040D56E /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -3615,6 +3817,121 @@
};
name = Release;
};
+ 584023242A406BF5007B27AC /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 4;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = TunnelObfuscation/Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Mullvad VPN AB. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug";
+ MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/TunnelObfuscation/module.private.modulemap;
+ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).TunnelObfuscation";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 584023252A406BF5007B27AC /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 5808273928487E3E006B77A4 /* Base.xcconfig */;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 4;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = TunnelObfuscation/Info.plist;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Mullvad VPN AB. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release";
+ MODULEMAP_PRIVATE_FILE = $PROJECT_DIR/TunnelObfuscation/module.private.modulemap;
+ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).TunnelObfuscation";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 58695AA42A4ADA9200328DB3 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = CKG9MXH72F;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.TunnelObfuscationTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 58695AA52A4ADA9200328DB3 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = CKG9MXH72F;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPN.TunnelObfuscationTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
5898D28D29017BD400EB5EBA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -4333,6 +4650,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 584023232A406BF5007B27AC /* Build configuration list for PBXNativeTarget "TunnelObfuscation" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 584023242A406BF5007B27AC /* Debug */,
+ 584023252A406BF5007B27AC /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 58695AA62A4ADA9200328DB3 /* Build configuration list for PBXNativeTarget "TunnelObfuscationTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58695AA42A4ADA9200328DB3 /* Debug */,
+ 58695AA52A4ADA9200328DB3 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
5898D28F29017BD400EB5EBA /* Build configuration list for PBXNativeTarget "TunnelProviderMessaging" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
index 29084e4730..44fd0a5d6a 100644
--- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
+++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme
@@ -194,6 +194,16 @@
ReferencedContainer = "container:MullvadVPN.xcodeproj">
</BuildableReference>
</TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "58695A9C2A4ADA9100328DB3"
+ BuildableName = "TunnelObfuscationTests.xctest"
+ BlueprintName = "TunnelObfuscationTests"
+ ReferencedContainer = "container:MullvadVPN.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
</Testables>
</TestAction>
<LaunchAction
@@ -216,6 +226,13 @@
ReferencedContainer = "container:MullvadVPN.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
+ <EnvironmentVariables>
+ <EnvironmentVariable
+ key = "LIBDISPATCH_COOPERATIVE_POOL_STRICT"
+ value = "1"
+ isEnabled = "YES">
+ </EnvironmentVariable>
+ </EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/TunnelObfuscationTests.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/TunnelObfuscationTests.xcscheme
new file mode 100644
index 0000000000..be288d68ea
--- /dev/null
+++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/TunnelObfuscationTests.xcscheme
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1430"
+ version = "1.7">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ shouldAutocreateTestPlan = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO"
+ parallelizable = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "58695A9C2A4ADA9100328DB3"
+ BuildableName = "TunnelObfuscationTests.xctest"
+ BlueprintName = "TunnelObfuscationTests"
+ ReferencedContainer = "container:MullvadVPN.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/ios/TunnelObfuscation/Info.plist b/ios/TunnelObfuscation/Info.plist
new file mode 100644
index 0000000000..0c67376eba
--- /dev/null
+++ b/ios/TunnelObfuscation/Info.plist
@@ -0,0 +1,5 @@
+<?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/>
+</plist>
diff --git a/ios/TunnelObfuscation/TunnelObfuscation.h b/ios/TunnelObfuscation/TunnelObfuscation.h
new file mode 100644
index 0000000000..4d381512bb
--- /dev/null
+++ b/ios/TunnelObfuscation/TunnelObfuscation.h
@@ -0,0 +1,19 @@
+//
+// TunnelObfuscation.h
+// TunnelObfuscation
+//
+// Created by pronebird on 2023-06-23.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+//! Project version number for TunnelObfuscation.
+FOUNDATION_EXPORT double TunnelObfuscationVersionNumber;
+
+//! Project version string for TunnelObfuscation.
+FOUNDATION_EXPORT const unsigned char TunnelObfuscationVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <TunnelObfuscation/PublicHeader.h>
+
+
diff --git a/ios/TunnelObfuscation/TunnelObfuscator.swift b/ios/TunnelObfuscation/TunnelObfuscator.swift
new file mode 100644
index 0000000000..f84ea73067
--- /dev/null
+++ b/ios/TunnelObfuscation/TunnelObfuscator.swift
@@ -0,0 +1,72 @@
+//
+// TunnelObfuscator.swift
+// TunnelObfuscation
+//
+// Created by pronebird on 19/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+import TunnelObfuscatorProxy
+
+/// Class that implements UDP over TCP obfuscation by accepting traffic on a local UDP port and proxying it over TCP to the remote endpoint.
+public final class TunnelObfuscator {
+ private let stateLock = NSLock()
+ private let remoteAddress: IPAddress
+ private let tcpPort: UInt16
+
+ private var proxyHandle = ProxyHandle(context: nil, port: 0)
+ private var isStarted = false
+
+ /// Returns local UDP port used by proxy and bound to 127.0.0.1 (IPv4).
+ /// The returned value can be zero if obfuscator hasn't started yet.
+ public var localUdpPort: UInt16 {
+ return stateLock.withLock { proxyHandle.port }
+ }
+
+ /// Initialize tunnel obfuscator with remote server address and TCP port where udp2tcp is running.
+ public init(remoteAddress: IPAddress, tcpPort: UInt16) {
+ self.remoteAddress = remoteAddress
+ self.tcpPort = tcpPort
+ }
+
+ deinit {
+ stop()
+ }
+
+ public func start() {
+ stateLock.withLock {
+ guard !isStarted else { return }
+
+ let result = withUnsafeMutablePointer(to: &proxyHandle) { proxyHandlePointer in
+ let addressData = remoteAddress.rawValue
+
+ return start_tunnel_obfuscator_proxy(
+ addressData.map { $0 },
+ UInt(addressData.count),
+ tcpPort,
+ proxyHandlePointer
+ )
+ }
+
+ assert(result == 0)
+
+ isStarted = true
+ }
+ }
+
+ public func stop() {
+ stateLock.withLock {
+ guard isStarted else { return }
+
+ let result = withUnsafeMutablePointer(to: &proxyHandle) {
+ stop_tunnel_obfuscator_proxy($0)
+ }
+
+ assert(result == 0)
+
+ isStarted = false
+ }
+ }
+}
diff --git a/ios/TunnelObfuscation/module.private.modulemap b/ios/TunnelObfuscation/module.private.modulemap
new file mode 100644
index 0000000000..879cbd009d
--- /dev/null
+++ b/ios/TunnelObfuscation/module.private.modulemap
@@ -0,0 +1,5 @@
+framework module TunnelObfuscatorProxy {
+ header "tunnel_obfuscator_proxy.h"
+ link "libtunnel_obfuscator_proxy"
+ export *
+} \ No newline at end of file
diff --git a/ios/TunnelObfuscation/tunnel-obfuscator-proxy/Cargo.toml b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/Cargo.toml
new file mode 100644
index 0000000000..3a0e59fc15
--- /dev/null
+++ b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "tunnel-obfuscator-proxy"
+version = "0.0.0"
+edition = "2021"
+license = "GPL-3.0"
+publish = false
+
+[lib]
+crate-type = [ "rlib", "staticlib" ]
+bench = false
+
+[target.'cfg(target_os = "ios")'.dependencies]
+tunnel-obfuscation = { path = "../../../tunnel-obfuscation" }
+tokio = { version = "1", features = ["sync"] }
+libc = "0.2"
+log = "0.4"
+oslog = "0.2"
+
+[target.'cfg(target_os = "ios")'.build-dependencies]
+cbindgen = { version = "0.24.3", default-features = false }
diff --git a/ios/TunnelObfuscation/tunnel-obfuscator-proxy/build.rs b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/build.rs
new file mode 100644
index 0000000000..475f26ba11
--- /dev/null
+++ b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/build.rs
@@ -0,0 +1,14 @@
+#[cfg(target_os = "ios")]
+fn main() {
+ let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
+
+ cbindgen::Builder::new()
+ .with_crate(crate_dir)
+ .with_language(cbindgen::Language::C)
+ .generate()
+ .expect("failed to generate bindings")
+ .write_to_file("include/tunnel_obfuscator_proxy.h");
+}
+
+#[cfg(not(target_os = "ios"))]
+fn main() {}
diff --git a/ios/TunnelObfuscation/tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h
new file mode 100644
index 0000000000..fdee41746f
--- /dev/null
+++ b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h
@@ -0,0 +1,16 @@
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+typedef struct ProxyHandle {
+ void *context;
+ uint16_t port;
+} ProxyHandle;
+
+int32_t start_tunnel_obfuscator_proxy(const uint8_t *peer_address,
+ uintptr_t peer_address_len,
+ uint16_t peer_port,
+ struct ProxyHandle *proxy_handle);
+
+int32_t stop_tunnel_obfuscator_proxy(struct ProxyHandle *proxy_handle);
diff --git a/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/ffi.rs b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/ffi.rs
new file mode 100644
index 0000000000..a6ea98a86d
--- /dev/null
+++ b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/ffi.rs
@@ -0,0 +1,90 @@
+#![cfg(target_os = "ios")]
+
+use super::{TunnelObfuscatorHandle, TunnelObfuscatorRuntime};
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
+use std::sync::Once;
+
+static INIT_LOGGING: Once = Once::new();
+
+#[repr(C)]
+pub struct ProxyHandle {
+ pub context: *mut libc::c_void,
+ pub port: u16,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn start_tunnel_obfuscator_proxy(
+ peer_address: *const u8,
+ peer_address_len: usize,
+ peer_port: u16,
+ proxy_handle: *mut ProxyHandle,
+) -> i32 {
+ INIT_LOGGING.call_once(|| {
+ let _ = oslog::OsLogger::new("net.mullvad.MullvadVPN.TunnelObfuscatorProxy")
+ .level_filter(log::LevelFilter::Info)
+ .init();
+ });
+
+ let peer_sock_addr: SocketAddr =
+ if let Some(ip_address) = parse_ip_addr(peer_address, peer_address_len) {
+ SocketAddr::new(ip_address, peer_port)
+ } else {
+ return -1;
+ };
+
+ let result = TunnelObfuscatorRuntime::new(peer_sock_addr).and_then(|runtime| runtime.run());
+
+ match result {
+ Ok((local_endpoint, obfuscator_handle)) => {
+ let boxed_handle = Box::new(obfuscator_handle);
+ std::ptr::write(
+ proxy_handle,
+ ProxyHandle {
+ context: Box::into_raw(boxed_handle) as *mut _,
+ port: local_endpoint.port(),
+ },
+ );
+ 0
+ }
+ Err(err) => {
+ log::error!("Failed to run tunnel obfuscator proxy {}", err);
+ err.raw_os_error().unwrap_or(-1)
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn stop_tunnel_obfuscator_proxy(proxy_handle: *mut ProxyHandle) -> i32 {
+ let context_ptr = unsafe { (*proxy_handle).context };
+ if context_ptr.is_null() {
+ return -1;
+ }
+
+ let obfuscator_handle: Box<TunnelObfuscatorHandle> =
+ unsafe { Box::from_raw(context_ptr as *mut _) };
+ obfuscator_handle.stop();
+ unsafe { (*proxy_handle).context = std::ptr::null_mut() };
+ 0
+}
+
+/// Constructs a new IP address from a pointer containing bytes representing an IP address.
+///
+/// SAFETY: `addr` must be a pointer to at least `addr_len` bytes.
+unsafe fn parse_ip_addr(addr: *const u8, addr_len: usize) -> Option<IpAddr> {
+ match addr_len {
+ 4 => {
+ // SAFETY: addr pointer must point to at least addr_len bytes
+ let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) };
+ Some(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).into())
+ }
+ 16 => {
+ // SAFETY: addr pointer must point to at least addr_len bytes
+ let bytes = unsafe { std::slice::from_raw_parts(addr, addr_len) };
+ let mut addr_arr = [0u8; 16];
+ addr_arr.as_mut_slice().copy_from_slice(bytes);
+
+ Some(Ipv6Addr::from(addr_arr).into())
+ }
+ _ => None,
+ }
+}
diff --git a/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/lib.rs b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/lib.rs
new file mode 100644
index 0000000000..09ac816bfa
--- /dev/null
+++ b/ios/TunnelObfuscation/tunnel-obfuscator-proxy/src/lib.rs
@@ -0,0 +1,93 @@
+#![cfg(target_os = "ios")]
+
+use std::io;
+use std::net::SocketAddr;
+use tokio::sync::oneshot;
+use tunnel_obfuscation::{create_obfuscator, Settings as ObfuscationSettings, Udp2TcpSettings};
+
+mod ffi;
+pub use ffi::{start_tunnel_obfuscator_proxy, stop_tunnel_obfuscator_proxy, ProxyHandle};
+
+pub struct TunnelObfuscatorRuntime {
+ runtime: tokio::runtime::Runtime,
+ settings: ObfuscationSettings,
+}
+
+impl TunnelObfuscatorRuntime {
+ pub fn new(peer: SocketAddr) -> io::Result<Self> {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()?;
+ let settings = ObfuscationSettings::Udp2Tcp(Udp2TcpSettings { peer });
+
+ Ok(Self { runtime, settings })
+ }
+
+ pub fn run(self) -> io::Result<(SocketAddr, TunnelObfuscatorHandle)> {
+ let (tx, rx) = oneshot::channel();
+ let (shutdown_tx, shutdown_rx) = oneshot::channel();
+ let (startup_tx, startup_rx) = oneshot::channel();
+ std::thread::spawn(move || {
+ self.run_service_inner(rx, startup_tx);
+ });
+
+ match startup_rx.blocking_recv() {
+ Ok(Ok(endpoint)) => Ok((endpoint, TunnelObfuscatorHandle { tx })),
+ Ok(Err(err)) => {
+ let _ = tx.send(shutdown_tx);
+ let _ = shutdown_rx.blocking_recv();
+ Err(io::Error::new(io::ErrorKind::Other, err))
+ }
+ Err(_) => {
+ let _ = tx.send(shutdown_tx);
+ let _ = shutdown_rx.blocking_recv();
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Tokio runtime crashed",
+ ))
+ }
+ }
+ }
+
+ fn run_service_inner(
+ self,
+ shutdown_rx: oneshot::Receiver<oneshot::Sender<()>>,
+ startup_done_tx: oneshot::Sender<io::Result<SocketAddr>>,
+ ) {
+ let Self {
+ settings, runtime, ..
+ } = self;
+
+ std::thread::spawn(move || {
+ runtime.spawn(async move {
+ match create_obfuscator(&settings).await {
+ Ok(obfuscator) => {
+ let endpoint = obfuscator.endpoint();
+ let _ = startup_done_tx.send(Ok(endpoint));
+ let _ = obfuscator.run().await;
+ }
+ Err(err) => {
+ let _ =
+ startup_done_tx.send(Err(io::Error::new(io::ErrorKind::Other, err)));
+ }
+ }
+ });
+ if let Ok(shutdown_tx) = runtime.block_on(shutdown_rx) {
+ std::mem::drop(runtime);
+ let _ = shutdown_tx.send(());
+ }
+ });
+ }
+}
+
+pub struct TunnelObfuscatorHandle {
+ tx: oneshot::Sender<oneshot::Sender<()>>,
+}
+
+impl TunnelObfuscatorHandle {
+ pub fn stop(self) {
+ let (shutdown_tx, shutdown_rx) = oneshot::channel();
+ let _ = self.tx.send(shutdown_tx);
+ let _ = shutdown_rx.blocking_recv();
+ }
+}
diff --git a/ios/TunnelObfuscationTests/TCPConnection.swift b/ios/TunnelObfuscationTests/TCPConnection.swift
new file mode 100644
index 0000000000..85a26ff970
--- /dev/null
+++ b/ios/TunnelObfuscationTests/TCPConnection.swift
@@ -0,0 +1,73 @@
+//
+// TCPConnection.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+
+/// Minimal implementation of TCP connection capable of receiving data.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class TCPConnection {
+ private let dispatchQueue = DispatchQueue(label: "TCPConnection")
+ private let nwConnection: NWConnection
+
+ init(nwConnection: NWConnection) {
+ self.nwConnection = nwConnection
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Establishes the TCP connection.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ nwConnection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.nwConnection.stateUpdateHandler = nil
+ }
+ nwConnection.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ nwConnection.cancel()
+ }
+
+ func receiveData(minimumLength: Int, maximumLength: Int) async throws -> Data {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.receive(
+ minimumIncompleteLength: minimumLength,
+ maximumLength: maximumLength
+ ) { content, contentContext, isComplete, error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else if let content {
+ continuation.resume(returning: content)
+ } else if isComplete {
+ continuation.resume(returning: Data())
+ }
+ }
+ }
+ }
+}
diff --git a/ios/TunnelObfuscationTests/TCPListener.swift b/ios/TunnelObfuscationTests/TCPListener.swift
new file mode 100644
index 0000000000..edfcde84d9
--- /dev/null
+++ b/ios/TunnelObfuscationTests/TCPListener.swift
@@ -0,0 +1,75 @@
+//
+// TCPListener.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Network
+
+/// Minimal implementation of a TCP listener.
+class TCPOneShotListener {
+ private let dispatchQueue = DispatchQueue(label: "TCPListener")
+ private let listener: NWListener
+
+ /// A stream of new TCP connections.
+ /// The caller may iterate over this stream to accept new TCP connections.
+ ///
+ /// `TCPConnection` objects are returned unopen, so the caller has to call `TCPConnection.start()` to accept the
+ /// connection before initiating the data exchange.
+ let newConnections: AsyncStream<TCPConnection>
+
+ init() throws {
+ let listener = try NWListener(using: .tcp)
+
+ newConnections = AsyncStream { continuation in
+ listener.newConnectionHandler = { nwConnection in
+ continuation.yield(TCPConnection(nwConnection: nwConnection))
+ }
+ continuation.onTermination = { _ in
+ listener.newConnectionHandler = nil
+ }
+ }
+
+ self.listener = listener
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Local TCP port bound by listener on which it accepts new connections.
+ var listenPort: UInt16 {
+ return listener.port?.rawValue ?? 0
+ }
+
+ /// Start listening on a randomly assigned port which should be available via `listenPort` once this call returns
+ /// control back to the caller.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ listener.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.listener.stateUpdateHandler = nil
+ }
+ listener.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ listener.cancel()
+ }
+}
diff --git a/ios/TunnelObfuscationTests/TCPUnsafeListener.swift b/ios/TunnelObfuscationTests/TCPUnsafeListener.swift
new file mode 100644
index 0000000000..7d7b9ed949
--- /dev/null
+++ b/ios/TunnelObfuscationTests/TCPUnsafeListener.swift
@@ -0,0 +1,81 @@
+//
+// TCPUnsafeListener.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Network
+
+/// Minimal implementation of a TCP listener.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class TCPUnsafeListener {
+ private let dispatchQueue = DispatchQueue(label: "TCPListener")
+ private let listener: NWListener
+
+ /// A stream of new TCP connections.
+ /// The caller may iterate over this stream to accept new TCP connections.
+ ///
+ /// `TCPConnection` objects are returned unopen, so the caller has to call `TCPConnection.start()` to accept the
+ /// connection before initiating the data exchange.
+ let newConnections: AsyncStream<TCPConnection>
+
+ init() throws {
+ let listener = try NWListener(using: .tcp)
+
+ newConnections = AsyncStream { continuation in
+ listener.newConnectionHandler = { nwConnection in
+ continuation.yield(TCPConnection(nwConnection: nwConnection))
+ }
+ continuation.onTermination = { @Sendable _ in
+ listener.newConnectionHandler = nil
+ }
+ }
+
+ self.listener = listener
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Local TCP port bound by listener on which it accepts new connections.
+ var listenPort: UInt16 {
+ return listener.port?.rawValue ?? 0
+ }
+
+ /// Start listening on a randomly assigned port which should be available via `listenPort` once this call returns
+ /// control back to the caller.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ listener.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case let .waiting(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.listener.stateUpdateHandler = nil
+ }
+ listener.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ listener.cancel()
+ }
+}
diff --git a/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift b/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift
new file mode 100644
index 0000000000..0774ef9829
--- /dev/null
+++ b/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift
@@ -0,0 +1,49 @@
+//
+// TunnelObfuscationTests.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Network
+import TunnelObfuscation
+import XCTest
+
+final class TunnelObfuscationTests: XCTestCase {
+ func testRunningObfuscatorProxy() async throws {
+ // Each packet is prefixed with u16 that contains a payload length.
+ let preambleLength = MemoryLayout<UInt16>.size
+ let markerData = Data([109, 117, 108, 108, 118, 97, 100])
+ let packetLength = markerData.count + preambleLength
+
+ let tcpListener = try TCPUnsafeListener()
+ try await tcpListener.start()
+
+ let obfuscator = TunnelObfuscator(remoteAddress: IPv4Address.loopback, tcpPort: tcpListener.listenPort)
+ obfuscator.start()
+
+ // Accept incoming connections
+ let connectionDataTask = Task {
+ for await newConnection in tcpListener.newConnections {
+ try await newConnection.start()
+
+ return try await newConnection.receiveData(
+ minimumLength: packetLength,
+ maximumLength: packetLength
+ )
+ }
+ throw POSIXError(.ECANCELED)
+ }
+
+ // Send marker data over UDP
+ let connection = UDPConnection(remote: IPv4Address.loopback, port: obfuscator.localUdpPort)
+ try await connection.start()
+ try await connection.sendData(markerData)
+
+ // Validate the sent data
+ let receivedData = try await connectionDataTask.value
+ XCTAssert(receivedData.count == packetLength)
+ XCTAssertEqual(receivedData[preambleLength...], markerData)
+ }
+}
diff --git a/ios/TunnelObfuscationTests/UDPConnection.swift b/ios/TunnelObfuscationTests/UDPConnection.swift
new file mode 100644
index 0000000000..8848643c05
--- /dev/null
+++ b/ios/TunnelObfuscationTests/UDPConnection.swift
@@ -0,0 +1,72 @@
+//
+// UDPConnection.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+
+/// Minimal implementation of UDP connection capable of sending data.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class UDPConnection {
+ private let dispatchQueue = DispatchQueue(label: "UDPConnection")
+ private let nwConnection: NWConnection
+
+ init(remote: IPAddress, port: UInt16) {
+ nwConnection = NWConnection(
+ host: NWEndpoint.Host("\(remote)"),
+ port: NWEndpoint.Port(integerLiteral: port),
+ using: .udp
+ )
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Establishes the UDP connection.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.nwConnection.stateUpdateHandler = nil
+ }
+ nwConnection.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ nwConnection.cancel()
+ }
+
+ func sendData(_ data: Data) async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.send(content: data, completion: .contentProcessed { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ })
+ }
+ }
+}