react-native-ovpn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/Openvpn.podspec +34 -0
- package/README.md +80 -0
- package/android/build.gradle +98 -0
- package/android/libs/README.md +46 -0
- package/android/libs/ics-openvpn.aar +0 -0
- package/android/src/main/AndroidManifest.xml +54 -0
- package/android/src/main/java/com/openvpn/NotificationHelper.kt +59 -0
- package/android/src/main/java/com/openvpn/OpenvpnEventBus.kt +52 -0
- package/android/src/main/java/com/openvpn/OpenvpnException.kt +6 -0
- package/android/src/main/java/com/openvpn/OpenvpnModule.kt +140 -0
- package/android/src/main/java/com/openvpn/OpenvpnPackage.kt +31 -0
- package/android/src/main/java/com/openvpn/OpenvpnService.kt +248 -0
- package/android/src/main/java/com/openvpn/PermissionLauncher.kt +39 -0
- package/android/src/main/java/com/openvpn/ProfileBuilder.kt +68 -0
- package/android/src/main/res/drawable/ic_vpn_default.xml +10 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/android/src/test/java/com/openvpn/NotificationHelperTest.kt +49 -0
- package/android/src/test/java/com/openvpn/ProfileBuilderTest.kt +83 -0
- package/app.plugin.js +3 -0
- package/ios/Openvpn-Bridging-Header.h +8 -0
- package/ios/Openvpn.h +5 -0
- package/ios/Openvpn.mm +123 -0
- package/ios/OpenvpnAppGroup.swift +59 -0
- package/ios/OpenvpnConstants.swift +46 -0
- package/ios/OpenvpnEventBridge.swift +58 -0
- package/ios/OpenvpnManager.swift +219 -0
- package/ios/PacketTunnelProvider/Info.plist +31 -0
- package/ios/PacketTunnelProvider/PacketTunnelProvider.swift +199 -0
- package/ios/PacketTunnelProvider/README.md +106 -0
- package/lib/module/NativeOpenvpn.js +5 -0
- package/lib/module/NativeOpenvpn.js.map +1 -0
- package/lib/module/OpenVPNClient.js +185 -0
- package/lib/module/OpenVPNClient.js.map +1 -0
- package/lib/module/errors.js +13 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/reconnect.js +51 -0
- package/lib/module/reconnect.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/plugin/src/android/index.d.ts +5 -0
- package/lib/typescript/plugin/src/android/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts.map +1 -0
- package/lib/typescript/plugin/src/index.d.ts +6 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/index.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts.map +1 -0
- package/lib/typescript/plugin/src/types.d.ts +14 -0
- package/lib/typescript/plugin/src/types.d.ts.map +1 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts +41 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts.map +1 -0
- package/lib/typescript/src/OpenVPNClient.d.ts +37 -0
- package/lib/typescript/src/OpenVPNClient.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +9 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/reconnect.d.ts +23 -0
- package/lib/typescript/src/reconnect.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +41 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +193 -0
- package/plugin/build/android/index.d.ts +4 -0
- package/plugin/build/android/index.js +24 -0
- package/plugin/build/android/withAndroidAarCheck.d.ts +4 -0
- package/plugin/build/android/withAndroidAarCheck.js +60 -0
- package/plugin/build/android/withAndroidLegacyPackaging.d.ts +4 -0
- package/plugin/build/android/withAndroidLegacyPackaging.js +18 -0
- package/plugin/build/android/withAndroidMinSdk.d.ts +4 -0
- package/plugin/build/android/withAndroidMinSdk.js +13 -0
- package/plugin/build/android/withAndroidNotificationIcon.d.ts +4 -0
- package/plugin/build/android/withAndroidNotificationIcon.js +64 -0
- package/plugin/build/android/withAndroidPermissions.d.ts +4 -0
- package/plugin/build/android/withAndroidPermissions.js +30 -0
- package/plugin/build/android/withAndroidService.d.ts +4 -0
- package/plugin/build/android/withAndroidService.js +40 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +18 -0
- package/plugin/build/ios/index.d.ts +4 -0
- package/plugin/build/ios/index.js +15 -0
- package/plugin/build/ios/withIosDeploymentTarget.d.ts +4 -0
- package/plugin/build/ios/withIosDeploymentTarget.js +28 -0
- package/plugin/build/ios/withIosEntitlements.d.ts +4 -0
- package/plugin/build/ios/withIosEntitlements.js +15 -0
- package/plugin/build/ios/withIosInfoPlist.d.ts +4 -0
- package/plugin/build/ios/withIosInfoPlist.js +14 -0
- package/plugin/build/types.d.ts +13 -0
- package/plugin/build/types.js +2 -0
- package/src/NativeOpenvpn.ts +46 -0
- package/src/OpenVPNClient.ts +239 -0
- package/src/errors.ts +29 -0
- package/src/index.ts +12 -0
- package/src/reconnect.ts +68 -0
- package/src/types.ts +53 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Read-only view over the App Group container, used by the host app to
|
|
4
|
+
/// observe stats and log lines the PacketTunnelProvider writes there.
|
|
5
|
+
///
|
|
6
|
+
/// The extension writes; the host reads. Atomic file writes keep readers
|
|
7
|
+
/// safe from torn writes; we tolerate brief absence by returning nil.
|
|
8
|
+
struct OpenvpnAppGroup {
|
|
9
|
+
|
|
10
|
+
let identifier: String
|
|
11
|
+
|
|
12
|
+
/// Returns the path to a file inside the shared container, or nil if the
|
|
13
|
+
/// App Group is not configured / accessible.
|
|
14
|
+
func url(for filename: String) -> URL? {
|
|
15
|
+
return FileManager.default.containerURL(
|
|
16
|
+
forSecurityApplicationGroupIdentifier: identifier
|
|
17
|
+
)?.appendingPathComponent(filename)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Reads the latest stats snapshot. Returns nil if the file is missing
|
|
21
|
+
/// or malformed.
|
|
22
|
+
///
|
|
23
|
+
/// JSONSerialization returns numbers as `NSNumber`. A direct `as? Int64`
|
|
24
|
+
/// against `NSNumber` always fails in Swift; we must go through
|
|
25
|
+
/// `NSNumber.int64Value`.
|
|
26
|
+
func readStats() -> (bytesIn: Int64, bytesOut: Int64, durationMs: Int64)? {
|
|
27
|
+
guard let url = url(for: OpenvpnConstants.statsFilename),
|
|
28
|
+
let data = try? Data(contentsOf: url),
|
|
29
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
30
|
+
let bytesIn = (json["bytesIn"] as? NSNumber)?.int64Value,
|
|
31
|
+
let bytesOut = (json["bytesOut"] as? NSNumber)?.int64Value,
|
|
32
|
+
let durationMs = (json["durationMs"] as? NSNumber)?.int64Value
|
|
33
|
+
else {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
return (bytesIn, bytesOut, durationMs)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Reads any new log lines since the given byte offset. Returns the lines
|
|
40
|
+
/// and the new offset (the file's current size). Returns nil if missing.
|
|
41
|
+
func readLog(sinceOffset: UInt64) -> (lines: [String], newOffset: UInt64)? {
|
|
42
|
+
guard let url = url(for: OpenvpnConstants.logFilename) else { return nil }
|
|
43
|
+
guard let handle = try? FileHandle(forReadingFrom: url) else { return nil }
|
|
44
|
+
defer { try? handle.close() }
|
|
45
|
+
do {
|
|
46
|
+
try handle.seek(toOffset: sinceOffset)
|
|
47
|
+
} catch {
|
|
48
|
+
// File rotated / truncated under us — re-read from start.
|
|
49
|
+
do { try handle.seek(toOffset: 0) } catch { return nil }
|
|
50
|
+
}
|
|
51
|
+
let data: Data
|
|
52
|
+
do { data = try handle.readToEnd() ?? Data() } catch { return nil }
|
|
53
|
+
let lines = String(data: data, encoding: .utf8)?
|
|
54
|
+
.split(separator: "\n")
|
|
55
|
+
.map(String.init) ?? []
|
|
56
|
+
let newOffset = sinceOffset + UInt64(data.count)
|
|
57
|
+
return (lines, newOffset)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Centralized constants used across the iOS side.
|
|
4
|
+
enum OpenvpnConstants {
|
|
5
|
+
|
|
6
|
+
// MARK: Info.plist keys
|
|
7
|
+
|
|
8
|
+
/// Required key in the host app's Info.plist. Value is the App Group
|
|
9
|
+
/// identifier (e.g. "group.com.example.openvpn") that the host and the
|
|
10
|
+
/// PacketTunnelProvider extension share.
|
|
11
|
+
///
|
|
12
|
+
/// We propagate this value to the extension via providerConfiguration so
|
|
13
|
+
/// the extension does not need its own Info.plist entry.
|
|
14
|
+
static let infoPlistAppGroupKey = "OpenVPNAppGroupIdentifier"
|
|
15
|
+
|
|
16
|
+
// MARK: providerConfiguration keys
|
|
17
|
+
|
|
18
|
+
/// Key under which the .ovpn config string is stored in
|
|
19
|
+
/// NETunnelProviderProtocol.providerConfiguration.
|
|
20
|
+
static let configKey = "ovpn_config"
|
|
21
|
+
|
|
22
|
+
/// Username (UTF-8 string).
|
|
23
|
+
static let usernameKey = "ovpn_username"
|
|
24
|
+
|
|
25
|
+
/// Password (UTF-8 string). Encrypted at rest by iOS.
|
|
26
|
+
static let passwordKey = "ovpn_password"
|
|
27
|
+
|
|
28
|
+
/// Optional list of DNS servers (Array<String>) to override pushed DNS.
|
|
29
|
+
static let dnsKey = "ovpn_dns"
|
|
30
|
+
|
|
31
|
+
/// App Group identifier — propagated from host to extension so the
|
|
32
|
+
/// extension reads/writes the same shared container.
|
|
33
|
+
static let appGroupKey = "ovpn_app_group"
|
|
34
|
+
|
|
35
|
+
// MARK: RCT event channels
|
|
36
|
+
|
|
37
|
+
static let stateChannel = "OpenVpn:state"
|
|
38
|
+
static let statsChannel = "OpenVpn:stats"
|
|
39
|
+
static let logChannel = "OpenVpn:log"
|
|
40
|
+
static let errorChannel = "OpenVpn:error"
|
|
41
|
+
|
|
42
|
+
// MARK: App Group filenames
|
|
43
|
+
|
|
44
|
+
static let statsFilename = "openvpn-stats.json"
|
|
45
|
+
static let logFilename = "openvpn-log.txt"
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
/// Emits OpenVpn:state/stats/log/error events back to JS. Holds a weak
|
|
5
|
+
/// reference to the RCT bridge so it doesn't extend the bridge's lifetime.
|
|
6
|
+
@objc(OpenvpnEventBridge)
|
|
7
|
+
final class OpenvpnEventBridge: NSObject {
|
|
8
|
+
|
|
9
|
+
@objc static let shared = OpenvpnEventBridge()
|
|
10
|
+
|
|
11
|
+
override init() { super.init() }
|
|
12
|
+
|
|
13
|
+
private weak var bridge: RCTBridge?
|
|
14
|
+
|
|
15
|
+
@objc func attach(_ bridge: RCTBridge?) {
|
|
16
|
+
self.bridge = bridge
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@objc func detach() {
|
|
20
|
+
self.bridge = nil
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// MARK: Channels
|
|
24
|
+
|
|
25
|
+
@objc func emitState(_ state: String) {
|
|
26
|
+
emit(OpenvpnConstants.stateChannel, body: state)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@objc func emitStats(bytesIn: Int64, bytesOut: Int64, durationMs: Int64) {
|
|
30
|
+
emit(OpenvpnConstants.statsChannel, body: [
|
|
31
|
+
"bytesIn": NSNumber(value: bytesIn),
|
|
32
|
+
"bytesOut": NSNumber(value: bytesOut),
|
|
33
|
+
"durationMs": NSNumber(value: durationMs),
|
|
34
|
+
])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@objc func emitLog(_ line: String) {
|
|
38
|
+
emit(OpenvpnConstants.logChannel, body: line)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@objc func emitError(code: String, nativeMessage: String?) {
|
|
42
|
+
var body: [String: Any] = ["code": code]
|
|
43
|
+
if let msg = nativeMessage { body["nativeMessage"] = msg }
|
|
44
|
+
emit(OpenvpnConstants.errorChannel, body: body)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MARK: Internal
|
|
48
|
+
|
|
49
|
+
private func emit(_ name: String, body: Any) {
|
|
50
|
+
guard let bridge = self.bridge else { return }
|
|
51
|
+
bridge.enqueueJSCall(
|
|
52
|
+
"RCTDeviceEventEmitter",
|
|
53
|
+
method: "emit",
|
|
54
|
+
args: [name, body],
|
|
55
|
+
completion: nil
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NetworkExtension
|
|
3
|
+
|
|
4
|
+
/// High-level controller for the OpenVPN tunnel from the host app's side.
|
|
5
|
+
/// Manages NETunnelProviderManager lifecycle, starts/stops the extension,
|
|
6
|
+
/// observes status changes, and polls the App Group container for stats
|
|
7
|
+
/// and log lines written by the extension.
|
|
8
|
+
@objc(OpenvpnManager)
|
|
9
|
+
final class OpenvpnManager: NSObject {
|
|
10
|
+
|
|
11
|
+
@objc static let shared = OpenvpnManager()
|
|
12
|
+
|
|
13
|
+
override init() { super.init() }
|
|
14
|
+
|
|
15
|
+
private var statusObserver: NSObjectProtocol?
|
|
16
|
+
private var statsTimer: Timer?
|
|
17
|
+
private var logTimer: Timer?
|
|
18
|
+
private var logOffset: UInt64 = 0
|
|
19
|
+
private var sessionStartedAt: Date?
|
|
20
|
+
|
|
21
|
+
// MARK: Permission
|
|
22
|
+
|
|
23
|
+
/// Triggers iOS's "Allow VPN configuration?" prompt on first call by
|
|
24
|
+
/// saving an (empty) preference. Subsequent calls are silent.
|
|
25
|
+
@objc(requestPermissionWithCompletion:)
|
|
26
|
+
func requestPermission(completion: @escaping (Bool, Error?) -> Void) {
|
|
27
|
+
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
|
28
|
+
if let error = error {
|
|
29
|
+
completion(false, error)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
let manager = managers?.first ?? NETunnelProviderManager()
|
|
33
|
+
manager.localizedDescription = "OpenVPN"
|
|
34
|
+
manager.saveToPreferences { saveError in
|
|
35
|
+
if let saveError = saveError as NSError? {
|
|
36
|
+
if saveError.code == NEVPNError.configurationReadWriteFailed.rawValue {
|
|
37
|
+
completion(false, nil)
|
|
38
|
+
} else {
|
|
39
|
+
completion(false, saveError)
|
|
40
|
+
}
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
completion(true, nil)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: Connect / Disconnect
|
|
49
|
+
|
|
50
|
+
@objc(connectWithOvpn:username:password:dns:completion:)
|
|
51
|
+
func connect(
|
|
52
|
+
ovpn: String,
|
|
53
|
+
username: String,
|
|
54
|
+
password: String,
|
|
55
|
+
dns: [String],
|
|
56
|
+
completion: @escaping (Bool, Error?) -> Void
|
|
57
|
+
) {
|
|
58
|
+
guard let appGroup = appGroupIdentifier() else {
|
|
59
|
+
completion(false, NSError(
|
|
60
|
+
domain: "Openvpn",
|
|
61
|
+
code: 1,
|
|
62
|
+
userInfo: [NSLocalizedDescriptionKey:
|
|
63
|
+
"Missing OpenVPNAppGroupIdentifier in host app Info.plist"]
|
|
64
|
+
))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
|
69
|
+
if let error = error {
|
|
70
|
+
completion(false, error)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
let manager = managers?.first ?? NETunnelProviderManager()
|
|
74
|
+
manager.localizedDescription = "OpenVPN"
|
|
75
|
+
|
|
76
|
+
let protocolConfig = NETunnelProviderProtocol()
|
|
77
|
+
protocolConfig.providerBundleIdentifier = self.extensionBundleIdentifier()
|
|
78
|
+
protocolConfig.serverAddress = self.extractRemote(from: ovpn) ?? "openvpn-server"
|
|
79
|
+
protocolConfig.providerConfiguration = [
|
|
80
|
+
OpenvpnConstants.configKey: ovpn,
|
|
81
|
+
OpenvpnConstants.usernameKey: username,
|
|
82
|
+
OpenvpnConstants.passwordKey: password,
|
|
83
|
+
OpenvpnConstants.dnsKey: dns,
|
|
84
|
+
OpenvpnConstants.appGroupKey: appGroup,
|
|
85
|
+
]
|
|
86
|
+
manager.protocolConfiguration = protocolConfig
|
|
87
|
+
manager.isEnabled = true
|
|
88
|
+
|
|
89
|
+
manager.saveToPreferences { saveError in
|
|
90
|
+
if let saveError = saveError {
|
|
91
|
+
completion(false, saveError)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
manager.loadFromPreferences { loadError in
|
|
95
|
+
if let loadError = loadError {
|
|
96
|
+
completion(false, loadError)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
self.startObservingStatus(manager: manager)
|
|
100
|
+
do {
|
|
101
|
+
try manager.connection.startVPNTunnel()
|
|
102
|
+
self.beginPolling(appGroup: appGroup)
|
|
103
|
+
completion(true, nil)
|
|
104
|
+
} catch {
|
|
105
|
+
completion(false, error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@objc(disconnectWithCompletion:)
|
|
113
|
+
func disconnect(completion: @escaping (Bool, Error?) -> Void) {
|
|
114
|
+
NETunnelProviderManager.loadAllFromPreferences { managers, _ in
|
|
115
|
+
managers?.first?.connection.stopVPNTunnel()
|
|
116
|
+
self.stopPolling()
|
|
117
|
+
completion(true, nil)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: Status observation
|
|
122
|
+
|
|
123
|
+
private func startObservingStatus(manager: NETunnelProviderManager) {
|
|
124
|
+
if let observer = statusObserver {
|
|
125
|
+
NotificationCenter.default.removeObserver(observer)
|
|
126
|
+
}
|
|
127
|
+
statusObserver = NotificationCenter.default.addObserver(
|
|
128
|
+
forName: .NEVPNStatusDidChange,
|
|
129
|
+
object: manager.connection,
|
|
130
|
+
queue: .main
|
|
131
|
+
) { [weak self] _ in
|
|
132
|
+
let status = manager.connection.status
|
|
133
|
+
let mapped = Self.mapStatus(status)
|
|
134
|
+
OpenvpnEventBridge.shared.emitState(mapped)
|
|
135
|
+
|
|
136
|
+
if status == .connected, self?.sessionStartedAt == nil {
|
|
137
|
+
self?.sessionStartedAt = Date()
|
|
138
|
+
}
|
|
139
|
+
if status == .disconnected {
|
|
140
|
+
self?.sessionStartedAt = nil
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private static func mapStatus(_ status: NEVPNStatus) -> String {
|
|
146
|
+
switch status {
|
|
147
|
+
case .invalid: return "idle"
|
|
148
|
+
case .disconnected: return "disconnected"
|
|
149
|
+
case .connecting: return "connecting"
|
|
150
|
+
case .connected: return "connected"
|
|
151
|
+
case .reasserting: return "reconnecting"
|
|
152
|
+
case .disconnecting: return "disconnecting"
|
|
153
|
+
@unknown default: return "idle"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// MARK: Polling App Group for stats + logs
|
|
158
|
+
|
|
159
|
+
private func beginPolling(appGroup: String) {
|
|
160
|
+
let group = OpenvpnAppGroup(identifier: appGroup)
|
|
161
|
+
logOffset = 0
|
|
162
|
+
|
|
163
|
+
statsTimer?.invalidate()
|
|
164
|
+
statsTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
|
165
|
+
guard let stats = group.readStats() else { return }
|
|
166
|
+
OpenvpnEventBridge.shared.emitStats(
|
|
167
|
+
bytesIn: stats.bytesIn,
|
|
168
|
+
bytesOut: stats.bytesOut,
|
|
169
|
+
durationMs: stats.durationMs
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logTimer?.invalidate()
|
|
174
|
+
logTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
|
175
|
+
guard let self = self else { return }
|
|
176
|
+
guard let result = group.readLog(sinceOffset: self.logOffset) else { return }
|
|
177
|
+
for line in result.lines where !line.isEmpty {
|
|
178
|
+
OpenvpnEventBridge.shared.emitLog(line)
|
|
179
|
+
}
|
|
180
|
+
self.logOffset = result.newOffset
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private func stopPolling() {
|
|
185
|
+
statsTimer?.invalidate()
|
|
186
|
+
statsTimer = nil
|
|
187
|
+
logTimer?.invalidate()
|
|
188
|
+
logTimer = nil
|
|
189
|
+
logOffset = 0
|
|
190
|
+
sessionStartedAt = nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MARK: Helpers
|
|
194
|
+
|
|
195
|
+
private func appGroupIdentifier() -> String? {
|
|
196
|
+
return Bundle.main.object(forInfoDictionaryKey: OpenvpnConstants.infoPlistAppGroupKey) as? String
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private func extensionBundleIdentifier() -> String {
|
|
200
|
+
// Convention: extension bundle id is host bundle + ".OpenVPNTunnel".
|
|
201
|
+
// Consumers can override via the OpenVPNExtensionBundleIdentifier Info.plist key.
|
|
202
|
+
if let override = Bundle.main.object(forInfoDictionaryKey: "OpenVPNExtensionBundleIdentifier") as? String {
|
|
203
|
+
return override
|
|
204
|
+
}
|
|
205
|
+
let host = Bundle.main.bundleIdentifier ?? "com.example.app"
|
|
206
|
+
return "\(host).OpenVPNTunnel"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private func extractRemote(from ovpn: String) -> String? {
|
|
210
|
+
for line in ovpn.split(separator: "\n") {
|
|
211
|
+
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
212
|
+
if trimmed.hasPrefix("remote ") {
|
|
213
|
+
let parts = trimmed.split(separator: " ", maxSplits: 2, omittingEmptySubsequences: true)
|
|
214
|
+
if parts.count >= 2 { return String(parts[1]) }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string>OpenVPN Tunnel</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
11
|
+
<key>CFBundleIdentifier</key>
|
|
12
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
13
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
14
|
+
<string>6.0</string>
|
|
15
|
+
<key>CFBundleName</key>
|
|
16
|
+
<string>$(PRODUCT_NAME)</string>
|
|
17
|
+
<key>CFBundlePackageType</key>
|
|
18
|
+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
19
|
+
<key>CFBundleShortVersionString</key>
|
|
20
|
+
<string>1.0</string>
|
|
21
|
+
<key>CFBundleVersion</key>
|
|
22
|
+
<string>1</string>
|
|
23
|
+
<key>NSExtension</key>
|
|
24
|
+
<dict>
|
|
25
|
+
<key>NSExtensionPointIdentifier</key>
|
|
26
|
+
<string>com.apple.networkextension.packet-tunnel</string>
|
|
27
|
+
<key>NSExtensionPrincipalClass</key>
|
|
28
|
+
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
|
29
|
+
</dict>
|
|
30
|
+
</dict>
|
|
31
|
+
</plist>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PacketTunnelProvider.swift
|
|
3
|
+
// Template shipped by react-native-openvpn. Copy into your iOS app's
|
|
4
|
+
// Network Extension target. See ios/PacketTunnelProvider/README.md for
|
|
5
|
+
// the full target-creation walkthrough.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import NetworkExtension
|
|
9
|
+
import OpenVPNAdapter
|
|
10
|
+
|
|
11
|
+
final class PacketTunnelProvider: NEPacketTunnelProvider {
|
|
12
|
+
|
|
13
|
+
private lazy var adapter: OpenVPNAdapter = {
|
|
14
|
+
let a = OpenVPNAdapter()
|
|
15
|
+
a.delegate = self
|
|
16
|
+
return a
|
|
17
|
+
}()
|
|
18
|
+
|
|
19
|
+
private var startCompletion: ((Error?) -> Void)?
|
|
20
|
+
private var stopCompletion: (() -> Void)?
|
|
21
|
+
private var appGroup: String?
|
|
22
|
+
private var sessionStartedAt: Date?
|
|
23
|
+
private var statsTick: Timer?
|
|
24
|
+
|
|
25
|
+
override func startTunnel(
|
|
26
|
+
options: [String : NSObject]?,
|
|
27
|
+
completionHandler: @escaping (Error?) -> Void
|
|
28
|
+
) {
|
|
29
|
+
guard
|
|
30
|
+
let proto = self.protocolConfiguration as? NETunnelProviderProtocol,
|
|
31
|
+
let providerConfig = proto.providerConfiguration,
|
|
32
|
+
let configString = providerConfig["ovpn_config"] as? String
|
|
33
|
+
else {
|
|
34
|
+
completionHandler(NSError(
|
|
35
|
+
domain: "OpenVPN",
|
|
36
|
+
code: 1,
|
|
37
|
+
userInfo: [NSLocalizedDescriptionKey: "missing config"]
|
|
38
|
+
))
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
let username = providerConfig["ovpn_username"] as? String ?? ""
|
|
42
|
+
let password = providerConfig["ovpn_password"] as? String ?? ""
|
|
43
|
+
let dns = providerConfig["ovpn_dns"] as? [String] ?? []
|
|
44
|
+
self.appGroup = providerConfig["ovpn_app_group"] as? String
|
|
45
|
+
|
|
46
|
+
// Apply DNS overrides via the standard OpenVPN dhcp-option directive,
|
|
47
|
+
// appending one line per server to the config text. OpenVPN3-core parses
|
|
48
|
+
// these reliably; setting them via OpenVPNConfiguration.settings is
|
|
49
|
+
// unreliable across versions of OpenVPNAdapter.
|
|
50
|
+
var effectiveConfig = configString
|
|
51
|
+
for server in dns where !server.isEmpty {
|
|
52
|
+
effectiveConfig += "\ndhcp-option DNS \(server)"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let configurationData = Data(effectiveConfig.utf8)
|
|
56
|
+
let configuration = OpenVPNConfiguration()
|
|
57
|
+
configuration.fileContent = configurationData
|
|
58
|
+
configuration.settings = [:]
|
|
59
|
+
|
|
60
|
+
do {
|
|
61
|
+
let evaluation = try adapter.apply(configuration: configuration)
|
|
62
|
+
if evaluation.isAutologin == false {
|
|
63
|
+
let credentials = OpenVPNCredentials()
|
|
64
|
+
credentials.username = username
|
|
65
|
+
credentials.password = password
|
|
66
|
+
try adapter.provideCredentials(credentials)
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
completionHandler(error)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self.startCompletion = completionHandler
|
|
74
|
+
adapter.connect(using: self.packetFlow)
|
|
75
|
+
self.startStatsTimer()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override func stopTunnel(
|
|
79
|
+
with reason: NEProviderStopReason,
|
|
80
|
+
completionHandler: @escaping () -> Void
|
|
81
|
+
) {
|
|
82
|
+
self.stopCompletion = completionHandler
|
|
83
|
+
self.stopStatsTimer()
|
|
84
|
+
adapter.disconnect()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MARK: Stats writing
|
|
88
|
+
|
|
89
|
+
private func startStatsTimer() {
|
|
90
|
+
statsTick?.invalidate()
|
|
91
|
+
statsTick = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
92
|
+
self?.writeStatsToAppGroup()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func stopStatsTimer() {
|
|
97
|
+
statsTick?.invalidate()
|
|
98
|
+
statsTick = nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func writeStatsToAppGroup() {
|
|
102
|
+
guard let appGroup = appGroup,
|
|
103
|
+
let url = FileManager.default.containerURL(
|
|
104
|
+
forSecurityApplicationGroupIdentifier: appGroup
|
|
105
|
+
)?.appendingPathComponent("openvpn-stats.json")
|
|
106
|
+
else { return }
|
|
107
|
+
let bytesIn = adapter.transportStatistics.bytesIn
|
|
108
|
+
let bytesOut = adapter.transportStatistics.bytesOut
|
|
109
|
+
let durationMs: Int64 = {
|
|
110
|
+
guard let start = sessionStartedAt else { return 0 }
|
|
111
|
+
return Int64(Date().timeIntervalSince(start) * 1000)
|
|
112
|
+
}()
|
|
113
|
+
let payload: [String: Any] = [
|
|
114
|
+
"bytesIn": NSNumber(value: bytesIn),
|
|
115
|
+
"bytesOut": NSNumber(value: bytesOut),
|
|
116
|
+
"durationMs": NSNumber(value: durationMs),
|
|
117
|
+
]
|
|
118
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload) {
|
|
119
|
+
try? data.write(to: url, options: .atomic)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func appendLog(_ line: String) {
|
|
124
|
+
guard let appGroup = appGroup,
|
|
125
|
+
let url = FileManager.default.containerURL(
|
|
126
|
+
forSecurityApplicationGroupIdentifier: appGroup
|
|
127
|
+
)?.appendingPathComponent("openvpn-log.txt")
|
|
128
|
+
else { return }
|
|
129
|
+
let lineWithNewline = (line + "\n").data(using: .utf8) ?? Data()
|
|
130
|
+
if FileManager.default.fileExists(atPath: url.path),
|
|
131
|
+
let handle = try? FileHandle(forWritingTo: url) {
|
|
132
|
+
defer { try? handle.close() }
|
|
133
|
+
try? handle.seekToEnd()
|
|
134
|
+
try? handle.write(contentsOf: lineWithNewline)
|
|
135
|
+
} else {
|
|
136
|
+
try? lineWithNewline.write(to: url, options: .atomic)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: OpenVPNAdapterDelegate
|
|
142
|
+
|
|
143
|
+
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|
144
|
+
|
|
145
|
+
func openVPNAdapter(
|
|
146
|
+
_ openVPNAdapter: OpenVPNAdapter,
|
|
147
|
+
configureTunnelWithNetworkSettings settings: NEPacketTunnelNetworkSettings?,
|
|
148
|
+
completionHandler: @escaping (Error?) -> Void
|
|
149
|
+
) {
|
|
150
|
+
setTunnelNetworkSettings(settings, completionHandler: completionHandler)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func openVPNAdapter(
|
|
154
|
+
_ openVPNAdapter: OpenVPNAdapter,
|
|
155
|
+
handleEvent event: OpenVPNAdapterEvent,
|
|
156
|
+
message: String?
|
|
157
|
+
) {
|
|
158
|
+
switch event {
|
|
159
|
+
case .connected:
|
|
160
|
+
sessionStartedAt = Date()
|
|
161
|
+
startCompletion?(nil)
|
|
162
|
+
startCompletion = nil
|
|
163
|
+
case .disconnected:
|
|
164
|
+
// If a user-initiated stop is pending, complete it. Otherwise this is
|
|
165
|
+
// an unexpected disconnect — only THEN cancel the tunnel with the
|
|
166
|
+
// system (calling both is redundant and can confuse NEVPNStatus).
|
|
167
|
+
if let completion = stopCompletion {
|
|
168
|
+
completion()
|
|
169
|
+
stopCompletion = nil
|
|
170
|
+
} else {
|
|
171
|
+
cancelTunnelWithError(nil)
|
|
172
|
+
}
|
|
173
|
+
case .reconnecting:
|
|
174
|
+
break
|
|
175
|
+
default:
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
if let m = message { appendLog("[\(event)] \(m)") }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func openVPNAdapter(
|
|
182
|
+
_ openVPNAdapter: OpenVPNAdapter,
|
|
183
|
+
handleError error: Error
|
|
184
|
+
) {
|
|
185
|
+
appendLog("[error] \(error.localizedDescription)")
|
|
186
|
+
if let start = startCompletion {
|
|
187
|
+
start(error)
|
|
188
|
+
startCompletion = nil
|
|
189
|
+
}
|
|
190
|
+
cancelTunnelWithError(error)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func openVPNAdapter(
|
|
194
|
+
_ openVPNAdapter: OpenVPNAdapter,
|
|
195
|
+
handleLogMessage logMessage: String
|
|
196
|
+
) {
|
|
197
|
+
appendLog(logMessage)
|
|
198
|
+
}
|
|
199
|
+
}
|