Tunneling HTTPS Through HTTP
Amica - Devlog #1
In this post we will be exploring how HTTP
proxies handle HTTPS
request.
If you haven’t read the last post go check it out first, this will make more sense if you read that first.
To tunnel https
requests http
proxies use the CONNECT
http
method.
Using this method the proxy establishes a TCP
connection with the client
and the server
and relays the tcp packets
back and forth between these
two’s tcp
connection. Let’s look how this is done.
HTTP CONNECT method
The negotiation goes like this. The client will send a request resembling the
following to the proxy server
.
|
|
Upon receiving this the proxy
knows that the client
wants a tcp
connection to
google.com:443
, through this connection the client
and server
can establish
a secure connection. If the proxy
supports tunneling tcp
connections, it will send
the following response to the client
.
|
|
The proxy will then open a tcp
connection to the server
, google.com:433
in this case,
and starts to send the tcp
packets back and forth. Let’s look at the code to do this
in Rust
.
Rust implementation
The code in this section will continue from the last
post. Last time we where able to handle
http
request with the following code.
|
|
We will add the following just after the function declaration starts.
|
|
Let’s look at the important parts.
On line 4
we are peeking
because we don’t the kind of the request and we
don’t want to empty the inner buffer. If we read
to the buffer and it turns out
that the request is not a CONNECT
method, then the code below line 18
wouldn’t
know what to do with the request, because we took part of the request.
Line 6
checks if what we have peeked
starts_with
CONNECT
string, the numbers
are CONNECT
spelled in ascii. we are doing this because we don’t want to allocate
memory by creating a string from it.
Line 7
if it turns out to be a CONNECT
request, we empty the read
buffer.
Line 9
and 10
extract the host
from the request, we are using from_utf8_lossy
because we don’t care if the string has invalid characters.
Line 12
and 14
we connect to the server and inform the client we support tunneling
tcp
connections.
Line 16
then we give the client
and server
to bidi_read_write
to handle the
tcp
back and forth. Let’s have a look at that.
|
|
On line 2
and 3
we split the streams to get the read
and write
end.
Line 7
to 22
We are using tokio::select
to see who has data on thier read
buffer
and forward it to the write
end of the other one. We are matching on Ok(n)
on line
9
and 15
because read returns a Result
with the number of bytes read. If the
number of bytes read on both ends is 0
that means we have reached EOF
and we should
break the loop.
Testing with netcat
The thing about this is it works with any kind of server
and client
connection over
tcp.
For example we can setup a server and client with netcat
and tunnel the connection
through our proxy server
.
First we run our proxy server
with cargo run
. Let’s assume the proxy
is listening
on 127.0.0.1:9001
And the netcat
server is listening on 127.0.0.1:9001
.
|
|
In another window we run the following to connect to the proxy server.
|
|
An send the followin line
|
|
This tells the proxy “we want a tcp connection to 127.0.0.1:9002
”. The proxy responds
with:
|
|
Telling us we are good to go. from this point onward everything we type in the client
window will appear on the server
window and vice versa.
This commit can be found here.