I run a few instances of dumbproxy (simple but quite versatile forward proxy server) for my personal needs. Not so long ago, I implemented a new operation mode for it, allowing dumbproxy to be run as a subprocess and communicate with the parent process via stdin/stdout instead of listening port. It is very useful to use it as a ProxyCommand for OpenSSH client. More importantly, it made me realize that I’m just one small feature away from achieving something I hadn’t gotten around to doing: sending PPP tunnel through a HTTP proxy!
We already have TLS securing proxy connections, flexible authentication, (optional) active probing resistance, good firewall bypassing capabilities, including resistance to state-level VPN censorship. It would be nice to bring all of these benefits to some well-known VPN protocols, enabling direct IP forwarding. While OpenVPN already has limited support for proxies, and we could just point it to a local dumbproxy instance to forward connection to a remote (parent, in Squid terms) TLS-enabled proxy, I still wanted to tinker with PPP and pay tribute to one of the oldest and most fundamental tunneling protocols - PPP. Dial-up era tunnel running over modern HTTP/2, how cool is that?!
Starting Point
The instances I currently run have fairly basic configuration as described here, with a few additions:
- The certificate cache is stored in shared Redis instance in order to make instances completely stateless.
- Some domains are filtered by access filter JS script.
- .onion domains are redirected to Tor instance.
All in all, it’s a plain forward proxy setup with automatic certificates from LetsEncrypt and a local password database in a file.
By the way, there is a cloud-init spec available to go through setup for you.
Server Setup
Let’s take a look into the redirection script (-js-proxy-router option). Mine was looking like this:
/etc/dumbproxy-route.js:
There is already one redirection rule, which is irrelevant for now. Let’s add another one to send some special destination address into a pppd file /etc/ppp/options.vpn subprocess.
/etc/dumbproxy-route.js:
Make sure pppd is installed, it’s in ppp package in most Linux distributions:
pppd options will be
/etc/ppp/options.vpn:
That’s enough to establish a tunnel. However, we also need a few bits to make the system actually forward traffic.
Enable IP forwarding:
Masquerade traffic leaving through the default gateway interface:
Assuming you’re using iptables-persistent package to manage iptables, you can these changes persistent across reboots like this:
That’s it - we’re done with the server configuration.
Client Setup
Let’s create peer configuration for pppd.
/etc/ppp/peers/vpn:
where h2://LOGIN:[email protected] should be replaced with the specification of the remote proxy you just configured.
Here, we use the dumbproxy command as a pty command for pppd, funneling the connection into it. It connects through the upstream proxy to a fake address pppd:0, which the server-side router script recognizes and directs the connection straight into a pppd subprocess.
Install the dumbproxy binary (assuming Linux and amd64 architecture; for other architectures see latest release assets):
Tunnel configuration is done, but we also need to add a small script to configure routing after the PPP connection is established:
/etc/ppp/ip-up.d/vpn:
This script installs direct route to upstream proxy hosts, ensuring already encapsulated traffic won’t loop back into the tunnel. It also installs default route, preserving the original route after the PPP session shuts down.
Don’t forget to replace vps.example.org with your actual domain name and make the script executable.
Thas’s all - let’s try it out!
Checking Out
Let’s confirm we’ve achieved both datagram forwarding and that traffic goes through the remote server. You can contact a DNS echo server directly and see which IP address you use to reach it:
It should output the IP address of the machine at the remote end of the tunnel.
Now, the speed. Here’s my result:

Not bad, considering it’s a TCP-carried tunnel.
Bonus
But we can make it even weirder! Initially, PPP was used for communication over serial lines, often carried over phone lines using a modem. Typically, the modem was connected to the computer’s serial port (tty for pppd), and some program would to prepare it for actual data transfer, sending AT commands to the modem, dialing a number, maybe even sending a username-password over the line before the PPP session can be started. We can do something similar.
We can skip using dumbproxy on the client and instead use the openssl command line utility in conjunction with the chat program, which was typically used for setting up modem and expecting responses from it.
pppd peer config becomes
/etc/ppp/peers/vpn-lite:
Instead of a single pty option, we use a connect script plus the openssl s_client utility (which is basically like netcat but for SSL/TLS).
The connect script is:
/usr/local/bin/dialer.sh:
It’s just an invocation of chat program with an encoded login-password pair passed as a “phone number”. Similarly, you can start the connection with sudo pppd call vpn-lite command.
Of course, this uses HTTP/1.1 instead, but maybe that’s for the better - there’s no overhead from HTTP/2 frame encoding/decoding. Speed looks a bit better, but likely within the error margin:

.png)
