├── .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 | 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 | --------------------------------------------------------------------------------