Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions ProxyApp/ProxyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,6 @@ protocol ProxyApp {
// After the stop procedure is completed, the extension process is killed
func stopProxy() -> Bool

// Pass an array of strings, containing all the bundle ID names of the apps
// that will be managed by the Proxy.
//
// To get the bundle ID of an app, knowing its name use this command:
// `osascript -e 'id of app "Google Chrome"'`
func setBypassApps(apps: [String]) -> Void

func startDNSProxy() -> Bool
func stopDNSProxy() -> Bool
}
46 changes: 20 additions & 26 deletions ProxyApp/ProxyAppDefault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,28 @@ class ProxyAppDefault : ProxyApp {
var proxyManager: NETransparentProxyManager?
var proxyDNSManager: NEDNSProxyManager?
var extensionRequestDelegate = ExtensionRequestDelegate()
var bypassApps: [String] = []
var vpnOnlyApps: [String] = []
var networkInterface: String = ""
var options: [String : Any]
static let proxyManagerName = "PIA Split Tunnel Proxy"
static let proxyDNSManagerName = "PIA DNS Split Tunnel Proxy"
static let serverAddress = "127.0.0.1"

func setBypassApps(apps: [String]) -> Void {
self.bypassApps = apps
}

func setVpnOnlyApps(apps: [String]) -> Void {
self.vpnOnlyApps = apps
}

func setNetworkInterface(interface: String) -> Void {
self.networkInterface = interface

init() {
options = [
// To get the bundle ID of an app, knowing its name use this command:
// `osascript -e 'id of app "Google Chrome"'`
"bypassApps" : ["com.apple.nslookup", "com.apple.curl", "com.apple.ping"],
"vpnOnlyApps" : [],
"bindInterface" : "en0",
"serverAddress" : ProxyAppDefault.serverAddress,
"logFile" : "/tmp/STProxy.log",
"logLevel" : "debug",
// split tunnel is in normal mode, vpn device is the default route
"routeVpn" : true,
// vpn is connected
"isConnected" : true,
// The name of the unix group PIA whitelists in the firewall
"whitelistGroupName" : "piavpn"
]
}

// MARK: TRANSPARENT PROXY MANAGER FUNCTIONS
Expand Down Expand Up @@ -140,18 +145,7 @@ class ProxyAppDefault : ProxyApp {
do {
// This function is used to start the tunnel (the proxy)
// passing it the following settings
try session.startTunnel(options: [
"bypassApps" : self.bypassApps,
"vpnOnlyApps" : self.vpnOnlyApps,
"bindInterface" : self.networkInterface,
"serverAddress" : ProxyAppDefault.serverAddress,
"logFile" : "/tmp/STProxy.log",
"logLevel" : "debug",
"routeVpn" : true,
"isConnected" : true,
// The name of the unix group PIA whitelists in the firewall
"whitelistGroupName" : "piavpn"
] as [String : Any])
try session.startTunnel(options: self.options as [String : Any])
} catch {
os_log("startProxy error!")
print(error)
Expand Down
113 changes: 113 additions & 0 deletions ProxyExtension/IO/DNS/DnsFlowHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Foundation
import NetworkExtension
import NIO

// Responsible for handling DNS flows, both new flows and pre-existing
final class DnsFlowHandler: FlowHandlerProtocol {
let eventLoopGroup: MultiThreadedEventLoopGroup
var idGenerator: IDGenerator

// explicitly set these for tests
var proxySessionFactory: ProxySessionFactory
var networkInterfaceFactory: NetworkInterfaceFactory

init() {
self.idGenerator = IDGenerator()
self.proxySessionFactory = DefaultProxySessionFactory()
self.networkInterfaceFactory = DefaultNetworkInterfaceFactory()
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
}

deinit {
try! eventLoopGroup.syncShutdownGracefully()
}

public func handleNewFlow(_ flow: Flow, vpnState: VpnState) -> Bool {
// We need to handle two modes here:
// - Follow App Rules
// - VPN DNS Only
if(vpnState.dnsFollowAppRules) {
// TODO: Fetch these from vpnState. PIA-1942
// These are the backed up original ISP DNS servers.
let ispDnsServers: [String] = ["1.1.1.1"]
// This will probably be just the PIA server we are connected to
let piaDnsServers: [String] = ["8.8.8.8"]
switch dnsPolicyFor(flow: flow, vpnState: vpnState) {
case .proxyToPhysical:
return startProxySession(flow: flow, vpnState: vpnState, dnsServers: ispDnsServers)
case .proxyToVpn:
return startProxySession(flow: flow, vpnState: vpnState, dnsServers: piaDnsServers)
case .block:
return false
}
} else {
// currently not handling any DNS requests if mode is VPN DNS Only
return false
}
}

// The DNS policy we should apply to an app
enum DnsPolicy {
case proxyToPhysical, proxyToVpn, block
}

// The policy logic is easier compared to the transparent proxy one.
// bypass mode: always proxy to physical.
// only vpn mode: always proxy to vpn. Block if the vpn is disconnected
// unspecified mode: always proxy to the default route interface
private func dnsPolicyFor(flow: Flow, vpnState: VpnState) -> DnsPolicy {
let mode = FlowPolicy.modeFor(flow: flow, vpnState: vpnState)
if(mode == AppPolicy.Mode.bypass) {
return .proxyToPhysical
} else if(mode == AppPolicy.Mode.vpnOnly) {
if(vpnState.isConnected) {
return .proxyToVpn
} else {
return .block
}
} else { // unspecified case, it means the app has no specific settings
if(vpnState.routeVpn) { // normal ST mode, vpn has the default route
return .proxyToVpn
} else {
return .proxyToPhysical
}
}
}

// Instead of using the original endpoint as in the transparent proxy,
// we force a specific server for the DNS request, based on
private func startProxySession(flow: Flow, vpnState: VpnState, dnsServers: [String]) -> Bool {
let interface = networkInterfaceFactory.create(interfaceName: vpnState.bindInterface)

// Verify we have a valid bindIp - if not, trace it and ignore the flow
guard let bindIp = interface.ip4() else {
log(.error, "Cannot find ipv4 ip for interface: \(interface.interfaceName)" +
" - ignoring matched flow: \(flow.sourceAppSigningIdentifier)")
// TODO: Should block the flow instead - especially for vpnOnly flows?
return false
}

let sessionConfig = SessionConfig(bindIp: bindIp, eventLoopGroup: eventLoopGroup)

flow.openFlow { error in
guard error == nil else {
log(.error, "\(flow.sourceAppSigningIdentifier) \"\(error!.localizedDescription)\" in \(String(describing: flow.self)) open()")
return
}
self.handleFlowIO(flow, sessionConfig: sessionConfig)
}
return true
}

// Fire off a proxy session for each new flow
func handleFlowIO(_ flow: Flow, sessionConfig: SessionConfig) {
let nextId = idGenerator.generate()
if let tcpFlow = flow as? FlowTCP {
let tcpSession = proxySessionFactory.createTCP(flow: tcpFlow, config: sessionConfig, id: nextId)
tcpSession.start()
} else if let udpFlow = flow as? FlowUDP {
let udpSession = proxySessionFactory.createUDP(flow: udpFlow, config: sessionConfig, id: nextId)
udpSession.start()
}
}
}
4 changes: 1 addition & 3 deletions ProxyExtension/IO/FlowHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ struct SessionConfig {

protocol FlowHandlerProtocol {
func handleNewFlow(_ flow: Flow, vpnState: VpnState) -> Bool
func startProxySession(flow: Flow, vpnState: VpnState) -> Bool
}

// Responsible for handling flows, both new flows and pre-existing
Expand Down Expand Up @@ -51,8 +50,7 @@ final class FlowHandler: FlowHandlerProtocol {
}
}

// temporarly public
public func startProxySession(flow: Flow, vpnState: VpnState) -> Bool {
private func startProxySession(flow: Flow, vpnState: VpnState) -> Bool {
let interface = networkInterfaceFactory.create(interfaceName: vpnState.bindInterface)

// Verify we have a valid bindIp - if not, trace it and ignore the flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,69 @@ import Foundation
import NetworkExtension

final class SplitTunnelDNSProxyProvider : NEDNSProxyProvider {
public var flowHandler: FlowHandlerProtocol!
public var vpnState: VpnState!

// The engine
public var engine: ProxyEngineProtocol!

// The logger
public var logger: LoggerProtocol!
override func startProxy(options:[String: Any]? = nil, completionHandler: @escaping (Error?) -> Void) {

override func startProxy(options: [String: Any]? , completionHandler: @escaping (Error?) -> Void) {
let logLevel: String = options?["logLevel"] as? String ?? ""
let logFile: String = options?["logFile"] as? String ?? ""

self.logger = self.logger ?? Logger.instance

// Ensure the logger is initialized first
// Ensure the logger is initialized first.
// May be redundant
logger.updateLogger(logLevel: logLevel, logFile: logFile)

// init just once, set up swiftNIO event loop
self.flowHandler = FlowHandler()

var options = [
"bypassApps" : ["/usr/bin/curl", "org.mozilla.firefox"],

// assume the option array is passed when the DNS proxy is started
// or it's shared with the transparent proxy
let _options = [
"bypassApps" : ["com.apple.nslookup", "com.apple.curl", "com.apple.ping"],
"vpnOnlyApps" : [],
"bindInterface" : "en0",
"serverAddress" : "127.0.0.1",
// do we want to use the same log file or a different one?
"logFile" : "/tmp/STProxy.log",
"logLevel" : "debug",
"routeVpn" : true,
"isConnected" : true,
"dnsFollowAppRules": true,
"whitelistGroupName" : "piavpn"
] as [String : Any]
guard let vpnState2 = VpnStateFactory.create(options: options) else {
guard let vpnState = VpnStateFactory.create(options: _options) else {
log(.error, "provided incorrect list of options. They might be missing or an incorrect type")
return
}
vpnState = vpnState2

completionHandler(nil)
}

override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
// Right now we do not share the engine with the transparent proxy.
// This means we will have 2 SwiftNIO event loops and that both proxies are indipendent of eachother.
// This can be refactored later if the DNS proxy never runs when the transparent proxy is off
self.engine = self.engine ?? ProxyEngine(vpnState: vpnState, flowHandler: DnsFlowHandler())

// Whitelist this process in the firewall - error logging happens in function.
// May be redundant
guard FirewallWhitelister(groupName: vpnState.whitelistGroupName).whitelist() else {
return
}

completionHandler(nil)
log(.info, "DNS Proxy started!")
}

// Be aware that by returning false in NEDNSProxyProvider handleNewFlow(),
// the flow is discarded and the connection is closed.
// This is similar to how NEAppProxyProvider works, compared to what we use
// for traffic Split Tunnel which is NETransparentProxyProvider.
// This means that we need to handle ALL DNS requests when DNS Split Tunnel
// is enabled, even for non-managed apps.
// This means that we need to handle the DNS requests of ALL apps
// when DNS Split Tunnel is enabled
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
var appName = flow.sourceAppSigningIdentifier
if appName == "com.apple.nslookup" || appName == "com.apple.curl" {
flowHandler.startProxySession(flow: flow, vpnState: vpnState)
return true
} else {
return false
}
return engine.handleNewFlow(flow)
}

override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
log(.info, "DNS Proxy stopped!")
}
}
4 changes: 2 additions & 2 deletions ProxyExtension/SplitTunnelProxyProvider/ProxyEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ final class ProxyEngine: ProxyEngineProtocol {
public var flowHandler: FlowHandlerProtocol
public var messageHandler: MessageHandlerProtocol

init(vpnState: VpnState) {
init(vpnState: VpnState, flowHandler: FlowHandlerProtocol) {
self.vpnState = vpnState
self.flowHandler = FlowHandler()
self.flowHandler = flowHandler
self.messageHandler = MessageHandler()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class SplitTunnelProxyProvider : NETransparentProxyProvider {
return
}

self.engine = self.engine ?? ProxyEngine(vpnState: vpnState)
self.engine = self.engine ?? ProxyEngine(vpnState: vpnState, flowHandler: FlowHandler())

// Whitelist this process in the firewall - error logging happens in function
guard FirewallWhitelister(groupName: vpnState.whitelistGroupName).whitelist() else {
Expand All @@ -53,7 +53,7 @@ final class SplitTunnelProxyProvider : NETransparentProxyProvider {
SplitTunnelNetworkConfig(serverAddress: vpnState.serverAddress,
provider: self).apply(completionHandler)

log(.info, "Proxy started!")
log(.info, "Transparent Proxy started!")
}

// MARK: Managing flows
Expand All @@ -74,6 +74,6 @@ final class SplitTunnelProxyProvider : NETransparentProxyProvider {
}

override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
log(.info, "Proxy stopped!")
log(.info, "Transparent Proxy stopped!")
}
}
8 changes: 8 additions & 0 deletions ProxyExtension/SplitTunnelProxyProvider/VpnState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ struct VpnStateFactory {
}
vpnState.isConnected = isConnected
log(.info, "isConnected: \(isConnected)")

guard let dnsFollowAppRules = options!["dnsFollowAppRules"] as? Bool else {
log(.error, "Error: Cannot find dnsFollowAppRules in options")
return nil
}
vpnState.dnsFollowAppRules = dnsFollowAppRules
log(.info, "dnsFollowAppRules: \(dnsFollowAppRules)")

guard let whitelistGroupName = options!["whitelistGroupName"] as? String else {
log(.error, "Error: Cannot find whitelistGroupName in options")
Expand All @@ -78,5 +85,6 @@ struct VpnState: Equatable {
var serverAddress: String = ""
var routeVpn: Bool = false
var isConnected: Bool = false
var dnsFollowAppRules: Bool = false
var whitelistGroupName: String = ""
}
2 changes: 2 additions & 0 deletions ProxyTests/MessageHandlerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class MessageHandlerTest: QuickSpec {
"serverAddress": "1.1.1.1",
"routeVpn": true,
"isConnected": true,
"dnsFollowAppRules": true,
"whitelistGroupName": "acmevpn"
]

Expand All @@ -54,6 +55,7 @@ final class MessageHandlerTest: QuickSpec {
expect(vpnState.serverAddress).to(equal("1.1.1.1"))
expect(vpnState.routeVpn).to(equal(true))
expect(vpnState.isConnected).to(equal(true))
expect(vpnState.dnsFollowAppRules).to(equal(true))
expect(vpnState.whitelistGroupName).to(equal("acmevpn"))
}

Expand Down
Loading