diff --git a/.gitignore b/.gitignore
index cef89500..6aef4412 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ client/qrc_*.cpp
client/ui_*.h
client/ui_*.cpp
client/Makefile*
+client/fastlane/build/
client/*build-*
client/AmneziaVPN.xcodeproj
client/Debug-iphonesimulator/
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/Contents.json b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..25af9c29
--- /dev/null
+++ b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,62 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_40x40.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_60x60.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_58x58.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_87x87.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_80x80.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_120x120.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_120x120-1.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "filename" : "icon_180x180.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "filename" : "icon_1024x1024.png",
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_1024x1024.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_1024x1024.png
new file mode 100644
index 00000000..695a30a5
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_1024x1024.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120-1.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120-1.png
new file mode 100644
index 00000000..4349d194
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120-1.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120.png
new file mode 100644
index 00000000..4349d194
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_120x120.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_180x180.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_180x180.png
new file mode 100644
index 00000000..4bf6715a
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_180x180.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_40x40.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_40x40.png
new file mode 100644
index 00000000..ad3d0746
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_40x40.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_58x58.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_58x58.png
new file mode 100644
index 00000000..dbacdbe7
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_58x58.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_60x60.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_60x60.png
new file mode 100644
index 00000000..0778ec28
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_60x60.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_80x80.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_80x80.png
new file mode 100644
index 00000000..8a1fdc1d
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_80x80.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_87x87.png b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_87x87.png
new file mode 100644
index 00000000..e57c6fbb
Binary files /dev/null and b/client/AmneziaVPN/Images.xcassets/AppIcon.appiconset/icon_87x87.png differ
diff --git a/client/AmneziaVPN/Images.xcassets/Contents.json b/client/AmneziaVPN/Images.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/client/AmneziaVPN/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/client/fastlane/Appfile b/client/fastlane/Appfile
new file mode 100644
index 00000000..2e9184e1
--- /dev/null
+++ b/client/fastlane/Appfile
@@ -0,0 +1,7 @@
+app_identifier(["org.amnezia.AmneziaVPN", "org.amnezia.AmneziaVPN.network-extension"]) # The bundle identifier of your app
+apple_id("dartsyms@gmail.com") # Your Apple email address
+
+itc_team_id("122591025") # App Store Connect Team ID
+team_id("X7UJ388FXK") # Developer Portal Team ID
+#itc_team_id("119381903")
+#team_id("7VAGZXD78P")
diff --git a/client/fastlane/Fastfile b/client/fastlane/Fastfile
new file mode 100644
index 00000000..3027dbdc
--- /dev/null
+++ b/client/fastlane/Fastfile
@@ -0,0 +1,197 @@
+# Uncomment the line if you want fastlane to automatically update itself
+# update_fastlane
+
+project_name = "AmneziaVPN"
+project_scheme = "AmneziaVPN"
+project_identifier = "org.amnezia.AmneziaVPN"
+itunes_username = "dartsyms@gmail.com"
+
+before_all do
+ ENV['GYM_SCHEME'] = project_scheme
+end
+
+def tag_name(version_number, build_number)
+ "#{version_number}_#{build_number}"
+end
+
+def incrementBuild
+ increment_build_number(xcodeproj: "./#{project_name}.xcodeproj")
+end
+
+default_platform :ios
+
+# Fastfile actions accept additional configuration, but
+# don't worry, fastlane will prompt you for required
+# info which you can add here later
+lane :beta do
+ # build your iOS app
+ build_app(
+ scheme: "AmneziaVPN",
+ export_method: "ad-hoc"
+ )
+end
+
+lane :incrementVersion do
+ increment_build_number(
+ xcodeproj: "./#{project_name}.xcodeproj"
+ )
+
+ version_number = get_version_number(
+ xcodeproj: "./#{project_name}.xcodeproj",
+ target: project_name
+ )
+
+ build_number = get_build_number(
+ xcodeproj: "./#{project_name}.xcodeproj"
+ )
+
+ puts "Version = #{version_number} Build = #{build_number}"
+end
+
+desc "Update Certificates, Run All tests, Build app"
+desc "Add tag to git, send to Testflight, slack notification"
+lane :addToTestFlight do
+ certificates
+ clean_build_artifacts
+
+ build_app(
+ scheme: project_scheme,
+ configuration: "Release", # Debug / Release
+ export_method: "app-store", # Valid values are: app-store, ad-hoc, package, enterprise, development, developer-id
+ clean: true,
+ include_bitcode: false,
+ include_symbols: true,
+ export_options: {
+ provisioningProfiles: {
+ "org.amnezia.AmneziaVPN"=>"match AppStore org.amnezia.AmneziaVPN",
+ "org.amnezia.AmneziaVPN.network-extension"=>"match development AmneziaVPN networkextension",
+ }
+ },
+ output_directory: "fastlane/build/", # Destination directory. Defaults to current directory.
+ output_name: "#{project_name}.ipa"
+ )
+
+ testflight(
+ username: itunes_username,
+ app_identifier: project_identifier,
+ ipa: "fastlane/build/#{project_name}.ipa",
+ skip_waiting_for_build_processing: true
+ )
+
+ # Section for Slack
+ version_number = get_version_number(
+ xcodeproj: "./#{project_name}.xcodeproj",
+ target: project_name
+ )
+
+ build_number = get_build_number(
+ xcodeproj: "./#{project_name}.xcodeproj"
+ )
+
+ msg = "Build v.#{version_number} (#{build_number}) was Created for TestFlight"
+ puts msg
+ # msgToSlack(msg, true)
+ increment_build_number
+end
+
+lane :certificates do
+ match(
+ username: itunes_username,
+ type: "appstore",
+ app_identifier: [project_identifier],
+ readonly: true,
+ git_url: "https://github.com/amnezia-vpn/amnezia-ios-certificates.git"
+ )
+ notification(subtitle: "Finished", message: "Certificates done")
+end
+
+lane :adhoc_certificates do
+ match(
+ username: itunes_username,
+ type: "adhoc",
+ app_identifier: [project_identifier],
+ readonly: true,
+ git_url: "https://github.com/amnezia-vpn/amnezia-ios-certificates.git"
+ )
+ notification(subtitle: "Finished", message: "Certificates done")
+end
+
+lane :createAPNS do
+ get_push_certificate(
+ force: true, # create a new profile, even if the old one is still valid
+ app_identifier: project_identifier, # optional app identifier,
+ username: itunes_username,
+ p12_password: apns_pass,
+ pem_name: apns_pem_name,
+ output_path: "fastlane/APNS",
+ save_private_key: true,
+ new_profile: proc do |profile_path| # this block gets called when a new profile was generated
+ puts profile_path # the absolute path to the new PEM file
+ # insert the code to upload the PEM file to the server
+ end
+ )
+
+ # msgToSlack("Successfully APNS Created", true)
+end
+
+desc "Distribute app via Firebase for testers"
+lane :firebase_test do
+ adhoc_certificates
+ build_ios_app(
+ scheme: project_scheme,
+ output_directory: "./fastlane/builds/#{project_scheme}",
+ output_name: "#{project_scheme}.ipa",
+ export_options: {
+ method: "ad-hoc",
+ compileBitcode: false
+ },
+ clean: true,
+ include_bitcode: true,
+ include_symbols: true,
+ skip_package_ipa: false
+ )
+
+# firebase_app_distribution(
+# app: "1:880592554695:ios:af464a9ed02207f6284ce2",
+# testers: "dartsyms@gmail.com, shogun14@yandex.ru, qa@wevied.com",
+# release_notes: "Another build: ready to test."
+# )
+
+ clean_build_artifacts
+ increment_build_number
+end
+
+desc "Distribute app via Firebase for testers"
+lane :distribute_firebase do
+ adhoc_certificates
+ clean_build_artifacts
+
+ build_ios_app(
+ scheme: project_scheme,
+ output_directory: "./fastlane/builds/#{project_scheme}",
+ output_name: "#{project_scheme}.ipa",
+ export_options: {
+ method: "ad-hoc",
+ compileBitcode: false
+ },
+ clean: true,
+ include_bitcode: true,
+ include_symbols: true,
+ skip_package_ipa: false
+ )
+
+# firebase_app_distribution(
+# app: "1:880592554695:ios:af464a9ed02207f6284ce2",
+# testers: "dartsyms@gmail.com, shogun14@yandex.ru, qa@wevied.com",
+# release_notes: "Another build: ready to test."
+# )
+end
+
+def msgToSlack(msg, result)
+ slack(
+ message: msg,
+ success: result,
+ default_payloads: [:lane, :test_result, :git_branch, :git_author] # Optional, lets you specify a whitelist of default payloads to include. Pass an empty array to suppress all the default payloads.
+ # Don't add this key, or pass nil, if you want all the default payloads. The available default payloads are: `lane`, `test_result`, `git_branch`, `git_author`, `last_git_commit_message`, `last_git_commit_hash`.
+ )
+end
diff --git a/client/fastlane/Matchfile b/client/fastlane/Matchfile
new file mode 100644
index 00000000..2da8aed0
--- /dev/null
+++ b/client/fastlane/Matchfile
@@ -0,0 +1,13 @@
+git_url("https://github.com/amnezia-vpn/amnezia-ios-certificates")
+
+storage_mode("git")
+
+type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
+
+# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
+# username("user@fastlane.tools") # Your Apple Developer Portal username
+
+# For all available options run `fastlane match --help`
+# Remove the # in the beginning of the line to enable the other options
+
+# The docs are available on https://docs.fastlane.tools/actions/match
diff --git a/client/fastlane/Pluginfile b/client/fastlane/Pluginfile
new file mode 100644
index 00000000..50b5b39e
--- /dev/null
+++ b/client/fastlane/Pluginfile
@@ -0,0 +1,6 @@
+# Autogenerated by fastlane
+#
+# Ensure this file is checked in to source control!
+
+#gem 'fastlane-plugin-run_tests_firebase_testlab'
+#gem 'fastlane-plugin-firebase_app_distribution'
diff --git a/client/fastlane/README.md b/client/fastlane/README.md
new file mode 100644
index 00000000..3375b46f
--- /dev/null
+++ b/client/fastlane/README.md
@@ -0,0 +1,88 @@
+fastlane documentation
+----
+
+# Installation
+
+Make sure you have the latest version of the Xcode command line tools installed:
+
+```sh
+xcode-select --install
+```
+
+For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
+
+# Available Actions
+
+### beta
+
+```sh
+[bundle exec] fastlane beta
+```
+
+
+
+### incrementVersion
+
+```sh
+[bundle exec] fastlane incrementVersion
+```
+
+
+
+### addToTestFlight
+
+```sh
+[bundle exec] fastlane addToTestFlight
+```
+
+Update Certificates, Run All tests, Build app
+
+Add tag to git, send to Testflight, slack notification
+
+### certificates
+
+```sh
+[bundle exec] fastlane certificates
+```
+
+
+
+### adhoc_certificates
+
+```sh
+[bundle exec] fastlane adhoc_certificates
+```
+
+
+
+### createAPNS
+
+```sh
+[bundle exec] fastlane createAPNS
+```
+
+
+
+### firebase_test
+
+```sh
+[bundle exec] fastlane firebase_test
+```
+
+Distribute app via Firebase for testers
+
+### distribute_firebase
+
+```sh
+[bundle exec] fastlane distribute_firebase
+```
+
+Distribute app via Firebase for testers
+
+----
+
+This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
+
+More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
+
+The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
diff --git a/client/fastlane/report.xml b/client/fastlane/report.xml
new file mode 100644
index 00000000..279b432b
--- /dev/null
+++ b/client/fastlane/report.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/ios/app/Info.plist b/client/ios/app/Info.plist
index 3c0f289e..73a71c11 100644
--- a/client/ios/app/Info.plist
+++ b/client/ios/app/Info.plist
@@ -25,13 +25,15 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
+ 3
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
LSSupportsOpeningDocumentsInPlace
+ NSCameraUsageDescription
+ Amnezia VPN needs access to the camera for reading QR-codes.
UILaunchStoryboardName
AmneziaVPNLaunchScreen
UIRequiresFullScreen
diff --git a/client/ios/app/main.entitlements b/client/ios/app/main.entitlements
index 23a6914f..d9f00bb1 100644
--- a/client/ios/app/main.entitlements
+++ b/client/ios/app/main.entitlements
@@ -8,13 +8,13 @@
com.apple.security.application-groups
- group.ru.kotit.AmneziaVPN.udev
+ group.org.amnezia.AmneziaVPN
com.apple.security.files.user-selected.read-write
keychain-access-groups
- $(AppIdentifierPrefix)group.ru.kotit.AmneziaVPN.udev
+ $(AppIdentifierPrefix)group.org.amnezia.AmneziaVPN
diff --git a/client/ios/networkextension/AmneziaVPNNetworkExtension.entitlements b/client/ios/networkextension/AmneziaVPNNetworkExtension.entitlements
index 7d350b19..fd6983a9 100644
--- a/client/ios/networkextension/AmneziaVPNNetworkExtension.entitlements
+++ b/client/ios/networkextension/AmneziaVPNNetworkExtension.entitlements
@@ -8,11 +8,17 @@
com.apple.security.application-groups
- group.ru.kotit.AmneziaVPN.udev
+ group.org.amnezia.AmneziaVPN
keychain-access-groups
- $(AppIdentifierPrefix)group.ru.kotit.AmneziaVPN.udev
+ $(AppIdentifierPrefix)group.org.amnezia.AmneziaVPN
+
+
+
+
+
+
diff --git a/client/macos/networkextension/Info.plist b/client/macos/networkextension/Info.plist
index 96d82459..b19b1255 100644
--- a/client/macos/networkextension/Info.plist
+++ b/client/macos/networkextension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
+ 3
ITSAppUsesNonExemptEncryption
LSMinimumSystemVersion
diff --git a/client/macos/networkextension/WireGuardNetworkExtension-Bridging-Header.h b/client/macos/networkextension/WireGuardNetworkExtension-Bridging-Header.h
index bac84dfe..8fbada72 100644
--- a/client/macos/networkextension/WireGuardNetworkExtension-Bridging-Header.h
+++ b/client/macos/networkextension/WireGuardNetworkExtension-Bridging-Header.h
@@ -5,6 +5,8 @@
#include "macos/gobridge/wireguard.h"
#include "wireguard-go-version.h"
#include "3rd/wireguard-apple/Sources/WireGuardKitC/WireGuardKitC.h"
+//#include "../../platforms/ios/Shadowsocks.h"
+
#include
#include
diff --git a/client/platforms/ios/Shadowsocks.h b/client/platforms/ios/Shadowsocks.h
new file mode 100644
index 00000000..8b9e3278
--- /dev/null
+++ b/client/platforms/ios/Shadowsocks.h
@@ -0,0 +1,65 @@
+// Copyright 2018 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef Shadowsocks_h
+#define Shadowsocks_h
+
+#import
+
+/**
+ * Manages the lifecycle and configuration of ss-local, the Shadowsocks client library.
+ */
+@interface Shadowsocks : NSObject
+
+extern const int kShadowsocksLocalPort;
+
+typedef NS_ENUM(NSInteger, ErrorCode) {
+ noError = 0,
+ undefinedError = 1,
+ vpnPermissionNotGranted = 2,
+ invalidServerCredentials = 3,
+ udpRelayNotEnabled = 4,
+ serverUnreachable = 5,
+ vpnStartFailure = 6,
+ illegalServerConfiguration = 7,
+ shadowsocksStartFailure = 8,
+ configureSystemProxyFailure = 9,
+ noAdminPermissions = 10,
+ unsupportedRoutingTable = 11,
+ systemMisconfigured = 12
+};
+
+@property (nonatomic) NSDictionary *config;
+
+/**
+ * Initializes the object with a Shadowsocks server configuration, |config|.
+ */
+- (id)init:(NSDictionary *)config;
+
+/**
+ * Starts ss-local on a separate thread with the configuration supplied in the constructor.
+ * If |checkConnectivity| is true, verifies that the server credentials are valid and that
+ * the remote supports UDP forwarding, calling |completion| with the result.
+ */
+- (void)startWithConnectivityChecks:(bool)checkConnectivity
+ completion:(void (^)(ErrorCode))completion;
+
+/**
+ * Stops the thread running ss-local. Calls |completion| with the success of the operation.
+ */
+- (void)stop:(void (^)(ErrorCode))completion;
+
+@end
+
+#endif /* Shadowsocks_h */
\ No newline at end of file
diff --git a/client/platforms/ios/Shadowsocks.m b/client/platforms/ios/Shadowsocks.m
new file mode 100644
index 00000000..ca843659
--- /dev/null
+++ b/client/platforms/ios/Shadowsocks.m
@@ -0,0 +1,202 @@
+// Copyright 2018 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Shadowsocks.h"
+#include
+#include
+#import "ShadowsocksConnectivity.h"
+#import
+
+const int kShadowsocksLocalPort = 9999;
+static const int kShadowsocksTimeoutSecs = INT_MAX;
+static const int kShadowsocksTcpAndUdpMode =
+ 1; // See https://github.com/shadowsocks/shadowsocks-libev/blob/4ea517/src/jconf.h#L44
+static char *const kShadowsocksLocalAddress = "127.0.0.1";
+
+@interface Shadowsocks ()
+@property (nonatomic) pthread_t ssLocalThreadId;
+@property (nonatomic, copy) void (^startCompletion)(ErrorCode);
+@property (nonatomic, copy) void (^stopCompletion)(ErrorCode);
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+@property (nonatomic) dispatch_group_t dispatchGroup;
+@property(nonatomic) bool checkConnectivity;
+@property(nonatomic) ShadowsocksConnectivity *ssConnectivity;
+@end
+
+@implementation Shadowsocks
+
+- (id) init:(NSDictionary *)config {
+ self = [super init];
+ if (self) {
+ _config = config;
+ _dispatchQueue = dispatch_queue_create("Shadowsocks", DISPATCH_QUEUE_SERIAL);
+ _dispatchGroup = dispatch_group_create();
+ _ssConnectivity = [[ShadowsocksConnectivity alloc] initWithPort:kShadowsocksLocalPort];
+ }
+ return self;
+}
+
+- (void)startWithConnectivityChecks:(bool)checkConnectivity
+ completion:(void (^)(ErrorCode))completion {
+ if (self.ssLocalThreadId != 0) {
+ return completion(shadowsocksStartFailure);
+ }
+ self.checkConnectivity = checkConnectivity;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // Start ss-local from the main application thread.
+ self.startCompletion = completion;
+ [self startShadowsocksThread];
+ });
+}
+
+-(void)stop:(void (^)(ErrorCode))completion {
+ if (self.ssLocalThreadId == 0) {
+ return;
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // The ev_loop in the ss-local thread will not break unless it is signaled to stop from the main
+ // application thread.
+ self.stopCompletion = completion;
+ pthread_kill(self.ssLocalThreadId, SIGUSR1);
+ self.ssLocalThreadId = 0;
+ });
+}
+
+#pragma mark - Lifecycle
+
+void shadowsocksCallback(int socks_fd, int udp_fd, void *udata) {
+ if (socks_fd <= 0 || udp_fd <= 0) {
+ return;
+ }
+ Shadowsocks* ss = (__bridge Shadowsocks *)udata;
+ [ss checkServerConnectivity];
+}
+
+- (void)startShadowsocks {
+ if (self.config == nil) {
+ self.startCompletion(illegalServerConfiguration);
+ return;
+ }
+ int port = [self.config[@"port"] intValue];
+ char *host = (char *)[self.config[@"host"] UTF8String];
+ char *password = (char *)[self.config[@"password"] UTF8String];
+ char *method = (char *)[self.config[@"method"] UTF8String];
+ const profile_t profile = {.remote_host = host,
+ .local_addr = kShadowsocksLocalAddress,
+ .method = method,
+ .password = password,
+ .remote_port = port,
+ .local_port = kShadowsocksLocalPort,
+ .timeout = kShadowsocksTimeoutSecs,
+ .acl = NULL,
+ .log = NULL,
+ .fast_open = 0,
+ .mode = kShadowsocksTcpAndUdpMode,
+ .verbose = 0};
+ int success = start_ss_local_server_with_callback(profile, shadowsocksCallback,
+ (__bridge void *)self);
+ if (success < 0) {
+ self.startCompletion(shadowsocksStartFailure);
+ return;
+ }
+
+ if (self.stopCompletion) {
+ self.stopCompletion(noError);
+ self.stopCompletion = nil;
+ }
+}
+
+// Entry point for the Shadowsocks POSIX thread.
+void *startShadowsocks(void *udata) {
+ Shadowsocks* ss = (__bridge Shadowsocks *)udata;
+ [ss startShadowsocks];
+ return NULL;
+}
+
+// Starts a POSIX thread that runs ss-local.
+- (void)startShadowsocksThread {
+ pthread_attr_t attr;
+ int err = pthread_attr_init(&attr);
+ if (err) {
+ self.startCompletion(shadowsocksStartFailure);
+ return;
+ }
+ err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ if (err) {
+ self.startCompletion(shadowsocksStartFailure);
+ return;
+ }
+ err = pthread_create(&_ssLocalThreadId, &attr, &startShadowsocks, (__bridge void *)self);
+ if (err) {
+ self.startCompletion(shadowsocksStartFailure);
+ }
+ err = pthread_attr_destroy(&attr);
+ if (err) {
+ self.startCompletion(shadowsocksStartFailure);
+ return;
+ }
+}
+
+#pragma mark - Connectivity
+
+/**
+ * Checks that the remote server is reachable, allows UDP forwarding, and the credentials are valid.
+ * Synchronizes and parallelizes the execution of the connectivity checks and calls
+ * |startCompletion| with the combined outcome.
+ * Only performs the tests if |checkConnectivity| is true; otherwise calls |startCompletion|
+ * with success.
+ */
+- (void) checkServerConnectivity {
+ if (!self.checkConnectivity) {
+ self.startCompletion(noError);
+ return;
+ }
+ __block BOOL isRemoteUdpForwardingEnabled = false;
+ __block BOOL serverCredentialsAreValid = false;
+ __block BOOL isServerReachable = false;
+
+ // Enter the group once for each check
+ dispatch_group_enter(self.dispatchGroup);
+ dispatch_group_enter(self.dispatchGroup);
+ dispatch_group_enter(self.dispatchGroup);
+
+ [self.ssConnectivity isUdpForwardingEnabled:^(BOOL enabled) {
+ isRemoteUdpForwardingEnabled = enabled;
+ dispatch_group_leave(self.dispatchGroup);
+ }];
+ [self.ssConnectivity isReachable:self.config[@"host"]
+ port:[self.config[@"port"] intValue]
+ completion:^(BOOL isReachable) {
+ isServerReachable = isReachable;
+ dispatch_group_leave(self.dispatchGroup);
+ }];
+ [self.ssConnectivity checkServerCredentials:^(BOOL valid) {
+ serverCredentialsAreValid = valid;
+ dispatch_group_leave(self.dispatchGroup);
+ }];
+
+ dispatch_group_notify(self.dispatchGroup, self.dispatchQueue, ^{
+ if (isRemoteUdpForwardingEnabled) {
+ self.startCompletion(noError);
+ } else if (serverCredentialsAreValid) {
+ self.startCompletion(udpRelayNotEnabled);
+ } else if (isServerReachable) {
+ self.startCompletion(invalidServerCredentials);
+ } else {
+ self.startCompletion(serverUnreachable);
+ }
+ });
+}
+
+@end
diff --git a/client/platforms/ios/ShadowsocksConnectivity.h b/client/platforms/ios/ShadowsocksConnectivity.h
new file mode 100644
index 00000000..a2c68bdc
--- /dev/null
+++ b/client/platforms/ios/ShadowsocksConnectivity.h
@@ -0,0 +1,50 @@
+// Copyright 2018 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ShadowsocksConnectivity_h
+#define ShadowsocksConnectivity_h
+
+#import
+
+/**
+ * Non-thread-safe class to perform Shadowsocks connectivity checks.
+ */
+@interface ShadowsocksConnectivity : NSObject
+
+/**
+ * Initializes the object with a local Shadowsocks port, |shadowsocksPort|.
+ */
+- (id)initWithPort:(uint16_t)shadowsocksPort;
+
+/**
+ * Verifies that the server has enabled UDP forwarding. Performs an end-to-end test by sending
+ * a DNS request through the proxy. This method is a superset of |checkServerCredentials|, as its
+ * success implies that the server credentials are valid.
+ */
+- (void)isUdpForwardingEnabled:(void (^)(BOOL))completion;
+
+/**
+ * Verifies that the server credentials are valid. Performs an end-to-end authentication test
+ * by issuing an HTTP HEAD request to a target domain through the proxy.
+ */
+- (void)checkServerCredentials:(void (^)(BOOL))completion;
+
+/**
+ * Checks that the server is reachable on |host| and |port|.
+ */
+- (void)isReachable:(NSString *)host port:(uint16_t)port completion:(void (^)(BOOL))completion;
+
+@end
+
+#endif /* ShadowsocksConnectivity_h */
diff --git a/client/platforms/ios/ShadowsocksConnectivity.m b/client/platforms/ios/ShadowsocksConnectivity.m
new file mode 100644
index 00000000..d6723ba1
--- /dev/null
+++ b/client/platforms/ios/ShadowsocksConnectivity.m
@@ -0,0 +1,333 @@
+// Copyright 2018 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "ShadowsocksConnectivity.h"
+#include
+#import
+#import
+
+//@import CocoaAsyncSocket;
+
+static char *const kShadowsocksLocalAddress = "127.0.0.1";
+
+static char *const kDnsResolverAddress = "208.67.222.222"; // OpenDNS
+static const uint16_t kDnsResolverPort = 53;
+static const size_t kDnsRequestNumBytes = 28;
+
+static const size_t kSocksHeaderNumBytes = 10;
+static const uint8_t kSocksMethodsResponseNumBytes = 2;
+static const size_t kSocksConnectResponseNumBytes = 10;
+static const uint8_t kSocksVersion = 0x5;
+static const uint8_t kSocksMethodNoAuth = 0x0;
+static const uint8_t kSocksCmdConnect = 0x1;
+static const uint8_t kSocksAtypIpv4 = 0x1;
+static const uint8_t kSocksAtypDomainname = 0x3;
+
+static const NSTimeInterval kTcpSocketTimeoutSecs = 10.0;
+static const NSTimeInterval kUdpSocketTimeoutSecs = 1.0;
+static const long kSocketTagHttpRequest = 100;
+static const int kUdpForwardingMaxChecks = 5;
+static const uint16_t kHttpPort = 80;
+
+@interface ShadowsocksConnectivity ()
+
+@property(nonatomic) uint16_t shadowsocksPort;
+
+@property(nonatomic, copy) void (^udpForwardingCompletion)(BOOL);
+@property(nonatomic, copy) void (^reachabilityCompletion)(BOOL);
+@property(nonatomic, copy) void (^credentialsCompletion)(BOOL);
+
+@property(nonatomic) dispatch_queue_t dispatchQueue;
+@property(nonatomic) GCDAsyncUdpSocket *udpSocket;
+@property(nonatomic) GCDAsyncSocket *credentialsSocket;
+@property(nonatomic) GCDAsyncSocket *reachabilitySocket;
+
+@property(nonatomic) bool isRemoteUdpForwardingEnabled;
+@property(nonatomic) bool areServerCredentialsValid;
+@property(nonatomic) bool isServerReachable;
+@property(nonatomic) int udpForwardingNumChecks;
+@end
+
+@implementation ShadowsocksConnectivity
+
+- (id)initWithPort:(uint16_t)shadowsocksPort {
+ self = [super init];
+ if (self) {
+ _shadowsocksPort = shadowsocksPort;
+ _dispatchQueue = dispatch_queue_create("ShadowsocksConnectivity", DISPATCH_QUEUE_SERIAL);
+ }
+ return self;
+}
+
+#pragma mark - UDP Forwarding
+
+struct socks_udp_header {
+ uint16_t rsv;
+ uint8_t frag;
+ uint8_t atyp;
+ uint32_t addr;
+ uint16_t port;
+};
+
+- (void)isUdpForwardingEnabled:(void (^)(BOOL))completion {
+ self.isRemoteUdpForwardingEnabled = false;
+ self.udpForwardingNumChecks = 0;
+ self.udpForwardingCompletion = completion;
+ self.udpSocket =
+ [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:self.dispatchQueue];
+ struct in_addr dnsResolverAddress;
+ if (!inet_aton(kDnsResolverAddress, &dnsResolverAddress)) {
+ [self udpForwardingCheckDone:false];
+ return;
+ }
+ struct socks_udp_header socksHeader = {
+ .atyp = kSocksAtypIpv4,
+ .addr = dnsResolverAddress.s_addr, // Already in network order
+ .port = htons(kDnsResolverPort)};
+ uint8_t *dnsRequest = [self getDnsRequest];
+ size_t packetNumBytes = kSocksHeaderNumBytes + kDnsRequestNumBytes;
+ uint8_t socksPacket[packetNumBytes];
+ memset(socksPacket, 0, packetNumBytes);
+ memcpy(socksPacket, &socksHeader, kSocksHeaderNumBytes);
+ memcpy(socksPacket + kSocksHeaderNumBytes, dnsRequest, kDnsRequestNumBytes);
+
+ NSData *packetData = [[NSData alloc] initWithBytes:socksPacket length:packetNumBytes];
+
+ dispatch_source_t timer =
+ dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.dispatchQueue);
+ if (!timer) {
+ [self udpForwardingCheckDone:false];
+ return;
+ }
+ dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0),
+ kUdpSocketTimeoutSecs * NSEC_PER_SEC, 0);
+ __weak ShadowsocksConnectivity *weakSelf = self;
+ dispatch_source_set_event_handler(timer, ^{
+ if (++weakSelf.udpForwardingNumChecks > kUdpForwardingMaxChecks ||
+ weakSelf.isRemoteUdpForwardingEnabled) {
+ dispatch_source_cancel(timer);
+ if (!weakSelf.isRemoteUdpForwardingEnabled) {
+ [weakSelf udpForwardingCheckDone:false];
+ }
+ [weakSelf.udpSocket close];
+ return;
+ }
+ [weakSelf.udpSocket sendData:packetData
+ toHost:[[NSString alloc] initWithUTF8String:kShadowsocksLocalAddress]
+ port:self.shadowsocksPort
+ withTimeout:kUdpSocketTimeoutSecs
+ tag:0];
+ if (![weakSelf.udpSocket receiveOnce:nil]) {
+ }
+ });
+ dispatch_resume(timer);
+}
+
+// Returns a byte representation of a DNS request for "google.com".
+- (uint8_t *)getDnsRequest {
+ static uint8_t kDnsRequest[] = {
+ 0, 0, // [0-1] query ID
+ 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD).
+ 0, 1, // [4-5] QDCOUNT (number of queries)
+ 0, 0, // [6-7] ANCOUNT (number of answers)
+ 0, 0, // [8-9] NSCOUNT (number of name server records)
+ 0, 0, // [10-11] ARCOUNT (number of additional records)
+ 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm',
+ 0, // null terminator of FQDN (root TLD)
+ 0, 1, // QTYPE, set to A
+ 0, 1 // QCLASS, set to 1 = IN (Internet)
+ };
+ return kDnsRequest;
+}
+
+#pragma mark - GCDAsyncUdpSocketDelegate
+
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock
+ didNotSendDataWithTag:(long)tag
+ dueToError:(NSError *)error {
+}
+
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock
+ didReceiveData:(NSData *)data
+ fromAddress:(NSData *)address
+ withFilterContext:(id)filterContext {
+ if (!self.isRemoteUdpForwardingEnabled) {
+ // Only report success if it hasn't been done so already.
+ [self udpForwardingCheckDone:true];
+ }
+}
+
+- (void)udpForwardingCheckDone:(BOOL)enabled {
+ self.isRemoteUdpForwardingEnabled = enabled;
+ if (self.udpForwardingCompletion != NULL) {
+ self.udpForwardingCompletion(self.isRemoteUdpForwardingEnabled);
+ self.udpForwardingCompletion = NULL;
+ }
+}
+
+#pragma mark - Credentials
+
+struct socks_methods_request {
+ uint8_t ver;
+ uint8_t nmethods;
+ uint8_t method;
+};
+
+struct socks_request_header {
+ uint8_t ver;
+ uint8_t cmd;
+ uint8_t rsv;
+ uint8_t atyp;
+};
+
+- (void)checkServerCredentials:(void (^)(BOOL))completion {
+ self.areServerCredentialsValid = false;
+ self.credentialsCompletion = completion;
+ self.credentialsSocket =
+ [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.dispatchQueue];
+ NSError *error;
+ [self.credentialsSocket
+ connectToHost:[[NSString alloc] initWithUTF8String:kShadowsocksLocalAddress]
+ onPort:self.shadowsocksPort
+ withTimeout:kTcpSocketTimeoutSecs
+ error:&error];
+ if (error) {
+ [self serverCredentialsCheckDone];
+ return;
+ }
+
+ struct socks_methods_request methodsRequest = {
+ .ver = kSocksVersion, .nmethods = 0x1, .method = kSocksMethodNoAuth};
+ NSData *methodsRequestData =
+ [[NSData alloc] initWithBytes:&methodsRequest length:sizeof(struct socks_methods_request)];
+ [self.credentialsSocket writeData:methodsRequestData withTimeout:kTcpSocketTimeoutSecs tag:0];
+ [self.credentialsSocket readDataToLength:kSocksMethodsResponseNumBytes
+ withTimeout:kTcpSocketTimeoutSecs
+ tag:0];
+
+ size_t socksRequestHeaderNumBytes = sizeof(struct socks_request_header);
+ NSString *domain = [self chooseRandomDomain];
+ uint8_t domainNameNumBytes = domain.length;
+ size_t socksRequestNumBytes = socksRequestHeaderNumBytes + domainNameNumBytes +
+ sizeof(uint16_t) /* port */ +
+ sizeof(uint8_t) /* domain name length */;
+
+ struct socks_request_header socksRequestHeader = {
+ .ver = kSocksVersion, .cmd = kSocksCmdConnect, .atyp = kSocksAtypDomainname};
+ uint8_t socksRequest[socksRequestNumBytes];
+ memset(socksRequest, 0x0, socksRequestNumBytes);
+ memcpy(socksRequest, &socksRequestHeader, socksRequestHeaderNumBytes);
+ socksRequest[socksRequestHeaderNumBytes] = domainNameNumBytes;
+ memcpy(socksRequest + socksRequestHeaderNumBytes + sizeof(uint8_t), [domain UTF8String],
+ domainNameNumBytes);
+ uint16_t httpPort = htons(kHttpPort);
+ memcpy(socksRequest + socksRequestHeaderNumBytes + sizeof(uint8_t) + domainNameNumBytes,
+ &httpPort, sizeof(uint16_t));
+
+ NSData *socksRequestData =
+ [[NSData alloc] initWithBytes:socksRequest length:socksRequestNumBytes];
+ [self.credentialsSocket writeData:socksRequestData withTimeout:kTcpSocketTimeoutSecs tag:0];
+ [self.credentialsSocket readDataToLength:kSocksConnectResponseNumBytes
+ withTimeout:kTcpSocketTimeoutSecs
+ tag:0];
+
+ NSString *httpRequest =
+ [[NSString alloc] initWithFormat:@"HEAD / HTTP/1.1\r\nHost: %@\r\n\r\n", domain];
+ [self.credentialsSocket
+ writeData:[NSData dataWithBytes:[httpRequest UTF8String] length:httpRequest.length]
+ withTimeout:kTcpSocketTimeoutSecs
+ tag:kSocketTagHttpRequest];
+ [self.credentialsSocket readDataWithTimeout:kTcpSocketTimeoutSecs tag:kSocketTagHttpRequest];
+ [self.credentialsSocket disconnectAfterReading];
+}
+
+// Returns a statically defined array containing domain names for validating server credentials.
++ (const NSArray *)getCredentialsValidationDomains {
+ static const NSArray *kCredentialsValidationDomains;
+ static dispatch_once_t kDispatchOnceToken;
+ dispatch_once(&kDispatchOnceToken, ^{
+ // We have chosen these domains due to their neutrality.
+ kCredentialsValidationDomains =
+ @[ @"eff.org", @"ietf.org", @"w3.org", @"wikipedia.org", @"example.com" ];
+ });
+ return kCredentialsValidationDomains;
+}
+
+// Returns a random domain from |kCredentialsValidationDomains|.
+- (NSString *)chooseRandomDomain {
+ const NSArray *domains = [ShadowsocksConnectivity getCredentialsValidationDomains];
+ int index = arc4random_uniform((uint32_t)domains.count);
+ return domains[index];
+}
+
+// Calls |credentialsCompletion| once with |areServerCredentialsValid|.
+- (void)serverCredentialsCheckDone {
+ if (self.credentialsCompletion != NULL) {
+ self.credentialsCompletion(self.areServerCredentialsValid);
+ self.credentialsCompletion = NULL;
+ }
+}
+
+#pragma mark - Reachability
+
+- (void)isReachable:(NSString *)host port:(uint16_t)port completion:(void (^)(BOOL))completion {
+ self.isServerReachable = false;
+ self.reachabilityCompletion = completion;
+ self.reachabilitySocket =
+ [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.dispatchQueue];
+ NSError *error;
+ [self.reachabilitySocket connectToHost:host
+ onPort:port
+ withTimeout:kTcpSocketTimeoutSecs
+ error:&error];
+ if (error) {
+ return;
+ }
+}
+
+// Calls |reachabilityCompletion| once with |isServerReachable|.
+- (void)reachabilityCheckDone {
+ if (self.reachabilityCompletion != NULL) {
+ self.reachabilityCompletion(self.isServerReachable);
+ self.reachabilityCompletion = NULL;
+ }
+}
+
+#pragma mark - GCDAsyncSocketDelegate
+
+- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
+ // We don't need to inspect any of the data, as the SOCKS responses are hardcoded in ss-local and
+ // the fact that we have read the HTTP response indicates that the server credentials are valid.
+ if (tag == kSocketTagHttpRequest && data != nil) {
+ NSString *httpResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ self.areServerCredentialsValid = httpResponse != nil && [httpResponse hasPrefix:@"HTTP/1.1"];
+ }
+}
+
+- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
+ if ([self.reachabilitySocket isEqual:sock]) {
+ self.isServerReachable = true;
+ [self.reachabilitySocket disconnect];
+ }
+}
+
+- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
+ if ([self.reachabilitySocket isEqual:sock]) {
+ [self reachabilityCheckDone];
+ } else {
+ [self serverCredentialsCheckDone];
+ }
+}
+
+@end
diff --git a/client/platforms/ios/Subnet.swift b/client/platforms/ios/Subnet.swift
new file mode 100644
index 00000000..cfd83bda
--- /dev/null
+++ b/client/platforms/ios/Subnet.swift
@@ -0,0 +1,86 @@
+// Copyright 2018 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+// Represents an IP subnetwork.
+@objcMembers
+class Subnet: NSObject {
+ static let kReservedSubnets = [
+ "10.0.0.0/8",
+ "100.64.0.0/10",
+ "169.254.0.0/16",
+ "172.16.0.0/12",
+ "192.0.0.0/24",
+ "192.0.2.0/24",
+ "192.31.196.0/24",
+ "192.52.193.0/24",
+ "192.88.99.0/24",
+ "192.168.0.0/16",
+ "192.175.48.0/24",
+ "198.18.0.0/15",
+ "198.51.100.0/24",
+ "203.0.113.0/24",
+ "240.0.0.0/4"
+ ]
+
+ // Parses a CIDR subnet into a Subnet object. Returns nil on failure.
+ static func parse(_ cidrSubnet: String) -> Subnet? {
+ let components = cidrSubnet.components(separatedBy: "/")
+ guard components.count == 2 else {
+ NSLog("Malformed CIDR subnet")
+ return nil
+ }
+ guard let prefix = UInt16(components[1]) else {
+ NSLog("Invalid subnet prefix")
+ return nil
+ }
+ return Subnet(address: components[0], prefix: prefix)
+ }
+
+ // Returns a list of reserved Subnets.
+ static func getReservedSubnets() -> [Subnet] {
+ var subnets: [Subnet] = []
+ for cidrSubnet in kReservedSubnets {
+ if let subnet = self.parse(cidrSubnet) {
+ subnets.append(subnet)
+ }
+ }
+ return subnets
+ }
+
+ public var address: String
+ public var prefix: UInt16
+ public var mask: String
+
+ init(address: String, prefix: UInt16) {
+ self.address = address
+ self.prefix = prefix
+ let mask = (0xffffffff as UInt32) << (32 - prefix);
+ self.mask = mask.IPv4String()
+ }
+}
+
+extension UInt32 {
+ // Returns string representation of the integer as an IP address.
+ public func IPv4String() -> String {
+ let ip = self
+ let a = UInt8((ip>>24) & 0xff)
+ let b = UInt8((ip>>16) & 0xff)
+ let c = UInt8((ip>>8) & 0xff)
+ let d = UInt8(ip & 0xff)
+ return "\(a).\(b).\(c).\(d)"
+ }
+}
+
diff --git a/client/platforms/ios/iostunnel.swift b/client/platforms/ios/iostunnel.swift
index 1bcc148b..080a8946 100644
--- a/client/platforms/ios/iostunnel.swift
+++ b/client/platforms/ios/iostunnel.swift
@@ -5,8 +5,8 @@ import Foundation
import NetworkExtension
import os
import OpenVPNAdapter
-import ShadowSocks
-//import Tun2Socks
+//import ShadowSocks
+//import Tun2socks
enum TunnelProtoType: String {
case wireguard, openvpn, shadowsocks, none
@@ -28,11 +28,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return adapter
}()
- private var shadowSocksPort: Int32 = 0
- private var isShadowsocksRunning: Bool = false
- var ssCompletion: ShadowsocksProxyCompletion = nil
- private let ssQueue = DispatchQueue(label: "org.amnezia.shadowsocks")
private var shadowSocksConfig: Data? = nil
+ var ssCompletion: ShadowsocksProxyCompletion = nil
+
+// private var shadowSocksPort: Int32 = 0
+// private var isShadowsocksRunning: Bool = false
+// private let ssQueue = DispatchQueue(label: "org.amnezia.shadowsocks")
// private var tun2socksWriter: AmneziaTun2SocksWriter? = nil
// private var tun2socksTunnel: Tun2socksOutlineTunnelProtocol? = nil
@@ -67,9 +68,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
case .openvpn:
startOpenVPN(completionHandler: completionHandler)
case .shadowsocks:
- startShadowSocks { error in
-
- }
+ startShadowSocks(completionHandler: completionHandler)
case .none:
break
}
@@ -207,8 +206,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
wg_log(.error, message: "Cannot start startShadowSocks()")
return
}
-// self.shadowSocksConfig = ssConfiguration
-//
+ self.shadowSocksConfig = ssConfiguration
+
// guard let config = self.shadowSocksConfig else { return }
// guard let ssConfig = try? JSONSerialization.jsonObject(with: config, options: []) as? [String: Any] else {
// self.ssCompletion?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
@@ -216,36 +215,50 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// userInfo: [NSLocalizedDescriptionKey: "Cannot parse json for ss in tunnel"]))
// return
// }
-
-// let sshost = ssConfig["local_addr"] as? String
-// let ssport = ssConfig["local_port"] as? Int ?? Int(self.shadowSocksPort)
//
+// let sshost = ssConfig["local_addr"] as? String
+// let ssport = ssConfig["local_port"] as? Int ?? 8585
// let method = ssConfig["method"] as? String
// let password = ssConfig["password"] as? String
+//
+// var errorCode: Int = 0
+// ShadowsocksCheckConnectivity(sshost, ssport, password, method, &errorCode, nil)
+// if (errorCode != 0) {
+// self.ssCompletion?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
+// code: 100,
+// userInfo: [NSLocalizedDescriptionKey: "Error checking ss connectivity: \(errorCode)"]))
+// return
+// }
-
-// Thread.detachNewThread { [weak self] in
- setupAndLaunchShadowSocksProxy(withConfig: ssConfiguration, ssHandler: { [weak self] port, error in
- wg_log(.info,
- message: "Prepare to start openvpn, self is \(self == nil ? "null" : "not null")")
+ self.setupAndlaunchOpenVPN(withConfig: ovpnConfiguration) { error in
guard error == nil else {
- wg_log(.error, message: "Stopping tunnel: \(error?.localizedDescription ?? "none")")
+ wg_log(.error, message: "Start OpenVPN tunnel error : \(error?.localizedDescription ?? "none")")
completionHandler(error!)
return
}
-
- self?.setupAndlaunchOpenVPN(withConfig: ovpnConfiguration) { error in
- guard error == nil else {
- wg_log(.error, message: "Start OpenVPN tunnel error : \(error?.localizedDescription ?? "none")")
- completionHandler(error!)
- return
- }
- wg_log(.error, message: "OpenVPN tunnel connected.")
- }
-
-// self?.startTun2Socks(host: sshost, port: ssport, password: password, cipher: method, isUDPEnabled: false, error: nil)
- })
-// }
+ wg_log(.error, message: "OpenVPN tunnel connected.")
+// self.startTun2Socks(host: sshost, port: ssport, password: password, cipher: method, isUDPEnabled: false, error: nil)
+ }
+//// Thread.detachNewThread { [weak self] in
+//// setupAndLaunchShadowSocksProxy(withConfig: ssConfiguration, ssHandler: { [weak self] port, error in
+//// wg_log(.info,
+//// message: "Prepare to start openvpn, self is \(self == nil ? "null" : "not null")")
+//// guard error == nil else {
+//// wg_log(.error, message: "Stopping tunnel: \(error?.localizedDescription ?? "none")")
+//// completionHandler(error!)
+//// return
+//// }
+////
+//// self?.setupAndlaunchOpenVPN(withConfig: ovpnConfiguration) { error in
+//// guard error == nil else {
+//// wg_log(.error, message: "Start OpenVPN tunnel error : \(error?.localizedDescription ?? "none")")
+//// completionHandler(error!)
+//// return
+//// }
+//// wg_log(.error, message: "OpenVPN tunnel connected.")
+//// }
+//// })
+//// }
}
private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
@@ -294,7 +307,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// self.processQueue.sync { self.processPackets() }
// }
// }
-//
+
// private func processPackets() {
// wg_log(.info, message: "Inside startTun2SocksPacketForwarder")
// packetFlow.readPacketObjects { [weak self] packets in
@@ -341,100 +354,100 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
ovpnAdapter.connect(using: packetFlow)
}
- private func setupAndLaunchShadowSocksProxy(withConfig config: Data, ssHandler: ShadowsocksProxyCompletion) {
- let str = String(decoding: config, as: UTF8.self)
- wg_log(.info, message: "config: \(str)")
- ssCompletion = ssHandler
- guard let ssConfig = try? JSONSerialization.jsonObject(with: config, options: []) as? [String: Any] else {
- ssHandler?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
- code: 100,
- userInfo: [NSLocalizedDescriptionKey: "Cannot parse json for ss in tunnel"]))
- return
- }
-
- wg_log(.info, message: "SS Config: \(ssConfig)")
-
- guard let remoteHost = ssConfig["server"] as? String, // UnsafeMutablePointer,
- let remotePort = ssConfig["server_port"] as? Int32,
- let localAddress = ssConfig["local_addr"] as? String, //UnsafeMutablePointer,
- let localPort = ssConfig["local_port"] as? Int32,
- let method = ssConfig["method"] as? String, //UnsafeMutablePointer,
- let password = ssConfig["password"] as? String,//UnsafeMutablePointer,
- let timeout = ssConfig["timeout"] as? Int32
- else {
- ssHandler?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
- code: 100,
- userInfo: [NSLocalizedDescriptionKey: "Cannot assing profile params for ss in tunnel"]))
- return
- }
-
- /* An example profile
- *
- * const profile_t EXAMPLE_PROFILE = {
- * .remote_host = "example.com",
- * .local_addr = "127.0.0.1",
- * .method = "bf-cfb",
- * .password = "barfoo!",
- * .remote_port = 8338,
- * .local_port = 1080,
- * .timeout = 600;
- * .acl = NULL,
- * .log = NULL,
- * .fast_open = 0,
- * .mode = 0,
- * .verbose = 0
- * };
- */
-
- var profile: profile_t = .init()
- memset(&profile, 0, MemoryLayout.size)
- profile.remote_host = strdup(remoteHost)
- profile.remote_port = remotePort
- profile.local_addr = strdup(localAddress)
- profile.local_port = localPort
- profile.method = strdup(method)
- profile.password = strdup(password)
- profile.timeout = timeout
- profile.acl = nil
- profile.log = nil
- profile.mtu = 1600
- profile.fast_open = 1
- profile.mode = 0
- profile.verbose = 1
-
- wg_log(.debug, message: "Prepare to start shadowsocks proxy server...")
- let observer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
- ssQueue.sync { [weak self] in
- let success = start_ss_local_server_with_callback(profile, { socks_fd, udp_fd, data in
- wg_log(.debug, message: "Inside cb callback")
- wg_log(.debug, message: "Params: socks_fd -> \(socks_fd), udp_fd -> \(udp_fd)")
- if let obs = data {
- wg_log(.debug, message: "Prepare to call onShadowsocksCallback() with socks port \(socks_fd) and udp port \(udp_fd)")
- let mySelf = Unmanaged.fromOpaque(obs).takeUnretainedValue()
- mySelf.onShadowsocksCallback(fd: socks_fd)
- }
- }, observer)
- if success != -1 {
- wg_log(.error, message: "ss proxy started on port \(localPort)")
- self?.shadowSocksPort = localPort
- self?.isShadowsocksRunning = true
- } else {
- wg_log(.error, message: "Failed to start ss proxy")
- }
- }
- }
-
- private func onShadowsocksCallback(fd: Int32) {
- wg_log(.debug, message: "Inside onShadowsocksCallback() with port \(fd)")
- var error: NSError? = nil
- if fd > 0 {
-// shadowSocksPort = getSockPort(for: fd)
- isShadowsocksRunning = true
- } else {
- error = NSError(domain: Bundle.main.bundleIdentifier ?? "unknown", code: 100, userInfo: [NSLocalizedDescriptionKey : "Failed to start shadowsocks proxy"])
- }
- ssCompletion?(shadowSocksPort, error)
- }
+// private func setupAndLaunchShadowSocksProxy(withConfig config: Data, ssHandler: ShadowsocksProxyCompletion) {
+// let str = String(decoding: config, as: UTF8.self)
+// wg_log(.info, message: "config: \(str)")
+// ssCompletion = ssHandler
+// guard let ssConfig = try? JSONSerialization.jsonObject(with: config, options: []) as? [String: Any] else {
+// ssHandler?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
+// code: 100,
+// userInfo: [NSLocalizedDescriptionKey: "Cannot parse json for ss in tunnel"]))
+// return
+// }
+//
+// wg_log(.info, message: "SS Config: \(ssConfig)")
+//
+// guard let remoteHost = ssConfig["server"] as? String, // UnsafeMutablePointer,
+// let remotePort = ssConfig["server_port"] as? Int32,
+// let localAddress = ssConfig["local_addr"] as? String, //UnsafeMutablePointer,
+// let localPort = ssConfig["local_port"] as? Int32,
+// let method = ssConfig["method"] as? String, //UnsafeMutablePointer,
+// let password = ssConfig["password"] as? String,//UnsafeMutablePointer,
+// let timeout = ssConfig["timeout"] as? Int32
+// else {
+// ssHandler?(0, NSError(domain: Bundle.main.bundleIdentifier ?? "unknown",
+// code: 100,
+// userInfo: [NSLocalizedDescriptionKey: "Cannot assing profile params for ss in tunnel"]))
+// return
+// }
+//
+// /* An example profile
+// *
+// * const profile_t EXAMPLE_PROFILE = {
+// * .remote_host = "example.com",
+// * .local_addr = "127.0.0.1",
+// * .method = "bf-cfb",
+// * .password = "barfoo!",
+// * .remote_port = 8338,
+// * .local_port = 1080,
+// * .timeout = 600;
+// * .acl = NULL,
+// * .log = NULL,
+// * .fast_open = 0,
+// * .mode = 0,
+// * .verbose = 0
+// * };
+// */
+//
+// var profile: profile_t = .init()
+// memset(&profile, 0, MemoryLayout.size)
+// profile.remote_host = strdup(remoteHost)
+// profile.remote_port = remotePort
+// profile.local_addr = strdup(localAddress)
+// profile.local_port = localPort
+// profile.method = strdup(method)
+// profile.password = strdup(password)
+// profile.timeout = timeout
+// profile.acl = nil
+// profile.log = nil
+// profile.mtu = 1600
+// profile.fast_open = 1
+// profile.mode = 0
+// profile.verbose = 1
+//
+// wg_log(.debug, message: "Prepare to start shadowsocks proxy server...")
+// let observer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
+// ssQueue.sync { [weak self] in
+// let success = start_ss_local_server_with_callback(profile, { socks_fd, udp_fd, data in
+// wg_log(.debug, message: "Inside cb callback")
+// wg_log(.debug, message: "Params: socks_fd -> \(socks_fd), udp_fd -> \(udp_fd)")
+// if let obs = data {
+// wg_log(.debug, message: "Prepare to call onShadowsocksCallback() with socks port \(socks_fd) and udp port \(udp_fd)")
+// let mySelf = Unmanaged.fromOpaque(obs).takeUnretainedValue()
+// mySelf.onShadowsocksCallback(fd: socks_fd)
+// }
+// }, observer)
+// if success != -1 {
+// wg_log(.error, message: "ss proxy started on port \(localPort)")
+// self?.shadowSocksPort = localPort
+// self?.isShadowsocksRunning = true
+// } else {
+// wg_log(.error, message: "Failed to start ss proxy")
+// }
+// }
+// }
+//
+// private func onShadowsocksCallback(fd: Int32) {
+// wg_log(.debug, message: "Inside onShadowsocksCallback() with port \(fd)")
+// var error: NSError? = nil
+// if fd > 0 {
+//// shadowSocksPort = getSockPort(for: fd)
+// isShadowsocksRunning = true
+// } else {
+// error = NSError(domain: Bundle.main.bundleIdentifier ?? "unknown", code: 100, userInfo: [NSLocalizedDescriptionKey : "Failed to start shadowsocks proxy"])
+// }
+// ssCompletion?(shadowSocksPort, error)
+// }
private func getSockPort(for fd: Int32) -> Int32 {
var addr_in = sockaddr_in();
diff --git a/client/scripts/commons.sh b/client/scripts/commons.sh
index 0d3e0ae0..e459b372 100644
--- a/client/scripts/commons.sh
+++ b/client/scripts/commons.sh
@@ -112,4 +112,3 @@ die() {
exit 1
}
-
diff --git a/client/scripts/pp_ios.xcconfig b/client/scripts/pp_ios.xcconfig
index f538b790..891f84fe 100644
--- a/client/scripts/pp_ios.xcconfig
+++ b/client/scripts/pp_ios.xcconfig
@@ -17,6 +17,6 @@ SDKROOT[arch=armv7s] = iphoneos
VALID_ARCHS[sdk=iphoneos*] = arm64
-PROJECT_TEMP_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/PacketProcessor/build/PacketProcessor.build
-CONFIGURATION_BUILD_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/PacketProcessor/build/Release-iphoneos
-BUILT_PRODUCTS_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/PacketProcessor/build/Release-iphoneos
\ No newline at end of file
+PROJECT_TEMP_DIR = $(SRCROOT)/client/3rd/PacketProcessor/build/PacketProcessor.build
+CONFIGURATION_BUILD_DIR = $(SRCROOT)/client/3rd/PacketProcessor/build/Release-iphoneos
+BUILT_PRODUCTS_DIR = $(SRCROOT)/client/3rd/PacketProcessor/build/Release-iphoneos
\ No newline at end of file
diff --git a/client/scripts/ss_ios.xcconfig b/client/scripts/ss_ios.xcconfig
index 7c355554..19e42e00 100644
--- a/client/scripts/ss_ios.xcconfig
+++ b/client/scripts/ss_ios.xcconfig
@@ -19,6 +19,6 @@ SDKROOT[arch=armv7s] = iphoneos
VALID_ARCHS[sdk=iphoneos*] = arm64
-PROJECT_TEMP_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/ShadowSocks/build/ShadowSocks.build
-CONFIGURATION_BUILD_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/ShadowSocks/build/Release-iphoneos
-BUILT_PRODUCTS_DIR = /Users/sanchez/work/vied/ios/vpn/desktop-client-bkp/client/3rd/ShadowSocks/build/Release-iphoneos
\ No newline at end of file
+PROJECT_TEMP_DIR = $(SRCROOT)/client/3rd/ShadowSocks/build/ShadowSocks.build
+CONFIGURATION_BUILD_DIR = $(SRCROOT)/client/3rd/ShadowSocks/build/Release-iphoneos
+BUILT_PRODUCTS_DIR = $(SRCROOT)/client/3rd/ShadowSocks/build/Release-iphoneos
\ No newline at end of file
diff --git a/client/scripts/xcode_patcher.rb b/client/scripts/xcode_patcher.rb
index 48512782..56f85638 100644
--- a/client/scripts/xcode_patcher.rb
+++ b/client/scripts/xcode_patcher.rb
@@ -110,6 +110,9 @@ class XCodeprojPatcher
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] ||= '-Onone'
end
end
+
+# images_ref = @target_main.new_reference('ios/app/Images.xcassets')
+# @target_main.add_resources([images_ref])
if networkExtension
# WireGuard group
@@ -252,10 +255,14 @@ class XCodeprojPatcher
config.build_settings['SWIFT_OBJC_BRIDGING_HEADER'] ||= 'macos/networkextension/WireGuardNetworkExtension-Bridging-Header.h'
config.build_settings['SWIFT_PRECOMPILE_BRIDGING_HEADER'] = 'NO'
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'YES'
+# config.build_settings['STRIP_BITCODE_FROM_COPIED_FILES'] = 'NO'
config.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= [
"$(inherited)",
"$(PROJECT_DIR)/3rd",
- "$(PROJECT_DIR)/3rd/OpenVPNAdapter/build/Debug-iphoneos"
+ "$(PROJECT_DIR)/3rd/OpenVPNAdapter/build/Release-iphoneos",
+# "$(PROJECT_DIR)/3rd/ShadowSocks/build/Release-iphoneos/",
+# "$(PROJECT_DIR)/3rd/PacketProcessor/build/Release-iphoneos/",
+# "$(PROJECT_DIR)/3rd/outline-go-tun2socks/build/ios/"
]
# Versions and names
@@ -341,6 +348,11 @@ class XCodeprojPatcher
'platforms/ios/iostunnel.swift',
'platforms/ios/iosglue.mm',
'platforms/ios/ioslogger.swift',
+# 'platforms/ios/Shadowsocks.h',
+# 'platforms/ios/Shadowsocks.m',
+# 'platforms/ios/ShadowsocksConnectivity.h',
+# 'platforms/ios/ShadowsocksConnectivity.m',
+# 'platforms/ios/Subnet.swift',
].each { |filename|
file = group.new_file(filename)
@target_extension.add_file_references([file])
@@ -369,14 +381,14 @@ class XCodeprojPatcher
framework_ref = frameworks_group.new_file('3rd/OpenVPNAdapter/build/Release-iphoneos/OpenVPNAdapter.framework')
frameworks_build_phase.add_file_reference(framework_ref)
- framework_ref = frameworks_group.new_file('3rd/ShadowSocks/build/Release-iphoneos/ShadowSocks.framework')
- frameworks_build_phase.add_file_reference(framework_ref)
-
-# framework_ref = frameworks_group.new_file('3rd/PacketProcessor/build/Release-iphoneos/PacketProcessor.framework')
+# framework_ref = frameworks_group.new_file('3rd/ShadowSocks/build/Release-iphoneos/ShadowSocks.framework')
+# frameworks_build_phase.add_file_reference(framework_ref)
+#
+# framework_ref = frameworks_group.new_file('3rd/PacketProcessor/PacketProcessor/CocoaAsyncSocket/CocoaAsyncSocket.framework')
+# frameworks_build_phase.add_file_reference(framework_ref)
+#
+# framework_ref = frameworks_group.new_file('3rd/outline-go-tun2socks/build/ios/Tun2socks.framework')
# frameworks_build_phase.add_file_reference(framework_ref)
-
- framework_ref = frameworks_group.new_file('3rd/outline-go-tun2socks/build/ios/Tun2Socks.xcframework')
- frameworks_build_phase.add_file_reference(framework_ref)
# This fails: @target_main.add_dependency @target_extension
diff --git a/client/xcode.xconfig b/client/xcode.xconfig
index b93841e8..62778188 100644
--- a/client/xcode.xconfig
+++ b/client/xcode.xconfig
@@ -1,5 +1,5 @@
-DEVELOPMENT_TEAM = 7VAGZXD78P
+DEVELOPMENT_TEAM = X7UJ388FXK
-GROUP_ID_IOS = group.ru.kotit.AmneziaVPN.udev
-APP_ID_IOS = ru.kotit.AmneziaVPN
-NETEXT_ID_IOS = ru.kotit.AmneziaVPN.network-extension
+GROUP_ID_IOS = group.org.amnezia.AmneziaVPN
+APP_ID_IOS = org.amnezia.AmneziaVPN
+NETEXT_ID_IOS = org.amnezia.AmneziaVPN.network-extension