Skip to content

Commit

Permalink
Add shadowsocks obfuscation as an option
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Nov 13, 2024
1 parent e763bf5 commit 26b75ba
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import MullvadRustRuntimeProxy
import MullvadTypes
import Network

public enum TunnelObfuscationProtocol {
case udpOverTcp
case shadowsocks
}

public protocol TunnelObfuscation {
init(remoteAddress: IPAddress, tcpPort: UInt16)
init(remoteAddress: IPAddress, tcpPort: UInt16, obfuscationProtocol: TunnelObfuscationProtocol)
func start()
func stop()
var localUdpPort: UInt16 { get }
Expand All @@ -21,11 +26,15 @@ public protocol TunnelObfuscation {
var transportLayer: TransportLayer { get }
}

/// Class that implements UDP over TCP obfuscation by accepting traffic on a local UDP port and proxying it over TCP to the remote endpoint.
public final class UDPOverTCPObfuscator: TunnelObfuscation {
/// Class that implements obfuscation by accepting traffic on a local port and proxying it to the remote endpoint.
///
/// The obfuscation happens either by wrapping UDP traffic into TCP traffic, or by using a local shadowsocks server
/// to encrypt the UDP traffic sent.
public final class TunnelObfuscator: TunnelObfuscation {
private let stateLock = NSLock()
private let remoteAddress: IPAddress
internal let tcpPort: UInt16
internal let obfuscationProtocol: TunnelObfuscationProtocol

private var proxyHandle = ProxyHandle(context: nil, port: 0)
private var isStarted = false
Expand All @@ -38,12 +47,20 @@ public final class UDPOverTCPObfuscator: TunnelObfuscation {

public var remotePort: UInt16 { tcpPort }

public var transportLayer: TransportLayer { .tcp }
public var transportLayer: TransportLayer {
switch obfuscationProtocol {
case .udpOverTcp:
.tcp
case .shadowsocks:
.udp
}
}

/// Initialize tunnel obfuscator with remote server address and TCP port where udp2tcp is running.
public init(remoteAddress: IPAddress, tcpPort: UInt16) {
public init(remoteAddress: IPAddress, tcpPort: UInt16, obfuscationProtocol: TunnelObfuscationProtocol) {
self.remoteAddress = remoteAddress
self.tcpPort = tcpPort
self.obfuscationProtocol = obfuscationProtocol
}

deinit {
Expand All @@ -54,13 +71,19 @@ public final class UDPOverTCPObfuscator: TunnelObfuscation {
stateLock.withLock {
guard !isStarted else { return }

let obfuscationProtocol = switch obfuscationProtocol {
case .udpOverTcp: TunnelObfuscatorProtocol(0)
case .shadowsocks: TunnelObfuscatorProtocol(1)
}

let result = withUnsafeMutablePointer(to: &proxyHandle) { proxyHandlePointer in
let addressData = remoteAddress.rawValue

return start_tunnel_obfuscator_proxy(
addressData.map { $0 },
UInt(addressData.count),
tcpPort,
obfuscationProtocol,
proxyHandlePointer
)
}
Expand Down
10 changes: 10 additions & 0 deletions ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
#include <stdint.h>
#include <stdlib.h>

/**
* SAFETY: `TunnelObfuscatorProtocol` values must either be `0` or `1`
*/
enum TunnelObfuscatorProtocol {
UdpOverTcp = 0,
Shadowsocks,
};
typedef uint8_t TunnelObfuscatorProtocol;

/**
* A thin wrapper around [`mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState`] that
* can start a local forwarder (see [`Self::start`]).
Expand Down Expand Up @@ -179,6 +188,7 @@ int32_t stop_shadowsocks_proxy(struct ProxyHandle *proxy_config);
int32_t start_tunnel_obfuscator_proxy(const uint8_t *peer_address,
uintptr_t peer_address_len,
uint16_t peer_port,
TunnelObfuscatorProtocol obfuscation_protocol,
struct ProxyHandle *proxy_handle);

int32_t stop_tunnel_obfuscator_proxy(struct ProxyHandle *proxy_handle);
6 changes: 4 additions & 2 deletions ios/MullvadRustRuntimeTests/TCPConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import Network

/// Minimal implementation of TCP connection capable of receiving data.
/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
class TCPConnection {
class TCPConnection: Connection {
private let dispatchQueue = DispatchQueue(label: "TCPConnection")
private let nwConnection: NWConnection

init(nwConnection: NWConnection) {
required init(nwConnection: NWConnection) {
self.nwConnection = nwConnection
}

static var connectionParameters: NWParameters { .tcp }

deinit {
cancel()
}
Expand Down
48 changes: 45 additions & 3 deletions ios/MullvadRustRuntimeTests/TunnelObfuscationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import Network
import XCTest

final class TunnelObfuscationTests: XCTestCase {
func testRunningObfuscatorProxy() async throws {
func testRunningUdpOverTcpObfuscatorProxy() async throws {
// Each packet is prefixed with u16 that contains a payload length.
let preambleLength = MemoryLayout<UInt16>.size
let markerData = Data([109, 117, 108, 108, 118, 97, 100])
let packetLength = markerData.count + preambleLength

let tcpListener = try TCPUnsafeListener()
let tcpListener = try UnsafeListener<TCPConnection>()
try await tcpListener.start()

let obfuscator = UDPOverTCPObfuscator(remoteAddress: IPv4Address.loopback, tcpPort: tcpListener.listenPort)
let obfuscator = TunnelObfuscator(
remoteAddress: IPv4Address.loopback,
tcpPort: tcpListener.listenPort,
obfuscationProtocol: .udpOverTcp
)
obfuscator.start()

// Accept incoming connections
Expand All @@ -46,4 +50,42 @@ final class TunnelObfuscationTests: XCTestCase {
XCTAssert(receivedData.count == packetLength)
XCTAssertEqual(receivedData[preambleLength...], markerData)
}

func testRunningShadowsocksObfuscatorProxy() async throws {
let markerData = Data([109, 117, 108, 108, 118, 97, 100])

let localUdpListener = try UnsafeListener<UDPConnection>()
try await localUdpListener.start()

let localObfuscator = TunnelObfuscator(
remoteAddress: IPv4Address.loopback,
tcpPort: localUdpListener.listenPort,
obfuscationProtocol: .shadowsocks
)
localObfuscator.start()

// Accept incoming connections
let localConnectionDataTask = Task {
for await obfuscatedConnection in localUdpListener.newConnections {
try await obfuscatedConnection.start()

let readDatagram = try await obfuscatedConnection.readSingleDatagram()
// Write into the connection the unencrypted payload that was just read
try await obfuscatedConnection.sendData(readDatagram)
return readDatagram
}
throw POSIXError(.ECANCELED)
}

// Send marker data over UDP
let connection = UDPConnection(remote: IPv4Address.loopback, port: localObfuscator.localUdpPort)
try await connection.start()
try await connection.sendData(markerData)
let readDataFromObfuscator = try await connection.readSingleDatagram()

// As the connection data is encrypted and this test does not run a shadowsocks server to decrypt the payload
// The connection from the local UDP listener writes back what it read from the obfuscator, unencrypted
_ = try await localConnectionDataTask.value
XCTAssertEqual(readDataFromObfuscator, markerData)
}
}
35 changes: 31 additions & 4 deletions ios/MullvadRustRuntimeTests/UDPConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,31 @@
import Foundation
import Network

protocol Connection {
init(nwConnection: NWConnection)
static var connectionParameters: NWParameters { get }
}

/// Minimal implementation of UDP connection capable of sending data.
/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
class UDPConnection {
class UDPConnection: Connection {
private let dispatchQueue = DispatchQueue(label: "UDPConnection")
private let nwConnection: NWConnection

init(remote: IPAddress, port: UInt16) {
nwConnection = NWConnection(
convenience init(remote: IPAddress, port: UInt16) {
self.init(nwConnection: NWConnection(
host: NWEndpoint.Host("\(remote)"),
port: NWEndpoint.Port(integerLiteral: port),
using: .udp
)
))
}

required init(nwConnection: NWConnection) {
self.nwConnection = nwConnection
}

static var connectionParameters: NWParameters { .udp }

deinit {
cancel()
}
Expand Down Expand Up @@ -58,6 +69,22 @@ class UDPConnection {
nwConnection.cancel()
}

func readSingleDatagram() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
nwConnection.receiveMessage { data, _, _, error in
guard let data else {
continuation.resume(throwing: POSIXError(.EIO))
return
}
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(with: .success(data))
}
}
}

func sendData(_ data: Data) async throws {
return try await withCheckedThrowingContinuation { continuation in
nwConnection.send(content: data, completion: .contentProcessed { error in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// TCPUnsafeListener.swift
// UnsafeListener.swift
// MullvadRustRuntimeTests
//
// Created by pronebird on 27/06/2023.
Expand All @@ -8,25 +8,24 @@

import Network

/// Minimal implementation of a TCP listener.
/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
class TCPUnsafeListener {
private let dispatchQueue = DispatchQueue(label: "TCPListener")
class UnsafeListener<T: Connection> {
private let dispatchQueue = DispatchQueue(label: "com.test.unsafeListener")
private let listener: NWListener

/// A stream of new TCP connections.
/// The caller may iterate over this stream to accept new TCP connections.
/// A stream of new connections.
/// The caller may iterate over this stream to accept new connections.
///
/// `TCPConnection` objects are returned unopen, so the caller has to call `TCPConnection.start()` to accept the
/// `Connection` objects are returned unopen, so the caller has to call `Connection.start()` to accept the
/// connection before initiating the data exchange.
let newConnections: AsyncStream<TCPConnection>
let newConnections: AsyncStream<T>

init() throws {
let listener = try NWListener(using: .tcp)
let listener = try NWListener(using: T.connectionParameters)

newConnections = AsyncStream { continuation in
listener.newConnectionHandler = { nwConnection in
continuation.yield(TCPConnection(nwConnection: nwConnection))
continuation.yield(T(nwConnection: nwConnection))
}
continuation.onTermination = { @Sendable _ in
listener.newConnectionHandler = nil
Expand All @@ -40,7 +39,7 @@ class TCPUnsafeListener {
cancel()
}

/// Local TCP port bound by listener on which it accepts new connections.
/// Local port bound by listener on which it accepts new connections.
var listenPort: UInt16 {
return listener.port?.rawValue ?? 0
}
Expand Down
18 changes: 9 additions & 9 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -841,10 +841,10 @@
A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; };
A9D9A4AE2C36CFE9004088DD /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = A9D9A4AD2C36CFE9004088DD /* WireGuardKitTypes */; };
A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */; };
A9D9A4B22C36D12D004088DD /* UDPOverTCPObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */; };
A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* TunnelObfuscator.swift */; };
A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */; };
A9D9A4C42C36D53C004088DD /* MullvadRustRuntime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */; };
A9D9A4CC2C36D54E004088DD /* TCPUnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */; };
A9D9A4CC2C36D54E004088DD /* UnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* UnsafeListener.swift */; };
A9D9A4CD2C36D54E004088DD /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; };
A9D9A4CE2C36D54E004088DD /* TunnelObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */; };
A9D9A4CF2C36D54E004088DD /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */; };
Expand Down Expand Up @@ -1506,7 +1506,7 @@
583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = "<group>"; };
583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = "<group>"; };
583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = "<group>"; };
584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscator.swift; sourceTree = "<group>"; };
584023212A406BF5007B27AC /* TunnelObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscator.swift; sourceTree = "<group>"; };
584023282A407F5F007B27AC /* libmullvad_ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_ios.a; path = "../target/aarch64-apple-ios/debug/libmullvad_ios.a"; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
5840BE34279EDB16002836BA /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
Expand All @@ -1525,7 +1525,7 @@
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDNSTextCell.swift; sourceTree = "<group>"; };
58561C98239A5D1500BD6B5E /* IPv4Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4Endpoint.swift; sourceTree = "<group>"; };
5859A55429CD9DD800F66591 /* changes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = changes.txt; sourceTree = "<group>"; };
585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPUnsafeListener.swift; sourceTree = "<group>"; };
585A02E82A4B283000C6CAFF /* UnsafeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsafeListener.swift; sourceTree = "<group>"; };
585A02EA2A4B285800C6CAFF /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPConnection.swift; sourceTree = "<group>"; };
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4090,7 +4090,7 @@
A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */,
A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */,
F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */,
584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */,
584023212A406BF5007B27AC /* TunnelObfuscator.swift */,
014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */,
);
path = MullvadRustRuntime;
Expand All @@ -4099,12 +4099,12 @@
A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */ = {
isa = PBXGroup;
children = (
A9C308392C19DDA7008715F1 /* MullvadPostQuantum+Stubs.swift */,
A98F1B502C19C48D003C869E /* EphemeralPeerExchangeActorTests.swift */,
A9C308392C19DDA7008715F1 /* MullvadPostQuantum+Stubs.swift */,
585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */,
585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */,
58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */,
585A02EA2A4B285800C6CAFF /* UDPConnection.swift */,
585A02E82A4B283000C6CAFF /* UnsafeListener.swift */,
);
path = MullvadRustRuntimeTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -6233,7 +6233,7 @@
A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */,
014449952CA293B100C0C2F2 /* EncryptedDNSProxy.swift in Sources */,
A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */,
A9D9A4B22C36D12D004088DD /* UDPOverTCPObfuscator.swift in Sources */,
A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */,
A9173C322C36CCDD00F6A08C /* PacketTunnelProvider+TCPConnection.swift in Sources */,
F05919802C45515200C301F3 /* EphemeralPeerExchangeActor.swift in Sources */,
);
Expand All @@ -6247,7 +6247,7 @@
F08B6B772C52878400D0A121 /* EphemeralPeerExchangeActorTests.swift in Sources */,
A9D9A4CF2C36D54E004088DD /* TCPConnection.swift in Sources */,
A9D9A4CE2C36D54E004088DD /* TunnelObfuscationTests.swift in Sources */,
A9D9A4CC2C36D54E004088DD /* TCPUnsafeListener.swift in Sources */,
A9D9A4CC2C36D54E004088DD /* UnsafeListener.swift in Sources */,
A9D9A4CD2C36D54E004088DD /* UDPConnection.swift in Sources */,
F0A1638A2C47B77300592300 /* ServerRelaysResponse+Stubs.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard let self = self else { return }
tunnelSettingsListener.onNewSettings?(settings.tunnelSettings)
},
protocolObfuscator: ProtocolObfuscator<UDPOverTCPObfuscator>()
protocolObfuscator: ProtocolObfuscator<TunnelObfuscator>()
)

let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider)
Expand Down
4 changes: 3 additions & 1 deletion ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: ProtocolObfuscat
return endpoint
}

// At this point, the only possible obfuscation methods should be either `.udpOverTcp` or `.shadowsocks`
let obfuscator = Obfuscator(
remoteAddress: endpoint.ipv4Relay.ip,
tcpPort: remotePort
tcpPort: remotePort,
obfuscationProtocol: obfuscationMethod == .shadowsocks ? .shadowsocks : .udpOverTcp
)

obfuscator.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct TunnelObfuscationStub: TunnelObfuscation {
var transportLayer: TransportLayer { .udp }

let remotePort: UInt16
init(remoteAddress: IPAddress, tcpPort: UInt16) {
init(remoteAddress: IPAddress, tcpPort: UInt16, obfuscationProtocol: TunnelObfuscationProtocol) {
remotePort = tcpPort
}

Expand Down
Loading

0 comments on commit 26b75ba

Please sign in to comment.