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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
PACKAGE_FOR_RELEASE: 1
CODESIGN_IDENTITY: ${{ vars.PIA_CODESIGN_IDENTITY }}
TEAM_ID: ${{ vars.PIA_APPLE_TEAM_ID }}
runs-on: macos-13
runs-on: macos-14
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15
xcode-version: '16.0'
- uses: actions/checkout@v3
- name: Install PIA's signing certificate
run: |
Expand Down
20 changes: 11 additions & 9 deletions ProxyExtension/IO/FlowProtocols.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import NetworkExtension
import Network

// Flow base protocol we use in place of NEAppProxyFlow, it also provides shortcuts for common functions.
protocol Flow {
Expand All @@ -25,18 +26,19 @@ extension Flow {
// Check if the address is an IPv6 address. IPv6 addresses always contain a ":"
// We can't do the opposite (such as just checking for "." for an IPv4 address) due to IPv4-mapped IPv6 addresses
// which are IPv6 addresses but include IPv4 address notation.
if let endpoint = flowTCP.remoteEndpoint as? NWHostEndpoint {
// We have a valid NWHostEndpoint - let's see if it's IPv6
if endpoint.hostname.contains(":") {
let endpoint: Network.NWEndpoint? = flowTCP.flowEndpoint
if endpoint != nil {
// We have a valid NWEndpoint - let's see if it's IPv6
if endpoint.host.contains(":") {
return true
}
}
} else if let flowUDP = self as? FlowUDP {
// Use localEndpoint for UDP flows as UDP (as a "connectionless protocol")
// doesn't have a fixed remoteEndpoint
if let endpoint = flowUDP.localEndpoint as? NWHostEndpoint {
// We have a valid NWHostEndpoint - let's see if it's IPv6
if endpoint.hostname.contains(":") {
// doesn't have a fixed flowEndpoint
if let endpoint = flowUDP.localEndpoint {
// We have a valid NWEndpoint - let's see if it's IPv6
if endpoint.host.contains(":") {
return true
}
}
Expand All @@ -51,13 +53,13 @@ extension Flow {
// FlowTCP and FlowUDP protocols abstract the relevant parts of NEAppProxyTCPFlow
// and NEAppProxyUDPFlow for increased flexibility and improved testability.
protocol FlowTCP: Flow {
var remoteEndpoint: NWEndpoint { get }
var flowEndpoint: Network.NWEndpoint { get }
func readData(completionHandler: @escaping (Data?, Error?) -> Void)
func write(_ data: Data, withCompletionHandler completionHandler: @escaping (Error?) -> Void)
}

protocol FlowUDP: Flow {
func readDatagrams(completionHandler: @escaping ([Data]?, [NWEndpoint]?, Error?) -> Void)
func writeDatagrams(_ datagrams: [Data], sentBy remoteEndpoints: [NWEndpoint], completionHandler: @escaping (Error?) -> Void)
var localEndpoint: NWEndpoint? { get }
var localEndpoint: Network.NWEndpoint? { get }
}
16 changes: 14 additions & 2 deletions ProxyExtension/IO/NEAppProxyFlow+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ extension NEAppProxyFlow: Flow {

// Open a flow
func openFlow(completionHandler: @escaping (Error?) -> Void) {
open(withLocalEndpoint: nil, completionHandler: completionHandler)
if #available(macOS 15.0, *) {
open(withLocalFlowEndpoint: nil, completionHandler: completionHandler)
} else {
open(withLocalEndpoint: nil, completionHandler: completionHandler)
}
}

// Flow metadata
Expand All @@ -25,5 +29,13 @@ extension NEAppProxyFlow: Flow {
// Enforce Flow protocol conformance for NEAppProxyTCPFlow and NEAppProxyUDPFlow subclasses.
// This approach allows using Flow protocols universally instead of specific NEAppProxyFlow classes,
// facilitating easier stubbing/mocking in tests as Flow protocols are simpler to satisfy.
extension NEAppProxyTCPFlow: FlowTCP {}
extension NEAppProxyTCPFlow: FlowTCP {
var flowEndpoint: NWEndpoint {
if #available(macOS 15.0, *) {
self.remoteFlowEndpoint
} else {
self.remoteEndpoint
}
}
}
extension NEAppProxyUDPFlow: FlowUDP {}
12 changes: 6 additions & 6 deletions ProxyExtension/IO/ProxySession/ChannelCreatorTCP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@ final class ChannelCreatorTCP {
self.config = config
}

public func create(_ onBytesReceived: @escaping (UInt64) -> Void)
public func create(_ onBytesReceived: @escaping (UInt64) -> Void)
-> EventLoopFuture<Channel> {
guard let endpoint = flow.remoteEndpoint as? NWHostEndpoint else {
guard let endpoint = flow.flowEndpoint as? NWEndpoint else {
return makeFailedFuture(
ProxySessionError.BadEndpoint("flow.remoteEndpoint is not an NWHostEndpoint"))
ProxySessionError.BadEndpoint("flow.flowEndpoint is not an NWEndpoint"))
}

let bootstrap = ClientBootstrap(group: config.eventLoopGroup)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.channelInitializer { channel in
let inboundHandler = InboundHandlerTCP(flow: self.flow, id: self.id,
let inboundHandler = InboundHandlerTCP(flow: self.flow, id: self.id,
onBytesReceived: onBytesReceived)
return channel.pipeline.addHandler(inboundHandler)
}

return bindSourceAddressAndConnect(bootstrap, endpoint: endpoint)
}

private func bindSourceAddressAndConnect(_ bootstrap: ClientBootstrap, endpoint: NWHostEndpoint)
private func bindSourceAddressAndConnect(_ bootstrap: ClientBootstrap, endpoint: NWEndpoint)
-> EventLoopFuture<Channel> {
do {

// Determine the appropriate IP address based on
// Determine the appropriate IP address based on
// whether the flow is IPv4 or IPv6
// For IPv4 flows we want to bind to the "bind ip" but for IPv6 flows
// we want to bind to the IPv6 wildcard address "::" (just out of paranoia, probably do not need to do this).
Expand Down
10 changes: 5 additions & 5 deletions ProxyExtension/IO/ProxySession/ChannelCreatorUDP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@ final class ChannelCreatorUDP {
self.config = config
}

public func create(_ onBytesReceived: @escaping (UInt64) -> Void)
public func create(_ onBytesReceived: @escaping (UInt64) -> Void)
-> EventLoopFuture<Channel> {

let bootstrap = DatagramBootstrap(group: config.eventLoopGroup)
.channelInitializer { channel in
let inboundHandler = InboundHandlerUDP(flow: self.flow, id: self.id,
let inboundHandler = InboundHandlerUDP(flow: self.flow, id: self.id,
onBytesReceived: onBytesReceived)
return channel.pipeline.addHandler(inboundHandler)
}

return bindSourceAddress(bootstrap)
}

private func bindSourceAddress(_ bootstrap: DatagramBootstrap)
private func bindSourceAddress(_ bootstrap: DatagramBootstrap)
-> EventLoopFuture<Channel> {
do {

var localEndpoint: String
// Used by IPv6
if let endpoint = flow.localEndpoint as? NWHostEndpoint {
if let endpoint = flow.localEndpoint as? NWEndpoint {
localEndpoint = endpoint.hostname
} else {
log(.warning, "id: \(self.id) Could not convert flow.localEndpoint to NWHostEndpoint, defaulting to :: for ipv6 flows")
log(.warning, "id: \(self.id) Could not convert flow.localEndpoint to NWEndpoint, defaulting to :: for ipv6 flows")
localEndpoint = "::"
}

Expand Down
10 changes: 5 additions & 5 deletions ProxyExtension/IO/ProxySession/FlowForwarderUDP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ final class FlowForwarderUDP {

public func scheduleFlowRead(_ onBytesTransmitted: @escaping ByteCountFunc) {
flow.readDatagrams { outboundData, outboundEndpoints, flowError in
if flowError == nil, let datas = outboundData,
if flowError == nil, let datas = outboundData,
!datas.isEmpty, let endpoints = outboundEndpoints, !endpoints.isEmpty {
self.forwardToChannel(datas: datas, endpoints: endpoints,
self.forwardToChannel(datas: datas, endpoints: endpoints,
onBytesTransmitted: onBytesTransmitted)
} else {
self.handleReadError(error: flowError)
Expand All @@ -47,8 +47,8 @@ final class FlowForwarderUDP {

private func createDatagram(data: Data, endpoint: NWEndpoint)
-> AddressedEnvelope<ByteBuffer>? {
guard let endpoint = endpoint as? NWHostEndpoint else {
log(.error, "id: \(self.id) datagram creation failed - NWEndpoint is not an NWHostEndpoint")
guard let endpoint = endpoint as? NWEndpoint else {
log(.error, "id: \(self.id) datagram creation failed - NWEndpoint is not an NWEndpoint")
return nil
}
let buffer = channel.allocator.buffer(bytes: data)
Expand All @@ -61,7 +61,7 @@ final class FlowForwarderUDP {
}
}

private func forwardToChannel(datas: [Data], endpoints: [NWEndpoint],
private func forwardToChannel(datas: [Data], endpoints: [NWEndpoint],
onBytesTransmitted: @escaping ByteCountFunc) {
var readIsScheduled = false
for (data, endpoint) in zip(datas, endpoints) {
Expand Down
8 changes: 4 additions & 4 deletions ProxyExtension/IO/ProxySession/InboundHandlerUDP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ final class InboundHandlerUDP: InboundHandler {
return
}

let endpoint = NWHostEndpoint(hostname: input.remoteAddress.ipAddress!,
let endpoint = NWEndpoint.hostPort(host: input.remoteAddress.ipAddress!,
port: String(input.remoteAddress.port!))

forwardToFlow(context: context, data: Data(bytes), endpoint: endpoint,
forwardToFlow(context: context, data: Data(bytes), endpoint: endpoint,
onBytesReceived: onBytesReceived)
}

Expand All @@ -46,8 +46,8 @@ final class InboundHandlerUDP: InboundHandler {
}
}

private func forwardToFlow(context: ChannelHandlerContext, data: Data,
endpoint: NWHostEndpoint, onBytesReceived: @escaping ByteCountFunc) {
private func forwardToFlow(context: ChannelHandlerContext, data: Data,
endpoint: NWEndpoint, onBytesReceived: @escaping ByteCountFunc) {
// new traffic is ready to be read on the socket
// we want to write that data to the flow
flow.writeDatagrams([data], sentBy: [endpoint]) { flowError in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct SplitTunnelNetworkConfig {
private func subnetRule(subnet: String?, prefix: Int) -> NENetworkRule {
return NENetworkRule(
// port "0" means any port
remoteNetwork: subnet != nil ? NWHostEndpoint(hostname: subnet!, port: "0") : nil,
remoteNetwork: subnet != nil ? NWEndpoint.hostPort(host: subnet!, port: "0") : nil,
remotePrefix: prefix,
localNetwork: nil,
localPrefix: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import NetworkExtension

// NETransparentProxyProvider is a subclass of NEAppProxyProvider.
// The behaviour is different compared to its super class:
// - Returning NO from handleNewFlow: and handleNewUDPFlow:initialRemoteEndpoint:
// - Returning NO from handleNewFlow:
// causes the flow to go to through the default system routing,
// instead of being closed with a "Connection Refused" error.
// - NEDNSSettings and NEProxySettings specified in NETransparentProxyNetworkSettings are ignored.
Expand Down Expand Up @@ -72,7 +72,7 @@ final class SplitTunnelProxyProvider : NETransparentProxyProvider {
override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
return engine.handleNewFlow(flow)
}

override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
engine.handleAppMessage(messageData, completionHandler: completionHandler)
}
Expand Down
16 changes: 8 additions & 8 deletions ProxyTests/FlowPolicyTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class FlowPolicySpec: QuickSpec {

it("ignores Ipv6 bypass flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.curl"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -24,7 +24,7 @@ class FlowPolicySpec: QuickSpec {

it("ignores Ipv4 bypass flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "1.1.1.1", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "1.1.1.1", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.curl"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -38,7 +38,7 @@ class FlowPolicySpec: QuickSpec {

it("proxies Ipv6 bypass flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.curl"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -48,7 +48,7 @@ class FlowPolicySpec: QuickSpec {

it("proxies Ipv4 bypass flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "1.1.1.1", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "1.1.1.1", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.curl"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -62,7 +62,7 @@ class FlowPolicySpec: QuickSpec {

it("blocks Ipv4 vpnOnly flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "1.1.1.1", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "1.1.1.1", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.safari"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -72,7 +72,7 @@ class FlowPolicySpec: QuickSpec {

it("blocks Ipv6 vpnOnly flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.safari"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -86,7 +86,7 @@ class FlowPolicySpec: QuickSpec {

it("proxies Ipv4 vpnOnly flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "1.1.1.1", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "1.1.1.1", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.safari"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand All @@ -96,7 +96,7 @@ class FlowPolicySpec: QuickSpec {

it("blocks Ipv6 vpnOnly flows") {
let mockFlow = MockFlowTCP()
mockFlow.remoteEndpoint = NWHostEndpoint(hostname: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.flowEndpoint = NWEndpoint.hostPort(host: "2b17:fb8c:8b61:8f15:28b7:d783:0448:81a3", port: "1337")
mockFlow.sourceAppSigningIdentifier = "com.apple.safari"

let policy = FlowPolicy.policyFor(flow: mockFlow, vpnState: vpnState)
Expand Down
2 changes: 1 addition & 1 deletion ProxyTests/InboundHandlerUDPTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class InboundHandlerUDPTest: QuickSpec {

// Note these are arrays for UDP
let expectedData = [Data(buffer.readableBytesView)]
let expectedEndpoints = [NWHostEndpoint(hostname: host, port: String(port))]
let expectedEndpoints = [NWEndpoint.hostPort(host: host, port: String(port))]
// UDP also requires an endpoint
let endpoint = try SocketAddress(ipAddress: host, port: port)
let envelope = AddressedEnvelope<ByteBuffer>(remoteAddress: endpoint, data: buffer)
Expand Down
2 changes: 1 addition & 1 deletion ProxyTests/Mocks/MockFlowTCP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class MockFlowTCP: FlowTCP, Equatable, Mock {
public var sourceAppAuditToken: Data? = nil

// Required by FlowTCP
public var remoteEndpoint: NWEndpoint = NWHostEndpoint(hostname: "8.8.8.8", port: "1337")
public var flowEndpoint: NWEndpoint = NWEndpoint.hostPort(host: "8.8.8.8", port: "1337")

// Reads from our flow
// Unlike the real readData on NEAppProxyTCPFlow this does not dispatch the
Expand Down
2 changes: 1 addition & 1 deletion ProxyTests/Mocks/MockFlowUDP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ final class MockFlowUDP: FlowUDP, Equatable, Mock {
completionHandler(flowError)
}

var localEndpoint: NWEndpoint? = NWHostEndpoint(hostname: "0.0.0.0", port: "0")
var localEndpoint: NWEndpoint? = NWEndpoint.hostPort(host: "0.0.0.0", port: "0")
}
2 changes: 1 addition & 1 deletion ProxyTests/ProxySessionUDPTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ProxySessionUDPTest: QuickSpec {
// The proxy works by reading from the flow and then writing to the channel
// so a successful read from the flow should result in a corresponding write to the channel
it("should write to the channel") {
let endpoint = NWHostEndpoint(hostname: "8.8.8.8", port: "1337")
let endpoint = NWEndpoint.hostPort(host: "8.8.8.8", port: "1337")

// A flow read will succeed when there's data available (so this one should succeed)
let mockFlow = MockFlowUDP(data: [Data([0x1])], endpoints: [endpoint] )
Expand Down
Loading