├── .gitmodules
├── PurpleHaze
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Icon.png
│ │ ├── icon_20pt.png
│ │ ├── icon_29pt.png
│ │ ├── icon_40pt.png
│ │ ├── icon_76pt.png
│ │ ├── icon_20pt@2x-1.png
│ │ ├── icon_20pt@2x.png
│ │ ├── icon_20pt@3x.png
│ │ ├── icon_29pt@2x-1.png
│ │ ├── icon_29pt@2x.png
│ │ ├── icon_29pt@3x.png
│ │ ├── icon_40pt@2x-1.png
│ │ ├── icon_40pt@2x.png
│ │ ├── icon_40pt@3x.png
│ │ ├── icon_60pt@2x.png
│ │ ├── icon_60pt@3x.png
│ │ ├── icon_76pt@2x.png
│ │ ├── icon_83.5@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Settings.bundle
│ ├── en.lproj
│ │ └── Root.strings
│ └── Root.plist
├── PurpleHaze.entitlements
├── AppDelegate.swift
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
└── ViewController.swift
├── PurpleHaze.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── Extension
├── Swift-Bridging-Header.h
├── Iodine.entitlements
├── IodineDelegate.swift
├── Info.plist
├── Iodine.h
├── IodineSettings.swift
├── tun-shim.c
├── StdioRedirect.swift
├── PacketTunnelProvider.swift
└── Iodine.swift
├── CodeSigning.xcconfig.sample
├── Build.xcconfig
├── .gitignore
├── README.md
├── .github
└── workflows
│ └── build.yml
└── LICENSE
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "iodine"]
2 | path = iodine
3 | url = https://github.com/yarrick/iodine.git
4 |
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/PurpleHaze/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_76pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osy/PurpleHaze/HEAD/PurpleHaze/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/PurpleHaze.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Extension/Swift-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 | #include
5 | #include
6 | #include "common.h"
7 | #include "tun.h"
8 | #include "client.h"
9 | #include "util.h"
10 | #include "Iodine.h"
11 |
--------------------------------------------------------------------------------
/CodeSigning.xcconfig.sample:
--------------------------------------------------------------------------------
1 | // Your Team ID (see https://developer.apple.com/account/#!/membership)
2 | DEVELOPMENT_TEAM = XYZ0123456
3 |
4 | // Prefix of unique bundle IDs registered to you in Apple Developer Portal.
5 | // You need to register:
6 | // - com.myuniquename.Purple-Haze
7 | PRODUCT_BUNDLE_PREFIX = com.myuniquename
8 |
--------------------------------------------------------------------------------
/PurpleHaze.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "platform" : "ios",
6 | "reference" : "systemPurpleColor"
7 | },
8 | "idiom" : "universal"
9 | }
10 | ],
11 | "info" : {
12 | "author" : "xcode",
13 | "version" : 1
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Extension/Iodine.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.networking.networkextension
6 |
7 | packet-tunnel-provider
8 |
9 | com.apple.security.application-groups
10 |
11 | group.$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/PurpleHaze/PurpleHaze.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.networking.networkextension
6 |
7 | packet-tunnel-provider
8 |
9 | com.apple.security.application-groups
10 |
11 | group.$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Extension/IodineDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | public protocol IodineDelegate: AnyObject {
20 | func iodineError(_ error: Error?)
21 | func iodineReadData(_ data: [Data], withProtocols protocols: [NSNumber])
22 | func iodineDidStop()
23 | }
24 |
--------------------------------------------------------------------------------
/Build.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | // Configuration settings file format documentation can be found at:
18 | // https://help.apple.com/xcode/#/dev745c5c974
19 |
20 | MARKETING_VERSION = 1.0.2
21 | CURRENT_PROJECT_VERSION = 4
22 |
23 | // Codesigning settings defined optionally
24 | #include? "CodeSigning.xcconfig"
25 |
--------------------------------------------------------------------------------
/PurpleHaze/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 |
19 | @main
20 | class AppDelegate: UIResponder, UIApplicationDelegate {
21 | var window: UIWindow?
22 |
23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
24 | // Override point for customization after application launch.
25 | return true
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Iodine
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSExtension
24 |
25 | NSExtensionPointIdentifier
26 | com.apple.networkextension.packet-tunnel
27 | NSExtensionPrincipalClass
28 | $(PRODUCT_MODULE_NAME).PacketTunnelProvider
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Extension/Iodine.h:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | #ifndef Iodine_h
18 | #define Iodine_h
19 |
20 | #include
21 | #include
22 | #include
23 |
24 | extern _Nonnull const CFStringRef IodineSetMTUNotification;
25 | extern _Nonnull const CFStringRef IodineSetIPNotification;
26 | extern _Nonnull const CFStringRef kIodineMTU;
27 | extern _Nonnull const CFStringRef kIodineClientIP;
28 | extern _Nonnull const CFStringRef kIodineServerIP;
29 | extern _Nonnull const CFStringRef kIodineSubnetMask;
30 |
31 | static inline void iodine_srand(void) {
32 | srand((unsigned) time(NULL));
33 | }
34 |
35 | #endif /* Iodine_h */
36 |
--------------------------------------------------------------------------------
/Extension/IodineSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | public final class IodineSettings {
18 | public static let lastSavedSettings = "LastSavedSettings"
19 | public static let ipv6Support = "IPv6Support"
20 | public static let nameserverHost = "NameserverHost"
21 | public static let topDomain = "TopDomain"
22 | public static let password = "Password"
23 | public static let maxDownstreamFragmentSize = "MaxDownstreamFragmentSize"
24 | public static let rawMode = "RawMode"
25 | public static let lazyMode = "LazyMode"
26 | public static let selectTimeout = "SelectTimeout"
27 | public static let hostnameMaxLength = "HostnameMaxLength"
28 | public static let forceDnsType = "ForceDnsType"
29 | public static let forceEncoding = "ForceEncoding"
30 |
31 | public static let dnsServer = "DNSServer"
32 | public static let pacConfigUrl = "PACConfigURL"
33 | public static let pacJavascript = "PACJavascript"
34 | public static let httpProxyServer = "HTTPProxyServer"
35 | public static let httpProxyPort = "HTTPProxyPort"
36 | public static let httpsProxyServer = "HTTPSProxyServer"
37 | public static let httpsProxyPort = "HTTPSProxyPort"
38 | }
39 |
--------------------------------------------------------------------------------
/PurpleHaze/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/PurpleHaze/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSupportsIndirectInputEvents
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 | UIUserInterfaceStyle
47 | Light
48 | ITSAppUsesNonExemptEncryption
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | # CocoaPods
34 | #
35 | # We recommend against adding the Pods directory to your .gitignore. However
36 | # you should judge for yourself, the pros and cons are mentioned at:
37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
38 | #
39 | # Pods/
40 | #
41 | # Add this line if you want to avoid checking in source code from the Xcode workspace
42 | # *.xcworkspace
43 |
44 | # Carthage
45 | #
46 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
47 | # Carthage/Checkouts
48 |
49 | Carthage/Build/
50 |
51 | # fastlane
52 | #
53 | # It is recommended to not store the screenshots in the git repo.
54 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
55 | # For more information about the recommended setup visit:
56 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
57 |
58 | fastlane/report.xml
59 | fastlane/Preview.html
60 | fastlane/screenshots/**/*.png
61 | fastlane/test_output
62 |
63 | # Code Injection
64 | #
65 | # After new code Injection tools there's a generated folder /iOSInjectionProject
66 | # https://github.com/johnno1962/injectionforxcode
67 |
68 | iOSInjectionProject/
69 |
70 | .DS_Store
71 | CodeSigning.xcconfig
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Purple Haze
2 |
3 | A DNS tunnel client for iOS based on [Iodine][1]. A paid Apple Developer account is required to build because of the entitlements needed for [Network Extensions][2].
4 |
5 | ## Build
6 |
7 | 1. Make sure you cloned the submodules: `git submodule init && git submodule update`
8 | 2. Copy `CodeSigning.xcconfig.sample` to `CodeSigning.xcconfig` and fill in `DEVELOPMENT_TEAM` with your Team ID [(found here)][3] and choose a unique `PRODUCT_BUNDLE_PREFIX`.
9 | 3. Open `PurpleHaze.xcodeproj` and build it.
10 |
11 | ## Usage
12 |
13 | [Read iodine's documentations for instructions on setting up a server.][1] Once you have `iodined` running on your computer and the nameserver pointed to your IP, you can tunnel into the private subnet created by `iodined` from Purple Haze. Note that without additional configuration, you cannot use the tunnel to browse the web (or connect to WAN). You can then setup a SSH tunnel (by connecting to `10.0.0.1` or whatever your iodine server IP is set to) or a HTTP(S) proxy and configuring Purple Haze to use that proxy in the advanced settings.
14 |
15 | If you are running `iodined` on a Linux machine/VM, then you can do the following to forward the TAP traffic to the internet.
16 |
17 | ```
18 | # sysctl -e net.ipv4.ip_forward=1
19 | # iptables -t nat -A POSTROUTING -s 10.0.0.0/255.255.224.0 -o eth0 -j MASQUERADE
20 | ```
21 |
22 | (Where `10.0.0.0/255.255.224.0` is the IP/subnet of your `iodined` TAP interface and `eth0` is the Ethernet interface.) Note this could pose a security issue as Iodine's authentication is pretty weak.
23 |
24 | ## Troubleshooting Tips
25 |
26 | * Make sure you built and are running the same release of [iodine][1] server from GitHub as the client in Purple Haze.
27 | * Iodine server seems more stable on Linux than macOS. If you are having trouble connecting to iodined, try running it from a Linux VM.
28 | * Try running iodine client on your computer on the same network to debug connection issues.
29 | * DNS tunneling to bypass paid WiFi is a well known trick and likely won't work on any modern network.
30 |
31 | ## About
32 |
33 | ### What's with the name?
34 |
35 | * "Purple Haze" is a great Jimi Hendrix song.
36 | * [Iodine][1], the DNS tunnel this project is based off (itself named after the atomic number 53 which is also the port number for DNS), is a purple gas at room temperature.
37 | * Purple was the [codename for the original iPhone](https://en.wikipedia.org/wiki/List_of_Apple_codenames#iPhone).
38 |
39 | [1]: https://github.com/yarrick/iodine
40 | [2]: https://developer.apple.com/documentation/networkextension/nepackettunnelprovider
41 | [3]: https://developer.apple.com/account/#!/membership
42 |
--------------------------------------------------------------------------------
/PurpleHaze/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_20pt@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "icon_20pt@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "icon_29pt@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "icon_29pt@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "icon_40pt@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "icon_40pt@3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "icon_60pt@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "icon_60pt@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "icon_20pt.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "icon_20pt@2x-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "icon_29pt.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "icon_29pt@2x-1.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "icon_40pt.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "icon_40pt@2x-1.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "icon_76pt.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "icon_76pt@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "icon_83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "Icon.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches:
5 | - 'main'
6 | - 'dev'
7 | tags-ignore:
8 | - '**'
9 | paths-ignore:
10 | - 'LICENSE'
11 | - '**.md'
12 | pull_request:
13 | release:
14 | types: [created]
15 | workflow_dispatch:
16 | inputs:
17 | test_release:
18 | description: 'Test release?'
19 | required: true
20 | default: 'false'
21 |
22 | jobs:
23 | build-ios:
24 | name: Build IPA
25 | runs-on: macos-11
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v2
29 | with:
30 | submodules: recursive
31 | - name: Build
32 | run: xcodebuild archive -archivePath PurpleHaze -scheme "Purple Haze" -configuration Release CODE_SIGNING_ALLOWED=NO
33 | - name: Fake Sign
34 | run: |
35 | sed 's/$(PRODUCT_BUNDLE_PREFIX:default=com.osy86)/com.osy86/g' PurpleHaze/PurpleHaze.entitlements > PurpleHaze.entitlements
36 | sed 's/$(PRODUCT_BUNDLE_PREFIX:default=com.osy86)/com.osy86/g' Extension/Iodine.entitlements > Iodine.entitlements
37 | codesign --force --sign - --entitlements "Iodine.entitlements" --timestamp=none "PurpleHaze.xcarchive/Products/Applications/Purple Haze.app/PlugIns/Iodine.appex"
38 | codesign --force --sign - --entitlements "PurpleHaze.entitlements" --timestamp=none "PurpleHaze.xcarchive/Products/Applications/Purple Haze.app"
39 | - name: Compress
40 | run: tar cf PurpleHaze.xcarchive.tgz PurpleHaze.xcarchive
41 | - name: Upload
42 | uses: actions/upload-artifact@v2
43 | with:
44 | name: PurpleHaze
45 | path: PurpleHaze.xcarchive.tgz
46 | package-ipa:
47 | name: Package IPA
48 | runs-on: ubuntu-20.04
49 | needs: build-ios
50 | if: github.event_name == 'release' || github.event.inputs.test_release == 'true'
51 | steps:
52 | - name: Download Artifact
53 | uses: actions/download-artifact@v2
54 | with:
55 | name: PurpleHaze
56 | - name: Package IPA
57 | run: |
58 | tar xf PurpleHaze.xcarchive.tgz
59 | mv PurpleHaze.xcarchive/Products/Applications Payload
60 | zip -r PurpleHaze.ipa Payload -x "._*" -x ".DS_Store" -x "__MACOSX"
61 | - name: Upload Release Asset
62 | if: github.event_name == 'release'
63 | uses: actions/upload-release-asset@v1.0.2
64 | env:
65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66 | with:
67 | upload_url: ${{ github.event.release.upload_url }}
68 | asset_path: PurpleHaze.ipa
69 | asset_name: PurpleHaze.ipa
70 | asset_content_type: application/octet-stream
71 | - name: Send Dispatch Event
72 | if: github.event_name == 'release'
73 | continue-on-error: true
74 | uses: peter-evans/repository-dispatch@v1
75 | with:
76 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
77 | repository: ${{ secrets.DISPATCH_ALTSTORE_REPO_NAME }}
78 | event-type: new-release
79 |
--------------------------------------------------------------------------------
/Extension/tun-shim.c:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include "Iodine.h"
23 | #include "tun.h"
24 |
25 | const CFStringRef IodineSetMTUNotification = CFSTR("IodineSetMTUNotification");
26 | const CFStringRef IodineSetIPNotification = CFSTR("IodineSetIPNotification");
27 | const CFStringRef kIodineMTU = CFSTR("MTU");
28 | const CFStringRef kIodineClientIP = CFSTR("ClientIP");
29 | const CFStringRef kIodineServerIP = CFSTR("ServerIP");
30 | const CFStringRef kIodineSubnetMask = CFSTR("SubnetMask");
31 |
32 | int open_tun(const char *tun_device) {
33 | fprintf(stderr, "Unimplemented function open_tun() called!\n");
34 | abort();
35 | }
36 |
37 | void close_tun(int tun_fd) {
38 | if (tun_fd > 0) {
39 | close(tun_fd);
40 | }
41 | }
42 |
43 | int write_tun(int tun_fd, char *data, size_t len) {
44 | return (int)write(tun_fd, data, len);
45 | }
46 |
47 | ssize_t read_tun(int tun_fd, char *data, size_t len) {
48 | return read(tun_fd, data, len);
49 | }
50 |
51 | static void AddIntegerValue(CFMutableDictionaryRef dictionary, const CFStringRef key, int32_t value)
52 | {
53 | CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &value);
54 | CFDictionaryAddValue(dictionary, key, number);
55 | CFRelease(number);
56 | }
57 |
58 | static void AddStringValue(CFMutableDictionaryRef dictionary, const CFStringRef key, const char *string)
59 | {
60 | CFStringRef cfstr = CFStringCreateWithCString(kCFAllocatorDefault, string, kCFStringEncodingASCII);
61 | CFDictionaryAddValue(dictionary, key, cfstr);
62 | CFRelease(cfstr);
63 | }
64 |
65 | int tun_setip(const char *ip, const char *other_ip, int netbits) {
66 | int netmask;
67 | struct in_addr net;
68 |
69 | netmask = 0;
70 | for (int i = 0; i < netbits; i++) {
71 | netmask = (netmask << 1) | 1;
72 | }
73 | netmask <<= (32 - netbits);
74 | net.s_addr = htonl(netmask);
75 | CFNotificationCenterRef local = CFNotificationCenterGetLocalCenter();
76 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, NULL, &kCFTypeDictionaryValueCallBacks);
77 | AddStringValue(dict, kIodineClientIP, ip);
78 | AddStringValue(dict, kIodineServerIP, other_ip);
79 | AddStringValue(dict, kIodineSubnetMask, inet_ntoa(net));
80 | CFNotificationCenterPostNotification(local, IodineSetIPNotification, NULL, dict, TRUE);
81 | CFRelease(dict);
82 | return 0;
83 | }
84 |
85 | int tun_setmtu(const unsigned mtu) {
86 | CFNotificationCenterRef local = CFNotificationCenterGetLocalCenter();
87 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, NULL, &kCFTypeDictionaryValueCallBacks);
88 | AddIntegerValue(dict, kIodineMTU, mtu);
89 | CFNotificationCenterPostNotification(local, IodineSetMTUNotification, NULL, dict, TRUE);
90 | CFRelease(dict);
91 | return 0;
92 | }
93 |
--------------------------------------------------------------------------------
/Extension/StdioRedirect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | public class StdioRedirect {
20 | static public var standardError = FileHandle.standardError
21 | static public let shared = StdioRedirect()
22 |
23 | private var standardOutput: Pipe
24 | private var standardError: Pipe
25 | private var origStdout: Int32 = -1
26 | private var origStderr: Int32 = -1
27 |
28 | private static var logUrl: URL? {
29 | guard let bundleId = Bundle.main.bundleIdentifier as NSString? else {
30 | return nil
31 | }
32 | let groupBundleId = "group." + bundleId.deletingPathExtension
33 | let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupBundleId)
34 | return containerUrl?.appendingPathComponent("log.txt")
35 | }
36 |
37 | private init() {
38 | //var lastOutputLine = ""
39 | standardOutput = Pipe()
40 | standardOutput.fileHandleForReading.readabilityHandler = { handle in
41 | //let data = lastOutputLine.data(using: .utf8)! + handle.availableData
42 | //lastOutputLine = StdioRedirect.logData(data)
43 | try! handle.availableData.append(fileURL: StdioRedirect.logUrl!)
44 | }
45 | //var lastErrorLine = ""
46 | standardError = Pipe()
47 | standardError.fileHandleForReading.readabilityHandler = { handle in
48 | //let data = lastErrorLine.data(using: .utf8)! + handle.availableData
49 | //lastErrorLine = StdioRedirect.logData(data)
50 | try! handle.availableData.append(fileURL: StdioRedirect.logUrl!)
51 | }
52 | }
53 |
54 | // used for debugging only
55 | private static func logData(_ data: Data) -> String {
56 | guard let log = String(data: data, encoding: .utf8), log.count > 0 else {
57 | return ""
58 | }
59 | var last = ""
60 | var lines = log.split(whereSeparator: \.isNewline)
61 | if !log.last!.isNewline && lines.count > 0 {
62 | last = String(lines.popLast()!)
63 | }
64 | for line in lines {
65 | if !line.contains("[Iodine]") {
66 | NSLog("[Iodine] %@", String(line))
67 | }
68 | }
69 | return last
70 | }
71 |
72 | public func start() throws {
73 | origStdout = dup(STDOUT_FILENO)
74 | guard origStdout >= 0 else {
75 | throw RedirectError.cannotDuplicateStdout
76 | }
77 | origStderr = dup(STDERR_FILENO)
78 | guard origStderr >= 0 else {
79 | throw RedirectError.cannotDuplicateStderr
80 | }
81 | guard dup2(standardOutput.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) >= 0 else {
82 | throw RedirectError.cannotRedirectStdout
83 | }
84 | guard dup2(standardError.fileHandleForWriting.fileDescriptor, STDERR_FILENO) >= 0 else {
85 | throw RedirectError.cannotRedirectStderr
86 | }
87 | guard let logUrl = StdioRedirect.logUrl else {
88 | throw RedirectError.cannotAccessGroupContainer
89 | }
90 | // remove file if it exists
91 | try? FileManager.default.removeItem(at: logUrl)
92 | }
93 |
94 | public func stop() {
95 | if origStdout >= 0 {
96 | dup2(origStdout, STDOUT_FILENO)
97 | close(origStdout)
98 | origStdout = -1
99 | }
100 | if origStderr >= 0 {
101 | dup2(origStderr, STDERR_FILENO)
102 | close(origStderr)
103 | origStderr = -1
104 | }
105 | }
106 |
107 | public enum RedirectError: Error {
108 | case cannotDuplicateStdout
109 | case cannotDuplicateStderr
110 | case cannotRedirectStdout
111 | case cannotRedirectStderr
112 | case cannotAccessGroupContainer
113 | }
114 | }
115 |
116 | fileprivate extension Data {
117 | func append(fileURL: URL) throws {
118 | if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
119 | defer {
120 | fileHandle.closeFile()
121 | }
122 | fileHandle.seekToEndOfFile()
123 | fileHandle.write(self)
124 | } else {
125 | try write(to: fileURL, options: .atomic)
126 | }
127 | }
128 | }
129 |
130 | extension FileHandle: TextOutputStream {
131 | public func write(_ string: String) {
132 | guard let data = string.data(using: .utf8) else {
133 | fatalError()
134 | }
135 | self.write(data)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Extension/PacketTunnelProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import NetworkExtension
18 |
19 | class PacketTunnelProvider: NEPacketTunnelProvider {
20 | private var iodine: Iodine?
21 |
22 | override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
23 | var mtu: Int?
24 | var clientIp: String?
25 | var serverIp: String?
26 | var subnetMask: String?
27 | do {
28 | try StdioRedirect.shared.start()
29 | } catch {
30 | completionHandler(error)
31 | return
32 | }
33 | let savedOptions: [String: Any]?
34 | if let options = options {
35 | UserDefaults.standard.set(options, forKey: IodineSettings.lastSavedSettings)
36 | savedOptions = options
37 | } else {
38 | savedOptions = UserDefaults.standard.dictionary(forKey: IodineSettings.lastSavedSettings)
39 | }
40 | iodine = Iodine(options: savedOptions)
41 | iodine!.delegate = self
42 | NotificationCenter.default.addObserver(forName: IodineSetMTUNotification as NSNotification.Name, object: nil, queue: nil) { notification in
43 | mtu = notification.userInfo![kIodineMTU] as? Int
44 | }
45 | NotificationCenter.default.addObserver(forName: IodineSetIPNotification as NSNotification.Name, object: nil, queue: nil) { notification in
46 | clientIp = notification.userInfo![kIodineClientIP] as? String
47 | serverIp = notification.userInfo![kIodineServerIP] as? String
48 | subnetMask = notification.userInfo![kIodineSubnetMask] as? String
49 | }
50 | do {
51 | try iodine!.start()
52 | guard clientIp != nil && serverIp != nil && subnetMask != nil else {
53 | throw IodineError.internalError
54 | }
55 | } catch {
56 | print("ERROR starting iodine: \(error.localizedDescription)", to: &StdioRedirect.standardError)
57 | completionHandler(error)
58 | return
59 | }
60 | print("Network server: \(serverIp!), client: \(clientIp!), subnet: \(subnetMask!)", to: &StdioRedirect.standardError)
61 | let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "255.255.255.255")
62 | let ipv4 = NEIPv4Settings(addresses: [clientIp!], subnetMasks: [subnetMask!])
63 | ipv4.includedRoutes = [.default(), .init(destinationAddress: serverIp!, subnetMask: subnetMask!)]
64 | settings.ipv4Settings = ipv4
65 | print("Network MTU: \(mtu ?? 0)", to: &StdioRedirect.standardError)
66 | if let mtu = mtu {
67 | settings.mtu = NSNumber(value: mtu)
68 | }
69 | configureDnsAndProxy(for: settings, with: savedOptions)
70 | setTunnelNetworkSettings(settings) { error in
71 | if error == nil {
72 | print("Tunnel started successfully!", to: &StdioRedirect.standardError)
73 | self.readPackets()
74 | } else {
75 | print("ERROR in setTunnelNetworkSettings: \(error?.localizedDescription ?? "(unknown)")", to: &StdioRedirect.standardError)
76 | }
77 | completionHandler(error)
78 | }
79 | }
80 |
81 | override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
82 | iodine = nil
83 | StdioRedirect.shared.stop()
84 | completionHandler()
85 | }
86 |
87 | override func sleep(completionHandler: @escaping () -> Void) {
88 | iodine?.stop()
89 | completionHandler()
90 | }
91 |
92 | override func wake() {
93 | do {
94 | try iodine?.start()
95 | } catch {
96 | cancelTunnelWithError(error)
97 | }
98 | }
99 |
100 | private func readPackets() {
101 | packetFlow.readPackets { packets, protocols in
102 | for (i, packet) in packets.enumerated() {
103 | self.iodine?.writeData(packet, family: protocols[i].int32Value)
104 | }
105 | self.readPackets()
106 | }
107 | }
108 |
109 | private func configureDnsAndProxy(for config: NETunnelNetworkSettings, with options: [String: Any]?) {
110 | var dnsServer = "8.8.8.8"
111 | if let customDns = options?[IodineSettings.dnsServer] as? String, customDns.count > 0 {
112 | dnsServer = customDns
113 | }
114 | print("Network DNS: \(dnsServer)", to: &StdioRedirect.standardError)
115 | config.dnsSettings = NEDNSSettings(servers: [dnsServer])
116 | let proxy = NEProxySettings()
117 | if let pacConfigUrl = options?[IodineSettings.pacConfigUrl] as? String, pacConfigUrl.count > 0 {
118 | proxy.autoProxyConfigurationEnabled = true
119 | proxy.proxyAutoConfigurationURL = URL(string: pacConfigUrl)
120 | print("Network PAC URL: \(pacConfigUrl)", to: &StdioRedirect.standardError)
121 | }
122 | if let pacJavascript = options?[IodineSettings.pacJavascript] as? String, pacJavascript.count > 0 {
123 | proxy.autoProxyConfigurationEnabled = true
124 | proxy.proxyAutoConfigurationJavaScript = pacJavascript
125 | print("Network PAC Javascript: \(pacJavascript)", to: &StdioRedirect.standardError)
126 | }
127 | let httpProxyPortString = options?[IodineSettings.httpProxyPort] as? String
128 | let httpProxyPort = Int(httpProxyPortString ?? "0") ?? 0
129 | if let httpProxyServer = options?[IodineSettings.httpProxyServer] as? String, httpProxyServer.count > 0 {
130 | proxy.httpEnabled = true
131 | proxy.httpServer = NEProxyServer(address: httpProxyServer, port: httpProxyPort)
132 | print("Network HTTP Proxy: \(httpProxyServer):\(httpProxyPort)", to: &StdioRedirect.standardError)
133 | }
134 | let httpsProxyPortString = options?[IodineSettings.httpsProxyPort] as? String
135 | let httpsProxyPort = Int(httpsProxyPortString ?? "0") ?? 0
136 | if let httpsProxyServer = options?[IodineSettings.httpsProxyServer] as? String, httpsProxyServer.count > 0 {
137 | proxy.httpsEnabled = true
138 | proxy.httpsServer = NEProxyServer(address: httpsProxyServer, port: httpsProxyPort)
139 | print("Network HTTPS Proxy: \(httpsProxyServer):\(httpsProxyPort)", to: &StdioRedirect.standardError)
140 | }
141 | }
142 | }
143 |
144 | extension PacketTunnelProvider: IodineDelegate {
145 | func iodineError(_ error: Error?) {
146 | print("ERROR: \(error?.localizedDescription ?? "(unknown)")", to: &StdioRedirect.standardError)
147 | cancelTunnelWithError(error)
148 | }
149 |
150 | func iodineReadData(_ data: [Data], withProtocols protocols: [NSNumber]) {
151 | packetFlow.writePackets(data, withProtocols: protocols)
152 | }
153 |
154 | func iodineDidStop() {
155 | print("Tunnel stopped.", to: &StdioRedirect.standardError)
156 | cancelTunnelWithError(nil)
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/PurpleHaze/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | StringsTable
6 | Root
7 | PreferenceSpecifiers
8 |
9 |
10 | Type
11 | PSGroupSpecifier
12 | Title
13 | Server
14 |
15 |
16 | Type
17 | PSTextFieldSpecifier
18 | Title
19 | Top Domain
20 | Key
21 | TopDomain
22 | DefaultValue
23 |
24 | IsSecure
25 |
26 | KeyboardType
27 | URL
28 | AutocapitalizationType
29 | None
30 | AutocorrectionType
31 | No
32 |
33 |
34 | Type
35 | PSTextFieldSpecifier
36 | Title
37 | Password
38 | Key
39 | Password
40 | DefaultValue
41 |
42 | IsSecure
43 |
44 | KeyboardType
45 | Alphabet
46 | AutocapitalizationType
47 | None
48 | AutocorrectionType
49 | No
50 |
51 |
52 | Type
53 | PSGroupSpecifier
54 | Title
55 | Network Settings
56 |
57 |
58 | Type
59 | PSTextFieldSpecifier
60 | Title
61 | DNS Server
62 | Key
63 | DNSServer
64 | DefaultValue
65 | 8.8.8.8
66 | IsSecure
67 |
68 | KeyboardType
69 | URL
70 | AutocapitalizationType
71 | None
72 | AutocorrectionType
73 | No
74 |
75 |
76 | Type
77 | PSTextFieldSpecifier
78 | Title
79 | PAC Config URL
80 | Key
81 | PACConfigURL
82 | DefaultValue
83 |
84 | IsSecure
85 |
86 | KeyboardType
87 | URL
88 | AutocapitalizationType
89 | None
90 | AutocorrectionType
91 | No
92 |
93 |
94 | Type
95 | PSTextFieldSpecifier
96 | Title
97 | PAC Javascript
98 | Key
99 | PACJavascript
100 | DefaultValue
101 |
102 | IsSecure
103 |
104 | KeyboardType
105 | Alphabet
106 | AutocapitalizationType
107 | None
108 | AutocorrectionType
109 | No
110 |
111 |
112 | Type
113 | PSTextFieldSpecifier
114 | Title
115 | HTTP Proxy Server
116 | Key
117 | HTTPProxyServer
118 | DefaultValue
119 |
120 | IsSecure
121 |
122 | KeyboardType
123 | URL
124 | AutocapitalizationType
125 | None
126 | AutocorrectionType
127 | No
128 |
129 |
130 | Type
131 | PSTextFieldSpecifier
132 | Title
133 | HTTP Proxy Port
134 | Key
135 | HTTPProxyPort
136 | DefaultValue
137 |
138 | IsSecure
139 |
140 | KeyboardType
141 | NumberPad
142 | AutocapitalizationType
143 | None
144 | AutocorrectionType
145 | No
146 |
147 |
148 | Type
149 | PSTextFieldSpecifier
150 | Title
151 | HTTPS Proxy Server
152 | Key
153 | HTTPSProxyServer
154 | DefaultValue
155 |
156 | IsSecure
157 |
158 | KeyboardType
159 | URL
160 | AutocapitalizationType
161 | None
162 | AutocorrectionType
163 | No
164 |
165 |
166 | Type
167 | PSTextFieldSpecifier
168 | Title
169 | HTTPS Proxy Port
170 | Key
171 | HTTPSProxyPort
172 | DefaultValue
173 |
174 | IsSecure
175 |
176 | KeyboardType
177 | NumberPad
178 | AutocapitalizationType
179 | None
180 | AutocorrectionType
181 | No
182 |
183 |
184 | Type
185 | PSGroupSpecifier
186 | Title
187 | Advanced
188 |
189 |
190 | Type
191 | PSTextFieldSpecifier
192 | Title
193 | Nameserver Host
194 | Key
195 | NameserverHost
196 | DefaultValue
197 |
198 | IsSecure
199 |
200 | KeyboardType
201 | URL
202 | AutocapitalizationType
203 | None
204 | AutocorrectionType
205 | No
206 |
207 |
208 | Type
209 | PSToggleSwitchSpecifier
210 | Title
211 | IPv6 Support
212 | Key
213 | IPv6Support
214 | DefaultValue
215 |
216 |
217 |
218 | Type
219 | PSTextFieldSpecifier
220 | Title
221 | Max Downstream Fragment Size (-m)
222 | Key
223 | MaxDownstreamFragmentSize
224 | DefaultValue
225 | 0
226 | IsSecure
227 |
228 | KeyboardType
229 | NumberPad
230 | AutocapitalizationType
231 | None
232 | AutocorrectionType
233 | No
234 |
235 |
236 | Type
237 | PSToggleSwitchSpecifier
238 | Title
239 | Try Raw UDP (-r)
240 | Key
241 | RawMode
242 | DefaultValue
243 |
244 |
245 |
246 | Type
247 | PSToggleSwitchSpecifier
248 | Title
249 | Lazy Mode (-L)
250 | Key
251 | LazyMode
252 | DefaultValue
253 |
254 |
255 |
256 | Type
257 | PSTextFieldSpecifier
258 | Title
259 | Request Timeout (-I)
260 | Key
261 | SelectTimeout
262 | DefaultValue
263 | 4
264 | IsSecure
265 |
266 | KeyboardType
267 | NumberPad
268 | AutocapitalizationType
269 | None
270 | AutocorrectionType
271 | No
272 |
273 |
274 | Type
275 | PSTextFieldSpecifier
276 | Title
277 | Hostname Max Length (-M)
278 | Key
279 | HostnameMaxLength
280 | DefaultValue
281 | 255
282 | IsSecure
283 |
284 | KeyboardType
285 | NumberPad
286 | AutocapitalizationType
287 | None
288 | AutocorrectionType
289 | No
290 |
291 |
292 | Type
293 | PSMultiValueSpecifier
294 | Title
295 | Force DNS Type (-T)
296 | Key
297 | ForceDnsType
298 | DefaultValue
299 |
300 | Titles
301 |
302 | Default
303 | NULL
304 | PRIVATE
305 | CNAME
306 | A
307 | MX
308 | SRV
309 | TXT
310 |
311 | Values
312 |
313 |
314 | NULL
315 | PRIVATE
316 | CNAME
317 | A
318 | MX
319 | SRV
320 | TXT
321 |
322 |
323 |
324 | Type
325 | PSMultiValueSpecifier
326 | Title
327 | Force Downstream Encoding (-O)
328 | Key
329 | ForceEncoding
330 | DefaultValue
331 |
332 | Titles
333 |
334 | Default
335 | base32
336 | base64
337 | base64u
338 | base128
339 | raw
340 |
341 | Values
342 |
343 |
344 | base32
345 | base64
346 | base64u
347 | base128
348 | raw
349 |
350 |
351 |
352 |
353 |
354 |
--------------------------------------------------------------------------------
/PurpleHaze/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import UIKit
18 | import NetworkExtension
19 |
20 | class ViewController: UIViewController {
21 | @IBOutlet var topDomainTextField: UITextField!
22 | @IBOutlet var passwordTextField: UITextField!
23 | @IBOutlet var logTextView: UITextView!
24 | @IBOutlet var vpnStartSwitch: UISwitch!
25 | @IBOutlet var activityIndicator: UIActivityIndicatorView!
26 |
27 | var vpnObserver: Any?
28 | var vpnManager: NEVPNManager? {
29 | didSet {
30 | guard let manager = vpnManager else {
31 | vpnObserver = nil
32 | return
33 | }
34 | vpnObserver = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: manager.connection, queue: nil, using: { [weak self] _ in
35 | guard let _self = self else {
36 | return
37 | }
38 | if manager.connection.status == .connecting {
39 | _self.vpnWillConnect(for: manager)
40 | } else if manager.connection.status == .disconnecting {
41 | _self.vpnWillDisconnect(for: manager)
42 | } else if manager.connection.status == .disconnected {
43 | _self.vpnDidDisconnect(for: manager)
44 | } else if manager.connection.status == .connected {
45 | _self.vpnDidConnect(for: manager)
46 | }
47 | })
48 | }
49 | }
50 |
51 | var lastLogIndex: UInt64 = 0
52 |
53 | override func viewDidLoad() {
54 | super.viewDidLoad()
55 | topDomainTextField.text = UserDefaults.standard.string(forKey: IodineSettings.topDomain)
56 | passwordTextField.text = UserDefaults.standard.string(forKey: IodineSettings.password)
57 | initVpn()
58 | refreshLog()
59 | }
60 |
61 | func showError(message: String?) {
62 | DispatchQueue.main.async {
63 | let alertMessage = message ?? NSLocalizedString("An error has occurred.", comment: "Main")
64 | let alert = UIAlertController(title: nil, message: alertMessage, preferredStyle: .alert)
65 | alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Main"), style: .default, handler: nil))
66 | self.present(alert, animated: true)
67 | self.vpnStartSwitch.isOn = self.vpnManager?.connection.status == .connected
68 | }
69 | }
70 |
71 | func showError(_ error: Error?) {
72 | showError(message: error?.localizedDescription)
73 | }
74 |
75 | @IBAction func advancedSettingsPressed(_ sender: Any) {
76 | _ = UIApplication.shared.openURL(URL(string: UIApplication.openSettingsURLString)!)
77 | }
78 |
79 | @IBAction func helpButtonPressed(_ sender: Any) {
80 | _ = UIApplication.shared.openURL(URL(string: "https://github.com/osy/PurpleHaze")!)
81 | }
82 |
83 | @IBAction func vpnStartSwitchChanged(_ sender: Any) {
84 | if vpnStartSwitch.isOn {
85 | clearLog()
86 | guard let topdomain = topDomainTextField.text, topdomain.count > 0 else {
87 | showError(message: NSLocalizedString("You must specify a top domain.", comment: "ViewController"))
88 | return
89 | }
90 | UserDefaults.standard.set(topdomain, forKey: IodineSettings.topDomain)
91 | UserDefaults.standard.set(passwordTextField.text, forKey: IodineSettings.password)
92 | setupVpn(with: vpnManager)
93 | } else {
94 | guard let vpnManager = vpnManager else {
95 | return
96 | }
97 | stopTunnel(with: vpnManager)
98 | }
99 | }
100 |
101 | @IBAction func dismissTextField(_ sender: Any) {
102 | }
103 | }
104 |
105 | extension ViewController {
106 | var tunnelBundleId: String {
107 | Bundle.main.bundleIdentifier!.appending(".Iodine")
108 | }
109 |
110 | func initVpn(onExistingManager: @escaping (NEVPNManager) -> () = { _ in }) {
111 | DispatchQueue.main.async {
112 | self.vpnStartSwitch.isEnabled = false
113 | }
114 | NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
115 | if !(managers?.isEmpty ?? true), let manager = managers?[0] {
116 | self.vpnManager = manager
117 | }
118 | DispatchQueue.main.async {
119 | self.vpnStartSwitch.isOn = self.vpnManager?.connection.status == .connected
120 | self.vpnStartSwitch.isEnabled = true
121 | }
122 | if error != nil {
123 | self.showError(error)
124 | } else if let manager = self.vpnManager {
125 | onExistingManager(manager)
126 | }
127 | }
128 | }
129 |
130 | func setupVpn(with manager: NEVPNManager? = nil) {
131 | DispatchQueue.global(qos: .utility).async {
132 | if let manager = manager {
133 | self.startExistingTunnel(with: manager)
134 | } else {
135 | self.createAndStartTunnel()
136 | }
137 | }
138 | }
139 |
140 | func createAndStartTunnel() {
141 | let manager = NETunnelProviderManager()
142 | manager.localizedDescription = NSLocalizedString("Iodine DNS Tunnel", comment: "Main")
143 | let proto = NETunnelProviderProtocol()
144 | proto.providerBundleIdentifier = tunnelBundleId
145 | proto.serverAddress = ""
146 | manager.protocolConfiguration = proto
147 | manager.isEnabled = true
148 | let lock = DispatchSemaphore(value: 0)
149 | var error: Error?
150 | manager.saveToPreferences { err in
151 | error = err
152 | lock.signal()
153 | }
154 | lock.wait()
155 | if let err = error {
156 | showError(err)
157 | } else {
158 | initVpn(onExistingManager: startExistingTunnel)
159 | }
160 | }
161 |
162 | func startExistingTunnel(with manager: NEVPNManager) {
163 | if manager.connection.status == .connected {
164 | // Connection already established, nothing to do here
165 | return
166 | }
167 | let options = UserDefaults.standard.dictionaryRepresentation().mapValues { value in
168 | value as! NSObject
169 | }
170 | do {
171 | try manager.connection.startVPNTunnel(options: options)
172 | } catch NEVPNError.configurationDisabled {
173 | showError(message: NSLocalizedString("VPN has been disabled in settings or another VPN configuration is selected.", comment: "Main"))
174 | return
175 | } catch NEVPNError.configurationInvalid {
176 | showError(message: NSLocalizedString("VPN configuration is invalid.", comment: "Main"))
177 | vpnManager = nil
178 | return
179 | } catch {
180 | showError(error)
181 | return
182 | }
183 | }
184 |
185 | func stopTunnel(with manager: NEVPNManager) {
186 | if manager.connection.status == .disconnected {
187 | return
188 | }
189 |
190 | manager.connection.stopVPNTunnel()
191 | }
192 |
193 | func vpnWillConnect(for manager: NEVPNManager) {
194 | DispatchQueue.main.async {
195 | self.vpnStartSwitch.isOn = true
196 | self.vpnStartSwitch.isEnabled = false
197 | self.activityIndicator.startAnimating()
198 | }
199 | }
200 |
201 | func vpnDidConnect(for manager: NEVPNManager) {
202 | DispatchQueue.main.async {
203 | self.vpnStartSwitch.isOn = true
204 | self.vpnStartSwitch.isEnabled = true
205 | self.activityIndicator.stopAnimating()
206 | }
207 | }
208 |
209 | func vpnWillDisconnect(for manager: NEVPNManager) {
210 | DispatchQueue.main.async {
211 | self.vpnStartSwitch.isOn = false
212 | self.vpnStartSwitch.isEnabled = false
213 | self.activityIndicator.startAnimating()
214 | }
215 | }
216 |
217 | func vpnDidDisconnect(for manager: NEVPNManager) {
218 | DispatchQueue.main.async {
219 | self.vpnStartSwitch.isOn = false
220 | self.vpnStartSwitch.isEnabled = true
221 | self.activityIndicator.stopAnimating()
222 | }
223 | }
224 | }
225 |
226 | extension ViewController {
227 | private var logUrl: URL? {
228 | guard let bundleId = Bundle.main.bundleIdentifier else {
229 | return nil
230 | }
231 | let groupBundleId = "group." + bundleId
232 | let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupBundleId)
233 | return containerUrl?.appendingPathComponent("log.txt")
234 | }
235 |
236 | private func getNewLogLines() -> String? {
237 | guard let logUrl = logUrl else {
238 | return nil
239 | }
240 | guard let handle = try? FileHandle(forReadingFrom: logUrl) else {
241 | lastLogIndex = 0
242 | return nil
243 | }
244 | defer {
245 | handle.closeFile()
246 | }
247 | handle.seek(toFileOffset: lastLogIndex)
248 | let newlines = String(data: handle.readDataToEndOfFile(), encoding: .utf8)
249 | lastLogIndex = handle.offsetInFile
250 | return newlines
251 | }
252 |
253 | func clearLog() {
254 | guard let logUrl = logUrl else {
255 | return
256 | }
257 | try? FileManager.default.removeItem(at: logUrl)
258 | lastLogIndex = 0
259 | DispatchQueue.main.async {
260 | self.logTextView.text = ""
261 | }
262 | }
263 |
264 | func refreshLog() {
265 | if let lines = getNewLogLines() {
266 | print(lines, terminator: "")
267 | if lines.count > 0 {
268 | DispatchQueue.main.async {
269 | self.logTextView.text += lines
270 | self.logTextView.scrollToBottom()
271 | }
272 | }
273 | } else {
274 | DispatchQueue.main.async {
275 | self.logTextView.text = ""
276 | }
277 | }
278 | DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1.0, execute: refreshLog)
279 | }
280 | }
281 |
282 | extension UITextView {
283 | func scrollToBottom() {
284 | let textCount: Int = text.count
285 | guard textCount >= 1 else { return }
286 | scrollRangeToVisible(NSRange(location: textCount - 1, length: 1))
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/PurpleHaze/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/Extension/Iodine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright © 2021 osy. All rights reserved.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | import Foundation
18 |
19 | final public class Iodine: NSObject {
20 | public private(set) var ipv6Support: Bool = true
21 | public private(set) var nameserverHost: String? = nil
22 | public private(set) var topDomain: String? = nil
23 | public private(set) var password: String? = nil
24 | public private(set) var maxDownstreamFragmentSize: Int = 0
25 | public private(set) var rawMode: Bool = true
26 | public private(set) var lazyMode: Bool = true
27 | public private(set) var selectTimeout: Int = 4
28 | public private(set) var hostnameMaxLength: Int = 0xFF
29 | public private(set) var forceDnsType: String? = nil
30 | public private(set) var forceEncoding: String? = nil
31 |
32 | public weak var delegate: IodineDelegate?
33 |
34 | private var iodineToNetworkExtensionSocket: Int32 = -1
35 | private var networkExtensionToIodineSocket: Int32 = -1
36 | private var dnsSocket: Int32 = -1
37 | private var topDomainPtr: UnsafeMutablePointer?
38 | private var passwordPtr: UnsafeMutablePointer?
39 |
40 | private var inputStream: InputStream?
41 | private var outputStream: OutputStream?
42 |
43 | private var runQueue: DispatchQueue?
44 | private var isSetupComplete: Bool = false
45 | private var isRunning: Bool = false
46 | private var nameserver = sockaddr_storage()
47 |
48 | private var defaultNameserver: sockaddr_storage? {
49 | var state = __res_9_state()
50 | var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: Int(NI_MAXSERV))
51 | res_9_ninit(&state)
52 | defer {
53 | res_9_nclose(&state)
54 | }
55 | let count = res_9_getservers(&state, &servers, NI_MAXSERV)
56 | guard count > 0 else {
57 | return nil
58 | }
59 | servers.removeLast(Int(NI_MAXSERV - count))
60 | var result = sockaddr_storage()
61 | guard var server = servers.first(where: { server in
62 | self.ipv6Support || server.sin6.sin6_family != AF_INET6
63 | }) else {
64 | return nil
65 | }
66 | withUnsafePointer(to: &server) { addr in
67 | let ptr = UnsafeRawPointer(addr).assumingMemoryBound(to: sockaddr_storage.self)
68 | result = ptr.pointee
69 | }
70 | return result
71 | }
72 |
73 | public var tunDnsServer: String? {
74 | guard var defaultNameserver = defaultNameserver else {
75 | return nil
76 | }
77 | let dnsHostname = String(cString: format_addr(&defaultNameserver, Int32(defaultNameserver.ss_len)))
78 | return dnsHostname
79 | }
80 |
81 | public convenience init(options: [String : Any]? = nil) {
82 | self.init()
83 | guard let options = options else {
84 | return
85 | }
86 | if let ipv6Support = options[IodineSettings.ipv6Support] as? Bool {
87 | self.ipv6Support = ipv6Support
88 | }
89 | if let nameserverHost = options[IodineSettings.nameserverHost] as? String, nameserverHost.count > 0 {
90 | self.nameserverHost = nameserverHost
91 | }
92 | if let topDomain = options[IodineSettings.topDomain] as? String, topDomain.count > 0 {
93 | self.topDomain = topDomain
94 | }
95 | if let password = options[IodineSettings.password] as? String, password.count > 0 {
96 | self.password = String(password.prefix(32))
97 | }
98 | let maxDownstreamFragmentSize: Int?
99 | if let maxDownstreamFragmentSizeString = options[IodineSettings.maxDownstreamFragmentSize] as? String, maxDownstreamFragmentSizeString.count > 0 {
100 | maxDownstreamFragmentSize = Int(maxDownstreamFragmentSizeString)
101 | } else {
102 | maxDownstreamFragmentSize = options[IodineSettings.maxDownstreamFragmentSize] as? Int
103 | }
104 | if let maxDownstreamFragmentSize = maxDownstreamFragmentSize {
105 | if maxDownstreamFragmentSize > 0xffff {
106 | self.maxDownstreamFragmentSize = 0xffff
107 | } else {
108 | self.maxDownstreamFragmentSize = maxDownstreamFragmentSize
109 | }
110 | }
111 | if let rawMode = options[IodineSettings.rawMode] as? Bool {
112 | self.rawMode = rawMode
113 | }
114 | if let lazyMode = options[IodineSettings.lazyMode] as? Bool {
115 | self.lazyMode = lazyMode
116 | if !lazyMode {
117 | self.selectTimeout = 1
118 | }
119 | }
120 | let selectTimeout: Int?
121 | if let selectTimeoutString = options[IodineSettings.selectTimeout] as? String, selectTimeoutString.count > 0 {
122 | selectTimeout = Int(selectTimeoutString)
123 | } else {
124 | selectTimeout = options[IodineSettings.selectTimeout] as? Int
125 | }
126 | if let selectTimeout = selectTimeout {
127 | if selectTimeout < 1 {
128 | self.selectTimeout = 1
129 | } else {
130 | self.selectTimeout = selectTimeout
131 | }
132 | }
133 | let hostnameMaxLength: Int?
134 | if let hostnameMaxLengthString = options[IodineSettings.hostnameMaxLength] as? String, hostnameMaxLengthString.count > 0 {
135 | hostnameMaxLength = Int(hostnameMaxLengthString)
136 | } else {
137 | hostnameMaxLength = options[IodineSettings.hostnameMaxLength] as? Int
138 | }
139 | if let hostnameMaxLength = hostnameMaxLength {
140 | if hostnameMaxLength > 255 {
141 | self.hostnameMaxLength = 255
142 | } else if hostnameMaxLength < 10 {
143 | self.hostnameMaxLength = 10
144 | } else {
145 | self.hostnameMaxLength = hostnameMaxLength
146 | }
147 | }
148 | if let forceDnsType = options[IodineSettings.forceDnsType] as? String, forceDnsType.count > 0 {
149 | self.forceDnsType = forceDnsType
150 | }
151 | if let forceEncoding = options[IodineSettings.forceEncoding] as? String, forceEncoding.count > 0 {
152 | self.forceEncoding = forceEncoding
153 | }
154 | }
155 |
156 | private func setup() throws {
157 | guard !isSetupComplete else {
158 | throw IodineError.internalError
159 | }
160 |
161 | if let forceDnsType = forceDnsType {
162 | let forceDnsTypePtr = forceDnsType.unsafeAllocate()!
163 | client_set_qtype(forceDnsTypePtr)
164 | forceDnsTypePtr.deallocate()
165 | }
166 |
167 | if let forceEncoding = forceEncoding {
168 | let forceEncodingPtr = forceEncoding.unsafeAllocate()
169 | client_set_qtype(forceEncodingPtr)
170 | forceEncodingPtr?.deallocate()
171 | }
172 |
173 | let family = ipv6Support ? AF_UNSPEC : AF_INET
174 | if let host = nameserverHost {
175 | let hostPtr = host.unsafeAllocate()!
176 | let len = get_addr(hostPtr, DNS_PORT, family, 0, &nameserver)
177 | hostPtr.deallocate()
178 | guard len > 0 else {
179 | throw IodineError.invalidNameserverHost
180 | }
181 | client_set_nameserver(&nameserver, len)
182 | } else {
183 | guard let defaultNameserver = defaultNameserver else {
184 | throw IodineError.defaultDnsNotFound
185 | }
186 | nameserver = defaultNameserver
187 | client_set_nameserver(&nameserver, Int32(nameserver.ss_len))
188 | }
189 |
190 | guard let topdomain = topDomain else {
191 | throw IodineError.invalidTopdomain()
192 | }
193 | var errormsg: UnsafeMutablePointer? = nil
194 | if let topDomainPtr = topDomainPtr {
195 | topDomainPtr.deallocate()
196 | }
197 | topDomainPtr = topdomain.unsafeAllocate()
198 | guard check_topdomain(topDomainPtr, 0, &errormsg) == 0 else {
199 | throw IodineError.invalidTopdomain(message: errormsg?.string)
200 | }
201 | client_set_topdomain(topDomainPtr)
202 |
203 | client_set_selecttimeout(Int32(selectTimeout))
204 | client_set_lazymode(lazyMode ? 1 : 0)
205 | client_set_hostname_maxlen(Int32(hostnameMaxLength))
206 |
207 | if let password = password {
208 | if let passwordPtr = passwordPtr {
209 | passwordPtr.deallocate()
210 | }
211 | passwordPtr = password.unsafeAllocate(capacity: 33)
212 | client_set_password(passwordPtr)
213 | }
214 |
215 | runQueue = DispatchQueue(label: "Iodine")
216 | isSetupComplete = true
217 | }
218 |
219 | public func start() throws {
220 | guard !isRunning else {
221 | throw IodineError.internalError
222 | }
223 |
224 | iodine_srand()
225 | client_init()
226 |
227 | if !isSetupComplete {
228 | try setup()
229 | }
230 |
231 | var fds = [Int32](repeating: -1, count: 2)
232 | guard socketpair(AF_UNIX, SOCK_STREAM, 0, &fds) == 0 else {
233 | throw IodineError.socketCreateFailed
234 | }
235 | iodineToNetworkExtensionSocket = fds[0]
236 | networkExtensionToIodineSocket = fds[1]
237 |
238 | print("Connecting to DNS...", to: &StdioRedirect.standardError)
239 | dnsSocket = open_dns_from_host(nil, 0, Int32(nameserver.ss_family), AI_PASSIVE)
240 | guard dnsSocket >= 0 else {
241 | stop()
242 | throw IodineError.dnsOpenFailed
243 | }
244 |
245 | print("Opening streams...", to: &StdioRedirect.standardError)
246 | guard openStream() else {
247 | stop()
248 | throw IodineError.internalError
249 | }
250 |
251 | let dnsHostname = String(cString: format_addr(&nameserver, Int32(nameserver.ss_len)))
252 | print("Sending DNS queries for \(topDomain ?? "(null)") to \(dnsHostname)", to: &StdioRedirect.standardError);
253 | guard client_handshake(dnsSocket, rawMode ? 1 : 0, maxDownstreamFragmentSize > 0 ? 0 : 1, Int32(maxDownstreamFragmentSize)) == 0 else {
254 | stop()
255 | throw IodineError.handshakeFailed
256 | }
257 |
258 | if client_get_conn() == CONN_RAW_UDP {
259 | let rawAddr = client_get_raw_addr()
260 | print("Sending raw traffic directly to \(rawAddr?.string ?? "(unknown)")", to: &StdioRedirect.standardError)
261 | }
262 |
263 | print("Connection setup complete, transmitting data.", to: &StdioRedirect.standardError)
264 |
265 | self.isRunning = true
266 | runQueue!.async {
267 | client_tunnel(self.networkExtensionToIodineSocket, self.dnsSocket)
268 | self.isRunning = false
269 | self.delegate?.iodineDidStop()
270 | }
271 | }
272 |
273 | public func stop() {
274 | closeStream()
275 | if iodineToNetworkExtensionSocket >= 0 {
276 | close(iodineToNetworkExtensionSocket)
277 | iodineToNetworkExtensionSocket = -1
278 | }
279 | if networkExtensionToIodineSocket >= 0 {
280 | close(networkExtensionToIodineSocket)
281 | networkExtensionToIodineSocket = -1
282 | }
283 | if dnsSocket >= 0 {
284 | close_dns(dnsSocket)
285 | dnsSocket = -1
286 | }
287 | client_stop()
288 | runQueue!.sync {
289 | print("Service has stopped.", to: &StdioRedirect.standardError)
290 | }
291 | }
292 | }
293 |
294 | extension Iodine: StreamDelegate {
295 | fileprivate func openStream() -> Bool {
296 | var readStream: Unmanaged?
297 | var writeStream: Unmanaged?
298 |
299 | CFStreamCreatePairWithSocket(kCFAllocatorDefault,
300 | iodineToNetworkExtensionSocket,
301 | &readStream,
302 | &writeStream)
303 |
304 | inputStream = readStream?.takeRetainedValue()
305 | if inputStream == nil {
306 | return false
307 | }
308 | outputStream = writeStream?.takeRetainedValue()
309 | if outputStream == nil {
310 | inputStream = nil
311 | return false
312 | }
313 | inputStream!.delegate = self
314 | inputStream!.schedule(in: .main, forMode: .common)
315 | outputStream!.schedule(in: .main, forMode: .common)
316 | inputStream!.open()
317 | outputStream!.open()
318 | return true
319 | }
320 |
321 | fileprivate func closeStream() {
322 | if inputStream != nil {
323 | inputStream!.remove(from: .current, forMode: .common)
324 | inputStream!.close()
325 | inputStream = nil
326 | }
327 | if outputStream != nil {
328 | outputStream!.remove(from: .current, forMode: .common)
329 | outputStream!.close()
330 | outputStream = nil
331 | }
332 | }
333 |
334 | public func stream(_ stream: Stream, handle: Stream.Event) {
335 | switch handle {
336 | case .hasBytesAvailable:
337 | do {
338 | let (data, protocols) = try readDataFrom(stream: stream as! InputStream)
339 | delegate?.iodineReadData(data, withProtocols: protocols)
340 | } catch {
341 | delegate?.iodineError(error)
342 | }
343 | case .errorOccurred:
344 | delegate?.iodineError(stream.streamError)
345 | default:
346 | break
347 | }
348 | }
349 |
350 | private func readPacketFrom(stream: InputStream, headerLength: Int, lengthOffset: Int, lengthIncludesHeader: Bool) throws -> Data {
351 | let buffer = UnsafeMutablePointer.allocate(capacity: headerLength)
352 | defer {
353 | buffer.deallocate()
354 | }
355 | guard stream.read(buffer, maxLength: headerLength) == headerLength else {
356 | throw IodineError.incompletePacket
357 | }
358 | let raw = UnsafeRawPointer(buffer.advanced(by: lengthOffset)).load(as: UInt16.self)
359 | let lengthField = NSSwapBigShortToHost(raw)
360 | let payloadLength: Int
361 | if lengthIncludesHeader {
362 | payloadLength = Int(lengthField) - headerLength
363 | } else {
364 | payloadLength = Int(lengthField)
365 | }
366 | guard payloadLength >= 0 else {
367 | throw IodineError.invalidPacket
368 | }
369 | var data = Data(bytes: buffer, count: headerLength)
370 | let remaining = UnsafeMutablePointer.allocate(capacity: payloadLength)
371 | defer {
372 | remaining.deallocate()
373 | }
374 | guard stream.read(remaining, maxLength: payloadLength) == payloadLength else {
375 | throw IodineError.incompletePacket
376 | }
377 | data.append(Data(bytes: remaining, count: payloadLength))
378 | return data
379 | }
380 |
381 | private func readDataFrom(stream: InputStream) throws -> ([Data], [NSNumber]) {
382 | var packets = [Data]()
383 | var protocols = [NSNumber]()
384 | while stream.hasBytesAvailable {
385 | let headerBuf = UnsafeMutablePointer.allocate(capacity: 4)
386 | defer {
387 | headerBuf.deallocate()
388 | }
389 | guard stream.read(headerBuf, maxLength: 4) == 4 else {
390 | throw IodineError.incompletePacket
391 | }
392 | let raw = UnsafeRawPointer(headerBuf).load(as: UInt32.self)
393 | let family = Int32(NSSwapBigIntToHost(raw))
394 | if family == AF_INET || family == 0x0800 {
395 | packets.append(try readPacketFrom(stream: stream, headerLength: 20, lengthOffset: 2, lengthIncludesHeader: true))
396 | protocols.append(NSNumber(value: AF_INET))
397 | } else if family == AF_INET6 || family == 0x86DD {
398 | packets.append(try readPacketFrom(stream: stream, headerLength: 40, lengthOffset: 4, lengthIncludesHeader: false))
399 | protocols.append(NSNumber(value: AF_INET6))
400 | } else {
401 | throw IodineError.unrecognizedFamily(family)
402 | }
403 | }
404 | return (packets, protocols)
405 | }
406 |
407 | public func writeData(_ data: Data, family: Int32) {
408 | guard isRunning, let outputStream = outputStream else {
409 | delegate?.iodineError(IodineError.notConnected)
410 | return
411 | }
412 | var familyNtohl = family.bigEndian
413 | var packet = withUnsafeBytes(of: &familyNtohl) { bytes in
414 | Data(bytes: bytes.baseAddress!, count: bytes.count)
415 | }
416 | packet.append(data)
417 | packet.withUnsafeBytes { bytes in
418 | _ = outputStream.write(bytes.bindMemory(to: UInt8.self).baseAddress!, maxLength: packet.count)
419 | }
420 | }
421 | }
422 |
423 | public enum IodineError: LocalizedError {
424 | case internalError
425 | case invalidNameserverHost
426 | case defaultDnsNotFound
427 | case invalidTopdomain(message: String? = nil)
428 | case socketCreateFailed
429 | case dnsOpenFailed
430 | case handshakeFailed
431 | case notConnected
432 | case invalidPacket
433 | case incompletePacket
434 | case unrecognizedFamily(_ family: Int32)
435 |
436 | public var errorDescription: String? {
437 | switch self {
438 | case .internalError:
439 | return NSLocalizedString("An internal error has occurred.", comment: "Iodine")
440 | case .invalidNameserverHost:
441 | return NSLocalizedString("Invalid nameserver host.", comment: "Iodine")
442 | case .defaultDnsNotFound:
443 | return NSLocalizedString("Cannot get default DNS address.", comment: "Iodine")
444 | case .invalidTopdomain(let message):
445 | return NSLocalizedString("Invalid top domain: \(message ?? "unknown cause")", comment: "Iodine")
446 | case .socketCreateFailed:
447 | return NSLocalizedString("Failed to create socketpair for communication.", comment: "Iodine")
448 | case .dnsOpenFailed:
449 | return NSLocalizedString("Failed to open connection to DNS server.", comment: "Iodine")
450 | case .handshakeFailed:
451 | return NSLocalizedString("Failed to establish handshake with server.", comment: "Iodine")
452 | case .notConnected:
453 | return NSLocalizedString("Not connected.", comment: "Iodine")
454 | case .invalidPacket:
455 | return NSLocalizedString("Invalid packet.", comment: "Iodine")
456 | case .incompletePacket:
457 | return NSLocalizedString("Incomplete packet.", comment: "Iodine")
458 | case .unrecognizedFamily(let family):
459 | return NSLocalizedString("Unrecognized family \(family).", comment: "Iodine")
460 | }
461 | }
462 | }
463 |
464 | fileprivate extension String {
465 | func unsafeAllocate(capacity: Int = 0) -> UnsafeMutablePointer? {
466 | let utf8 = self.utf8CString
467 | let result = UnsafeMutableBufferPointer.allocate(capacity: capacity > 0 ? capacity : utf8.count)
468 | let (_, lastIndex) = result.initialize(from: utf8)
469 | let remaining = UnsafeMutableBufferPointer(rebasing: result.suffix(from: lastIndex))
470 | remaining.initialize(repeating: 0)
471 | return result.baseAddress
472 | }
473 | }
474 |
475 | fileprivate extension UnsafePointer where Pointee == CChar {
476 | var string: String {
477 | String(cString: self)
478 | }
479 | }
480 |
481 | fileprivate extension UnsafeMutablePointer where Pointee == CChar {
482 | var string: String {
483 | String(cString: self)
484 | }
485 | }
486 |
--------------------------------------------------------------------------------
/PurpleHaze.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | CE996E5F26FFF94E005E4AC2 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE996E5E26FFF94E005E4AC2 /* Settings.bundle */; };
11 | CED110D526FBAE78005B6445 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED110D426FBAE78005B6445 /* AppDelegate.swift */; };
12 | CED110D926FBAE78005B6445 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED110D826FBAE78005B6445 /* ViewController.swift */; };
13 | CED110DC26FBAE78005B6445 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CED110DA26FBAE78005B6445 /* Main.storyboard */; };
14 | CED110DE26FBAE7F005B6445 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CED110DD26FBAE7F005B6445 /* Assets.xcassets */; };
15 | CED110E126FBAE7F005B6445 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CED110DF26FBAE7F005B6445 /* LaunchScreen.storyboard */; };
16 | CED1112726FBB55F005B6445 /* Build.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = CED1112626FBB55F005B6445 /* Build.xcconfig */; };
17 | CED1113326FBDD57005B6445 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED1113226FBDD57005B6445 /* NetworkExtension.framework */; };
18 | CED1113626FBDD57005B6445 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1113526FBDD57005B6445 /* PacketTunnelProvider.swift */; };
19 | CED1113B26FBDD57005B6445 /* Iodine.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CED1113026FBDD57005B6445 /* Iodine.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
20 | CED1114C26FBDE3B005B6445 /* base128.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110FA26FBAFA9005B6445 /* base128.c */; };
21 | CED1114D26FBDE3B005B6445 /* common.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110F426FBAFA9005B6445 /* common.c */; };
22 | CED1114E26FBDE3B005B6445 /* encoding.c in Sources */ = {isa = PBXBuildFile; fileRef = CED1110126FBAFA9005B6445 /* encoding.c */; };
23 | CED1114F26FBDE3B005B6445 /* read.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110F626FBAFA9005B6445 /* read.c */; };
24 | CED1115026FBDE3B005B6445 /* login.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110FD26FBAFA9005B6445 /* login.c */; };
25 | CED1115126FBDE3B005B6445 /* base32.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110FC26FBAFA9005B6445 /* base32.c */; };
26 | CED1115226FBDE3B005B6445 /* base64u.c in Sources */ = {isa = PBXBuildFile; fileRef = CED1111A26FBB2FD005B6445 /* base64u.c */; };
27 | CED1115326FBDE3B005B6445 /* md5.c in Sources */ = {isa = PBXBuildFile; fileRef = CED1110226FBAFA9005B6445 /* md5.c */; };
28 | CED1115426FBDE3B005B6445 /* dns.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110F326FBAFA8005B6445 /* dns.c */; };
29 | CED1115526FBDE3B005B6445 /* util.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110F126FBAFA8005B6445 /* util.c */; };
30 | CED1115626FBDE3B005B6445 /* client.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110FE26FBAFA9005B6445 /* client.c */; };
31 | CED1115726FBDE3C005B6445 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = CED110F226FBAFA8005B6445 /* base64.c */; };
32 | CED1115F26FBDF04005B6445 /* tun-shim.c in Sources */ = {isa = PBXBuildFile; fileRef = CED1111F26FBB365005B6445 /* tun-shim.c */; };
33 | CED1116726FBDFBA005B6445 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED1113226FBDD57005B6445 /* NetworkExtension.framework */; };
34 | CED1116F26FBE12C005B6445 /* Iodine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1116E26FBE12C005B6445 /* Iodine.swift */; };
35 | CED1117726FC0892005B6445 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CED1117626FC0892005B6445 /* libresolv.9.tbd */; };
36 | CED1118726FC2728005B6445 /* IodineSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1118626FC2728005B6445 /* IodineSettings.swift */; };
37 | CED1118C26FC2758005B6445 /* IodineSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1118626FC2728005B6445 /* IodineSettings.swift */; };
38 | CED1119026FC4169005B6445 /* IodineDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1118F26FC4169005B6445 /* IodineDelegate.swift */; };
39 | CED1119C26FCE92C005B6445 /* StdioRedirect.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED1119B26FCE92C005B6445 /* StdioRedirect.swift */; };
40 | /* End PBXBuildFile section */
41 |
42 | /* Begin PBXContainerItemProxy section */
43 | CED1113926FBDD57005B6445 /* PBXContainerItemProxy */ = {
44 | isa = PBXContainerItemProxy;
45 | containerPortal = CED110C926FBAE78005B6445 /* Project object */;
46 | proxyType = 1;
47 | remoteGlobalIDString = CED1112F26FBDD57005B6445;
48 | remoteInfo = Iodine;
49 | };
50 | /* End PBXContainerItemProxy section */
51 |
52 | /* Begin PBXCopyFilesBuildPhase section */
53 | CED1113F26FBDD57005B6445 /* Embed App Extensions */ = {
54 | isa = PBXCopyFilesBuildPhase;
55 | buildActionMask = 2147483647;
56 | dstPath = "";
57 | dstSubfolderSpec = 13;
58 | files = (
59 | CED1113B26FBDD57005B6445 /* Iodine.appex in Embed App Extensions */,
60 | );
61 | name = "Embed App Extensions";
62 | runOnlyForDeploymentPostprocessing = 0;
63 | };
64 | /* End PBXCopyFilesBuildPhase section */
65 |
66 | /* Begin PBXFileReference section */
67 | CE996E5E26FFF94E005E4AC2 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; };
68 | CED110D126FBAE78005B6445 /* Purple Haze.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Purple Haze.app"; sourceTree = BUILT_PRODUCTS_DIR; };
69 | CED110D426FBAE78005B6445 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
70 | CED110D826FBAE78005B6445 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
71 | CED110DB26FBAE78005B6445 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
72 | CED110DD26FBAE7F005B6445 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
73 | CED110E026FBAE7F005B6445 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
74 | CED110E226FBAE7F005B6445 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
75 | CED110ED26FBAFA8005B6445 /* md5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = md5.h; path = iodine/src/md5.h; sourceTree = ""; };
76 | CED110EF26FBAFA8005B6445 /* encoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = encoding.h; path = iodine/src/encoding.h; sourceTree = ""; };
77 | CED110F026FBAFA8005B6445 /* dns.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = dns.h; path = iodine/src/dns.h; sourceTree = ""; };
78 | CED110F126FBAFA8005B6445 /* util.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = util.c; path = iodine/src/util.c; sourceTree = ""; };
79 | CED110F226FBAFA8005B6445 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base64.c; path = iodine/src/base64.c; sourceTree = ""; };
80 | CED110F326FBAFA8005B6445 /* dns.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = dns.c; path = iodine/src/dns.c; sourceTree = ""; };
81 | CED110F426FBAFA9005B6445 /* common.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = common.c; path = iodine/src/common.c; sourceTree = ""; };
82 | CED110F526FBAFA9005B6445 /* login.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = login.h; path = iodine/src/login.h; sourceTree = ""; };
83 | CED110F626FBAFA9005B6445 /* read.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = read.c; path = iodine/src/read.c; sourceTree = ""; };
84 | CED110F826FBAFA9005B6445 /* read.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = read.h; path = iodine/src/read.h; sourceTree = ""; };
85 | CED110FA26FBAFA9005B6445 /* base128.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base128.c; path = iodine/src/base128.c; sourceTree = ""; };
86 | CED110FB26FBAFA9005B6445 /* client.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = client.h; path = iodine/src/client.h; sourceTree = ""; };
87 | CED110FC26FBAFA9005B6445 /* base32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base32.c; path = iodine/src/base32.c; sourceTree = ""; };
88 | CED110FD26FBAFA9005B6445 /* login.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = login.c; path = iodine/src/login.c; sourceTree = ""; };
89 | CED110FE26FBAFA9005B6445 /* client.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = client.c; path = iodine/src/client.c; sourceTree = ""; };
90 | CED110FF26FBAFA9005B6445 /* common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = common.h; path = iodine/src/common.h; sourceTree = ""; };
91 | CED1110026FBAFA9005B6445 /* util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = util.h; path = iodine/src/util.h; sourceTree = ""; };
92 | CED1110126FBAFA9005B6445 /* encoding.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = encoding.c; path = iodine/src/encoding.c; sourceTree = ""; };
93 | CED1110226FBAFA9005B6445 /* md5.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = md5.c; path = iodine/src/md5.c; sourceTree = ""; };
94 | CED1111626FBB19D005B6445 /* tun.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = tun.h; path = iodine/src/tun.h; sourceTree = ""; };
95 | CED1111A26FBB2FD005B6445 /* base64u.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = base64u.c; path = "$(DERIVED_FILE_DIR)/base64u.c"; sourceTree = ""; };
96 | CED1111F26FBB365005B6445 /* tun-shim.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = "tun-shim.c"; sourceTree = ""; };
97 | CED1112626FBB55F005B6445 /* Build.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; };
98 | CED1112A26FBB5AB005B6445 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
99 | CED1113026FBDD57005B6445 /* Iodine.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Iodine.appex; sourceTree = BUILT_PRODUCTS_DIR; };
100 | CED1113226FBDD57005B6445 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
101 | CED1113526FBDD57005B6445 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; };
102 | CED1113726FBDD57005B6445 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
103 | CED1113826FBDD57005B6445 /* Iodine.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Iodine.entitlements; sourceTree = ""; };
104 | CED1116626FBDFBA005B6445 /* PurpleHaze.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PurpleHaze.entitlements; sourceTree = ""; };
105 | CED1116E26FBE12C005B6445 /* Iodine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Iodine.swift; sourceTree = ""; };
106 | CED1117226FBE34E005B6445 /* Swift-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Swift-Bridging-Header.h"; sourceTree = ""; };
107 | CED1117626FC0892005B6445 /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; };
108 | CED1117C26FC1A9A005B6445 /* Iodine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Iodine.h; sourceTree = ""; };
109 | CED1118626FC2728005B6445 /* IodineSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IodineSettings.swift; sourceTree = ""; };
110 | CED1118F26FC4169005B6445 /* IodineDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IodineDelegate.swift; sourceTree = ""; };
111 | CED1119B26FCE92C005B6445 /* StdioRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StdioRedirect.swift; sourceTree = ""; };
112 | /* End PBXFileReference section */
113 |
114 | /* Begin PBXFrameworksBuildPhase section */
115 | CED110CE26FBAE78005B6445 /* Frameworks */ = {
116 | isa = PBXFrameworksBuildPhase;
117 | buildActionMask = 2147483647;
118 | files = (
119 | CED1116726FBDFBA005B6445 /* NetworkExtension.framework in Frameworks */,
120 | );
121 | runOnlyForDeploymentPostprocessing = 0;
122 | };
123 | CED1112D26FBDD57005B6445 /* Frameworks */ = {
124 | isa = PBXFrameworksBuildPhase;
125 | buildActionMask = 2147483647;
126 | files = (
127 | CED1117726FC0892005B6445 /* libresolv.9.tbd in Frameworks */,
128 | CED1113326FBDD57005B6445 /* NetworkExtension.framework in Frameworks */,
129 | );
130 | runOnlyForDeploymentPostprocessing = 0;
131 | };
132 | /* End PBXFrameworksBuildPhase section */
133 |
134 | /* Begin PBXGroup section */
135 | CED110C826FBAE78005B6445 = {
136 | isa = PBXGroup;
137 | children = (
138 | CED1112A26FBB5AB005B6445 /* README.md */,
139 | CED1112626FBB55F005B6445 /* Build.xcconfig */,
140 | CED110EB26FBAF0D005B6445 /* Iodine */,
141 | CED110D326FBAE78005B6445 /* PurpleHaze */,
142 | CED1113426FBDD57005B6445 /* Extension */,
143 | CED1113126FBDD57005B6445 /* Frameworks */,
144 | CED110D226FBAE78005B6445 /* Products */,
145 | );
146 | sourceTree = "";
147 | };
148 | CED110D226FBAE78005B6445 /* Products */ = {
149 | isa = PBXGroup;
150 | children = (
151 | CED110D126FBAE78005B6445 /* Purple Haze.app */,
152 | CED1113026FBDD57005B6445 /* Iodine.appex */,
153 | );
154 | name = Products;
155 | sourceTree = "";
156 | };
157 | CED110D326FBAE78005B6445 /* PurpleHaze */ = {
158 | isa = PBXGroup;
159 | children = (
160 | CED110D426FBAE78005B6445 /* AppDelegate.swift */,
161 | CED110D826FBAE78005B6445 /* ViewController.swift */,
162 | CED110DA26FBAE78005B6445 /* Main.storyboard */,
163 | CED110DD26FBAE7F005B6445 /* Assets.xcassets */,
164 | CED110DF26FBAE7F005B6445 /* LaunchScreen.storyboard */,
165 | CED110E226FBAE7F005B6445 /* Info.plist */,
166 | CED1116626FBDFBA005B6445 /* PurpleHaze.entitlements */,
167 | CE996E5E26FFF94E005E4AC2 /* Settings.bundle */,
168 | );
169 | path = PurpleHaze;
170 | sourceTree = "";
171 | };
172 | CED110EB26FBAF0D005B6445 /* Iodine */ = {
173 | isa = PBXGroup;
174 | children = (
175 | CED110FC26FBAFA9005B6445 /* base32.c */,
176 | CED110F226FBAFA8005B6445 /* base64.c */,
177 | CED1111A26FBB2FD005B6445 /* base64u.c */,
178 | CED110FA26FBAFA9005B6445 /* base128.c */,
179 | CED110FE26FBAFA9005B6445 /* client.c */,
180 | CED110FB26FBAFA9005B6445 /* client.h */,
181 | CED110F426FBAFA9005B6445 /* common.c */,
182 | CED110FF26FBAFA9005B6445 /* common.h */,
183 | CED110F326FBAFA8005B6445 /* dns.c */,
184 | CED110F026FBAFA8005B6445 /* dns.h */,
185 | CED1110126FBAFA9005B6445 /* encoding.c */,
186 | CED110EF26FBAFA8005B6445 /* encoding.h */,
187 | CED110FD26FBAFA9005B6445 /* login.c */,
188 | CED110F526FBAFA9005B6445 /* login.h */,
189 | CED1110226FBAFA9005B6445 /* md5.c */,
190 | CED110ED26FBAFA8005B6445 /* md5.h */,
191 | CED110F626FBAFA9005B6445 /* read.c */,
192 | CED110F826FBAFA9005B6445 /* read.h */,
193 | CED1111626FBB19D005B6445 /* tun.h */,
194 | CED110F126FBAFA8005B6445 /* util.c */,
195 | CED1110026FBAFA9005B6445 /* util.h */,
196 | );
197 | name = Iodine;
198 | sourceTree = "";
199 | };
200 | CED1113126FBDD57005B6445 /* Frameworks */ = {
201 | isa = PBXGroup;
202 | children = (
203 | CED1117626FC0892005B6445 /* libresolv.9.tbd */,
204 | CED1113226FBDD57005B6445 /* NetworkExtension.framework */,
205 | );
206 | name = Frameworks;
207 | sourceTree = "";
208 | };
209 | CED1113426FBDD57005B6445 /* Extension */ = {
210 | isa = PBXGroup;
211 | children = (
212 | CED1117C26FC1A9A005B6445 /* Iodine.h */,
213 | CED1116E26FBE12C005B6445 /* Iodine.swift */,
214 | CED1118F26FC4169005B6445 /* IodineDelegate.swift */,
215 | CED1118626FC2728005B6445 /* IodineSettings.swift */,
216 | CED1113526FBDD57005B6445 /* PacketTunnelProvider.swift */,
217 | CED1119B26FCE92C005B6445 /* StdioRedirect.swift */,
218 | CED1117226FBE34E005B6445 /* Swift-Bridging-Header.h */,
219 | CED1111F26FBB365005B6445 /* tun-shim.c */,
220 | CED1113726FBDD57005B6445 /* Info.plist */,
221 | CED1113826FBDD57005B6445 /* Iodine.entitlements */,
222 | );
223 | path = Extension;
224 | sourceTree = "";
225 | };
226 | /* End PBXGroup section */
227 |
228 | /* Begin PBXNativeTarget section */
229 | CED110D026FBAE78005B6445 /* Purple Haze */ = {
230 | isa = PBXNativeTarget;
231 | buildConfigurationList = CED110E526FBAE7F005B6445 /* Build configuration list for PBXNativeTarget "Purple Haze" */;
232 | buildPhases = (
233 | CED110CD26FBAE78005B6445 /* Sources */,
234 | CED110CE26FBAE78005B6445 /* Frameworks */,
235 | CED110CF26FBAE78005B6445 /* Resources */,
236 | CED1113F26FBDD57005B6445 /* Embed App Extensions */,
237 | );
238 | buildRules = (
239 | );
240 | dependencies = (
241 | CED1113A26FBDD57005B6445 /* PBXTargetDependency */,
242 | );
243 | name = "Purple Haze";
244 | productName = "Purple Haze";
245 | productReference = CED110D126FBAE78005B6445 /* Purple Haze.app */;
246 | productType = "com.apple.product-type.application";
247 | };
248 | CED1112F26FBDD57005B6445 /* Iodine */ = {
249 | isa = PBXNativeTarget;
250 | buildConfigurationList = CED1113C26FBDD57005B6445 /* Build configuration list for PBXNativeTarget "Iodine" */;
251 | buildPhases = (
252 | CED1115C26FBDEE2005B6445 /* Generate base64u.c */,
253 | CED1112C26FBDD57005B6445 /* Sources */,
254 | CED1112D26FBDD57005B6445 /* Frameworks */,
255 | CED1112E26FBDD57005B6445 /* Resources */,
256 | );
257 | buildRules = (
258 | );
259 | dependencies = (
260 | );
261 | name = Iodine;
262 | productName = Iodine;
263 | productReference = CED1113026FBDD57005B6445 /* Iodine.appex */;
264 | productType = "com.apple.product-type.app-extension";
265 | };
266 | /* End PBXNativeTarget section */
267 |
268 | /* Begin PBXProject section */
269 | CED110C926FBAE78005B6445 /* Project object */ = {
270 | isa = PBXProject;
271 | attributes = {
272 | LastSwiftUpdateCheck = 1240;
273 | LastUpgradeCheck = 1240;
274 | ORGANIZATIONNAME = osy;
275 | TargetAttributes = {
276 | CED110D026FBAE78005B6445 = {
277 | CreatedOnToolsVersion = 12.4;
278 | LastSwiftMigration = 1240;
279 | };
280 | CED1112F26FBDD57005B6445 = {
281 | CreatedOnToolsVersion = 12.4;
282 | };
283 | };
284 | };
285 | buildConfigurationList = CED110CC26FBAE78005B6445 /* Build configuration list for PBXProject "PurpleHaze" */;
286 | compatibilityVersion = "Xcode 9.3";
287 | developmentRegion = en;
288 | hasScannedForEncodings = 0;
289 | knownRegions = (
290 | en,
291 | Base,
292 | );
293 | mainGroup = CED110C826FBAE78005B6445;
294 | productRefGroup = CED110D226FBAE78005B6445 /* Products */;
295 | projectDirPath = "";
296 | projectRoot = "";
297 | targets = (
298 | CED110D026FBAE78005B6445 /* Purple Haze */,
299 | CED1112F26FBDD57005B6445 /* Iodine */,
300 | );
301 | };
302 | /* End PBXProject section */
303 |
304 | /* Begin PBXResourcesBuildPhase section */
305 | CED110CF26FBAE78005B6445 /* Resources */ = {
306 | isa = PBXResourcesBuildPhase;
307 | buildActionMask = 2147483647;
308 | files = (
309 | CED110E126FBAE7F005B6445 /* LaunchScreen.storyboard in Resources */,
310 | CED1112726FBB55F005B6445 /* Build.xcconfig in Resources */,
311 | CE996E5F26FFF94E005E4AC2 /* Settings.bundle in Resources */,
312 | CED110DE26FBAE7F005B6445 /* Assets.xcassets in Resources */,
313 | CED110DC26FBAE78005B6445 /* Main.storyboard in Resources */,
314 | );
315 | runOnlyForDeploymentPostprocessing = 0;
316 | };
317 | CED1112E26FBDD57005B6445 /* Resources */ = {
318 | isa = PBXResourcesBuildPhase;
319 | buildActionMask = 2147483647;
320 | files = (
321 | );
322 | runOnlyForDeploymentPostprocessing = 0;
323 | };
324 | /* End PBXResourcesBuildPhase section */
325 |
326 | /* Begin PBXShellScriptBuildPhase section */
327 | CED1115C26FBDEE2005B6445 /* Generate base64u.c */ = {
328 | isa = PBXShellScriptBuildPhase;
329 | buildActionMask = 2147483647;
330 | files = (
331 | );
332 | inputFileListPaths = (
333 | );
334 | inputPaths = (
335 | "$(SRCROOT)/iodine/src/base64.c",
336 | );
337 | name = "Generate base64u.c";
338 | outputFileListPaths = (
339 | );
340 | outputPaths = (
341 | "$(DERIVED_FILE_DIR)/base64u.c",
342 | );
343 | runOnlyForDeploymentPostprocessing = 0;
344 | shellPath = /bin/sh;
345 | shellScript = "echo '/* No use in editing, produced by Makefile! */' > \"$SCRIPT_OUTPUT_FILE_0\"\nsed -e 's/\\([Bb][Aa][Ss][Ee]64\\)/\\1u/g ; s/0123456789+/0123456789_/' < \"$SCRIPT_INPUT_FILE_0\" >> \"$SCRIPT_OUTPUT_FILE_0\"\n";
346 | };
347 | /* End PBXShellScriptBuildPhase section */
348 |
349 | /* Begin PBXSourcesBuildPhase section */
350 | CED110CD26FBAE78005B6445 /* Sources */ = {
351 | isa = PBXSourcesBuildPhase;
352 | buildActionMask = 2147483647;
353 | files = (
354 | CED110D926FBAE78005B6445 /* ViewController.swift in Sources */,
355 | CED110D526FBAE78005B6445 /* AppDelegate.swift in Sources */,
356 | CED1118C26FC2758005B6445 /* IodineSettings.swift in Sources */,
357 | );
358 | runOnlyForDeploymentPostprocessing = 0;
359 | };
360 | CED1112C26FBDD57005B6445 /* Sources */ = {
361 | isa = PBXSourcesBuildPhase;
362 | buildActionMask = 2147483647;
363 | files = (
364 | CED1114F26FBDE3B005B6445 /* read.c in Sources */,
365 | CED1114D26FBDE3B005B6445 /* common.c in Sources */,
366 | CED1114C26FBDE3B005B6445 /* base128.c in Sources */,
367 | CED1115426FBDE3B005B6445 /* dns.c in Sources */,
368 | CED1115026FBDE3B005B6445 /* login.c in Sources */,
369 | CED1115226FBDE3B005B6445 /* base64u.c in Sources */,
370 | CED1115F26FBDF04005B6445 /* tun-shim.c in Sources */,
371 | CED1114E26FBDE3B005B6445 /* encoding.c in Sources */,
372 | CED1115326FBDE3B005B6445 /* md5.c in Sources */,
373 | CED1115626FBDE3B005B6445 /* client.c in Sources */,
374 | CED1116F26FBE12C005B6445 /* Iodine.swift in Sources */,
375 | CED1119C26FCE92C005B6445 /* StdioRedirect.swift in Sources */,
376 | CED1119026FC4169005B6445 /* IodineDelegate.swift in Sources */,
377 | CED1115526FBDE3B005B6445 /* util.c in Sources */,
378 | CED1118726FC2728005B6445 /* IodineSettings.swift in Sources */,
379 | CED1115726FBDE3C005B6445 /* base64.c in Sources */,
380 | CED1115126FBDE3B005B6445 /* base32.c in Sources */,
381 | CED1113626FBDD57005B6445 /* PacketTunnelProvider.swift in Sources */,
382 | );
383 | runOnlyForDeploymentPostprocessing = 0;
384 | };
385 | /* End PBXSourcesBuildPhase section */
386 |
387 | /* Begin PBXTargetDependency section */
388 | CED1113A26FBDD57005B6445 /* PBXTargetDependency */ = {
389 | isa = PBXTargetDependency;
390 | target = CED1112F26FBDD57005B6445 /* Iodine */;
391 | targetProxy = CED1113926FBDD57005B6445 /* PBXContainerItemProxy */;
392 | };
393 | /* End PBXTargetDependency section */
394 |
395 | /* Begin PBXVariantGroup section */
396 | CED110DA26FBAE78005B6445 /* Main.storyboard */ = {
397 | isa = PBXVariantGroup;
398 | children = (
399 | CED110DB26FBAE78005B6445 /* Base */,
400 | );
401 | name = Main.storyboard;
402 | sourceTree = "";
403 | };
404 | CED110DF26FBAE7F005B6445 /* LaunchScreen.storyboard */ = {
405 | isa = PBXVariantGroup;
406 | children = (
407 | CED110E026FBAE7F005B6445 /* Base */,
408 | );
409 | name = LaunchScreen.storyboard;
410 | sourceTree = "";
411 | };
412 | /* End PBXVariantGroup section */
413 |
414 | /* Begin XCBuildConfiguration section */
415 | CED110E326FBAE7F005B6445 /* Debug */ = {
416 | isa = XCBuildConfiguration;
417 | baseConfigurationReference = CED1112626FBB55F005B6445 /* Build.xcconfig */;
418 | buildSettings = {
419 | ALWAYS_SEARCH_USER_PATHS = NO;
420 | CLANG_ANALYZER_NONNULL = YES;
421 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
422 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
423 | CLANG_CXX_LIBRARY = "libc++";
424 | CLANG_ENABLE_MODULES = YES;
425 | CLANG_ENABLE_OBJC_ARC = YES;
426 | CLANG_ENABLE_OBJC_WEAK = YES;
427 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
428 | CLANG_WARN_BOOL_CONVERSION = YES;
429 | CLANG_WARN_COMMA = YES;
430 | CLANG_WARN_CONSTANT_CONVERSION = YES;
431 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
432 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
433 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
434 | CLANG_WARN_EMPTY_BODY = YES;
435 | CLANG_WARN_ENUM_CONVERSION = YES;
436 | CLANG_WARN_INFINITE_RECURSION = YES;
437 | CLANG_WARN_INT_CONVERSION = YES;
438 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
439 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
440 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
441 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
442 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
443 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
444 | CLANG_WARN_STRICT_PROTOTYPES = YES;
445 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
446 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
447 | CLANG_WARN_UNREACHABLE_CODE = YES;
448 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
449 | COPY_PHASE_STRIP = NO;
450 | DEBUG_INFORMATION_FORMAT = dwarf;
451 | ENABLE_STRICT_OBJC_MSGSEND = YES;
452 | ENABLE_TESTABILITY = YES;
453 | GCC_C_LANGUAGE_STANDARD = gnu11;
454 | GCC_DYNAMIC_NO_PIC = NO;
455 | GCC_NO_COMMON_BLOCKS = YES;
456 | GCC_OPTIMIZATION_LEVEL = 0;
457 | GCC_PREPROCESSOR_DEFINITIONS = (
458 | "DEBUG=1",
459 | "$(inherited)",
460 | );
461 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
462 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
463 | GCC_WARN_UNDECLARED_SELECTOR = YES;
464 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
465 | GCC_WARN_UNUSED_FUNCTION = YES;
466 | GCC_WARN_UNUSED_VARIABLE = YES;
467 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
468 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
469 | MTL_FAST_MATH = YES;
470 | ONLY_ACTIVE_ARCH = YES;
471 | SDKROOT = iphoneos;
472 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
473 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
474 | };
475 | name = Debug;
476 | };
477 | CED110E426FBAE7F005B6445 /* Release */ = {
478 | isa = XCBuildConfiguration;
479 | baseConfigurationReference = CED1112626FBB55F005B6445 /* Build.xcconfig */;
480 | buildSettings = {
481 | ALWAYS_SEARCH_USER_PATHS = NO;
482 | CLANG_ANALYZER_NONNULL = YES;
483 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
484 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
485 | CLANG_CXX_LIBRARY = "libc++";
486 | CLANG_ENABLE_MODULES = YES;
487 | CLANG_ENABLE_OBJC_ARC = YES;
488 | CLANG_ENABLE_OBJC_WEAK = YES;
489 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
490 | CLANG_WARN_BOOL_CONVERSION = YES;
491 | CLANG_WARN_COMMA = YES;
492 | CLANG_WARN_CONSTANT_CONVERSION = YES;
493 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
494 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
495 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
496 | CLANG_WARN_EMPTY_BODY = YES;
497 | CLANG_WARN_ENUM_CONVERSION = YES;
498 | CLANG_WARN_INFINITE_RECURSION = YES;
499 | CLANG_WARN_INT_CONVERSION = YES;
500 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
501 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
502 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
503 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
504 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
505 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
506 | CLANG_WARN_STRICT_PROTOTYPES = YES;
507 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
508 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
509 | CLANG_WARN_UNREACHABLE_CODE = YES;
510 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
511 | COPY_PHASE_STRIP = NO;
512 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
513 | ENABLE_NS_ASSERTIONS = NO;
514 | ENABLE_STRICT_OBJC_MSGSEND = YES;
515 | GCC_C_LANGUAGE_STANDARD = gnu11;
516 | GCC_NO_COMMON_BLOCKS = YES;
517 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
518 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
519 | GCC_WARN_UNDECLARED_SELECTOR = YES;
520 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
521 | GCC_WARN_UNUSED_FUNCTION = YES;
522 | GCC_WARN_UNUSED_VARIABLE = YES;
523 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
524 | MTL_ENABLE_DEBUG_INFO = NO;
525 | MTL_FAST_MATH = YES;
526 | SDKROOT = iphoneos;
527 | SWIFT_COMPILATION_MODE = wholemodule;
528 | SWIFT_OPTIMIZATION_LEVEL = "-O";
529 | VALIDATE_PRODUCT = YES;
530 | };
531 | name = Release;
532 | };
533 | CED110E626FBAE7F005B6445 /* Debug */ = {
534 | isa = XCBuildConfiguration;
535 | buildSettings = {
536 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
537 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
538 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
539 | CLANG_ENABLE_MODULES = YES;
540 | CODE_SIGN_ENTITLEMENTS = PurpleHaze/PurpleHaze.entitlements;
541 | INFOPLIST_FILE = PurpleHaze/Info.plist;
542 | LD_RUNPATH_SEARCH_PATHS = (
543 | "$(inherited)",
544 | "@executable_path/Frameworks",
545 | );
546 | PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze";
547 | PRODUCT_NAME = "$(TARGET_NAME)";
548 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
549 | SWIFT_VERSION = 5.0;
550 | TARGETED_DEVICE_FAMILY = "1,2";
551 | };
552 | name = Debug;
553 | };
554 | CED110E726FBAE7F005B6445 /* Release */ = {
555 | isa = XCBuildConfiguration;
556 | buildSettings = {
557 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
558 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
559 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
560 | CLANG_ENABLE_MODULES = YES;
561 | CODE_SIGN_ENTITLEMENTS = PurpleHaze/PurpleHaze.entitlements;
562 | INFOPLIST_FILE = PurpleHaze/Info.plist;
563 | LD_RUNPATH_SEARCH_PATHS = (
564 | "$(inherited)",
565 | "@executable_path/Frameworks",
566 | );
567 | PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze";
568 | PRODUCT_NAME = "$(TARGET_NAME)";
569 | SWIFT_VERSION = 5.0;
570 | TARGETED_DEVICE_FAMILY = "1,2";
571 | };
572 | name = Release;
573 | };
574 | CED1113D26FBDD57005B6445 /* Debug */ = {
575 | isa = XCBuildConfiguration;
576 | buildSettings = {
577 | CODE_SIGN_ENTITLEMENTS = Extension/Iodine.entitlements;
578 | CODE_SIGN_STYLE = Automatic;
579 | GCC_PREPROCESSOR_DEFINITIONS = (
580 | DARWIN,
581 | __APPLE_USE_RFC_3542,
582 | "GITREVISION=\\\"(unknown)\\\"",
583 | "$(inherited)",
584 | );
585 | INFOPLIST_FILE = Extension/Info.plist;
586 | LD_RUNPATH_SEARCH_PATHS = (
587 | "$(inherited)",
588 | "@executable_path/Frameworks",
589 | "@executable_path/../../Frameworks",
590 | );
591 | PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze.Iodine";
592 | PRODUCT_NAME = "$(TARGET_NAME)";
593 | SKIP_INSTALL = YES;
594 | SWIFT_OBJC_BRIDGING_HEADER = "Extension/Swift-Bridging-Header.h";
595 | SWIFT_VERSION = 5.0;
596 | TARGETED_DEVICE_FAMILY = "1,2";
597 | };
598 | name = Debug;
599 | };
600 | CED1113E26FBDD57005B6445 /* Release */ = {
601 | isa = XCBuildConfiguration;
602 | buildSettings = {
603 | CODE_SIGN_ENTITLEMENTS = Extension/Iodine.entitlements;
604 | CODE_SIGN_STYLE = Automatic;
605 | GCC_PREPROCESSOR_DEFINITIONS = (
606 | DARWIN,
607 | __APPLE_USE_RFC_3542,
608 | "GITREVISION=\\\"(unknown)\\\"",
609 | "$(inherited)",
610 | );
611 | INFOPLIST_FILE = Extension/Info.plist;
612 | LD_RUNPATH_SEARCH_PATHS = (
613 | "$(inherited)",
614 | "@executable_path/Frameworks",
615 | "@executable_path/../../Frameworks",
616 | );
617 | PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.osy86).Purple-Haze.Iodine";
618 | PRODUCT_NAME = "$(TARGET_NAME)";
619 | SKIP_INSTALL = YES;
620 | SWIFT_OBJC_BRIDGING_HEADER = "Extension/Swift-Bridging-Header.h";
621 | SWIFT_VERSION = 5.0;
622 | TARGETED_DEVICE_FAMILY = "1,2";
623 | };
624 | name = Release;
625 | };
626 | /* End XCBuildConfiguration section */
627 |
628 | /* Begin XCConfigurationList section */
629 | CED110CC26FBAE78005B6445 /* Build configuration list for PBXProject "PurpleHaze" */ = {
630 | isa = XCConfigurationList;
631 | buildConfigurations = (
632 | CED110E326FBAE7F005B6445 /* Debug */,
633 | CED110E426FBAE7F005B6445 /* Release */,
634 | );
635 | defaultConfigurationIsVisible = 0;
636 | defaultConfigurationName = Release;
637 | };
638 | CED110E526FBAE7F005B6445 /* Build configuration list for PBXNativeTarget "Purple Haze" */ = {
639 | isa = XCConfigurationList;
640 | buildConfigurations = (
641 | CED110E626FBAE7F005B6445 /* Debug */,
642 | CED110E726FBAE7F005B6445 /* Release */,
643 | );
644 | defaultConfigurationIsVisible = 0;
645 | defaultConfigurationName = Release;
646 | };
647 | CED1113C26FBDD57005B6445 /* Build configuration list for PBXNativeTarget "Iodine" */ = {
648 | isa = XCConfigurationList;
649 | buildConfigurations = (
650 | CED1113D26FBDD57005B6445 /* Debug */,
651 | CED1113E26FBDD57005B6445 /* Release */,
652 | );
653 | defaultConfigurationIsVisible = 0;
654 | defaultConfigurationName = Release;
655 | };
656 | /* End XCConfigurationList section */
657 | };
658 | rootObject = CED110C926FBAE78005B6445 /* Project object */;
659 | }
660 |
--------------------------------------------------------------------------------