summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-01-08 10:03:25 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-01-08 10:03:25 +0100
commitaca939c260f718e3806a67b0a0703a33d4c8157f (patch)
tree02d7d5e94b9429fd53c6f34e22559fd962320872
parent9d98bd30203a11728f9d90f444136af94bf32ea9 (diff)
parentdb72d9b4089c0cd807a1ef938a18b5ef640e3c5a (diff)
downloadmullvadvpn-aca939c260f718e3806a67b0a0703a33d4c8157f.tar.xz
mullvadvpn-aca939c260f718e3806a67b0a0703a33d4c8157f.zip
Merge branch 'add-authentication-support-for-socks5-access-methods-ios-417'
-rw-r--r--ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift10
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift57
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift22
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Connection.swift25
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Constants.swift2
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5Error.swift3
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift12
-rw-r--r--ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift83
-rw-r--r--ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift5
-rw-r--r--ios/MullvadREST/Transport/TransportProvider.swift6
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
11 files changed, 205 insertions, 24 deletions
diff --git a/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
index 87624656f9..986f8276fa 100644
--- a/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
+++ b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift
@@ -20,4 +20,14 @@ extension AnyIPEndpoint {
.ipv6(endpoint)
}
}
+
+ /// Convert `AnyIPEndpoint` to `NWEndpoint`.
+ var nwEndpoint: NWEndpoint {
+ switch self {
+ case let .ipv4(endpoint):
+ .hostPort(host: .ipv4(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port))
+ case let .ipv6(endpoint):
+ .hostPort(host: .ipv6(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port))
+ }
+ }
}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
index 39a240d41e..a72dfc6108 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift
@@ -6,9 +6,66 @@
//
import Foundation
+import Network
/// Authentication methods supported by socks protocol.
enum Socks5AuthenticationMethod: UInt8 {
case notRequired = 0x00
case usernamePassword = 0x02
}
+
+struct Socks5Authentication {
+ let connection: NWConnection
+ let endpoint: Socks5Endpoint
+ let configuration: Socks5Configuration
+
+ typealias AuthenticationComplete = () -> Void
+ typealias AuthenticationFailure = (Error) -> Void
+
+ func authenticate(onComplete: @escaping AuthenticationComplete, onFailure: @escaping AuthenticationFailure) {
+ guard let username = configuration.username, let password = configuration.password else {
+ onFailure(Socks5Error.invalidUsernameOrPassword)
+ return
+ }
+ let authenticateCommand = Socks5UsernamePasswordCommand(username: username, password: password)
+
+ connection.send(content: authenticateCommand.rawData, completion: .contentProcessed { error in
+ if let error {
+ onFailure(error)
+ } else {
+ readNegotiationReply(onComplete: onComplete, onFailure: onFailure)
+ }
+ })
+ }
+
+ func readNegotiationReply(
+ onComplete: @escaping AuthenticationComplete,
+ onFailure: @escaping AuthenticationFailure
+ ) {
+ let replySize = MemoryLayout<Socks5UsernamePasswordReply>.size
+
+ // Read in one shot, the payload is very small to not care about a reading loop.
+ connection.receive(exactLength: replySize) { data, _, _, error in
+ guard let data else {
+ if let error {
+ onFailure(error)
+ } else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ }
+ return
+ }
+
+ guard let reply = Socks5UsernamePasswordReply(from: data) else {
+ onFailure(Socks5Error.unexpectedEndOfStream)
+ return
+ }
+
+ guard reply.version == Socks5Constants.usernamePasswordAuthenticationProtocol else {
+ onFailure(Socks5Error.invalidSocksVersion)
+ return
+ }
+
+ onComplete()
+ }
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
index 2ec3a94b00..4f7ebc06f9 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift
@@ -8,25 +8,19 @@
import Foundation
import MullvadTypes
-import Network
/// Socks5 configuration.
/// - See: ``URLSessionSocks5Transport``
public struct Socks5Configuration {
- public let address: AnyIPAddress
- public let port: UInt16
+ /// The socks proxy endpoint.
+ public var proxyEndpoint: AnyIPEndpoint
- public init(address: AnyIPAddress, port: UInt16) {
- self.address = address
- self.port = port
- }
+ public var username: String?
+ public var password: String?
- var nwEndpoint: NWEndpoint {
- switch self.address {
- case let .ipv4(endpoint):
- .hostPort(host: .ipv4(endpoint), port: NWEndpoint.Port(integerLiteral: port))
- case let .ipv6(endpoint):
- .hostPort(host: .ipv6(endpoint), port: NWEndpoint.Port(integerLiteral: port))
- }
+ public init(proxyEndpoint: AnyIPEndpoint, username: String? = nil, password: String? = nil) {
+ self.proxyEndpoint = proxyEndpoint
+ self.username = username
+ self.password = password
}
}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift
index a3163029fe..be232f8437 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift
@@ -12,7 +12,7 @@ import Network
final class Socks5Connection {
/// The remote endpoint to which the client wants to establish connection over the socks proxy.
let remoteServerEndpoint: Socks5Endpoint
-
+ let configuration: Socks5Configuration
/**
Initializes a new connection passing data between local and remote TCP connection over the socks proxy.
@@ -26,12 +26,14 @@ final class Socks5Connection {
queue: DispatchQueue,
localConnection: NWConnection,
socksProxyEndpoint: NWEndpoint,
- remoteServerEndpoint: Socks5Endpoint
+ remoteServerEndpoint: Socks5Endpoint,
+ configuration: Socks5Configuration
) {
self.queue = queue
self.remoteServerEndpoint = remoteServerEndpoint
self.localConnection = localConnection
self.remoteConnection = NWConnection(to: socksProxyEndpoint, using: .tcp)
+ self.configuration = configuration
}
/**
@@ -173,7 +175,10 @@ final class Socks5Connection {
/// Start handshake with the socks proxy.
private func sendHandshake() {
- let handshake = Socks5Handshake()
+ var handshake = Socks5Handshake()
+ if configuration.username != nil && configuration.password != nil {
+ handshake.methods.append(.usernamePassword)
+ }
let negotiation = Socks5HandshakeNegotiation(
connection: remoteConnection,
handshake: handshake,
@@ -191,8 +196,18 @@ final class Socks5Connection {
connect()
case .usernamePassword:
- // TODO: handle authentication
- break
+ // Username + password authentication sends the data in plain text to the server
+ // And then continues like the `notRequired` case after the server has authenticated the client.
+ let authentication = Socks5Authentication(
+ connection: remoteConnection,
+ endpoint: remoteServerEndpoint,
+ configuration: configuration
+ )
+ authentication.authenticate(onComplete: { [self] in
+ connect()
+ }, onFailure: { [self] error in
+ handleError(error)
+ })
}
}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift
index 325c7527da..f62642d270 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift
@@ -10,4 +10,6 @@ import Foundation
enum Socks5Constants {
/// Socks version.
static let socksVersion: UInt8 = 0x05
+
+ static let usernamePasswordAuthenticationProtocol: UInt8 = 0x01
}
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift
index 992c402160..8be764cbb1 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift
@@ -34,6 +34,9 @@ public enum Socks5Error: Error {
/// Server replied with unsupported authentication method.
case unsupportedAuthMethod
+ /// Invalid username or password was provided to the server
+ case invalidUsernameOrPassword
+
/// None of the auth methods listed by the client are acceptable.
case unacceptableAuthMethods
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift
index 0b26bc3b92..29a1b2f70a 100644
--- a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift
+++ b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift
@@ -26,6 +26,8 @@ public final class Socks5ForwardingProxy {
/// Remote server that socks proxy should connect to.
public let remoteServerEndpoint: Socks5Endpoint
+ public let configuration: Socks5Configuration
+
/// Local TCP port that clients should use to communicate with the remote server.
/// This property is set once the proxy is successfully started.
public var listenPort: UInt16? {
@@ -46,9 +48,14 @@ public final class Socks5ForwardingProxy {
- socksProxyEndpoint: socks proxy endpoint.
- remoteServerEndpoint: remote server that socks proxy should connect to.
*/
- public init(socksProxyEndpoint: NWEndpoint, remoteServerEndpoint: Socks5Endpoint) {
+ public init(
+ socksProxyEndpoint: NWEndpoint,
+ remoteServerEndpoint: Socks5Endpoint,
+ configuration: Socks5Configuration
+ ) {
self.socksProxyEndpoint = socksProxyEndpoint
self.remoteServerEndpoint = remoteServerEndpoint
+ self.configuration = configuration
}
deinit {
@@ -251,7 +258,8 @@ public final class Socks5ForwardingProxy {
queue: queue,
localConnection: connection,
socksProxyEndpoint: socksProxyEndpoint,
- remoteServerEndpoint: remoteServerEndpoint
+ remoteServerEndpoint: remoteServerEndpoint,
+ configuration: configuration
)
socks5Connection.setStateHandler { [weak self] socks5Connection, state in
if case let .stopped(error) = state {
diff --git a/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift b/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift
new file mode 100644
index 0000000000..42e50fe5d4
--- /dev/null
+++ b/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift
@@ -0,0 +1,83 @@
+//
+// Socks5UsernamePasswordCommand.swift
+// MullvadREST
+//
+// Created by Marco Nikic on 2023-12-13.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/**
+ The payload sent to the server is the following diagram
+ +----+------+----------+------+----------+
+ |VER | ULEN | UNAME | PLEN | PASSWD |
+ +----+------+----------+------+----------+
+ | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+ +----+------+----------+------+----------+
+
+ VER: The current version of this method, always 1
+ ULEN: The length of `username`
+ UNAME: The username
+ PLEN: The length of `password`
+ PASSWD: The password
+
+ **/
+struct Socks5UsernamePasswordCommand {
+ let username: String
+ let password: String
+
+ var rawData: Data {
+ var data = Data()
+ guard username.count < UInt8.max,
+ password.count < UInt8.max,
+ let usernameData = username.data(using: .utf8),
+ let passwordData = password.data(using: .utf8)
+ else { return data }
+
+ // Protocol version
+ data.append(Socks5Constants.usernamePasswordAuthenticationProtocol)
+
+ // Username length
+ data.append(UInt8(username.count))
+
+ // Username
+ data.append(usernameData)
+
+ // Password length
+ data.append(UInt8(password.count))
+
+ // Password
+ data.append(passwordData)
+
+ return data
+ }
+}
+
+/**
+ The expected answer payload looks like this
+ +-----+--------+
+ | VER | STATUS |
+ +-----+--------+
+ | 1 | 1 |
+ +-----+--------+
+ */
+struct Socks5UsernamePasswordReply {
+ let version: UInt8
+ let status: Socks5StatusCode
+
+ /// - Parameter data: The bytes read from the network connection sent by a socks5 server as a reply to a `Socks5UsernamePasswordCommand`.
+ init?(from data: Data) {
+ let expectedSize = MemoryLayout<Self>.size
+ guard data.count == expectedSize else { return nil }
+ var iterator = data.makeIterator()
+
+ guard let readVersion = iterator.next(),
+ readVersion == Socks5Constants.usernamePasswordAuthenticationProtocol else { return nil }
+ self.version = readVersion
+
+ guard let readStatus = iterator.next(),
+ let statusCode = Socks5StatusCode(rawValue: readStatus) else { return nil }
+ self.status = statusCode
+ }
+}
diff --git a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift
index 89121db0d0..6c3a67159b 100644
--- a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift
+++ b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift
@@ -45,8 +45,9 @@ public class URLSessionSocks5Transport: RESTTransport {
let apiAddress = addressCache.getCurrentEndpoint()
socksProxy = Socks5ForwardingProxy(
- socksProxyEndpoint: configuration.nwEndpoint,
- remoteServerEndpoint: apiAddress.socksEndpoint
+ socksProxyEndpoint: configuration.proxyEndpoint.nwEndpoint,
+ remoteServerEndpoint: apiAddress.socksEndpoint,
+ configuration: configuration
)
socksProxy.setErrorHandler { [weak self] error in
diff --git a/ios/MullvadREST/Transport/TransportProvider.swift b/ios/MullvadREST/Transport/TransportProvider.swift
index d0baf7c092..e88afd33dc 100644
--- a/ios/MullvadREST/Transport/TransportProvider.swift
+++ b/ios/MullvadREST/Transport/TransportProvider.swift
@@ -78,10 +78,14 @@ public final class TransportProvider: RESTTransportProvider {
}
}
+ // TODO: Pass the socks5 username, password, and ip+port combo here.
private func socks5() -> RESTTransport? {
return URLSessionSocks5Transport(
urlSession: urlSessionTransport.urlSession,
- configuration: Socks5Configuration(address: .ipv4(.loopback), port: 8889),
+ configuration: Socks5Configuration(proxyEndpoint: AnyIPEndpoint.ipv4(IPv4Endpoint(
+ ip: .loopback,
+ port: 8889
+ ))),
addressCache: addressCache
)
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2db19e7335..60a74fa1b5 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -599,6 +599,7 @@
A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; };
A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; };
+ A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */; };
A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */; };
A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */; };
A97D25B22B0CB02D00946B2D /* ProtocolObfuscatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */; };
@@ -1730,6 +1731,7 @@
A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredDeviceData.swift; sourceTree = "<group>"; };
A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = "<group>"; };
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; };
+ A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5UsernamePasswordCommand.swift; sourceTree = "<group>"; };
A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscator.swift; sourceTree = "<group>"; };
A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscationStub.swift; sourceTree = "<group>"; };
A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscatorTests.swift; sourceTree = "<group>"; };
@@ -3282,6 +3284,7 @@
A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */,
A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */,
A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */,
+ A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */,
A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */,
);
path = Socks5;
@@ -4278,6 +4281,7 @@
A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */,
06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */,
A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */,
+ A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */,
A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */,
06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */,
F0DDE4182B220458006B57A7 /* ShadowsocksConfiguration.swift in Sources */,