refactor: extract and simplify OpenVPN reachability and network change handling logic (#2402)

This commit is contained in:
yyy-amnezia
2026-03-24 16:12:59 +02:00
committed by GitHub
parent fa69da6d56
commit 4103c5bbcf
2 changed files with 95 additions and 15 deletions

View File

@@ -126,13 +126,7 @@ extension PacketTunnelProvider {
} }
vpnReachability.startTracking { [weak self] status in vpnReachability.startTracking { [weak self] status in
switch status { self?.handleOpenVPNReachabilityChange(status)
case .reachableViaWiFi, .reachableViaWWAN:
ovpnLog(.info, message: "Reachability changed, reconnecting OpenVPN session")
self?.ovpnAdapter?.reconnect(afterTimeInterval: 1)
default:
break
}
} }
startHandler = completionHandler startHandler = completionHandler

View File

@@ -46,8 +46,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var didReceiveInitialPathUpdate = false private var didReceiveInitialPathUpdate = false
private var currentPath: Network.NWPath? private var currentPath: Network.NWPath?
private var currentPathSignature: String? private var currentPathSignature: String?
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
private var pendingNetworkChangeWorkItem: DispatchWorkItem? private var pendingNetworkChangeWorkItem: DispatchWorkItem?
private var isApplyingNetworkChange = false private var isApplyingNetworkChange = false
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
var splitTunnelType: Int? var splitTunnelType: Int?
var splitTunnelSites: [String]? var splitTunnelSites: [String]?
@@ -81,9 +83,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return } guard hasMeaningfulChange, let proto = self.protoType else { return }
// OpenVPN and WireGuard/AWG handle network changes internally. // WireGuard/AWG manages network changes internally in its own adapter.
// Restarting them here can race their own reconnect logic and break tunnel setup. if proto == .wireguard {
if proto == .wireguard || proto == .openvpn { return
}
if proto == .openvpn {
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
return
}
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
return return
} }
@@ -199,6 +210,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return return
} }
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
didReceiveInitialPathUpdate = false didReceiveInitialPathUpdate = false
updateActiveInterfaceIndexForCurrentPath() updateActiveInterfaceIndexForCurrentPath()
@@ -217,6 +230,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
guard let protoType else { guard let protoType else {
completionHandler() completionHandler()
return return
@@ -284,8 +300,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let workItem = DispatchWorkItem { [weak self] in let workItem = DispatchWorkItem { [weak self] in
guard let self else { return } guard let self else { return }
self.pendingNetworkChangeWorkItem = nil
if self.isApplyingNetworkChange { if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Skipping network change while restart is already in progress") xrayLog(.debug, message: "Skipping network change while restart is already in progress")
return return
} }
@@ -303,6 +320,69 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
pendingNetworkChangeWorkItem = workItem pendingNetworkChangeWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem) networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
} }
private func scheduleOpenVPNReconnect(reason: String) {
guard protoType == .openvpn else { return }
pendingOpenVPNReconnectWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingOpenVPNReconnectWorkItem = nil
guard self.protoType == .openvpn else { return }
if self.reasserting {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard !self.reasserting else {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
}
}
pendingOpenVPNReconnectWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
defer { lastOpenVPNReachabilityStatus = status }
guard let previousStatus = lastOpenVPNReachabilityStatus else {
return
}
guard previousStatus != status else {
return
}
switch status {
case .reachableViaWiFi, .reachableViaWWAN:
scheduleOpenVPNReconnect(reason: "Reachability changed")
default:
break
}
}
private func cancelPendingOpenVPNReconnect() {
pendingOpenVPNReconnectWorkItem?.cancel()
pendingOpenVPNReconnectWorkItem = nil
lastOpenVPNReachabilityStatus = nil
}
private func cancelPendingNetworkChangeHandling() {
pendingNetworkChangeWorkItem?.cancel()
pendingNetworkChangeWorkItem = nil
isApplyingNetworkChange = false
}
} }
private extension PacketTunnelProvider { private extension PacketTunnelProvider {
@@ -311,8 +391,14 @@ private extension PacketTunnelProvider {
signatureComponents.append(path.isExpensive ? "exp" : "noexp") signatureComponents.append(path.isExpensive ? "exp" : "noexp")
signatureComponents.append(path.isConstrained ? "con" : "nocon") signatureComponents.append(path.isConstrained ? "con" : "nocon")
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other] // Ignore loopback and tunnel-style `.other` interfaces so Xray does not
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in // react to its own utun lifecycle as if the physical uplink changed.
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
let externalInterfaces = path.availableInterfaces.filter { interface in
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
}
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
if lhs.type == rhs.type { if lhs.type == rhs.type {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
@@ -333,8 +419,8 @@ private extension PacketTunnelProvider {
case .wiredEthernet: typeName = "ethernet" case .wiredEthernet: typeName = "ethernet"
case .wifi: typeName = "wifi" case .wifi: typeName = "wifi"
case .cellular: typeName = "cellular" case .cellular: typeName = "cellular"
case .loopback: typeName = "loopback" case .loopback, .other:
case .other: typeName = "other" continue
@unknown default: typeName = "unknown" @unknown default: typeName = "unknown"
} }
signatureComponents.append("\(typeName):\(interface.index)") signatureComponents.append("\(typeName):\(interface.index)")