import Foundation /// Representing all the information in one connect session. public final class ConnectSession { public enum EventSourceEnum { case proxy, adapter, tunnel } /// The requested host. /// /// This is the host received in the request. May be a domain, a real IP or a fake IP. public let requestedHost: String /// The real host for this session. /// /// If the session is initailized with a host domain, then `host == requestedHost`. /// Otherwise, the requested IP address is looked up in the DNS server to see if it corresponds to a domain if `fakeIPEnabled` is `true`. /// Unless there is a good reason not to, any socket shoule connect based on this directly. public var host: String /// The requested port. public let port: Int /// The rule to use to connect to remote. public var matchedRule: Rule? /// Whether If the `requestedHost` is an IP address. public let fakeIPEnabled: Bool public var error: Error? public var errorSource: EventSourceEnum? public var disconnectedBy: EventSourceEnum? /// The resolved IP address. /// /// - note: This will always be real IP address. public lazy var ipAddress: String = { [unowned self] in if self.isIP() { return self.host } else { let ip = Utils.DNS.resolve(self.host) guard self.fakeIPEnabled else { return ip } guard let dnsServer = DNSServer.currentServer else { return ip } guard let address = IPAddress(fromString: ip) else { return ip } guard dnsServer.isFakeIP(address) else { return ip } guard let session = dnsServer.lookupFakeIP(address) else { return ip } return session.realIP?.presentation ?? "" } }() /// The location of the host. // public lazy var country: String = { // [unowned self] in // guard let c = Utils.GeoIPLookup.Lookup(self.ipAddress) else { // return "" // } // return c // }() public init?(host: String, port: Int, fakeIPEnabled: Bool = true) { self.requestedHost = host self.port = port self.fakeIPEnabled = fakeIPEnabled self.host = host if fakeIPEnabled { guard lookupRealIP() else { return nil } } } public convenience init?(ipAddress: IPAddress, port: Port, fakeIPEnabled: Bool = true) { self.init(host: ipAddress.presentation, port: Int(port.value), fakeIPEnabled: fakeIPEnabled) } func disconnected(becauseOf error: Error? = nil, by source: EventSourceEnum) { if disconnectedBy == nil { self.error = error if error != nil { errorSource = source } disconnectedBy = source } } fileprivate func lookupRealIP() -> Bool { /// If custom DNS server is set up. guard let dnsServer = DNSServer.currentServer else { return true } // Only IPv4 is supported as of now. guard isIPv4() else { return true } let address = IPAddress(fromString: requestedHost)! guard dnsServer.isFakeIP(address) else { return true } // Look up fake IP reversely should never fail. guard let session = dnsServer.lookupFakeIP(address) else { return false } host = session.requestMessage.queries[0].name ipAddress = session.realIP?.presentation ?? "" matchedRule = session.matchedRule // if session.countryCode != nil { // country = session.countryCode! // } return true } public func isIPv4() -> Bool { return Utils.IP.isIPv4(host) } public func isIPv6() -> Bool { return Utils.IP.isIPv6(host) } public func isIP() -> Bool { return isIPv4() || isIPv6() } } extension ConnectSession: CustomStringConvertible { public var description: String { if requestedHost != host { return "<\(type(of: self)) host:\(host) port:\(port) requestedHost:\(requestedHost)>" } else { return "<\(type(of: self)) host:\(host) port:\(port)>" } } }