Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ concurrency:
cancel-in-progress: true

jobs:
transports:
name: GOST HTTP3 and SOCKS5
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.12.5"
python-version: "3.12"

- name: Install test dependencies
run: uv sync --locked --all-groups

- name: Download pinned GOST release
run: |
mkdir -p "$RUNNER_TEMP/megaproxy-gost"
cd "$RUNNER_TEMP/megaproxy-gost"
curl --fail --location --retry 3 --max-time 120 \
https://github.com/go-gost/gost/releases/download/v3.3.0/gost_3.3.0_linux_amd64.tar.gz \
--output gost_3.3.0_linux_amd64.tar.gz
echo '676fb7f78d267b6ae73df719c0c7f2b565dde7147da935cfafbc1e1da558b6d5 gost_3.3.0_linux_amd64.tar.gz' | sha256sum --check
tar -xzf gost_3.3.0_linux_amd64.tar.gz

- name: Verify authenticated SOCKS5 and MASQUE TCP and UDP
run: uv run --frozen python tests/integration/transports.py --gost "$RUNNER_TEMP/megaproxy-gost/gost"

ubuntu:
name: Ubuntu 24.04 host
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# MegaProxyServer

Optional [HTTP/3, MASQUE and SOCKS5 transports](docs/ru/transports.md) are controlled per host.
SOCKS5 is disabled by default and is not recommended because it does not encrypt proxy authentication or transport.

Provision and operate hardened HTTPS and SSH proxy servers with Ansible.

MegaProxyServer manages multiple Debian/Ubuntu hosts from one inventory, keeps proxy users global,
Expand Down
10 changes: 8 additions & 2 deletions docs/ru/config-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ users:

## Контракт MegaProxyConfig

Используются [формат v8](https://github.com/andre487/MegaProxyConfig/blob/602c9c2a689afda6fc0a685435a1d3f82d404120/docs/configuration.md)
и [протокол доставки](https://github.com/andre487/MegaProxyConfig/blob/602c9c2a689afda6fc0a685435a1d3f82d404120/docs/subscription-protocol.md).
Настройка генерации новых транспортов описана в [Транспортах прокси](transports.md).
`services.https.http3` включает поддержку и флаг предпочтения HTTP/3 на совместимых
HTTPS-маршрутах; отдельные MASQUE-профили требуют `masque_profiles: true`.
SOCKS5-профили генерируются только при `socks5.enabled: true`; SOCKS5 не рекомендуется
из-за отсутствия шифрования транспорта и реквизитов аутентификации.

Используются [формат v8](https://github.com/andre487/MegaProxyConfig/blob/5c0758c855712415bf2f1dfd6f9a97fd92d5dfee/docs/configuration.md)
и [протокол доставки](https://github.com/andre487/MegaProxyConfig/blob/5c0758c855712415bf2f1dfd6f9a97fd92d5dfee/docs/subscription-protocol.md).
Схемы и LICENSE сохранены в `schemas/`; commit и SHA-256 зафиксированы в
[lock-файле](../../schemas/megaproxy-config.lock.json). Проверки работают без скачивания `main`.
Выбор 403 вместо стандартного 401 с Basic challenge сделан намеренно по политике этого API.
Expand Down
102 changes: 102 additions & 0 deletions docs/ru/transports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Транспорты прокси

HTTPS и SSH остаются базовыми транспортами. HTTP/3 и SOCKS5 включаются отдельно
на каждом HTTPS-хосте и используют существующий GOST и пользователей `users.https`.
По умолчанию оба выключены.

```yaml
hosts:
proxy_example:
# address и admin как у обычного прокси
services:
https:
endpoint: proxy.example.com
certificate: domain
acme_email: admin@example.com
http3: true
masque_profiles: false
socks5:
enabled: false
port: 1080
udp_port_min: 40000
udp_port_max: 40100
```

## HTTP/3 и MASQUE

`http3: true` добавляет GOST MASQUE с listener `http3`, TLS 1.3 и HTTP Datagrams.
Это отдельный UDP-listener, обычный HTTPS/TCP продолжает работать. На прямом
маршруте UDP использует тот же номер порта, что HTTPS, обычно 443. Firewall открывает
нужный UDP-порт. Контейнеру добавляется только capability `NET_BIND_SERVICE`,
необходимая для привязки низких портов.

API подписок и MegaProxy JSON выставляют `proxy.preferHttp3: true` только когда
HTTPS и MASQUE доступны на одном hostname и номере порта. Android пробует QUIC,
а при поддерживаемых контрактом ошибках возвращается к HTTPS/TCP. Ошибки проверки
сертификата и аутентификации остаются отказами.

`masque_profiles: true` дополнительно публикует отдельные профили `type: MASQUE`.
Этот параметр требует `http3: true`; он не нужен для HTTPS с `preferHttp3`.
Отдельный MASQUE-профиль требует HTTP/3 и не переключается на HTTPS.
Профили получают логин и пароль текущего авторизованного пользователя.

HTTP/3 включается только на прямых маршрутах. Серверные SNI-цепочки остаются HTTPS/TCP
и не получают MASQUE или `preferHttp3`: GOST 3.3.0 передаёт некорректный Extended CONNECT
при собственном исходящем CONNECT-UDP. Это ограничение не разрешает превращать
цепочку в прямой маршрут. В нашем inventory HTTP/3 на хостах `cpx-*` выключен.
Клиентские HTTPS_JUMP-профили могут описывать два прямых HTTPS/MASQUE endpoint;
их поддержку и fallback реализует клиент согласно контракту MegaProxyConfig.

Маскировка GOST (`probe_resistance`) относится к HTTPS/TCP. Она сохраняет сайт-приманку,
robots.txt и настроенные knock-хосты. На UDP/MASQUE knock и сайт-приманка не применяются.
HTTP/3/MASQUE в GOST и клиентах остаются экспериментальными. Browser-проекции API
сохраняют совместимые HTTPS/SOCKS5; MASQUE доступен в полном и Android-ответе.

## SOCKS5 — не рекомендуется

**SOCKS5 не рекомендуется из-за проблем с безопасностью:** протокол не шифрует
транспорт. RFC 1929 передаёт логин и пароль без шифрования, а незашифрованный трафик
приложений также доступен наблюдателю. HTTPS приложения защищает содержимое своего
соединения, но не защищает SOCKS5-аутентификацию. Используйте HTTPS/MASQUE или SSH.

Для явного включения задайте `services.https.socks5.enabled: true`. Сервер требует
логин и пароль из `users.https`, не разрешает анонимный доступ и поддерживает TCP
CONNECT и UDP ASSOCIATE. Реквизиты ограничены 255 UTF-8 байт каждый.
SOCKS5 публикуется на прямом endpoint хоста; серверные HTTPS-цепочки к нему не применяются.

Firewall открывает TCP `port` и заданный диапазон UDP-relay. Число одновременно
выделенных relay-портов ограничено размером диапазона; измените диапазон, если его
недостаточно. На отключённом SOCKS5 listener и эти правила не создаются.

API автоматически выдаёт SOCKS5-профили только для включённых хостов. Firefox и
Android получают профили с реквизитами; Chromium их пропускает, поскольку не
поддерживает SOCKS5-аутентификацию. ProxyList/SuperProxy/FoxyProxy остаются HTTPS-экспортами;
новые транспорты включаются в MegaProxy JSON v8. Без них прежний JSON остаётся v7.

## Проверка

```sh
./mega-proxy validate
./mega-proxy plan --limit proxy_example
./mega-proxy apply --limit proxy_example
./mega-proxy verify --limit proxy_example
```

Изменения флагов нужно применить и на прокси, и на серверах конфигов. `--limit`
обновляет только выбранные машины. Отключение listener прекращает доступ; роль
firewall добавляет необходимые правила, но не удаляет ранее созданные правила.

Тесты генерации и совместимости входят в `./mega-proxy check`. Проверка реального
GOST 3.3.0 локально и в CI:

```sh
uv run python tests/integration/transports.py --gost /path/to/gost
```

Она проверяет SOCKS5 TCP/UDP, обязательную аутентификацию, диапазон relay-портов,
MASQUE TCP/UDP, доверенный TLS и отказ при неверных реквизитах.
В тестах используются временные ключи и реквизиты; публичные серверы не требуются.

Исходные настройки: [MASQUE GOST](https://gost.run/reference/handlers/masque/),
[HTTP/3 listener](https://gost.run/en/reference/listeners/http3/),
[контракт MegaProxyConfig](https://github.com/andre487/MegaProxyConfig/blob/5c0758c/docs/configuration.md).
7 changes: 7 additions & 0 deletions inventory.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ hosts:
endpoint: example.com
title: EU
port: 443
http3: false
masque_profiles: false
socks5:
enabled: false
port: 1080
udp_port_min: 40000
udp_port_max: 40100
certificate: domain
acme_email: admin@example.com
gost_version: 3.3.0
Expand Down
20 changes: 20 additions & 0 deletions playbooks/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@
changed_when: false
when: megaproxy_services.ssh is defined and megaproxy_services.ssh.enabled | bool

- name: Read HTTP3 UDP listeners
ansible.builtin.command: ss -H -lun
register: http3_listeners
changed_when: false
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.http3

- name: Require every configured MASQUE listener
ansible.builtin.assert:
that: "':' ~ (item.http3_port | string) in http3_listeners.stdout"
fail_msg: "MASQUE route {{ item.name }} is not listening on UDP port {{ item.http3_port }}"
loop: "{{ megaproxy_services.https.routes | default([]) | selectattr('http3_port') | list }}"
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.http3

- name: Require the optional SOCKS5 TCP listener
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ megaproxy_services.https.socks5.port }}"
timeout: 15
when: megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.socks5.enabled

- name: Read MegaProxy SSH host fingerprint
ansible.builtin.command: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
register: ssh_fingerprint
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [

[dependency-groups]
dev = [
"aioquic>=1.3,<2",
"pytest>=8.4,<10",
"ruff>=0.12,<0.15",
]
Expand Down
15 changes: 15 additions & 0 deletions roles/firewall/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{{ ([ansible_port | default(22)])
+ ([megaproxy_services.ssh.port] if megaproxy_services.ssh is defined and megaproxy_services.ssh.enabled else [])
+ ([megaproxy_services.https.port] if megaproxy_services.https is defined and megaproxy_services.https.enabled else [])
+ ([megaproxy_services.https.socks5.port] if megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.socks5.enabled else [])
+ ([80] if megaproxy_services.https is defined and megaproxy_services.https.enabled and megaproxy_services.https.certificate != 'self-signed' else [])
+ ([megaproxy_services.config_api.port, 80] if megaproxy_services.config_api is defined and megaproxy_services.config_api.enabled else [])
| map('string') | unique | list }}
Expand All @@ -15,6 +16,20 @@
register: ufw_allow
changed_when: "'Rule added' in ufw_allow.stdout"

- name: Build optional UDP firewall port list
ansible.builtin.set_fact:
megaproxy_allowed_udp_ports: >-
{{ ((megaproxy_services.https.routes | selectattr('http3_port') | map(attribute='http3_port') | map('string') | list)
+ ([megaproxy_services.https.socks5.udp_port_min | string ~ ':' ~ megaproxy_services.https.socks5.udp_port_max | string]
if megaproxy_services.https.socks5.enabled else []))
if megaproxy_services.https is defined and megaproxy_services.https.enabled else [] }}

- name: Allow enabled HTTP3 and SOCKS5 UDP relay ports
ansible.builtin.command: "ufw allow {{ item }}/udp"
loop: "{{ megaproxy_allowed_udp_ports }}"
register: ufw_allow_udp
changed_when: "'Rule added' in ufw_allow_udp.stdout"

- name: Read firewall policy
ansible.builtin.command: ufw status verbose
register: ufw_policy
Expand Down
33 changes: 33 additions & 0 deletions roles/https_proxy/templates/gost.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@ services:
maxVersion: VersionTLS13
alpn: [h2, http/1.1]
{% endfor %}
{% for route in megaproxy_services.https.routes if route.http3_port %}
- name: masque-{{ route.name }}
addr: ":{{ route.http3_port }}"
handler:
type: masque
auther: megaproxy-users
listener:
type: http3
metadata:
enableDatagrams: true
tls:
certFile: {{ megaproxy_container_certificate_directory }}/fullchain.pem
keyFile: {{ megaproxy_container_certificate_directory }}/privkey.pem
rejectUnknownSNI: {{ (not route.is_ip) | lower }}
serverNames: [{{ route.hostname | to_json }}]
options:
minVersion: VersionTLS13
maxVersion: VersionTLS13
alpn: [h3]
{% endfor %}
{% if megaproxy_services.https.socks5.enabled %}
- name: socks5
addr: ":{{ megaproxy_services.https.socks5.port }}"
handler:
type: socks5
auther: megaproxy-users
metadata:
udp: true
udp.bindRange.min: {{ megaproxy_services.https.socks5.udp_port_min }}
udp.bindRange.max: {{ megaproxy_services.https.socks5.udp_port_max }}
listener:
type: tcp
{% endif %}
{% if megaproxy_services.https.routes | selectattr('probe_resistance.enabled') | list %}
- name: decoy
addr: "127.0.0.1:18080"
Expand Down
2 changes: 1 addition & 1 deletion roles/https_proxy/templates/megaproxy-gost.service.j2
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Wants=network-online.target
[Service]
Type=simple
ExecStartPre=-/usr/bin/docker rm -f megaproxy-gost
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
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
ExecStop=/usr/bin/docker stop -t 10 megaproxy-gost
Restart=on-failure
RestartSec=5
Expand Down
32 changes: 30 additions & 2 deletions schemas/android-v8.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"type": "string",
"enum": [
"HTTPS",
"SOCKS5",
"MASQUE",
"HTTPS_JUMP",
"SSH",
Expand All @@ -144,8 +145,7 @@
},
"username": {
"type": "string",
"maxLength": 4096,
"pattern": "^[^:\\r\\n]*$"
"maxLength": 4096
},
"password": {
"type": "string",
Expand Down Expand Up @@ -216,6 +216,34 @@
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "SOCKS5"
}
},
"required": [
"type"
]
},
"then": {
"properties": {
"username": {
"type": "string",
"pattern": "^[^\\r\\n]*$"
}
}
},
"else": {
"properties": {
"username": {
"type": "string",
"pattern": "^[^:\\r\\n]*$"
}
}
}
}
]
},
Expand Down
6 changes: 3 additions & 3 deletions schemas/megaproxy-config.lock.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"repository": "https://github.com/andre487/MegaProxyConfig",
"commit": "602c9c2a689afda6fc0a685435a1d3f82d404120",
"commit": "5c0758c855712415bf2f1dfd6f9a97fd92d5dfee",
"sha256": {
"megaproxy-v8.schema.json": "682a6a751811364b7a0042d99478654581202e03973970a85a0ae1b7e8a3fb18",
"android-v8.schema.json": "f79d28117a43eb08e644e8500d917516f84224e6ced748fc3c876b6ed289c2ea",
"megaproxy-v8.schema.json": "7591c7f910452a99dda6a5eaf9622bca41298ed290f80dd90790eae35d545b60",
"android-v8.schema.json": "6fae5a54fa27fd5cc5da8001284a505d5231cacb9e4307a2ac8106c5191c90b6",
"MegaProxyConfig.LICENSE": "6c13885cba42ec32ee2f3720283ee61256643bd4a12fa736b4b6d9823e6e4ccc"
}
}
Loading
Loading