mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
fix: ios ovpn fix (#2360)
* feat: enhance OpenVPN support and configuration handling for iOS and macOS platforms
* Deps updated
* Deps updated
* feat: add OpenVPN configuration validation and regeneration logic to VpnConfigurationsController
* revert: restore pre-fix OpenVPN NE flow
* chore: add OpenVPN NE payload diagnostics
* Revert "revert: restore pre-fix OpenVPN NE flow"
This reverts commit ae99cc77e9.
* chore: remove openvpn config processing
---------
Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
@@ -298,14 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(MACOS_NE)
|
#elif defined(MACOS_NE)
|
||||||
// macOS build using Network Extension – hide OpenVPN-based containers
|
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
|
||||||
switch (c) {
|
switch (c) {
|
||||||
|
case DockerContainer::OpenVpn: return true;
|
||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
case DockerContainer::Awg2: return true;
|
case DockerContainer::Awg2: return true;
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg: return true;
|
||||||
case DockerContainer::Xray: return true;
|
case DockerContainer::Xray: return true;
|
||||||
case DockerContainer::SSXray: return true;
|
case DockerContainer::SSXray: return true;
|
||||||
case DockerContainer::OpenVpn:
|
|
||||||
case DockerContainer::Cloak:
|
case DockerContainer::Cloak:
|
||||||
case DockerContainer::ShadowSocks:
|
case DockerContainer::ShadowSocks:
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
|||||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
|
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||||
|
|
||||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<string>AmneziaVPNNetworkExtension</string>
|
<string>AmneziaVPNNetworkExtension</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>${APPLE_PROJECT_VERSION}</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
@@ -41,6 +41,6 @@
|
|||||||
<string>group.org.amnezia.AmneziaVPN</string>
|
<string>group.org.amnezia.AmneziaVPN</string>
|
||||||
|
|
||||||
<key>com.wireguard.macos.app_group_id</key>
|
<key>com.wireguard.macos.app_group_id</key>
|
||||||
<string>${BUILD_VPN_DEVELOPMENT_TEAM}.group.org.amnezia.AmneziaVPN</string>
|
<string>$(DEVELOPMENT_TEAM).group.org.amnezia.AmneziaVPN</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ struct OpenVPNConfig: Decodable {
|
|||||||
|
|
||||||
extension PacketTunnelProvider {
|
extension PacketTunnelProvider {
|
||||||
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
||||||
|
// Reset session-derived state so reconnects never reuse stale gateway/address data.
|
||||||
|
openVpnGatewayAddress = nil
|
||||||
|
openVpnLocalAddress = nil
|
||||||
|
openVpnLocalMask = nil
|
||||||
|
lastOpenVPNSettings = nil
|
||||||
|
|
||||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||||
@@ -25,7 +31,25 @@ extension PacketTunnelProvider {
|
|||||||
do {
|
do {
|
||||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||||
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
||||||
|
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
|
||||||
|
let ovpnPreview = String(openVPNConfig.config.prefix(512))
|
||||||
|
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
|
||||||
|
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
|
||||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||||
|
splitTunnelType = openVPNConfig.splitTunnelType
|
||||||
|
splitTunnelSites = openVPNConfig.splitTunnelSites
|
||||||
|
openVpnDnsServers = Self.extractDnsServers(from: openVPNConfig.config)
|
||||||
|
openVpnRemoteAddress = Self.extractRemoteHost(from: openVPNConfig.config)
|
||||||
|
openVpnRedirectGatewayDef1 = Self.hasRedirectGatewayDef1(in: openVPNConfig.config)
|
||||||
|
if let openVpnRemoteAddress {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "host=\(openVpnRemoteAddress)")
|
||||||
|
}
|
||||||
|
if !openVpnDnsServers.isEmpty {
|
||||||
|
ovpnLog(.info, title: "DNS", message: "servers=\(openVpnDnsServers)")
|
||||||
|
}
|
||||||
|
if openVpnRedirectGatewayDef1 {
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "redirect-gateway def1 detected")
|
||||||
|
}
|
||||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||||
} catch {
|
} catch {
|
||||||
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||||
@@ -73,6 +97,11 @@ extension PacketTunnelProvider {
|
|||||||
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
||||||
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
||||||
|
|
||||||
|
let hasCertTag = configString.contains("<cert>") && configString.contains("</cert>")
|
||||||
|
let hasKeyTag = configString.contains("<key>") && configString.contains("</key>")
|
||||||
|
let hasAuthUserPass = configString.contains("auth-user-pass")
|
||||||
|
ovpnLog(.info, title: "ConfigCreds", message: "inlineCert=\(hasCertTag) inlineKey=\(hasKeyTag) authUserPass=\(hasAuthUserPass)")
|
||||||
|
|
||||||
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
||||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||||
@@ -83,27 +112,98 @@ extension PacketTunnelProvider {
|
|||||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||||
|
|
||||||
if let start = configString.range(of: "<tls-auth>"),
|
if hasTlsAuthOpen && hasTlsAuthClose {
|
||||||
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||||
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
|
||||||
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
|
||||||
let sanitizedLines = keyBody
|
|
||||||
.split(whereSeparator: { $0.isNewline })
|
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
.filter { !$0.hasPrefix("#") }
|
|
||||||
|
|
||||||
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
|
||||||
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
|
||||||
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
|
||||||
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
var normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "ca",
|
||||||
|
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
endMarkers: ["-----END CERTIFICATE-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "cert",
|
||||||
|
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
endMarkers: ["-----END CERTIFICATE-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "key",
|
||||||
|
beginMarkers: [
|
||||||
|
"-----BEGIN PRIVATE KEY-----",
|
||||||
|
"-----BEGIN RSA PRIVATE KEY-----",
|
||||||
|
"-----BEGIN EC PRIVATE KEY-----",
|
||||||
|
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
||||||
|
],
|
||||||
|
endMarkers: [
|
||||||
|
"-----END PRIVATE KEY-----",
|
||||||
|
"-----END RSA PRIVATE KEY-----",
|
||||||
|
"-----END EC PRIVATE KEY-----",
|
||||||
|
"-----END ENCRYPTED PRIVATE KEY-----"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "tls-auth",
|
||||||
|
beginMarkers: ["-----BEGIN OpenVPN Static key V1-----"],
|
||||||
|
endMarkers: ["-----END OpenVPN Static key V1-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.stripUnsupportedOptions(forOpenVPNAdapter: normalizedConfig)
|
||||||
|
if !normalizedConfig.hasSuffix("\n") {
|
||||||
|
normalizedConfig.append("\n")
|
||||||
|
}
|
||||||
|
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
|
||||||
|
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
|
||||||
|
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
|
||||||
|
let redirectLines = normalizedLines
|
||||||
|
.map(String.init)
|
||||||
|
.filter { $0.lowercased().contains("redirect-gateway") }
|
||||||
|
if !redirectLines.isEmpty {
|
||||||
|
ovpnLog(.info, title: "ConfigRedirect", message: redirectLines.joined(separator: " | "))
|
||||||
|
}
|
||||||
|
let controlScalars = normalizedConfig.unicodeScalars.filter {
|
||||||
|
($0.value < 0x20 && $0 != "\n" && $0 != "\r" && $0 != "\t")
|
||||||
|
}
|
||||||
|
if !controlScalars.isEmpty {
|
||||||
|
ovpnLog(.error, title: "ConfigChars", message: "nonPrintableControlCount=\(controlScalars.count)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
let dumpBaseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||||
|
?? FileManager.default.temporaryDirectory
|
||||||
|
let dumpURL = dumpBaseURL.appendingPathComponent("amnezia_ovpn_adapter_config.conf")
|
||||||
|
do {
|
||||||
|
try normalizedConfig.write(to: dumpURL, atomically: true, encoding: .utf8)
|
||||||
|
ovpnLog(.info, title: "ConfigDump", message: "path=\(dumpURL.path) bytes=\(normalizedConfig.utf8.count)")
|
||||||
|
} catch {
|
||||||
|
ovpnLog(.error, title: "ConfigDump", message: "write failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
let sanitizedData = Data(normalizedConfig.utf8)
|
let sanitizedData = Data(normalizedConfig.utf8)
|
||||||
|
|
||||||
let configuration = OpenVPNConfiguration()
|
let configuration = OpenVPNConfiguration()
|
||||||
configuration.fileContent = sanitizedData
|
configuration.fileContent = sanitizedData
|
||||||
|
// Be explicit: enum default is 0 (enabled), we need stubs-only behavior.
|
||||||
|
configuration.compressionMode = .disabled
|
||||||
|
// A-012: emulate OpenVPN2 CLI capability advertisement as closely as possible.
|
||||||
|
configuration.peerInfo = [
|
||||||
|
"IV_VER": "2.6.10",
|
||||||
|
"IV_PLAT": "mac",
|
||||||
|
"IV_TCPNL": "1",
|
||||||
|
"IV_MTU": "1600",
|
||||||
|
"IV_NCP": "2",
|
||||||
|
"IV_CIPHERS": "AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
|
||||||
|
"IV_PROTO": "990",
|
||||||
|
"IV_LZO_STUB": "1",
|
||||||
|
"IV_COMP_STUB": "1",
|
||||||
|
"IV_COMP_STUBv2": "1"
|
||||||
|
]
|
||||||
|
if let peerInfo = configuration.peerInfo {
|
||||||
|
let peerInfoSummary = peerInfo.keys.sorted().map { "\($0)=\(peerInfo[$0] ?? "")" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "PeerInfoOverride", message: peerInfoSummary)
|
||||||
|
}
|
||||||
if configString.contains("cloak") {
|
if configString.contains("cloak") {
|
||||||
configuration.setPTCloak()
|
configuration.setPTCloak()
|
||||||
}
|
}
|
||||||
@@ -124,10 +224,15 @@ extension PacketTunnelProvider {
|
|||||||
if evaluation?.autologin == false {
|
if evaluation?.autologin == false {
|
||||||
ovpnLog(.info, message: "Implement login with user credentials")
|
ovpnLog(.info, message: "Implement login with user credentials")
|
||||||
}
|
}
|
||||||
|
if let evaluation {
|
||||||
|
ovpnLog(.info, title: "ConfigEval", message: "autologin=\(evaluation.autologin) externalPki=\(evaluation.externalPki)")
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
vpnReachability.startTracking { [weak self] status in
|
vpnReachability.startTracking { [weak self] status in
|
||||||
self?.handleOpenVPNReachabilityChange(status)
|
self?.handleOpenVPNReachabilityChange(status)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
startHandler = completionHandler
|
startHandler = completionHandler
|
||||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||||
@@ -143,6 +248,8 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
||||||
|
|
||||||
let response: [String: Any] = [
|
let response: [String: Any] = [
|
||||||
"rx_bytes": bytesin,
|
"rx_bytes": bytesin,
|
||||||
"tx_bytes": bytesout
|
"tx_bytes": bytesout
|
||||||
@@ -155,6 +262,10 @@ extension PacketTunnelProvider {
|
|||||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||||
|
|
||||||
stopHandler = completionHandler
|
stopHandler = completionHandler
|
||||||
|
openVpnGatewayAddress = nil
|
||||||
|
openVpnLocalAddress = nil
|
||||||
|
openVpnLocalMask = nil
|
||||||
|
lastOpenVPNSettings = nil
|
||||||
if vpnReachability.isTracking {
|
if vpnReachability.isTracking {
|
||||||
vpnReachability.stopTracking()
|
vpnReachability.stopTracking()
|
||||||
}
|
}
|
||||||
@@ -174,11 +285,99 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||||
completionHandler: @escaping (Error?) -> Void
|
completionHandler: @escaping (Error?) -> Void
|
||||||
) {
|
) {
|
||||||
|
guard var effectiveSettings = networkSettings else {
|
||||||
|
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "nil settings; skipping update")
|
||||||
|
completionHandler(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let splitType = splitTunnelType ?? 0
|
||||||
|
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
openVpnLocalAddress = ipv4Settings.addresses.first
|
||||||
|
openVpnLocalMask = ipv4Settings.subnetMasks.first
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverIP = openVPNAdapter.connectionInformation?.serverIP
|
||||||
|
let configRemote = openVpnRemoteAddress
|
||||||
|
let serverEndpoint: String? = {
|
||||||
|
if let ip = serverIP, Self.isIPv4Address(ip) { return ip }
|
||||||
|
if let ip = configRemote, Self.isIPv4Address(ip) { return ip }
|
||||||
|
return effectiveSettings.tunnelRemoteAddress
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let serverEndpoint,
|
||||||
|
Self.isIPv4Address(serverEndpoint),
|
||||||
|
effectiveSettings.tunnelRemoteAddress != serverEndpoint {
|
||||||
|
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: serverEndpoint)
|
||||||
|
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||||
|
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||||
|
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||||
|
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||||
|
updatedSettings.mtu = effectiveSettings.mtu
|
||||||
|
effectiveSettings = updatedSettings
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to server=\(serverEndpoint)")
|
||||||
|
} else if let serverEndpoint, !Self.isIPv4Address(serverEndpoint) {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "skip tunnelRemoteAddress override; non-ip serverEndpoint=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
|
||||||
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
||||||
// send empty string to NEDNSSettings.matchDomains
|
// send empty string to NEDNSSettings.matchDomains
|
||||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||||
|
if dnsSettings.servers.isEmpty, !openVpnDnsServers.isEmpty {
|
||||||
|
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||||
|
newSettings.matchDomains = dnsSettings.matchDomains
|
||||||
|
effectiveSettings.dnsSettings = newSettings
|
||||||
|
}
|
||||||
|
} else if !openVpnDnsServers.isEmpty {
|
||||||
|
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||||
|
effectiveSettings.dnsSettings = newSettings
|
||||||
|
}
|
||||||
|
|
||||||
if splitTunnelType == 1 {
|
effectiveSettings.dnsSettings?.matchDomains = [""]
|
||||||
|
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||||
|
let servers = dnsSettings.servers.joined(separator: ",")
|
||||||
|
let domains = dnsSettings.matchDomains?.joined(separator: ",") ?? ""
|
||||||
|
ovpnLog(.info, title: "DNS", message: "servers=[\(servers)] matchDomains=[\(domains)]")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "DNS", message: "dnsSettings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
let tunnelRemote = effectiveSettings.tunnelRemoteAddress
|
||||||
|
if !tunnelRemote.isEmpty {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress=\(tunnelRemote)")
|
||||||
|
} else if let remoteAddress = openVpnRemoteAddress {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress is empty, configRemote=\(remoteAddress)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let included = (ipv4Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||||
|
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||||
|
let router: String
|
||||||
|
#if os(macOS)
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
router = ipv4Settings.router ?? ""
|
||||||
|
} else {
|
||||||
|
router = ""
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
router = ""
|
||||||
|
#endif
|
||||||
|
ovpnLog(.info, title: "IPv4RoutesPre", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "IPv4RoutesPre", message: "ipv4Settings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||||
|
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||||
|
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||||
|
ovpnLog(.info, title: "IPv6RoutesPre", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitType == 1 {
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
|
|
||||||
guard let splitTunnelSites else {
|
guard let splitTunnelSites else {
|
||||||
@@ -194,9 +393,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
} else {
|
} else if splitType == 2 {
|
||||||
if splitTunnelType == 2 {
|
|
||||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||||
@@ -224,14 +422,418 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
destinationAddress: "\(allIPv6.address)",
|
destinationAddress: "\(allIPv6.address)",
|
||||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||||
}
|
}
|
||||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
effectiveSettings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||||
|
} else {
|
||||||
|
// Full tunnel: rely on adapter-provided routes.
|
||||||
|
}
|
||||||
|
|
||||||
|
if let serverEndpoint,
|
||||||
|
Self.isIPv4Address(serverEndpoint),
|
||||||
|
let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let hostMask = "255.255.255.255"
|
||||||
|
var excluded = ipv4Settings.excludedRoutes ?? []
|
||||||
|
let alreadyExcluded = excluded.contains {
|
||||||
|
$0.destinationAddress == serverEndpoint && $0.destinationSubnetMask == hostMask
|
||||||
|
}
|
||||||
|
if !alreadyExcluded {
|
||||||
|
excluded.append(NEIPv4Route(destinationAddress: serverEndpoint, subnetMask: hostMask))
|
||||||
|
ipv4Settings.excludedRoutes = excluded
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "excluded remoteAddress=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
} else if let serverEndpoint {
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "skip explicit remote exclude; non-ip server=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let localAddr = openVpnLocalAddress
|
||||||
|
var net30Gateway: String?
|
||||||
|
if let localAddr, let mask = openVpnLocalMask {
|
||||||
|
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||||
|
}
|
||||||
|
var gateway = net30Gateway
|
||||||
|
if let adapterGateway = openVPNAdapter.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||||
|
if let localAddr, adapterGateway == localAddr {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "ignore adapter gateway equal to local address=\(adapterGateway)")
|
||||||
|
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "ignore mismatched adapter gateway=\(adapterGateway), using net30 peer=\(net30Gateway)")
|
||||||
|
} else {
|
||||||
|
gateway = adapterGateway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openVpnGatewayAddress = gateway
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "gateway=\(gateway)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if splitType == 0, let gateway, !gateway.isEmpty, effectiveSettings.tunnelRemoteAddress != gateway {
|
||||||
|
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: gateway)
|
||||||
|
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||||
|
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||||
|
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||||
|
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||||
|
updatedSettings.mtu = effectiveSettings.mtu
|
||||||
|
effectiveSettings = updatedSettings
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to gateway=\(gateway) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
if var ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
if splitType == 0 {
|
||||||
|
let hasNet30Mask = ipv4Settings.subnetMasks.contains("255.255.255.252")
|
||||||
|
if hasNet30Mask {
|
||||||
|
let normalizedMasks = Array(repeating: "255.255.255.255",
|
||||||
|
count: ipv4Settings.subnetMasks.count)
|
||||||
|
let normalized = NEIPv4Settings(addresses: ipv4Settings.addresses,
|
||||||
|
subnetMasks: normalizedMasks)
|
||||||
|
normalized.includedRoutes = ipv4Settings.includedRoutes
|
||||||
|
normalized.excludedRoutes = ipv4Settings.excludedRoutes
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
normalized.router = ipv4Settings.router
|
||||||
|
}
|
||||||
|
ipv4Settings = normalized
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "normalized net30 /30 masks to /32 on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
ipv4Settings.router = gateway
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "set ipv4 router=\(gateway) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var included = ipv4Settings.includedRoutes ?? []
|
||||||
|
let hasDefault = included.contains {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||||
|
}
|
||||||
|
if hasDefault {
|
||||||
|
included.removeAll {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hasDef1Low = included.contains {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||||
|
}
|
||||||
|
let hasDef1High = included.contains {
|
||||||
|
$0.destinationAddress == "128.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||||
|
}
|
||||||
|
if (hasDefault || openVpnRedirectGatewayDef1) && !(hasDef1Low && hasDef1High) {
|
||||||
|
if !hasDef1Low {
|
||||||
|
let route = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "128.0.0.0")
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
route.gatewayAddress = gateway
|
||||||
|
}
|
||||||
|
included.append(route)
|
||||||
|
}
|
||||||
|
if !hasDef1High {
|
||||||
|
let route = NEIPv4Route(destinationAddress: "128.0.0.0", subnetMask: "128.0.0.0")
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
route.gatewayAddress = gateway
|
||||||
|
}
|
||||||
|
included.append(route)
|
||||||
|
}
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "ensured def1 routes (/1 + /1) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
included = included.map { route in
|
||||||
|
let isDef1 =
|
||||||
|
(route.destinationAddress == "0.0.0.0" && route.destinationSubnetMask == "128.0.0.0") ||
|
||||||
|
(route.destinationAddress == "128.0.0.0" && route.destinationSubnetMask == "128.0.0.0")
|
||||||
|
guard isDef1 else { return route }
|
||||||
|
if route.gatewayAddress == gateway {
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
let updatedRoute = NEIPv4Route(destinationAddress: route.destinationAddress,
|
||||||
|
subnetMask: route.destinationSubnetMask)
|
||||||
|
updatedRoute.gatewayAddress = gateway
|
||||||
|
return updatedRoute
|
||||||
|
}
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "set gateway=\(gateway) on macOS def1 routes")
|
||||||
|
}
|
||||||
|
ipv4Settings.includedRoutes = included
|
||||||
|
effectiveSettings.ipv4Settings = ipv4Settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let included = (ipv4Settings.includedRoutes ?? []).map {
|
||||||
|
let gw = $0.gatewayAddress ?? ""
|
||||||
|
return "\($0.destinationAddress)/\($0.destinationSubnetMask) gw=\(gw)"
|
||||||
|
}
|
||||||
|
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||||
|
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||||
|
let router: String
|
||||||
|
#if os(macOS)
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
router = ipv4Settings.router ?? ""
|
||||||
|
} else {
|
||||||
|
router = ""
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
router = ""
|
||||||
|
#endif
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "IPv4Routes", message: "ipv4Settings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||||
|
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||||
|
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||||
|
ovpnLog(.info, title: "IPv6Routes", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if effectiveSettings.ipv6Settings != nil {
|
||||||
|
effectiveSettings.ipv6Settings = nil
|
||||||
|
ovpnLog(.info, title: "IPv6", message: "cleared ipv6Settings on macOS")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lastOpenVPNSettings = effectiveSettings
|
||||||
|
|
||||||
// Set the network settings for the current tunneling session.
|
// Set the network settings for the current tunneling session.
|
||||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
setTunnelNetworkSettings(effectiveSettings) { error in
|
||||||
|
if let error {
|
||||||
|
ovpnLog(.error, title: "SetTunnelNetworkSettings", message: error.localizedDescription)
|
||||||
|
} else {
|
||||||
|
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "ok")
|
||||||
|
}
|
||||||
|
completionHandler(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDnsServers(from config: String) -> [String] {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
var servers: [String] = []
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("dhcp-option DNS ") {
|
||||||
|
let parts = trimmed.split(separator: " ")
|
||||||
|
if let last = parts.last {
|
||||||
|
servers.append(String(last))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractRemoteHost(from config: String) -> String? {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("remote ") {
|
||||||
|
let parts = trimmed.split(separator: " ")
|
||||||
|
if parts.count >= 2 {
|
||||||
|
return String(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hasRedirectGatewayDef1(in config: String) -> Bool {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("redirect-gateway") {
|
||||||
|
return trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" }).contains("def1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func net30Peer(for address: String, mask: String) -> String? {
|
||||||
|
guard mask == "255.255.255.252" else { return nil }
|
||||||
|
let parts = address.split(separator: ".")
|
||||||
|
guard parts.count == 4 else { return nil }
|
||||||
|
var octets: [Int] = []
|
||||||
|
for part in parts {
|
||||||
|
guard let num = Int(part), num >= 0 && num <= 255 else { return nil }
|
||||||
|
octets.append(num)
|
||||||
|
}
|
||||||
|
let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
|
||||||
|
let network = ip & ~3
|
||||||
|
let host = ip - network
|
||||||
|
let peerHost: Int
|
||||||
|
switch host {
|
||||||
|
case 1: peerHost = 2
|
||||||
|
case 2: peerHost = 1
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
let peerIP = network + peerHost
|
||||||
|
return "\((peerIP >> 24) & 0xff).\((peerIP >> 16) & 0xff).\((peerIP >> 8) & 0xff).\(peerIP & 0xff)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func logOpenVPNConnectionInfo() {
|
||||||
|
guard let info = ovpnAdapter?.connectionInformation else { return }
|
||||||
|
let message = "vpnIPv4=\(info.vpnIPv4 ?? "") gatewayIPv4=\(info.gatewayIPv4 ?? "") serverIP=\(info.serverIP ?? "") tun=\(info.tunName ?? "")"
|
||||||
|
ovpnLog(.info, title: "ConnInfo", message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeInlineBlock(
|
||||||
|
in config: String,
|
||||||
|
tag: String,
|
||||||
|
beginMarkers: [String],
|
||||||
|
endMarkers: [String]
|
||||||
|
) -> String {
|
||||||
|
guard !beginMarkers.isEmpty, !endMarkers.isEmpty else { return config }
|
||||||
|
|
||||||
|
var normalizedConfig = config
|
||||||
|
let openTag = "<\(tag)>"
|
||||||
|
let closeTag = "</\(tag)>"
|
||||||
|
var searchStart = normalizedConfig.startIndex
|
||||||
|
|
||||||
|
while let openRange = normalizedConfig.range(of: openTag, range: searchStart..<normalizedConfig.endIndex),
|
||||||
|
let closeRange = normalizedConfig.range(of: closeTag, range: openRange.upperBound..<normalizedConfig.endIndex) {
|
||||||
|
let rawBody = String(normalizedConfig[openRange.upperBound..<closeRange.lowerBound])
|
||||||
|
let lines = rawBody
|
||||||
|
.split(whereSeparator: \.isNewline)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
|
||||||
|
var beginIndex: Int?
|
||||||
|
var endIndex: Int?
|
||||||
|
for (idx, line) in lines.enumerated() {
|
||||||
|
if beginIndex == nil,
|
||||||
|
beginMarkers.contains(where: { line.contains($0) }) {
|
||||||
|
beginIndex = idx
|
||||||
|
}
|
||||||
|
if beginIndex != nil,
|
||||||
|
endMarkers.contains(where: { line.contains($0) }) {
|
||||||
|
endIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let beginIndex,
|
||||||
|
let endIndex,
|
||||||
|
endIndex >= beginIndex {
|
||||||
|
let extracted = lines[beginIndex...endIndex].joined(separator: "\n")
|
||||||
|
let replacement = "<\(tag)>\n\(extracted)\n</\(tag)>"
|
||||||
|
normalizedConfig.replaceSubrange(openRange.lowerBound..<closeRange.upperBound, with: replacement)
|
||||||
|
ovpnLog(.info, title: "ConfigInline", message: "tag=<\(tag)> linesIn=\(lines.count) linesOut=\(endIndex - beginIndex + 1)")
|
||||||
|
searchStart = normalizedConfig.index(openRange.lowerBound, offsetBy: replacement.count)
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "ConfigInline", message: "tag=<\(tag)> missing markers, keeping original body")
|
||||||
|
searchStart = closeRange.upperBound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func stripUnsupportedOptions(forOpenVPNAdapter config: String) -> String {
|
||||||
|
let unsupportedTokens: Set<String> = [
|
||||||
|
"block-ipv6",
|
||||||
|
"script-security",
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"resolv-retry",
|
||||||
|
"persist-key",
|
||||||
|
"persist-tun",
|
||||||
|
"compat-mode",
|
||||||
|
"disable-dco"
|
||||||
|
]
|
||||||
|
let inlineBlockTags: Set<String> = [
|
||||||
|
"ca",
|
||||||
|
"cert",
|
||||||
|
"key",
|
||||||
|
"pkcs12",
|
||||||
|
"tls-auth",
|
||||||
|
"tls-crypt",
|
||||||
|
"tls-crypt-v2",
|
||||||
|
"secret",
|
||||||
|
"crl-verify",
|
||||||
|
"extra-certs"
|
||||||
|
]
|
||||||
|
|
||||||
|
var removed: [String: Int] = [:]
|
||||||
|
var normalized: [String: Int] = [:]
|
||||||
|
var output: [String] = []
|
||||||
|
var activeInlineTag: String?
|
||||||
|
|
||||||
|
for rawLine in config.split(whereSeparator: \.isNewline) {
|
||||||
|
let line = String(rawLine)
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmedLowercased = trimmed.lowercased()
|
||||||
|
|
||||||
|
if let currentInlineTag = activeInlineTag {
|
||||||
|
output.append(line)
|
||||||
|
if trimmedLowercased == "</\(currentInlineTag)>" {
|
||||||
|
activeInlineTag = nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmedLowercased.hasPrefix("<"),
|
||||||
|
trimmedLowercased.hasSuffix(">"),
|
||||||
|
!trimmedLowercased.hasPrefix("</") {
|
||||||
|
let tagContent = String(trimmedLowercased.dropFirst().dropLast())
|
||||||
|
let tagName = tagContent
|
||||||
|
.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||||
|
.first
|
||||||
|
.map(String.init) ?? ""
|
||||||
|
if inlineBlockTags.contains(tagName) {
|
||||||
|
activeInlineTag = tagName
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmed.hasPrefix("#") || trimmed.hasPrefix(";") {
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||||
|
let token = parts.first.map(String.init)?.lowercased() ?? ""
|
||||||
|
if trimmedLowercased.hasPrefix("redirect-gateway") || token.hasPrefix("redirect-gateway") {
|
||||||
|
let hasDef1 = parts.dropFirst().contains { String($0).lowercased().hasPrefix("def1") }
|
||||||
|
if hasDef1 {
|
||||||
|
output.append("redirect-gateway def1")
|
||||||
|
normalized["redirect-gateway", default: 0] += 1
|
||||||
|
} else {
|
||||||
|
removed["redirect-gateway", default: 0] += 1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if let matchedUnsupported = unsupportedTokens.first(where: { token.hasPrefix($0) }) {
|
||||||
|
removed[matchedUnsupported, default: 0] += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
output.append(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removed.isEmpty {
|
||||||
|
let summary = removed.keys.sorted().map { "\($0)=\(removed[$0] ?? 0)" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "ConfigStrip", message: summary)
|
||||||
|
}
|
||||||
|
if !normalized.isEmpty {
|
||||||
|
let summary = normalized.keys.sorted().map { "\($0)=\(normalized[$0] ?? 0)" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "ConfigNormalize", message: summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isIPv4Address(_ value: String) -> Bool {
|
||||||
|
let parts = value.split(separator: ".")
|
||||||
|
if parts.count != 4 { return false }
|
||||||
|
for part in parts {
|
||||||
|
guard let num = Int(part), num >= 0 && num <= 255 else { return false }
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process events returned by the OpenVPN library
|
// Process events returned by the OpenVPN library
|
||||||
@@ -249,6 +851,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
|
|
||||||
startHandler(nil)
|
startHandler(nil)
|
||||||
self.startHandler = nil
|
self.startHandler = nil
|
||||||
|
|
||||||
|
logOpenVPNConnectionInfo()
|
||||||
|
refreshOpenVPNSettingsAfterConnect()
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
guard let stopHandler = stopHandler else { return }
|
guard let stopHandler = stopHandler else { return }
|
||||||
|
|
||||||
@@ -291,4 +896,41 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
// Handle log messages
|
// Handle log messages
|
||||||
ovpnLog(.info, message: logMessage)
|
ovpnLog(.info, message: logMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openVPNAdapterDidReceiveClockTick(_ openVPNAdapter: OpenVPNAdapter) {
|
||||||
|
let now = Date()
|
||||||
|
if now.timeIntervalSince(lastOpenVPNStatsLogTime) < 5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastOpenVPNStatsLogTime = now
|
||||||
|
|
||||||
|
let transport = openVPNAdapter.transportStatistics
|
||||||
|
let iface = openVPNAdapter.interfaceStatistics
|
||||||
|
let transportLine = "transport bytesIn=\(transport.bytesIn) bytesOut=\(transport.bytesOut) packetsIn=\(transport.packetsIn) packetsOut=\(transport.packetsOut)"
|
||||||
|
let ifaceLine = "iface bytesIn=\(iface.bytesIn) bytesOut=\(iface.bytesOut) packetsIn=\(iface.packetsIn) packetsOut=\(iface.packetsOut) errorsIn=\(iface.errorsIn) errorsOut=\(iface.errorsOut)"
|
||||||
|
ovpnLog(.info, title: "Stats", message: "\(transportLine) | \(ifaceLine)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshOpenVPNSettingsAfterConnect() {
|
||||||
|
let localAddr = openVpnLocalAddress
|
||||||
|
var net30Gateway: String?
|
||||||
|
if let localAddr, let mask = openVpnLocalMask {
|
||||||
|
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||||
|
}
|
||||||
|
var gateway = net30Gateway
|
||||||
|
if let adapterGateway = ovpnAdapter?.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||||
|
if let localAddr, adapterGateway == localAddr {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect ignoring adapter gateway equal to local address=\(adapterGateway)")
|
||||||
|
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect keeping net30 peer=\(net30Gateway), adapter gateway=\(adapterGateway)")
|
||||||
|
} else {
|
||||||
|
gateway = adapterGateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let gateway, !gateway.isEmpty else { return }
|
||||||
|
openVpnGatewayAddress = gateway
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect gateway=\(gateway)")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
var splitTunnelType: Int?
|
var splitTunnelType: Int?
|
||||||
var splitTunnelSites: [String]?
|
var splitTunnelSites: [String]?
|
||||||
|
var openVpnDnsServers: [String] = []
|
||||||
|
var openVpnRemoteAddress: String?
|
||||||
|
var openVpnRedirectGatewayDef1 = false
|
||||||
|
var openVpnLocalAddress: String?
|
||||||
|
var openVpnLocalMask: String?
|
||||||
|
var openVpnGatewayAddress: String?
|
||||||
|
var lastOpenVPNSettings: NEPacketTunnelNetworkSettings?
|
||||||
|
var lastOpenVPNStatsLogTime = Date.distantPast
|
||||||
|
|
||||||
let vpnReachability = OpenVPNReachability()
|
let vpnReachability = OpenVPNReachability()
|
||||||
|
|
||||||
@@ -83,8 +91,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||||
|
|
||||||
// WireGuard/AWG manages network changes internally in its own adapter.
|
// WireGuard/AWG and OpenVPN manages network changes internally in its own adapter.
|
||||||
if proto == .wireguard {
|
if proto == .wireguard || proto == .openvpn {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,9 +200,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||||
|
|
||||||
neLog(.info, message: "Start tunnel")
|
neLog(.info, message: "Start tunnel")
|
||||||
|
if let vpnProto = protocolConfiguration as? NEVPNProtocol {
|
||||||
|
if #available(iOS 14.0, macOS 11.0, *) {
|
||||||
|
var details = "includeAllNetworks=\(vpnProto.includeAllNetworks)"
|
||||||
|
if #available(iOS 14.2, macOS 11.0, *) {
|
||||||
|
details += " excludeLocalNetworks=\(vpnProto.excludeLocalNetworks)"
|
||||||
|
}
|
||||||
|
neLog(.info, title: "Protocol", message: details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration
|
let providerConfiguration = protocolConfiguration.providerConfiguration
|
||||||
|
let providerKeys = providerConfiguration?.keys.sorted().joined(separator: ",") ?? ""
|
||||||
|
var protocolDetails = "bundleId=\(protocolConfiguration.providerBundleIdentifier ?? "") keys=[\(providerKeys)]"
|
||||||
|
if let ovpnData = providerConfiguration?[Constants.ovpnConfigKey] as? Data {
|
||||||
|
let preview = String(decoding: ovpnData.prefix(512), as: UTF8.self)
|
||||||
|
protocolDetails += " ovpnBytes=\(ovpnData.count) ovpnPreview=\(preview)"
|
||||||
|
}
|
||||||
|
neLog(.info, title: "Protocol", message: protocolDetails)
|
||||||
|
|
||||||
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
||||||
protoType = .openvpn
|
protoType = .openvpn
|
||||||
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
||||||
@@ -449,6 +474,8 @@ extension WireGuardLogLevel {
|
|||||||
|
|
||||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||||
private let flow: NEPacketTunnelFlow
|
private let flow: NEPacketTunnelFlow
|
||||||
|
private var readLogCounter = 0
|
||||||
|
private var writeLogCounter = 0
|
||||||
|
|
||||||
init(flow: NEPacketTunnelFlow) {
|
init(flow: NEPacketTunnelFlow) {
|
||||||
self.flow = flow
|
self.flow = flow
|
||||||
@@ -457,15 +484,98 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
|||||||
|
|
||||||
@objc(readPacketsWithCompletionHandler:)
|
@objc(readPacketsWithCompletionHandler:)
|
||||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||||
flow.readPackets(completionHandler: completionHandler)
|
flow.readPackets { packets, protocols in
|
||||||
|
#if os(macOS)
|
||||||
|
if self.readLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||||
|
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
let header = Self.describePacketHeader(firstPacket)
|
||||||
|
ovpnLog(.info, title: "FlowRead", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||||
|
self.readLogCounter += 1
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
completionHandler(packets, protocols)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(writePackets:withProtocols:)
|
@objc(writePackets:withProtocols:)
|
||||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||||
flow.writePackets(packets, withProtocols: protocols)
|
#if os(macOS)
|
||||||
|
if writeLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||||
|
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
let header = Self.describePacketHeader(firstPacket)
|
||||||
|
ovpnLog(.info, title: "FlowWrite", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||||
|
writeLogCounter += 1
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return flow.writePackets(packets, withProtocols: protocols)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func describePacketHeader(_ packet: Data) -> String {
|
||||||
|
guard let versionNibble = packet.first.map({ Int($0 >> 4) }) else {
|
||||||
|
return "ip=unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionNibble == 4, packet.count >= 20 {
|
||||||
|
let ihl = Int(packet[0] & 0x0f) * 4
|
||||||
|
guard ihl >= 20, packet.count >= ihl else {
|
||||||
|
return "ip=ipv4 malformed"
|
||||||
|
}
|
||||||
|
|
||||||
|
let proto = packet[9]
|
||||||
|
let src = "\(packet[12]).\(packet[13]).\(packet[14]).\(packet[15])"
|
||||||
|
let dst = "\(packet[16]).\(packet[17]).\(packet[18]).\(packet[19])"
|
||||||
|
let l4Offset = ihl
|
||||||
|
let ports: String
|
||||||
|
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||||
|
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||||
|
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||||
|
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||||
|
} else {
|
||||||
|
ports = "sport=- dport=-"
|
||||||
|
}
|
||||||
|
let protoName: String
|
||||||
|
switch proto {
|
||||||
|
case 1: protoName = "ICMP"
|
||||||
|
case 6: protoName = "TCP"
|
||||||
|
case 17: protoName = "UDP"
|
||||||
|
default: protoName = "P\(proto)"
|
||||||
|
}
|
||||||
|
return "ip=ipv4 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionNibble == 6, packet.count >= 40 {
|
||||||
|
let proto = packet[6]
|
||||||
|
func hex16(_ start: Int) -> String {
|
||||||
|
let value = (UInt16(packet[start]) << 8) | UInt16(packet[start + 1])
|
||||||
|
return String(format: "%x", value)
|
||||||
|
}
|
||||||
|
let src = stride(from: 8, to: 24, by: 2).map(hex16).joined(separator: ":")
|
||||||
|
let dst = stride(from: 24, to: 40, by: 2).map(hex16).joined(separator: ":")
|
||||||
|
let l4Offset = 40
|
||||||
|
let ports: String
|
||||||
|
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||||
|
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||||
|
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||||
|
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||||
|
} else {
|
||||||
|
ports = "sport=- dport=-"
|
||||||
|
}
|
||||||
|
let protoName: String
|
||||||
|
switch proto {
|
||||||
|
case 58: protoName = "ICMPv6"
|
||||||
|
case 6: protoName = "TCP"
|
||||||
|
case 17: protoName = "UDP"
|
||||||
|
default: protoName = "P\(proto)"
|
||||||
|
}
|
||||||
|
return "ip=ipv6 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ip=v\(versionNibble) len=\(packet.count)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||||
|
|
||||||
extension NEProviderStopReason {
|
extension NEProviderStopReason {
|
||||||
var amneziaDescription: String {
|
var amneziaDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
|||||||
@@ -552,6 +552,16 @@ bool IosController::setupOpenVPN()
|
|||||||
|
|
||||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||||
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
||||||
|
QString openVPNConfigPreview = openVPNConfigStr.left(512);
|
||||||
|
QString ovpnPreview = ovpnConfig.left(512);
|
||||||
|
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload"
|
||||||
|
<< "jsonBytes=" << openVPNConfigStr.toUtf8().size()
|
||||||
|
<< "ovpnChars=" << ovpnConfig.size()
|
||||||
|
<< "splitTunnelType=" << m_rawConfig[config_key::splitTunnelType].toInt()
|
||||||
|
<< "splitTunnelSites=" << splitTunnelSites;
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload jsonPreview=" << openVPNConfigPreview;
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload ovpnPreview=" << ovpnPreview;
|
||||||
|
|
||||||
return startOpenVPN(openVPNConfigStr);
|
return startOpenVPN(openVPNConfigStr);
|
||||||
}
|
}
|
||||||
@@ -800,11 +810,59 @@ bool IosController::startOpenVPN(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
if (@available(iOS 14.0, macOS 11.0, *)) {
|
||||||
|
int splitTunnelType = 0;
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &parseError);
|
||||||
|
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
splitTunnelType = obj.value(config_key::splitTunnelType).toInt(0);
|
||||||
|
}
|
||||||
|
#if defined(MACOS_NE)
|
||||||
|
// On macOS NE use route-based full tunnel. includeAllNetworks enables
|
||||||
|
// policy-based drop-all mode and causes enforceRoutes to be ignored.
|
||||||
|
tunnelProtocol.includeAllNetworks = NO;
|
||||||
|
if (splitTunnelType == 0) {
|
||||||
|
tunnelProtocol.enforceRoutes = YES;
|
||||||
|
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||||
|
tunnelProtocol.excludeLocalNetworks = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
tunnelProtocol.includeAllNetworks = (splitTunnelType == 0);
|
||||||
|
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||||
|
// Keep existing iOS behavior.
|
||||||
|
if (splitTunnelType == 0) {
|
||||||
|
tunnelProtocol.excludeLocalNetworks = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
|
|
||||||
|
NETunnelProviderProtocol *appliedProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
|
||||||
|
NSData *ovpnPayload = appliedProtocol.providerConfiguration[@"ovpn"];
|
||||||
|
NSString *payloadPreview = @"";
|
||||||
|
if (ovpnPayload != nil) {
|
||||||
|
NSString *decodedPayload = [[NSString alloc] initWithData:ovpnPayload encoding:NSUTF8StringEncoding];
|
||||||
|
if (decodedPayload != nil) {
|
||||||
|
payloadPreview = [decodedPayload substringToIndex:MIN((NSUInteger)512, decodedPayload.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration"
|
||||||
|
<< "bundleId=" << QString::fromNSString(appliedProtocol.providerBundleIdentifier ?: @"")
|
||||||
|
<< "serverAddress=" << QString::fromNSString(appliedProtocol.serverAddress ?: @"")
|
||||||
|
<< "providerKeys=" << QString::fromNSString([[appliedProtocol.providerConfiguration.allKeys description] copy])
|
||||||
|
<< "ovpnBytes=" << (ovpnPayload != nil ? ovpnPayload.length : 0);
|
||||||
|
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration payloadPreview="
|
||||||
|
<< QString::fromNSString(payloadPreview);
|
||||||
|
|
||||||
startTunnel();
|
startTunnel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +872,9 @@ bool IosController::startWireGuard(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
@@ -828,7 +888,9 @@ bool IosController::startXray(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
|
|||||||
Reference in New Issue
Block a user