Commit 0a668e29 authored by Jose Blaya's avatar Jose Blaya

Merge branch 'feature/servers_offline' into feature/dip_connection

* feature/servers_offline:
  Update retry delay to 10s
  Fallback when the vpn connection fails
  Add `offline` attribute to Server

# Conflicts:
#	PIALibrary/Sources/VPN/PIATunnelProfile.swift
#	PIALibrary/Sources/VPN/PIAWGTunnelProfile.swift
parents 654f931c d7423a69
......@@ -86,7 +86,13 @@ public class Server: Hashable {
/// The response time for this address.
private(set) var responseTime: Int?
private(set) var available: Bool = true
public var description: String {
return "\(ip):0"
}
/// :nodoc:
public init(ip: String, cn: String) {
self.ip = ip
......@@ -97,6 +103,14 @@ public class Server: Hashable {
self.responseTime = time
}
func markServerAsUnavailable() {
available = false
}
func reset() {
available = true
}
}
/// The server name.
......@@ -117,11 +131,8 @@ public class Server: Hashable {
/// The server is virtually located.
public let geo: Bool
/// The best address for establishing an OpenVPN connection over TCP.
public let bestOpenVPNAddressForTCP: Address?
/// The best address for establishing an OpenVPN connection over UDP.
public let bestOpenVPNAddressForUDP: Address?
/// The server is unavailable.
public let offline: Bool
/// The best address for establishing an OpenVPN connection over TCP.
public let openVPNAddressesForTCP: [ServerAddressIP]?
......@@ -157,8 +168,6 @@ public class Server: Hashable {
name: String,
country: String,
hostname: String,
bestOpenVPNAddressForTCP: Address?,
bestOpenVPNAddressForUDP: Address?,
openVPNAddressesForTCP: [ServerAddressIP]? = nil,
openVPNAddressesForUDP: [ServerAddressIP]? = nil,
wireGuardAddressesForUDP: [ServerAddressIP]? = nil,
......@@ -166,6 +175,7 @@ public class Server: Hashable {
pingAddress: Address?,
responseTime: Int? = 0,
geo: Bool = false,
offline: Bool = false,
meta: ServerAddressIP? = nil,
dipExpire: Date? = nil,
dipToken: String? = nil,
......@@ -177,11 +187,10 @@ public class Server: Hashable {
self.country = country
self.hostname = hostname
self.geo = geo
self.offline = offline
self.regionIdentifier = regionIdentifier
identifier = hostname.components(separatedBy: ".").first ?? ""
self.bestOpenVPNAddressForTCP = bestOpenVPNAddressForTCP
self.bestOpenVPNAddressForUDP = bestOpenVPNAddressForUDP
self.openVPNAddressesForTCP = openVPNAddressesForTCP
self.openVPNAddressesForUDP = openVPNAddressesForUDP
self.wireGuardAddressesForUDP = wireGuardAddressesForUDP
......@@ -213,86 +222,62 @@ public class Server: Hashable {
extension Server {
func bestAddressForOpenVPNTCP() -> Address? {
if let addresses = openVPNAddressesForTCP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return nil
}
public func addresses() -> [ServerAddressIP] {
return bestOpenVPNAddressForTCP
}
func bestAddressForOpenVPNUDP() -> Address? {
if let addresses = openVPNAddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return nil
switch Client.providers.vpnProvider.currentVPNType {
case IKEv2Profile.vpnType:
return iKEv2AddressesForUDP ?? []
case PIATunnelProfile.vpnType:
return openVPNAddressesForTCP ?? []
case PIAWGTunnelProfile.vpnType:
return wireGuardAddressesForUDP ?? []
default:
return []
}
return bestOpenVPNAddressForUDP
}
func bestAddressForIKEv2() -> ServerAddressIP? {
public func ovpnAddresses(tcp: Bool) -> [ServerAddressIP] {
if let addresses = iKEv2AddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return sorted.first
if tcp {
return openVPNAddressesForTCP ?? []
} else {
return openVPNAddressesForUDP ?? []
}
return nil // currently using DNS
}
func bestAddressForWireGuard() -> ServerAddressIP? {
if let addresses = wireGuardAddressesForUDP {
let sorted = addresses.sorted(by: { $0.responseTime ?? 0 > $1.responseTime ?? 0 })
return sorted.first
public func bestAddress() -> ServerAddressIP? {
let availableServer = addresses().first(where: {$0.available})
if availableServer == nil {
addresses().map({$0.reset()})
return bestAddress()
}
return nil
return availableServer
}
public func bestPingAddress() -> [Address] {
switch Client.providers.vpnProvider.currentVPNType {
case IKEv2Profile.vpnType:
var addresses: [Address] = []
for address in iKEv2AddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
case PIATunnelProfile.vpnType:
var addresses: [Address] = []
for address in openVPNAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
case PIAWGTunnelProfile.vpnType:
var addresses: [Address] = []
for address in wireGuardAddressesForUDP ?? [] {
addresses.append(Address(hostname: address.ip, port: 0))
}
return addresses
default:
return []
public func bestAddressForOVPN(tcp: Bool) -> ServerAddressIP? {
let availableServer = ovpnAddresses(tcp: tcp).first(where: {$0.available})
if availableServer == nil {
ovpnAddresses(tcp: tcp).map({$0.reset()})
return bestAddress()
}
return availableServer
}
}
extension Server {
func updateResponseTime(_ time: Int, forAddress address: Address) {
func updateResponseTime(_ time: Int, forAddress address: ServerAddressIP) {
switch Client.providers.vpnProvider.currentVPNType {
case IKEv2Profile.vpnType:
let serverAddressIP = iKEv2AddressesForUDP?.first(where: {$0.ip == address.hostname })
let serverAddressIP = iKEv2AddressesForUDP?.first(where: {$0.ip == address.ip })
serverAddressIP?.updateResponseTime(time)
case PIATunnelProfile.vpnType:
let serverAddressIP = openVPNAddressesForUDP?.first(where: {$0.ip == address.hostname })
let serverAddressIP = openVPNAddressesForUDP?.first(where: {$0.ip == address.ip })
serverAddressIP?.updateResponseTime(time)
case PIAWGTunnelProfile.vpnType:
let serverAddressIP = wireGuardAddressesForUDP?.first(where: {$0.ip == address.hostname })
let serverAddressIP = wireGuardAddressesForUDP?.first(where: {$0.ip == address.ip })
serverAddressIP?.updateResponseTime(time)
default:
break
......
......@@ -109,6 +109,15 @@ extension Client {
/// Sets the maximum number of failed connectivity checks before giving up.
public var connectivityMaxAttempts: Int
/// Sets the timeout for VPN connectivity checks.
public var vpnConnectivityTimeout: TimeInterval
/// Sets the delay after which to retry VPN connectivity checks.
public var vpnConnectivityRetryDelay: TimeInterval
/// Sets the maximum number of failed VPN connectivity attempts before giving up.
public var vpnConnectivityMaxAttempts: Int
let maceHostname: String
let macePort: UInt16
......@@ -198,9 +207,13 @@ extension Client {
enablesConnectivityUpdates = false
connectivityVPNLag = 1000
connectivityTimeout = 3000
connectivityRetryDelay = 5000
connectivityRetryDelay = 10000
connectivityMaxAttempts = 3
vpnConnectivityTimeout = 2.0
vpnConnectivityRetryDelay = 5.0
vpnConnectivityMaxAttempts = 3
maceHostname = "209.222.18.222"
macePort = 1111
maceDelay = 5000
......
......@@ -33,7 +33,7 @@ class PingTask {
let identifier: String
let server: Server
let address: Server.Address
let address: Server.ServerAddressIP
let stateUpdateHandler: (PingTask) -> ()
var state = PingTaskState.pending {
didSet {
......@@ -41,7 +41,7 @@ class PingTask {
}
}
init(identifier: String, server: Server, address: Server.Address, stateUpdateHandler: @escaping (PingTask) -> ()) {
init(identifier: String, server: Server, address: Server.ServerAddressIP, stateUpdateHandler: @escaping (PingTask) -> ()) {
self.identifier = identifier
self.server = server
self.address = address
......@@ -54,7 +54,7 @@ class PingTask {
let persistence = Client.database.plain
self.state = .pending
log.debug("Starting to Ping \(server.identifier) with address: \(address.hostname)")
log.debug("Starting to Ping \(server.identifier) with address: \(address.ip)")
queue.async() { [weak self] in
......@@ -62,7 +62,7 @@ class PingTask {
return
}
let tcpAddress = Server.Address(hostname: address.hostname, port: 443)
let tcpAddress = Server.Address(hostname: address.ip, port: 443)
response = server.ping(toAddress: tcpAddress, withProtocol: .TCP)
DispatchQueue.main.async {
self?.parsePingResponse(response: response, withServer: server)
......
......@@ -61,7 +61,7 @@ class ServersPinger: DatabaseAccess {
log.verbose("Pinging \(server.identifier)")
for address in server.bestPingAddress() {
for address in server.addresses() {
let pingTask = PingTask(identifier: server.identifier, server: server, address: address, stateUpdateHandler: { (task) in
......
......@@ -30,10 +30,14 @@ class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
static let shared = VPNDaemon()
private(set) var hasEnabledUpdates: Bool
private var timer: Timer!
private var fallbackTimer: Timer!
private var numberOfAttempts: Int
private var isReconnecting: Bool
private init() {
hasEnabledUpdates = false
isReconnecting = false
numberOfAttempts = 0
}
func start() {
......@@ -62,30 +66,49 @@ class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
switch connection.status {
case .connected:
nextStatus = .connected
timer?.invalidate()
invalidateTimer()
reset()
case .connecting, .reasserting:
nextStatus = .connecting
nextStatus = .connecting
let previousStatus = accessedDatabase.transient.vpnStatus
if nextStatus != previousStatus {
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in
let status = NEVPNManager.shared().connection.status
if status == .invalid {
log.debug("NEVPNManager connection is invalid. Reconnecting...")
Client.providers.vpnProvider.reconnect(after: nil, forceDisconnect: true, { error in
timer.invalidate()
if fallbackTimer == nil {
fallbackTimer = Timer.scheduledTimer(withTimeInterval: Client.configuration.vpnConnectivityRetryDelay, repeats: true) { timer in
let address = Client.providers.serverProvider.targetServer.bestAddress()
address?.markServerAsUnavailable()
log.debug("NEVPNManager is still connecting. Reconnecting with a different server...")
self.numberOfAttempts += 1
if self.numberOfAttempts < Client.configuration.vpnConnectivityMaxAttempts {
self.updateUIWithAttemptNumber(self.numberOfAttempts)
self.isReconnecting = true
Client.providers.vpnProvider.reconnect(after: 0, { _ in
self.isReconnecting = false
})
} else {
log.debug("MAX number of VPN reconnections. Disconnecting...")
Client.providers.vpnProvider.disconnect({ error in
Macros.postNotification(.PIAVPNDidFail)
self.reset()
self.invalidateTimer()
})
}
}
}
case .disconnecting:
nextStatus = .disconnecting
case .disconnected:
nextStatus = .disconnected
if !isReconnecting {
invalidateTimer()
reset()
}
default:
nextStatus = .disconnected
}
......@@ -94,7 +117,10 @@ class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
guard (nextStatus != previousStatus) else {
return
}
accessedDatabase.transient.vpnStatus = nextStatus
if !isReconnecting {
accessedDatabase.transient.vpnStatus = nextStatus
}
if #available(iOS 12.0, *) {
if let error = connection.value(forKey: "_lastDisconnectError") {
......@@ -107,6 +133,27 @@ class VPNDaemon: Daemon, DatabaseAccess, ProvidersAccess {
}
// MARK: Invalidate
private func invalidateTimer() {
fallbackTimer?.invalidate()
fallbackTimer = nil
}
// MARK: Reset
private func reset() {
self.isReconnecting = false
self.numberOfAttempts = 0
self.updateUIWithAttemptNumber(0)
Client.providers.serverProvider.targetServer.addresses().forEach({$0.reset()})
}
// MARK: Update UI
private func updateUIWithAttemptNumber(_ number: Int) {
NotificationCenter.default.post(name: .PIADaemonsConnectingVPNStatus, object: number)
}
// MARK: Notifications
@objc private func neStatusDidChange(notification: Notification) {
......
......@@ -41,6 +41,8 @@ extension Notification.Name {
/// - Seealso: `Client.Daemons`
public static let PIADaemonsDidUpdateVPNStatus = Notification.Name("PIADaemonsDidUpdateVPNStatus")
public static let PIADaemonsConnectingVPNStatus = Notification.Name("PIADaemonsStillConnectingVPNStatus")
// MARK: Servers
/// The target server has been updated.
......
......@@ -178,7 +178,7 @@ public class IKEv2Profile: NetworkExtensionProfile {
}
let cfg = NEVPNProtocolIKEv2()
if let ip = configuration.server.bestAddressForIKEv2()?.ip {
if let ip = configuration.server.bestAddress()?.ip {
cfg.serverAddress = ip
} else {
cfg.serverAddress = configuration.server.hostname
......
......@@ -42,6 +42,7 @@ class GlossServer: GlossParser {
let dipToken: String? = "dipToken" <~~ json ?? nil
let geo: Bool = "geo" <~~ json ?? false
let offline: Bool = "offline" <~~ json ?? false
var meta: Server.ServerAddressIP?
if let metaServer: [String: Any] = "servers" <~~ json {
......@@ -116,8 +117,6 @@ class GlossServer: GlossParser {
name: name,
country: country,
hostname: hostname,
bestOpenVPNAddressForTCP: nil,
bestOpenVPNAddressForUDP: nil,
openVPNAddressesForTCP: ovpnTCPServerAddressIP,
openVPNAddressesForUDP: ovpnUDPServerAddressIP,
wireGuardAddressesForUDP: wgServerAddressIP,
......@@ -125,6 +124,7 @@ class GlossServer: GlossParser {
pingAddress: pingAddress,
responseTime: 0,
geo: geo,
offline: offline,
meta: meta,
dipToken: dipToken,
regionIdentifier: regionIdentifier
......@@ -158,13 +158,8 @@ extension Server: JSONEncodable {
"name" ~~> name,
"country" ~~> country,
"geo" ~~> geo,
"offline" ~~> offline,
"dns" ~~> hostname,
"openvpn_tcp" ~~> jsonify([
"best" ~~> bestOpenVPNAddressForTCP?.description
]),
"openvpn_udp" ~~> jsonify([
"best" ~~> bestOpenVPNAddressForUDP?.description
]),
"ping" ~~> pingAddress?.description,
"servers" ~~> jsonify([
"ovpnudp" ~~> ovpnTCPobj,
......
......@@ -245,7 +245,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
return
}
let dipRegion = Server(serial: firstServer.serial, name: firstServer.name, country: firstServer.country, hostname: firstServer.hostname, bestOpenVPNAddressForTCP: firstServer.bestOpenVPNAddressForTCP, bestOpenVPNAddressForUDP: firstServer.bestOpenVPNAddressForUDP, openVPNAddressesForTCP: [Server.ServerAddressIP(ip: ip, cn: cn)], openVPNAddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], wireGuardAddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], iKEv2AddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], pingAddress: firstServer.pingAddress, geo: false, meta: nil, dipExpire: Date(timeIntervalSince1970: TimeInterval(expirationTime)), dipToken: dipServer.dipToken, dipStatus: DedicatedIPStatus.active, regionIdentifier: firstServer.regionIdentifier)
let dipRegion = Server(serial: firstServer.serial, name: firstServer.name, country: firstServer.country, hostname: firstServer.hostname, openVPNAddressesForTCP: [Server.ServerAddressIP(ip: ip, cn: cn)], openVPNAddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], wireGuardAddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], iKEv2AddressesForUDP: [Server.ServerAddressIP(ip: ip, cn: cn)], pingAddress: firstServer.pingAddress, geo: false, meta: nil, dipExpire: Date(timeIntervalSince1970: TimeInterval(expirationTime)), dipToken: dipServer.dipToken, dipStatus: DedicatedIPStatus.active, regionIdentifier: firstServer.regionIdentifier)
dipRegions.append(dipRegion)
Client.database.secure.setDIPToken(dipServer.dipToken)
......
......@@ -40,8 +40,6 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
name: "France",
country: "fr",
hostname: "france.example.com",
bestOpenVPNAddressForTCP: nil,
bestOpenVPNAddressForUDP: nil,
pingAddress: nil,
responseTime: 0,
regionIdentifier: ""
......@@ -50,8 +48,6 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
name: "Germany",
country: "de",
hostname: "germany.example.com",
bestOpenVPNAddressForTCP: nil,
bestOpenVPNAddressForUDP: nil,
pingAddress: nil,
responseTime: 0,
regionIdentifier: ""
......@@ -60,8 +56,6 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
name: "Italy",
country: "it",
hostname: "italy.example.com",
bestOpenVPNAddressForTCP: nil,
bestOpenVPNAddressForUDP: nil,
pingAddress: nil,
responseTime: 0,
regionIdentifier: ""
......@@ -70,11 +64,19 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
name: "US East",
country: "us",
hostname: "us-east.example.com",
bestOpenVPNAddressForTCP: nil,
bestOpenVPNAddressForUDP: nil,
pingAddress: nil,
responseTime: 0,
regionIdentifier: ""
), Server(
serial: "8a55f03812851897f6e43b2ae22b1234",
name: "US East Offline",
country: "us",
hostname: "us-east.example.com",
pingAddress: nil,
responseTime: 0,
geo: true,
offline: true,
regionIdentifier: ""
)
]
......
......@@ -234,12 +234,12 @@ public class PIATunnelProfile: NetworkExtensionProfile {
if let piaCfg = customCfg as? OpenVPNTunnelProvider.Configuration {
var builder = piaCfg.builder()
if let protocols = builder.sessionConfiguration.endpointProtocols, protocols.contains(where: {$0.socketType == .tcp }) {
if let bestAddress = configuration.server.openVPNAddressesForTCP?.first?.ip {
if let bestAddress = configuration.server.bestAddressForOVPN(tcp: true)?.ip {
serverAddress = bestAddress
builder.resolvedAddresses = [bestAddress]
}
} else {
if let bestAddress = configuration.server.openVPNAddressesForUDP?.first?.ip {
if let bestAddress = configuration.server.bestAddressForOVPN(tcp: false)?.ip {
serverAddress = bestAddress
builder.resolvedAddresses = [bestAddress]
}
......
......@@ -238,8 +238,8 @@ public class PIAWGTunnelProfile: NetworkExtensionProfile {
var serverAddress = configuration.server.hostname
var serverCN = ""
if let ip = configuration.server.bestAddressForWireGuard()?.ip,
let cn = configuration.server.bestAddressForWireGuard()?.cn {
if let ip = configuration.server.bestAddress()?.ip,
let cn = configuration.server.bestAddress()?.cn {
serverAddress = ip
serverCN = cn
}
......@@ -253,7 +253,7 @@ public class PIAWGTunnelProfile: NetworkExtensionProfile {
let token = configuration.server.dipUsername() != nil ? configuration.server.dipUsername() : Client.providers.accountProvider.token
cfg.providerConfiguration = [PIAWireguardConfiguration.Keys.token: token,
PIAWireguardConfiguration.Keys.ping: configuration.server.bestPingAddress().first?.description,
PIAWireguardConfiguration.Keys.ping: configuration.server.bestAddress()?.description,
PIAWireguardConfiguration.Keys.serial: configuration.server.serial,
PIAWireguardConfiguration.Keys.cn: serverCN,
PIAWireguardConfiguration.Keys.useIP: true]
......
......@@ -62,10 +62,11 @@ class ServerTests: XCTestCase {
func testMockDownload() {
__testProviderDownload(factory: MockProviders())
}
// func testWebDownload() {
// __testProviderDownload(factory: Client.Providers())
// }
func testOfflineServers() {
__testProviderDownload(factory: MockProviders())
XCTAssertTrue(Client.providers.serverProvider.currentServers.filter({$0.offline == true}).count == 1)
}
private func __testProviderDownload(factory: Client.Providers) {
let exp = expectation(description: "download")
......@@ -86,13 +87,4 @@ class ServerTests: XCTestCase {
waitForExpectations(timeout: 5.0, handler: nil)
}
// func testSelection() {
// let cached = Client.providers.serverProvider.currentServers
// guard !cached.isEmpty else {
// return
// }
//
// let selected = cached[Int(arc4random()) % cached.count]
// Client.preferences.editable().preferredServer = selected
// }
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment