Skip to content

Commit 0d5d537

Browse files
authored
Add opt-in HTTP3, MASQUE profiles and authenticated SOCKS5 (#4)
1 parent 8cb1ada commit 0d5d537

23 files changed

Lines changed: 792 additions & 14 deletions

‎.github/workflows/integration.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,38 @@ concurrency:
1515
cancel-in-progress: true
1616

1717
jobs:
18+
transports:
19+
name: GOST HTTP3 and SOCKS5
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 10
22+
steps:
23+
- name: Check out repository
24+
uses: actions/checkout@v6
25+
with:
26+
persist-credentials: false
27+
28+
- name: Install uv
29+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
30+
with:
31+
version: "0.12.5"
32+
python-version: "3.12"
33+
34+
- name: Install test dependencies
35+
run: uv sync --locked --all-groups
36+
37+
- name: Download pinned GOST release
38+
run: |
39+
mkdir -p "$RUNNER_TEMP/megaproxy-gost"
40+
cd "$RUNNER_TEMP/megaproxy-gost"
41+
curl --fail --location --retry 3 --max-time 120 \
42+
https://github.com/go-gost/gost/releases/download/v3.3.0/gost_3.3.0_linux_amd64.tar.gz \
43+
--output gost_3.3.0_linux_amd64.tar.gz
44+
echo '676fb7f78d267b6ae73df719c0c7f2b565dde7147da935cfafbc1e1da558b6d5 gost_3.3.0_linux_amd64.tar.gz' | sha256sum --check
45+
tar -xzf gost_3.3.0_linux_amd64.tar.gz
46+
47+
- name: Verify authenticated SOCKS5 and MASQUE TCP and UDP
48+
run: uv run --frozen python tests/integration/transports.py --gost "$RUNNER_TEMP/megaproxy-gost/gost"
49+
1850
ubuntu:
1951
name: Ubuntu 24.04 host
2052
runs-on: ubuntu-latest

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# MegaProxyServer
22

3+
Optional [HTTP/3, MASQUE and SOCKS5 transports](docs/ru/transports.md) are controlled per host.
4+
SOCKS5 is disabled by default and is not recommended because it does not encrypt proxy authentication or transport.
5+
36
Provision and operate hardened HTTPS and SSH proxy servers with Ansible.
47

58
MegaProxyServer manages multiple Debian/Ubuntu hosts from one inventory, keeps proxy users global,

‎docs/ru/config-api.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,14 @@ users:
8989

9090
## Контракт MegaProxyConfig
9191

92-
Используются [формат v8](https://github.com/andre487/MegaProxyConfig/blob/602c9c2a689afda6fc0a685435a1d3f82d404120/docs/configuration.md)
93-
и [протокол доставки](https://github.com/andre487/MegaProxyConfig/blob/602c9c2a689afda6fc0a685435a1d3f82d404120/docs/subscription-protocol.md).
92+
Настройка генерации новых транспортов описана в [Транспортах прокси](transports.md).
93+
`services.https.http3` включает поддержку и флаг предпочтения HTTP/3 на совместимых
94+
HTTPS-маршрутах; отдельные MASQUE-профили требуют `masque_profiles: true`.
95+
SOCKS5-профили генерируются только при `socks5.enabled: true`; SOCKS5 не рекомендуется
96+
из-за отсутствия шифрования транспорта и реквизитов аутентификации.
97+
98+
Используются [формат v8](https://github.com/andre487/MegaProxyConfig/blob/5c0758c855712415bf2f1dfd6f9a97fd92d5dfee/docs/configuration.md)
99+
и [протокол доставки](https://github.com/andre487/MegaProxyConfig/blob/5c0758c855712415bf2f1dfd6f9a97fd92d5dfee/docs/subscription-protocol.md).
94100
Схемы и LICENSE сохранены в `schemas/`; commit и SHA-256 зафиксированы в
95101
[lock-файле](../../schemas/megaproxy-config.lock.json). Проверки работают без скачивания `main`.
96102
Выбор 403 вместо стандартного 401 с Basic challenge сделан намеренно по политике этого API.

‎docs/ru/transports.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Транспорты прокси
2+
3+
HTTPS и SSH остаются базовыми транспортами. HTTP/3 и SOCKS5 включаются отдельно
4+
на каждом HTTPS-хосте и используют существующий GOST и пользователей `users.https`.
5+
По умолчанию оба выключены.
6+
7+
```yaml
8+
hosts:
9+
proxy_example:
10+
# address и admin как у обычного прокси
11+
services:
12+
https:
13+
endpoint: proxy.example.com
14+
certificate: domain
15+
acme_email: admin@example.com
16+
http3: true
17+
masque_profiles: false
18+
socks5:
19+
enabled: false
20+
port: 1080
21+
udp_port_min: 40000
22+
udp_port_max: 40100
23+
```
24+
25+
## HTTP/3 и MASQUE
26+
27+
`http3: true` добавляет GOST MASQUE с listener `http3`, TLS 1.3 и HTTP Datagrams.
28+
Это отдельный UDP-listener, обычный HTTPS/TCP продолжает работать. На прямом
29+
маршруте UDP использует тот же номер порта, что HTTPS, обычно 443. Firewall открывает
30+
нужный UDP-порт. Контейнеру добавляется только capability `NET_BIND_SERVICE`,
31+
необходимая для привязки низких портов.
32+
33+
API подписок и MegaProxy JSON выставляют `proxy.preferHttp3: true` только когда
34+
HTTPS и MASQUE доступны на одном hostname и номере порта. Android пробует QUIC,
35+
а при поддерживаемых контрактом ошибках возвращается к HTTPS/TCP. Ошибки проверки
36+
сертификата и аутентификации остаются отказами.
37+
38+
`masque_profiles: true` дополнительно публикует отдельные профили `type: MASQUE`.
39+
Этот параметр требует `http3: true`; он не нужен для HTTPS с `preferHttp3`.
40+
Отдельный MASQUE-профиль требует HTTP/3 и не переключается на HTTPS.
41+
Профили получают логин и пароль текущего авторизованного пользователя.
42+
43+
HTTP/3 включается только на прямых маршрутах. Серверные SNI-цепочки остаются HTTPS/TCP
44+
и не получают MASQUE или `preferHttp3`: GOST 3.3.0 передаёт некорректный Extended CONNECT
45+
при собственном исходящем CONNECT-UDP. Это ограничение не разрешает превращать
46+
цепочку в прямой маршрут. В нашем inventory HTTP/3 на хостах `cpx-*` выключен.
47+
Клиентские HTTPS_JUMP-профили могут описывать два прямых HTTPS/MASQUE endpoint;
48+
их поддержку и fallback реализует клиент согласно контракту MegaProxyConfig.
49+
50+
Маскировка GOST (`probe_resistance`) относится к HTTPS/TCP. Она сохраняет сайт-приманку,
51+
robots.txt и настроенные knock-хосты. На UDP/MASQUE knock и сайт-приманка не применяются.
52+
HTTP/3/MASQUE в GOST и клиентах остаются экспериментальными. Browser-проекции API
53+
сохраняют совместимые HTTPS/SOCKS5; MASQUE доступен в полном и Android-ответе.
54+
55+
## SOCKS5 — не рекомендуется
56+
57+
**SOCKS5 не рекомендуется из-за проблем с безопасностью:** протокол не шифрует
58+
транспорт. RFC 1929 передаёт логин и пароль без шифрования, а незашифрованный трафик
59+
приложений также доступен наблюдателю. HTTPS приложения защищает содержимое своего
60+
соединения, но не защищает SOCKS5-аутентификацию. Используйте HTTPS/MASQUE или SSH.
61+
62+
Для явного включения задайте `services.https.socks5.enabled: true`. Сервер требует
63+
логин и пароль из `users.https`, не разрешает анонимный доступ и поддерживает TCP
64+
CONNECT и UDP ASSOCIATE. Реквизиты ограничены 255 UTF-8 байт каждый.
65+
SOCKS5 публикуется на прямом endpoint хоста; серверные HTTPS-цепочки к нему не применяются.
66+
67+
Firewall открывает TCP `port` и заданный диапазон UDP-relay. Число одновременно
68+
выделенных relay-портов ограничено размером диапазона; измените диапазон, если его
69+
недостаточно. На отключённом SOCKS5 listener и эти правила не создаются.
70+
71+
API автоматически выдаёт SOCKS5-профили только для включённых хостов. Firefox и
72+
Android получают профили с реквизитами; Chromium их пропускает, поскольку не
73+
поддерживает SOCKS5-аутентификацию. ProxyList/SuperProxy/FoxyProxy остаются HTTPS-экспортами;
74+
новые транспорты включаются в MegaProxy JSON v8. Без них прежний JSON остаётся v7.
75+
76+
## Проверка
77+
78+
```sh
79+
./mega-proxy validate
80+
./mega-proxy plan --limit proxy_example
81+
./mega-proxy apply --limit proxy_example
82+
./mega-proxy verify --limit proxy_example
83+
```
84+
85+
Изменения флагов нужно применить и на прокси, и на серверах конфигов. `--limit`
86+
обновляет только выбранные машины. Отключение listener прекращает доступ; роль
87+
firewall добавляет необходимые правила, но не удаляет ранее созданные правила.
88+
89+
Тесты генерации и совместимости входят в `./mega-proxy check`. Проверка реального
90+
GOST 3.3.0 локально и в CI:
91+
92+
```sh
93+
uv run python tests/integration/transports.py --gost /path/to/gost
94+
```
95+
96+
Она проверяет SOCKS5 TCP/UDP, обязательную аутентификацию, диапазон relay-портов,
97+
MASQUE TCP/UDP, доверенный TLS и отказ при неверных реквизитах.
98+
В тестах используются временные ключи и реквизиты; публичные серверы не требуются.
99+
100+
Исходные настройки: [MASQUE GOST](https://gost.run/reference/handlers/masque/),
101+
[HTTP/3 listener](https://gost.run/en/reference/listeners/http3/),
102+
[контракт MegaProxyConfig](https://github.com/andre487/MegaProxyConfig/blob/5c0758c/docs/configuration.md).

‎inventory.example.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ hosts:
2929
endpoint: example.com
3030
title: EU
3131
port: 443
32+
http3: false
33+
masque_profiles: false
34+
socks5:
35+
enabled: false
36+
port: 1080
37+
udp_port_min: 40000
38+
udp_port_max: 40100
3239
certificate: domain
3340
acme_email: admin@example.com
3441
gost_version: 3.3.0

‎playbooks/verify.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@
117117
changed_when: false
118118
when: megaproxy_services.ssh is defined and megaproxy_services.ssh.enabled | bool
119119

120+
- name: Read HTTP3 UDP listeners
121+
ansible.builtin.command: ss -H -lun
122+
register: http3_listeners
123+
changed_when: false
124+
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.http3
125+
126+
- name: Require every configured MASQUE listener
127+
ansible.builtin.assert:
128+
that: "':' ~ (item.http3_port | string) in http3_listeners.stdout"
129+
fail_msg: "MASQUE route {{ item.name }} is not listening on UDP port {{ item.http3_port }}"
130+
loop: "{{ megaproxy_services.https.routes | default([]) | selectattr('http3_port') | list }}"
131+
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.http3
132+
133+
- name: Require the optional SOCKS5 TCP listener
134+
ansible.builtin.wait_for:
135+
host: 127.0.0.1
136+
port: "{{ megaproxy_services.https.socks5.port }}"
137+
timeout: 15
138+
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.socks5.enabled
139+
120140
- name: Read MegaProxy SSH host fingerprint
121141
ansible.builtin.command: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
122142
register: ssh_fingerprint

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616

1717
[dependency-groups]
1818
dev = [
19+
"aioquic>=1.3,<2",
1920
"pytest>=8.4,<10",
2021
"ruff>=0.12,<0.15",
2122
]

‎roles/firewall/tasks/main.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{{ ([ansible_port | default(22)])
66
+ ([megaproxy_services.ssh.port] if megaproxy_services.ssh is defined and megaproxy_services.ssh.enabled else [])
77
+ ([megaproxy_services.https.port] if megaproxy_services.https is defined and megaproxy_services.https.enabled else [])
8+
+ ([megaproxy_services.https.socks5.port] if megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.socks5.enabled else [])
89
+ ([80] if megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.certificate != 'self-signed' else [])
910
+ ([megaproxy_services.config_api.port, 80] if megaproxy_services.config_api is defined and megaproxy_services.config_api.enabled else [])
1011
| map('string') | unique | list }}
@@ -15,6 +16,20 @@
1516
register: ufw_allow
1617
changed_when: "'Rule added' in ufw_allow.stdout"
1718

19+
- name: Build optional UDP firewall port list
20+
ansible.builtin.set_fact:
21+
megaproxy_allowed_udp_ports: >-
22+
{{ ((megaproxy_services.https.routes | selectattr('http3_port') | map(attribute='http3_port') | map('string') | list)
23+
+ ([megaproxy_services.https.socks5.udp_port_min | string ~ ':' ~ megaproxy_services.https.socks5.udp_port_max | string]
24+
if megaproxy_services.https.socks5.enabled else []))
25+
if megaproxy_services.https is defined and megaproxy_services.https.enabled else [] }}
26+
27+
- name: Allow enabled HTTP3 and SOCKS5 UDP relay ports
28+
ansible.builtin.command: "ufw allow {{ item }}/udp"
29+
loop: "{{ megaproxy_allowed_udp_ports }}"
30+
register: ufw_allow_udp
31+
changed_when: "'Rule added' in ufw_allow_udp.stdout"
32+
1833
- name: Read firewall policy
1934
ansible.builtin.command: ufw status verbose
2035
register: ufw_policy

‎roles/https_proxy/templates/gost.yml.j2‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ services:
2828
maxVersion: VersionTLS13
2929
alpn: [h2, http/1.1]
3030
{% endfor %}
31+
{% for route in megaproxy_services.https.routes if route.http3_port %}
32+
- name: masque-{{ route.name }}
33+
addr: ":{{ route.http3_port }}"
34+
handler:
35+
type: masque
36+
auther: megaproxy-users
37+
listener:
38+
type: http3
39+
metadata:
40+
enableDatagrams: true
41+
tls:
42+
certFile: {{ megaproxy_container_certificate_directory }}/fullchain.pem
43+
keyFile: {{ megaproxy_container_certificate_directory }}/privkey.pem
44+
rejectUnknownSNI: {{ (not route.is_ip) | lower }}
45+
serverNames: [{{ route.hostname | to_json }}]
46+
options:
47+
minVersion: VersionTLS13
48+
maxVersion: VersionTLS13
49+
alpn: [h3]
50+
{% endfor %}
51+
{% if megaproxy_services.https.socks5.enabled %}
52+
- name: socks5
53+
addr: ":{{ megaproxy_services.https.socks5.port }}"
54+
handler:
55+
type: socks5
56+
auther: megaproxy-users
57+
metadata:
58+
udp: true
59+
udp.bindRange.min: {{ megaproxy_services.https.socks5.udp_port_min }}
60+
udp.bindRange.max: {{ megaproxy_services.https.socks5.udp_port_max }}
61+
listener:
62+
type: tcp
63+
{% endif %}
3164
{% if megaproxy_services.https.routes | selectattr('probe_resistance.enabled') | list %}
3265
- name: decoy
3366
addr: "127.0.0.1:18080"

‎roles/https_proxy/templates/megaproxy-gost.service.j2‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Wants=network-online.target
77
[Service]
88
Type=simple
99
ExecStartPre=-/usr/bin/docker rm -f megaproxy-gost
10-
ExecStart=/usr/bin/docker run --name megaproxy-gost --network host --read-only --cap-drop ALL --security-opt no-new-privileges --memory 256m --pids-limit 128 -v /opt/megaproxy/gost.yml:/etc/gost/gost.yml:ro -v /opt/megaproxy/decoy:/opt/megaproxy/decoy:ro -v {{ megaproxy_certificate_volume }} gogost/gost:{{ megaproxy_services.https.gost_version }} -C /etc/gost/gost.yml
10+
ExecStart=/usr/bin/docker run --name megaproxy-gost --network host --read-only --cap-drop ALL {{ '--cap-add NET_BIND_SERVICE ' if (megaproxy_services.https.http3 and megaproxy_services.https.port < 1024) or (megaproxy_services.https.socks5.enabled and megaproxy_services.https.socks5.port < 1024) else '' }}--security-opt no-new-privileges --memory 256m --pids-limit 128 -v /opt/megaproxy/gost.yml:/etc/gost/gost.yml:ro -v /opt/megaproxy/decoy:/opt/megaproxy/decoy:ro -v {{ megaproxy_certificate_volume }} gogost/gost:{{ megaproxy_services.https.gost_version }} -C /etc/gost/gost.yml
1111
ExecStop=/usr/bin/docker stop -t 10 megaproxy-gost
1212
Restart=on-failure
1313
RestartSec=5

0 commit comments

Comments
 (0)