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.
Files changed (115) hide show
  1. package/LICENSE +20 -0
  2. package/Openvpn.podspec +34 -0
  3. package/README.md +80 -0
  4. package/android/build.gradle +98 -0
  5. package/android/libs/README.md +46 -0
  6. package/android/libs/ics-openvpn.aar +0 -0
  7. package/android/src/main/AndroidManifest.xml +54 -0
  8. package/android/src/main/java/com/openvpn/NotificationHelper.kt +59 -0
  9. package/android/src/main/java/com/openvpn/OpenvpnEventBus.kt +52 -0
  10. package/android/src/main/java/com/openvpn/OpenvpnException.kt +6 -0
  11. package/android/src/main/java/com/openvpn/OpenvpnModule.kt +140 -0
  12. package/android/src/main/java/com/openvpn/OpenvpnPackage.kt +31 -0
  13. package/android/src/main/java/com/openvpn/OpenvpnService.kt +248 -0
  14. package/android/src/main/java/com/openvpn/PermissionLauncher.kt +39 -0
  15. package/android/src/main/java/com/openvpn/ProfileBuilder.kt +68 -0
  16. package/android/src/main/res/drawable/ic_vpn_default.xml +10 -0
  17. package/android/src/main/res/values/strings.xml +6 -0
  18. package/android/src/test/java/com/openvpn/NotificationHelperTest.kt +49 -0
  19. package/android/src/test/java/com/openvpn/ProfileBuilderTest.kt +83 -0
  20. package/app.plugin.js +3 -0
  21. package/ios/Openvpn-Bridging-Header.h +8 -0
  22. package/ios/Openvpn.h +5 -0
  23. package/ios/Openvpn.mm +123 -0
  24. package/ios/OpenvpnAppGroup.swift +59 -0
  25. package/ios/OpenvpnConstants.swift +46 -0
  26. package/ios/OpenvpnEventBridge.swift +58 -0
  27. package/ios/OpenvpnManager.swift +219 -0
  28. package/ios/PacketTunnelProvider/Info.plist +31 -0
  29. package/ios/PacketTunnelProvider/PacketTunnelProvider.swift +199 -0
  30. package/ios/PacketTunnelProvider/README.md +106 -0
  31. package/lib/module/NativeOpenvpn.js +5 -0
  32. package/lib/module/NativeOpenvpn.js.map +1 -0
  33. package/lib/module/OpenVPNClient.js +185 -0
  34. package/lib/module/OpenVPNClient.js.map +1 -0
  35. package/lib/module/errors.js +13 -0
  36. package/lib/module/errors.js.map +1 -0
  37. package/lib/module/index.js +5 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/reconnect.js +51 -0
  41. package/lib/module/reconnect.js.map +1 -0
  42. package/lib/module/types.js +2 -0
  43. package/lib/module/types.js.map +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/plugin/src/android/index.d.ts +5 -0
  46. package/lib/typescript/plugin/src/android/index.d.ts.map +1 -0
  47. package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts +5 -0
  48. package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts.map +1 -0
  49. package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts +5 -0
  50. package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts.map +1 -0
  51. package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts +5 -0
  52. package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts.map +1 -0
  53. package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts +5 -0
  54. package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts.map +1 -0
  55. package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts +5 -0
  56. package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts.map +1 -0
  57. package/lib/typescript/plugin/src/android/withAndroidService.d.ts +5 -0
  58. package/lib/typescript/plugin/src/android/withAndroidService.d.ts.map +1 -0
  59. package/lib/typescript/plugin/src/index.d.ts +6 -0
  60. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  61. package/lib/typescript/plugin/src/ios/index.d.ts +5 -0
  62. package/lib/typescript/plugin/src/ios/index.d.ts.map +1 -0
  63. package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts +5 -0
  64. package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts.map +1 -0
  65. package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts +5 -0
  66. package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts.map +1 -0
  67. package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts +5 -0
  68. package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts.map +1 -0
  69. package/lib/typescript/plugin/src/types.d.ts +14 -0
  70. package/lib/typescript/plugin/src/types.d.ts.map +1 -0
  71. package/lib/typescript/src/NativeOpenvpn.d.ts +41 -0
  72. package/lib/typescript/src/NativeOpenvpn.d.ts.map +1 -0
  73. package/lib/typescript/src/OpenVPNClient.d.ts +37 -0
  74. package/lib/typescript/src/OpenVPNClient.d.ts.map +1 -0
  75. package/lib/typescript/src/errors.d.ts +9 -0
  76. package/lib/typescript/src/errors.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +5 -0
  78. package/lib/typescript/src/index.d.ts.map +1 -0
  79. package/lib/typescript/src/reconnect.d.ts +23 -0
  80. package/lib/typescript/src/reconnect.d.ts.map +1 -0
  81. package/lib/typescript/src/types.d.ts +41 -0
  82. package/lib/typescript/src/types.d.ts.map +1 -0
  83. package/package.json +193 -0
  84. package/plugin/build/android/index.d.ts +4 -0
  85. package/plugin/build/android/index.js +24 -0
  86. package/plugin/build/android/withAndroidAarCheck.d.ts +4 -0
  87. package/plugin/build/android/withAndroidAarCheck.js +60 -0
  88. package/plugin/build/android/withAndroidLegacyPackaging.d.ts +4 -0
  89. package/plugin/build/android/withAndroidLegacyPackaging.js +18 -0
  90. package/plugin/build/android/withAndroidMinSdk.d.ts +4 -0
  91. package/plugin/build/android/withAndroidMinSdk.js +13 -0
  92. package/plugin/build/android/withAndroidNotificationIcon.d.ts +4 -0
  93. package/plugin/build/android/withAndroidNotificationIcon.js +64 -0
  94. package/plugin/build/android/withAndroidPermissions.d.ts +4 -0
  95. package/plugin/build/android/withAndroidPermissions.js +30 -0
  96. package/plugin/build/android/withAndroidService.d.ts +4 -0
  97. package/plugin/build/android/withAndroidService.js +40 -0
  98. package/plugin/build/index.d.ts +5 -0
  99. package/plugin/build/index.js +18 -0
  100. package/plugin/build/ios/index.d.ts +4 -0
  101. package/plugin/build/ios/index.js +15 -0
  102. package/plugin/build/ios/withIosDeploymentTarget.d.ts +4 -0
  103. package/plugin/build/ios/withIosDeploymentTarget.js +28 -0
  104. package/plugin/build/ios/withIosEntitlements.d.ts +4 -0
  105. package/plugin/build/ios/withIosEntitlements.js +15 -0
  106. package/plugin/build/ios/withIosInfoPlist.d.ts +4 -0
  107. package/plugin/build/ios/withIosInfoPlist.js +14 -0
  108. package/plugin/build/types.d.ts +13 -0
  109. package/plugin/build/types.js +2 -0
  110. package/src/NativeOpenvpn.ts +46 -0
  111. package/src/OpenVPNClient.ts +239 -0
  112. package/src/errors.ts +29 -0
  113. package/src/index.ts +12 -0
  114. package/src/reconnect.ts +68 -0
  115. 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
+ }