CONNECT passthrough on nginx
A while ago I explored easy hacks on
how to encapsulate SSH into HTTPS on apache2. Since then I moved a few
my systems from apache2 to nginx to play a bit with http/2 and
http/3. I wanted to port my hack to nginx.
At the time nginx did not have direct support for CONNECT handling.
I used chobits/ngx_http_proxy_connect_module
external module to implement CONNECT. But around the end of 2025 the
extension stopped compiling against latest nginx versions and I
dropped CONNECT hack.
This extension is not meant to be portable across nginx versions: it
applies invasive patches to nxing core to add method support and tends
to break with minor nginx updates.
This week I wondered how can I get CONNECT handling back. I explored a
few other HTTP servers and found out that nginx-2.31.0 does have
native support for CONNECT starting with 2.31.0 as a
ngx_http_tunnel_module!
The admin guide
provided a simple example of setting it up.
The nginx Config Change
Here is a configuration example to match nginx variant of passing
through ssh connection to a single target. Suppose you have a simple
nginx config to handle static files as:
http {
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
// ... server_name and ssl_ stuff go here
location / {
root /path/to/public_html;
index index.html;
}
}
}
To get CONNECT for a limited set of targets it’s a matter of adding
tunnel_pass under an allowed condition:
--- before.conf
+++ after.conf
@@ -1,16 +1,25 @@
http {
+ map "$request_method:$host:$request_port" $allow_connect_host_port {
+ "CONNECT:ssh-server:22" 1;
+ }
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
// ... server_name and ssl_ stuff go here
location / {
root /path/to/public_html;
index index.html;
+
+ # CONNECT handing
+ resolver 127.0.0.1 [::1];
+ if ($allow_connect_host_port = 1) {
+ tunnel_pass;
+ }
}
}
}As nginx is slightly stricter about the used protocol than apache2
it requires client to connect as HTTP/1.1 and as a result pass the
Host: header. Thus, the client side looks this way.
~/.ssh/https-tunnel.bash:
#!/usr/bin/env bash
{ printf "CONNECT $1:$2 HTTP/1.1\r\nHost: $1\r\n\r\n"; cat; } | socat - "SSL:$3:$4".ssh/config:
Host ssh-via-http
Hostname ssh-server
Port 22
ProxyCommand socat - PROXY:http-server:%h:%p,proxyport=80
ServerAliveInterval 30
Parting Words
nginx-1.31.0 got has native support for CONNECT pass through for
both HTTP and HTTPS via
ngx_http_tunnel_module.