Skip to content

Commit a643269

Browse files
committed
Add fundamentals of SOCKS support to the VPN implementation
1 parent 7e82e09 commit a643269

15 files changed

+633
-70
lines changed

app/src/main/java/tech/httptoolkit/android/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
458458
listOf(lastProxy.ip),
459459
lastProxy.port,
460460
null,
461-
getCertificateFingerprint(lastProxy.certificate as X509Certificate)
461+
getCertificateFingerprint(lastProxy.certificate as X509Certificate),
462+
if (lastProxy.captureProtocol != null) listOf(lastProxy.captureProtocol.toString()) else listOf()
462463
)
463464
)
464465
connectToVpn(config)

app/src/main/java/tech/httptoolkit/android/ProxyDetails.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ data class ProxyInfo(
4040
* proxy, as part of validating the connection. This hash is included in QR codes to
4141
* protect against MitM of our MitM during setup.
4242
*/
43-
val certFingerprint: String
43+
val certFingerprint: String,
44+
45+
/**
46+
* Which protocols does the proxy say it supports? This is slightly broader than the 'real'
47+
* set of protocols, since we may include things like RAW (direct packet redirection),
48+
* and subtypes of SOCKS with custom metadata mechanisms etc.
49+
*/
50+
@Json(serializeNull = false)
51+
val proxyProtocols: List<String>?
4452
) : Parcelable
4553

4654
/**
@@ -73,9 +81,20 @@ data class ProxyConfig(
7381
/**
7482
* The HTTPS CA certificate of the proxy, obtained from the proxy itself during setup.
7583
*/
76-
val certificate: Certificate
84+
val certificate: Certificate,
85+
86+
/**
87+
* Preferred capture protocol for this proxy. If null, only the default RAW
88+
* HTTP redirect behavior is supported.
89+
*/
90+
val captureProtocol: ProxyCaptureProtocol?
7791
) : Parcelable
7892

93+
enum class ProxyCaptureProtocol {
94+
RAW,
95+
SOCKS5
96+
}
97+
7998
val CertificateConverter = object: Converter {
8099
override fun canConvert(cls: Class<*>): Boolean {
81100
return cls.isAssignableFrom(Certificate::class.java)

app/src/main/java/tech/httptoolkit/android/ProxySetup.kt

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,28 @@ fun parseConnectUri(uri: Uri): ProxyInfo {
4444
}
4545

4646
suspend fun getProxyConfig(proxyInfo: ProxyInfo): ProxyConfig {
47+
val protocol = if (proxyInfo.proxyProtocols?.contains("SOCKS5") == true)
48+
ProxyCaptureProtocol.SOCKS5
49+
else
50+
ProxyCaptureProtocol.RAW
51+
4752
return withContext(Dispatchers.IO) {
4853
return@withContext supervisorScope {
4954
Log.v(TAG, "Validating proxy info $proxyInfo")
5055
val proxyTests = proxyInfo.addresses.map { address ->
5156
async {
52-
testProxyAddress(
57+
val cert = testProxyAddress(
5358
address,
5459
proxyInfo.port,
5560
proxyInfo.certFingerprint
5661
)
62+
63+
ProxyConfig(
64+
address,
65+
proxyInfo.port,
66+
cert,
67+
protocol
68+
)
5769
}
5870
}
5971

@@ -68,11 +80,18 @@ suspend fun getProxyConfig(proxyInfo: ProxyInfo): ProxyConfig {
6880

6981
// If all network connections fail, and we have a local ADB tunnel, fallback to
7082
// using that connection instead.
71-
return@supervisorScope testProxyAddress(
83+
val cert = testProxyAddress(
7284
"127.0.0.1",
7385
proxyInfo.localTunnelPort,
7486
proxyInfo.certFingerprint
7587
)
88+
89+
return@supervisorScope ProxyConfig(
90+
"127.0.0.1",
91+
proxyInfo.port,
92+
cert,
93+
protocol
94+
)
7695
}
7796
}
7897
}
@@ -81,8 +100,8 @@ suspend fun getProxyConfig(proxyInfo: ProxyInfo): ProxyConfig {
81100
private suspend fun testProxyAddress(
82101
address: String,
83102
port: Int,
84-
expectedFingerprint: String
85-
): ProxyConfig {
103+
expectedFingerprint: String,
104+
): X509Certificate {
86105
return withContext(Dispatchers.IO) {
87106
val certFactory = CertificateFactory.getInstance("X.509")
88107

@@ -112,11 +131,7 @@ private suspend fun testProxyAddress(
112131
val foundCertFingerprint = getCertificateFingerprint(foundCert)
113132

114133
if (foundCertFingerprint == expectedFingerprint) {
115-
ProxyConfig(
116-
address,
117-
port,
118-
foundCert
119-
)
134+
return@withContext foundCert
120135
} else {
121136
throw CertificateException(
122137
"Proxy returned mismatched certificate: '${

app/src/main/java/tech/httptoolkit/android/ProxyVpnRunnable.kt

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@ package tech.httptoolkit.android
22

33
import android.os.ParcelFileDescriptor
44
import android.util.Log
5-
import android.util.SparseArray
65
import tech.httptoolkit.android.vpn.ClientPacketWriter
76
import tech.httptoolkit.android.vpn.SessionHandler
87
import tech.httptoolkit.android.vpn.SessionManager
98
import tech.httptoolkit.android.vpn.socket.SocketNIODataService
109
import io.sentry.Sentry
10+
import tech.httptoolkit.android.vpn.capture.CaptureController
11+
import tech.httptoolkit.android.vpn.capture.DirectHandler
12+
import tech.httptoolkit.android.vpn.capture.SOCKS5Handler
1113
import tech.httptoolkit.android.vpn.transport.PacketHeaderException
1214
import java.io.FileInputStream
1315
import java.io.FileOutputStream
1416
import java.io.InterruptedIOException
1517
import java.net.ConnectException
16-
import java.net.InetSocketAddress
1718
import java.nio.ByteBuffer
1819

1920
// Set on our VPN as the MTU, which should guarantee all packets fit this
2021
const val MAX_PACKET_LEN = 1500
2122

2223
class ProxyVpnRunnable(
2324
vpnInterface: ParcelFileDescriptor,
24-
proxyHost: String,
25-
proxyPort: Int,
25+
proxyConfig: ProxyConfig,
2626
redirectPorts: IntArray
2727
) : Runnable {
2828

@@ -40,20 +40,13 @@ class ProxyVpnRunnable(
4040
private val nioService = SocketNIODataService(vpnPacketWriter)
4141
private val dataServiceThread = Thread(nioService, "Socket NIO thread")
4242

43-
private val manager = SessionManager()
43+
private val captureController = CaptureController(proxyConfig, redirectPorts.toList())
44+
private val manager = SessionManager(captureController)
4445
private val handler = SessionHandler(manager, nioService, vpnPacketWriter)
4546

4647
// Allocate the buffer for a single packet.
4748
private val packet = ByteBuffer.allocate(MAX_PACKET_LEN)
4849

49-
// Our redirect rules, defining which traffic should be forwarded to what proxy address
50-
private val portRedirections = SparseArray<InetSocketAddress>().apply {
51-
val proxyAddress = InetSocketAddress(proxyHost, proxyPort)
52-
redirectPorts.forEach {
53-
append(it, proxyAddress)
54-
}
55-
}
56-
5750
override fun run() {
5851
if (running) {
5952
Log.w(TAG, "Vpn runnable started, but it's already running")
@@ -62,7 +55,6 @@ class ProxyVpnRunnable(
6255

6356
Log.i(TAG, "Vpn thread starting")
6457

65-
manager.setTcpPortRedirections(portRedirections)
6658
dataServiceThread.start()
6759
vpnPacketWriterThread.start()
6860

app/src/main/java/tech/httptoolkit/android/ProxyVpnService.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,7 @@ class ProxyVpnService : VpnService(), IProtectSocket {
247247

248248
vpnRunnable = ProxyVpnRunnable(
249249
vpnInterface,
250-
proxyConfig.ip,
251-
proxyConfig.port,
250+
proxyConfig,
252251
interceptedPorts.toIntArray()
253252
)
254253
Thread(vpnRunnable, "Vpn thread").start()

app/src/main/java/tech/httptoolkit/android/vpn/Session.java

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
import android.util.Log;
2020

21+
import androidx.annotation.Nullable;
22+
23+
import tech.httptoolkit.android.vpn.capture.ProxyProtocolHandler;
2124
import tech.httptoolkit.android.vpn.transport.ip.IPv4Header;
2225
import tech.httptoolkit.android.vpn.socket.ICloseSession;
2326
import tech.httptoolkit.android.vpn.transport.tcp.TCPHeader;
@@ -108,13 +111,18 @@ public class Session {
108111

109112
private final ICloseSession sessionCloser;
110113

114+
// Proxy protocol handler - set only while the proxy connection
115+
// is being established, then null afterwards.
116+
private ProxyProtocolHandler proxyHandler;
117+
111118
Session(
112119
SessionProtocol protocol,
113120
int sourceIp,
114121
int sourcePort,
115122
int destinationIp,
116123
int destinationPort,
117-
ICloseSession sessionCloser
124+
ICloseSession sessionCloser,
125+
ProxyProtocolHandler proxyHandler
118126
) {
119127
receivingStream = new ByteArrayOutputStream();
120128
sendingStream = new ByteArrayOutputStream();
@@ -126,18 +134,27 @@ public class Session {
126134
this.destPort = destinationPort;
127135

128136
this.sessionCloser = sessionCloser;
137+
this.proxyHandler = proxyHandler;
138+
}
139+
140+
@Nullable
141+
public ProxyProtocolHandler getProxySetupHandler() {
142+
if (this.proxyHandler == null) {
143+
return null;
144+
} else if (!this.proxyHandler.isPending()) {
145+
this.proxyHandler = null;
146+
return null;
147+
} else {
148+
return this.proxyHandler;
149+
}
129150
}
130151

131152
/**
132153
* append more data
133154
* @param data Data
134155
*/
135-
public synchronized void addReceivedData(byte[] data){
136-
try {
137-
receivingStream.write(data);
138-
} catch (IOException e) {
139-
Log.e(TAG, e.toString());
140-
}
156+
public synchronized void addReceivedData(ByteBuffer data){
157+
receivingStream.write(data.array(), data.position(), data.remaining());
141158
}
142159

143160
/**
@@ -176,10 +193,6 @@ public synchronized int setSendingData(ByteBuffer data) {
176193
return remaining;
177194
}
178195

179-
int getSendingDataSize(){
180-
return sendingStream.size();
181-
}
182-
183196
/**
184197
* dequeue data for sending to server
185198
* @return byte[]

app/src/main/java/tech/httptoolkit/android/vpn/SessionManager.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.jetbrains.annotations.NotNull;
2525

2626
import tech.httptoolkit.android.TagKt;
27+
import tech.httptoolkit.android.vpn.capture.CaptureController;
28+
import tech.httptoolkit.android.vpn.capture.ProxyProtocolHandler;
2729
import tech.httptoolkit.android.vpn.socket.DataConst;
2830
import tech.httptoolkit.android.vpn.socket.ICloseSession;
2931
import tech.httptoolkit.android.vpn.socket.SocketProtector;
@@ -36,6 +38,8 @@
3638
import java.nio.channels.DatagramChannel;
3739
import java.nio.channels.SocketChannel;
3840
import java.nio.channels.spi.AbstractSelectableChannel;
41+
import java.util.ArrayList;
42+
import java.util.List;
3943
import java.util.Map;
4044
import java.util.concurrent.ConcurrentHashMap;
4145

@@ -50,6 +54,12 @@ public class SessionManager implements ICloseSession {
5054
private final Map<String, Session> table = new ConcurrentHashMap<>();
5155
private SocketProtector protector = SocketProtector.getInstance();
5256

57+
private final CaptureController captureController;
58+
59+
public SessionManager(CaptureController captureController) {
60+
this.captureController = captureController;
61+
}
62+
5363
/**
5464
* keep java garbage collector from collecting a session
5565
* @param session Session
@@ -133,7 +143,7 @@ public Session createNewUDPSession(int ip, int port, int srcIp, int srcPort) thr
133143
Session existingSession = table.get(keys);
134144
if (existingSession != null) return existingSession;
135145

136-
Session session = new Session(SessionProtocol.UDP, srcIp, srcPort, ip, port, this);
146+
Session session = new Session(SessionProtocol.UDP, srcIp, srcPort, ip, port, this, null);
137147

138148
DatagramChannel channel;
139149

@@ -171,7 +181,17 @@ public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) thr
171181
// We return the initialized session, which will be reacked to indicate rejection.
172182
if (existingSession != null) return existingSession;
173183

174-
Session session = new Session(SessionProtocol.TCP, srcIp, srcPort, ip, port, this);
184+
String ips = PacketUtil.intToIPAddress(ip);
185+
boolean shouldCapture = captureController.shouldCapture(ips, port);
186+
SocketAddress socketAddress = shouldCapture
187+
? captureController.getProxyAddress()
188+
: new InetSocketAddress(ips, port);
189+
190+
ProxyProtocolHandler proxyHandler = shouldCapture
191+
? captureController.getProxyHandler(ips, port)
192+
: null;
193+
194+
Session session = new Session(SessionProtocol.TCP, srcIp, srcPort, ip, port, this, proxyHandler);
175195

176196
SocketChannel channel;
177197
channel = SocketChannel.open();
@@ -181,7 +201,6 @@ public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) thr
181201
channel.socket().setReceiveBufferSize(DataConst.MAX_RECEIVE_BUFFER_SIZE);
182202
channel.configureBlocking(false);
183203

184-
String ips = PacketUtil.intToIPAddress(ip);
185204
Log.d(TAG,"created new SocketChannel for " + key);
186205

187206
protector.protect(channel.socket());
@@ -190,12 +209,6 @@ public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) thr
190209
session.setChannel(channel);
191210

192211
// Initiate connection straight away, to reduce latency
193-
// We use the real address, unless tcpPortRedirection redirects us to a different
194-
// target address for traffic on this port.
195-
SocketAddress socketAddress = tcpPortRedirection.get(port) != null
196-
? tcpPortRedirection.get(port)
197-
: new InetSocketAddress(ips, port);
198-
199212
Log.d(TAG,"Initiate connecting to remote tcp server: " + socketAddress.toString());
200213
boolean connected = channel.connect(socketAddress);
201214
session.setConnected(connected);
@@ -205,9 +218,4 @@ public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) thr
205218
return session;
206219
}
207220

208-
private SparseArray<InetSocketAddress> tcpPortRedirection = new SparseArray<>();
209-
210-
public void setTcpPortRedirections(SparseArray<InetSocketAddress> tcpPortRedirection) {
211-
this.tcpPortRedirection = tcpPortRedirection;
212-
}
213221
}

0 commit comments

Comments
 (0)