Я использую несколько экземпляров 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. Скорость чуть выше, но разница в рамках погрешности:
