Skip to content

Recipe: PPP‐over‐HTTP2

Vladislav Yarmak edited this page Nov 18, 2025 · 1 revision

This guide explains how to tunnel arbitrary IP traffic through dumbproxy using PPP protocol between two Linux machines. Practically, it allows to achieve something close to SSTP protocol, but without most drawbacks inherent in it.

Initial Setup

This guide extends dumbproxy configuration on top of already existing server. Basic server setup is described here.

By the way, there is a cloud-init spec available to go through setup for you, if you're creating new server in the cloud.

Server Setup

Let's create redirection script (-js-proxy-router option).

/etc/dumbproxy-route.js:

function getProxy(req, dst, username) {
	if (dst.originalHost.toLowerCase() == "pppd") {
		return "cmd://?cmd=pppd&arg=file&arg=/etc/ppp/options.vpn"
	}
	return ""
}

It recognizes special destination host pppd and sends connection to a pppd subprocess.

Make sure pppd is installed, it's in ppp package in most Linux distributions:

apt install ppp

pppd options will be

/etc/ppp/options.vpn:

nodetach
notty
noauth
172.22.255.1:172.22.255.2
ms-dns 1.1.1.1
ms-dns 8.8.8.8

That's enough to establish a tunnel. However, we also need a few bits to make the system actually forward traffic.

Enable IP forwarding:

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && sysctl -p

Masquerade traffic leaving through the default gateway interface:

iptables -t nat -I POSTROUTING -o $(ip route show default | head -1 | grep -Po '(?<=dev\s)\s*\S+') -j MASQUERADE
iptables -t mangle -I FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Assuming you're using iptables-persistent package to manage iptables, you can these changes persistent across reboots like this:

/etc/init.d/netfilter-persistent save

That's it - we're done with the server configuration.

Client Setup

Let's create peer configuration for pppd.

/etc/ppp/peers/vpn:

nodetach
noauth
nodeflate
nobsdcomp
novj
novjccomp
ipparam vpn
usepeerdns
pty "/usr/local/bin/dumbproxy -proxy h2://LOGIN:[email protected] -mode stdio pppd 0"

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):

curl -Lo /usr/local/bin/dumbproxy \
	'https://github.com/SenseUnit/dumbproxy/releases/latest/download/dumbproxy.linux-amd64' \
	&& chmod +x /usr/local/bin/dumbproxy

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:

#!/bin/bash

INTERFACE="$1"
DEVICE="$2"
SPEED="$3"
LOCALIP="$4"
REMOTEIP="$5"
IPPARAM="$6"

if [[ "$IPPARAM" != "vpn" ]] ; then
	# Not our config
	exit 0
fi

PROTECT=("vps.example.org") # Preserve route for these addresses

default_route4=$(ip -4 route show default | head -1 | cut -d\  -f2-)
default_route6=$(ip -6 route show default | head -1 | cut -d\  -f2-)

for protect_address in "${PROTECT[@]}"; do
	>&2 echo "Protecting $protect_address..."

	if [[ "$default_route4" ]]; then
		for ip in $(getent ahostsv4 "$protect_address" | cut -f1 -d\  | sort | uniq); do
			ip -4 route replace "$ip" $default_route4
		done
	fi
	if [[ "$default_route6" ]]; then
		for ip in $(getent ahostsv6 "$protect_address" | cut -f1 -d\  | sort | uniq); do
			ip -6 route replace "$ip" $default_route6
		done
	fi
done

ip -4 route replace 0.0.0.0/1   dev "$INTERFACE"
ip -4 route replace 128.0.0.0/1 dev "$INTERFACE"
# Prevent ipv6 leaks
ip -6 route replace unreachable 2000::/3 

# Workaround for bug https://lists.opensuse.org/archives/list/[email protected]/thread/ZHDF667RJDGAEWJCJB7HGWNARKLAIPGK/
#if [[ "$DNS1" ]]; then
#	resolvconf="/var/run/ppp/resolv.conf.$INTERFACE"
#	chattr -i "$resolvconf"
#	echo "nameserver $DNS1" > "$resolvconf"
#	if [[ "$DNS2" ]]; then
#		echo "nameserver $DNS2" >> "$resolvconf"
#	fi
#	chmod 0644 "$resolvconf"
#	chattr +i "$resolvconf"
#	mount --bind --onlyonce "$resolvconf" /etc/resolv.conf
#fi

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!

user@ws:~> sudo pppd call vpn
Using interface ppp0
Connect: ppp0 <--> /dev/pts/4
MAIN    : 2025/11/18 03:54:20 main.go:656: INFO     Starting proxy server...
MAIN    : 2025/11/18 03:54:20 main.go:812: INFO     Proxy server started.
local  LL address fe80::b940:dde6:f755:0427
remote LL address fe80::e5da:861e:b382:4e83
Script /etc/ppp/ipv6-up finished (pid 47510), status = 0x0
Script /etc/ppp/ip-pre-up finished (pid 47515), status = 0x0
local  IP address 172.22.255.2
remote IP address 172.22.255.1
primary   DNS address 1.1.1.1
secondary DNS address 8.8.8.8
Script /etc/ppp/ip-up finished (pid 47520), status = 0x0

Clone this wiki locally