Я использую несколько экземпляров dumbproxy (это простой, но довольно универсальный прокси-сервер) для личных нужд. Недавно я реализовал новый режим работы для него, позволяющий запускать dumbproxy как подпроцесс и передавать данные через stdin/stdout вместо прослушивания порта. Это очень удобно использовать в качестве ProxyCommand для OpenSSH-клиента. Но самое главное – это навело меня на мысль, что я всего в одном небольшом изменении от реализации того, что давно хотел попробовать: передачу PPP через HTTP/2!
У dumbproxy уже есть TLS для защиты соединения с прокси, гибкая аутентификация, (опциональная) защита от active probing-а и хорошая устойчивость к фильтрации протокола. Было бы здорово добавить все эти плюсы к каким-либо известным протоколам частных сетей. Мне захотелось поэкспериментировать с PPP и отдать дань одному из самых старых фундаментальных туннельных протоколов. Сетевой протокол эпохи dial-up, работающий поверх современного HTTP/2, как же это круто!
Отправная точка
Я буду отталкиваться от самой простой конфигурации dumbproxy на своём сервере, описанной здесь, но имеющей несколько дополнений:
Кэш сертификатов хранится в общем Redis, чтобы сделать экземпляры серверов полностью статичными.
Несколько доменов фильтруются скриптом на JS.
Домен .onion перенаправляется в демон Tor.
В целом – это обычный форвард-прокси с автоматическими сертификатами от LetsEncrypt и локальной базой паролей в файле.
Кстати, есть готовая cloud-init спецификация для быстрой настройки сервера при его создании в облаке.
Настройка сервера
Посмотрим на JS-скрипт перенаправления (опция -js-proxy-router). У меня он выглядит так:
/etc/dumbproxy-route.js:
function getProxy(req, dst, username) {
if (dst.originalHost.replace(/\.$/, "").toLowerCase().endsWith(".onion")) {
return "socks5://127.0.0.1:9050"
}
return ""
}
Здесь уже есть одно правило, не относящееся к текущей задаче. Добавим новое, чтобы определённый адрес перенаправлять в подпроцесс pppd с нужным файлом опций.
/etc/dumbproxy-route.js:
function getProxy(req, dst, username) {
if (dst.originalHost.replace(/\.$/, "").toLowerCase().endsWith(".onion")) {
return "socks5://127.0.0.1:9050"
}
if (dst.originalHost.toLowerCase() == "pppd") {
return "cmd://?cmd=pppd&arg=file&arg=/etc/ppp/options.vpn"
}
return ""
}
Установите pppd, он есть в пакете ppp практически во всех дистрибутивах Linux:
apt install ppp
Параметры pppd будут такими:
/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
Этого достаточно для установления туннеля. Однако, чтобы трафик действительно пересылался, нужно кое-что ещё.
Включите пересылку IP-пакетов:
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && sysctl -p
Добавьте правило трансляции исходящего адреса в iptables:
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
Если Вы используете пакет iptables-persistent для управления iptables, можно сделать настройки постоянными:
/etc/init.d/netfilter-persistent save
На этом всё — настройка сервера завершена.
Настройка клиента
Создадим конфиг пира pppd.
/etc/ppp/peers/vpn:
nodetach
noauth
nodeflate
nobsdcomp
novj
novjccomp
ipparam vpn
usepeerdns
pty "/usr/local/bin/dumbproxy -proxy h2://LOGIN:PASSWORD@vps.example.org -mode stdio pppd 0"
Замените h2://LOGIN:PASSWORD@vps.example.org на адрес и параметры вашего удалённого прокси.
Здесь мы используем dumbproxy как команду pty для pppd, направляя сессию через неё. Она подключается к upstream-прокси, к "фейковому" адресу pppd:0, который на сервере распознаётся и отправляется в подпроцесс pppd.
Установите dumbproxy (для Linux amd64; для других архитектур смотрите релизы):
curl -Lo /usr/local/bin/dumbproxy \
'https://github.com/SenseUnit/dumbproxy/releases/latest/download/dumbproxy.linux-amd64' \
&& chmod +x /usr/local/bin/dumbproxy
Туннель готов, остаётся добавить небольшой скрипт для настройки маршрутизации после поднятия PPP:
#!/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/bugs@lists.opensuse.org/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
Скрипт устанавливает прямой маршрут до прокси, чтобы уже инкапсулированный трафик не попал обратно в туннель. А также прописывает дефолтный маршрут, сохраняя прежний после завершения PPP-сессии.
Не забудьте заменить vps.example.org на свой домен и сделать скрипт исполняемым.
Вот и всё — пробуем!
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
Проверка
Убедимся, что работает пересылка датаграмм и трафик идёт через удалённый сервер. Можно отправить запрос к DNS echo-серверу и посмотреть, с какого IP он нас видит:
dig +trace TXT whoami.ds.akahelp.net | grep -P 'IN\s+TXT'
В выводе должен быть IP-адрес, принадлежащий машине на удалённой стороне туннеля.
Теперь про скорость. Вот мой результат:

Неплохо — учитывая, что это туннель на внутри TCP-соединения.
Бонус
Можно добавить немного перчинки! Изначально PPP использовался для передачи данных по последовательной линии, чаще всего - модемом по телефонной линии. Обычно модем соединяли с последовательным портом компьютера (tty для pppd), а какой-то скрипт "готовил" его к соединению, отправлял AT-команды, набирал номер, возможно даже отправлял логин/пароль, а потом запускал PPP. Тут мы можем повторить нечто похожее.
Можно обойтись без dumbproxy на клиенте и использовать openssl в связке с программой chat, которая как раз применялась для инициализации модема.
Конфиг pppd станет таким:
/etc/ppp/peers/vpn-lite:
nodetach
noauth
nodeflate
nobsdcomp
novj
novjccomp
ipparam vpn
usepeerdns
connect /usr/local/bin/dialer.sh
pty "openssl s_client -brief -verify_return_error -ign_eof vps.example.org:443"
Вместо одного pty-аргумента используем connect-скрипт и утилиту openssl s_client (фактически — netcat для SSL/TLS).
Скрипт "набора номера":
#!/bin/sh
USERNAME="username"
PASSWORD="password"
AUTH="$(echo -n "$USERNAME:$PASSWORD" | base64)"
exec /usr/sbin/chat -v -T "$AUTH" \
TIMEOUT 5 \
ABORT 'HTTP/1.1 3' \
ABORT 'HTTP/1.1 4' \
ABORT 'HTTP/1.1 5' \
"" "CONNECT pppd:0 HTTP/1.1\r\nHost: pppd:0\r\nProxy-Authorization: Basic \T\r\n\r\n\c" \
"HTTP/1.1 200" ""
Это просто запуск chat с закодированной в base64 парой login/password в качестве "номера телефона". По аналогии, соединение можно стартовать командой sudo pppd call vpn-lite.
Конечно, здесь используется HTTP/1.1 вместо HTTP/2, но, возможно, это даже лучше — нет лишнего оверхеда на кодирование/декодирование фреймов HTTP/2. Скорость чуть выше, но разница в рамках погрешности:
