fix: adopt sing-box 1.14 DNS hijacking
This commit is contained in:
@@ -8,15 +8,18 @@
|
||||
## Контракт релиза 0.2.0
|
||||
|
||||
- строго `sing-box 1.14.0`, Linux build с `with_quic` и `with_gvisor`;
|
||||
- HY2XS/Hysteria 2.12.2 использует Gecko как единственный production obfs;
|
||||
- новые установки HY2XS/Hysteria 2.12.2 используют Gecko по умолчанию;
|
||||
- Gecko packet profile фиксирован: `512/1200`;
|
||||
- Salamander принимается только как явно заданный compatibility fallback;
|
||||
- TUN DNS сохраняет прежнюю архитектуру через `dns_mode=disabled` и явное
|
||||
- Salamander полностью поддерживается как явно согласованный compatibility
|
||||
fallback;
|
||||
- TUN DNS использует штатный `dns_mode=hijack` без явного `dns_address` и
|
||||
route action `hijack-dns`;
|
||||
- лицензия исходного кода — Mozilla Public License 2.0.
|
||||
|
||||
Salamander не является автоматическим fallback: клиент не может согласовать тип
|
||||
obfs с сервером. Production URI HY2XS обязан содержать `obfs=gecko`.
|
||||
obfs с сервером. Для новой установки production URI HY2XS должен содержать
|
||||
`obfs=gecko`; `obfs=salamander` допустим только при таком же явно настроенном
|
||||
режиме сервера.
|
||||
|
||||
## Что обеспечивает проект
|
||||
|
||||
@@ -36,6 +39,9 @@ obfs с сервером. Production URI HY2XS обязан содержать `
|
||||
не поддерживается и блокируется `preinst`. Старые policy, config, state и backups
|
||||
не мигрируют и не активируются.
|
||||
|
||||
Повторная установка пакета ровно 0.2.0 поверх согласованного состояния 0.2.0
|
||||
допускается; переход с любого другого релиза требует чистой установки.
|
||||
|
||||
Правильная последовательность: остановить сервисы, оставить anti-leak guard,
|
||||
удалить пакет 0.1.0, архивировать его данные, установить sing-box 1.14.0 и пакет
|
||||
0.2.0, настроить новую policy и применить новый Gecko URI. Полный runbook:
|
||||
|
||||
Vendored
+3
-2
@@ -1,8 +1,9 @@
|
||||
vpn-egressctl (0.2.0) unstable; urgency=medium
|
||||
|
||||
* Target sing-box 1.14.0 and add the fixed HY2XS Gecko profile.
|
||||
* Preserve the established DNS and routing contract explicitly.
|
||||
* Reject in-place upgrades and legacy rollback state.
|
||||
* Use native TUN DNS hijacking and remove the redundant route-level rule.
|
||||
* Reject cross-release upgrades while allowing a consistent 0.2.0 reinstall.
|
||||
* Reject unmanaged configuration adoption and legacy rollback state.
|
||||
* License the 0.2 release under MPL-2.0.
|
||||
|
||||
-- Flamy Studio <dev@flamy.studio> Tue, 08 Sep 2026 00:00:00 +0500
|
||||
|
||||
Vendored
+2
-2
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$1" = upgrade ]; then
|
||||
echo "vpn-egressctl 0.2.0 does not support in-place upgrades." >&2
|
||||
if [ "$1" = upgrade ] && [ "${2:-}" != "0.2.0" ]; then
|
||||
echo "vpn-egressctl 0.2.0 does not support in-place upgrades from another release." >&2
|
||||
echo "Remove the installed release, archive its state, then install 0.2.0 cleanly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+33
-17
@@ -3,7 +3,8 @@
|
||||
## Границы ответственности
|
||||
|
||||
Hysteria2 URI содержит переносимые реквизиты: auth, hostname/ports, SNI,
|
||||
`insecure`, тип obfs и obfs password. Production HY2XS выдаёт Gecko URI.
|
||||
`insecure`, тип obfs и obfs password. Новые production-установки HY2XS получают
|
||||
Gecko URI. Salamander остаётся явно согласуемым compatibility fallback.
|
||||
|
||||
`policy.json` хранит локальную инфраструктуру: `eth0`, `eth1`, TUN CIDR и MTU,
|
||||
route exclusions, таблицу 2022, marks, NFQUEUE, DNS, bandwidth и healthcheck.
|
||||
@@ -20,18 +21,24 @@ URI + policy
|
||||
├─ renderer_1_14_0
|
||||
└─ sing-box check на private candidate
|
||||
│
|
||||
unchanged ───── changed
|
||||
│ │
|
||||
▼ ├─ проверка provenance текущего config
|
||||
state.json ├─ versioned last-good backup
|
||||
├─ atomic replace
|
||||
├─ systemctl restart
|
||||
└─ bounded healthcheck
|
||||
current отсутствует ── current существует
|
||||
│ │
|
||||
│ provenance gate
|
||||
│ │
|
||||
│ unchanged ───── changed
|
||||
│ │ │
|
||||
│ ▼ ├─ versioned last-good backup
|
||||
│ state.json │
|
||||
└────────────────────────────────┼─ atomic replace
|
||||
├─ systemctl restart
|
||||
└─ bounded healthcheck
|
||||
```
|
||||
|
||||
Первое применение после чистой установки не создаёт `last-good`: предыдущего
|
||||
управляемого конфига нет. При следующих изменениях backup разрешён только если
|
||||
SHA установленного config совпадает со state этого же релиза и sing-box 1.14.0.
|
||||
Побайтовое совпадение с candidate не доказывает ownership: существующий config
|
||||
без такого state отклоняется и не «усыновляется» контроллером.
|
||||
|
||||
## Gecko и DNS
|
||||
|
||||
@@ -39,11 +46,20 @@ Renderer добавляет для Gecko `min_packet_size=512` и
|
||||
`max_packet_size=1200`. Эти параметры являются частью локального HY2XS
|
||||
compatibility profile и не читаются из URI.
|
||||
|
||||
В sing-box 1.14.0 TUN по умолчанию использует `dns_mode=hijack`, что меняет
|
||||
настройки интерфейса и platform-level interception. Проект явно задаёт
|
||||
`dns_mode=disabled` и сохраняет собственное route action `port 53 ->
|
||||
hijack-dns`. Bootstrap UDP DNS привязан к `eth0`; remote DoH использует detour
|
||||
`hy2-out`.
|
||||
Renderer явно задаёт штатный для sing-box 1.14.0 `dns_mode=hijack`. Поле
|
||||
`dns_address` не задаётся: sing-box выводит IPv4 DNS address `172.19.0.2` как
|
||||
следующий адрес после `172.19.0.1/30`, автоматически связывает его с DNS module
|
||||
и при `auto_redirect=true` устанавливает nftables DNAT для TCP/UDP 53. Поэтому
|
||||
отдельного route action `hijack-dns` в generated config нет.
|
||||
|
||||
Bootstrap UDP DNS привязан к `eth0`; remote DoH использует detour `hy2-out`.
|
||||
Собственный outbound sing-box исключается из auto-redirect по output mark, что
|
||||
необходимо подтвердить privileged Linux acceptance без bootstrap recursion.
|
||||
|
||||
Platform hijack не создаёт DNS listener на локальном LAN-адресе шлюза: трафик к
|
||||
такому адресу попадает в kernel `local` table раньше пользовательских rules.
|
||||
Если DHCP должен выдавать сам gateway как DNS server, требуется отдельный
|
||||
listener/service, который не входит в контракт 0.2.0.
|
||||
|
||||
## Endpoint routing
|
||||
|
||||
@@ -73,7 +89,7 @@ Metadata не обеспечивает доверие от root-компроме
|
||||
|
||||
## Граница установки
|
||||
|
||||
0.2.0 не содержит кода миграции 0.1.0. `debian/preinst` отклоняет in-place
|
||||
upgrade, а `postinst` отклоняет legacy policy/state или unmanaged
|
||||
`/etc/sing-box/config.json`. Same-release reinstall допускается, если state и
|
||||
config согласованы.
|
||||
0.2.0 не содержит кода миграции 0.1.0. `debian/preinst` отклоняет установку
|
||||
поверх любого другого релиза, а `postinst` отклоняет legacy policy/state или
|
||||
unmanaged `/etc/sing-box/config.json`. Same-release reinstall 0.2.0 допускается,
|
||||
если state и config согласованы.
|
||||
|
||||
@@ -59,8 +59,10 @@ obfs=salamander -> требуется obfs-password; compatibility fallback
|
||||
password без obfs -> ошибка
|
||||
```
|
||||
|
||||
Для HY2XS допустимым production-режимом считается только Gecko. Salamander не
|
||||
включается на сервере и не выбирается автоматически.
|
||||
Для новых HY2XS production-установок Gecko является режимом по умолчанию.
|
||||
Salamander полностью поддерживается как compatibility fallback, но требует
|
||||
явного совпадения режима и password на сервере и клиенте. Автоматического
|
||||
переключения между Gecko и Salamander нет.
|
||||
|
||||
Query parser применяет RFC percent-decoding, но не form decoding: сырой `+`
|
||||
сохраняется как `+`, `%2B` также превращается в `+`, а `%26`/`%3D` безопасно
|
||||
|
||||
+7
-2
@@ -7,6 +7,9 @@ In-place upgrade 0.1.0 → 0.2.0, автоматическая миграция
|
||||
отклоняет upgrade. Установка выполняется через удаление старого пакета,
|
||||
архивирование его данных и настройку 0.2.0 с нуля.
|
||||
|
||||
Повторная установка ровно 0.2.0 поверх согласованного состояния 0.2.0 не
|
||||
является миграцией и поддерживается без этого runbook.
|
||||
|
||||
Описанные действия выполнять из console/maintenance-доступа. До успешного E2E
|
||||
anti-leak guard должен оставаться в nftables.
|
||||
|
||||
@@ -62,7 +65,7 @@ systemctl daemon-reload
|
||||
проверку: архивировать указанный объект и повторить `dpkg --configure
|
||||
vpn-egressctl`.
|
||||
|
||||
## 5. Настроить новую policy и Gecko URI
|
||||
## 5. Настроить новую policy и URI
|
||||
|
||||
Редактировать новый `/etc/vpn-egress/policy.json` вручную. Из старой policy
|
||||
можно перенести осознанно проверенные локальные значения, но нельзя заменять ею
|
||||
@@ -78,7 +81,9 @@ vpn-egressctl check
|
||||
vpn-egressctl diff
|
||||
```
|
||||
|
||||
Проверить, что URI содержит `obfs=gecko`. `check` не запускает service.
|
||||
Для новой production-установки проверить, что URI содержит `obfs=gecko`.
|
||||
`obfs=salamander` допускается только для явно настроенного server-side
|
||||
compatibility fallback. `check` не запускает service.
|
||||
|
||||
## 6. Активировать защиту и config
|
||||
|
||||
|
||||
+7
-5
@@ -12,8 +12,10 @@ sudo vpn-egressctl import --stdin
|
||||
sudo vpn-egressctl import --file /run/credentials/new-hysteria2.uri
|
||||
```
|
||||
|
||||
Production URI HY2XS должен использовать Gecko. Не передавать URI через argv,
|
||||
environment, shell history, тикет или журнал.
|
||||
Для новой установки production URI HY2XS должен использовать Gecko. Salamander
|
||||
допустим как compatibility fallback только при явно совпадающей серверной
|
||||
настройке. Не передавать URI через argv, environment, shell history, тикет или
|
||||
журнал.
|
||||
|
||||
## Изменение policy
|
||||
|
||||
@@ -57,6 +59,6 @@ systemctl reset-failed vpn-egress-sync.service
|
||||
## Обновления package/engine
|
||||
|
||||
0.2.0 не объявляет совместимость с другими версиями приложения или sing-box.
|
||||
Любой следующий переход выполняется по отдельному release runbook как clean
|
||||
install. `apt upgrade` не считается допустимой процедурой обновления этого
|
||||
control plane.
|
||||
Переход на другой release выполняется по отдельному release runbook как clean
|
||||
install. Повторная установка ровно 0.2.0 допускается при согласованных state и
|
||||
config; `apt upgrade` на другой release не считается допустимой процедурой.
|
||||
|
||||
+9
-3
@@ -45,9 +45,15 @@ runtime-состояние проверяет `vpn-egressctl doctor`.
|
||||
|
||||
## DNS и TLS
|
||||
|
||||
`dns_mode=disabled` запрещает новому TUN-механизму 1.14 менять native DNS и
|
||||
platform interception. DNS контролируется явной route rule. Chrome QUIC
|
||||
parroting включён (`disable_chrome_parrot=false`); production acceptance обязан
|
||||
`dns_mode=hijack` включает native DNS integration и platform-level interception
|
||||
TCP/UDP 53. При `auto_redirect` sing-box направляет non-local DNS на автоматически
|
||||
выведенный TUN address `172.19.0.2`; явная route rule не используется. Linux не
|
||||
может этим механизмом перенаправить DNS, адресованный локальному IP самого
|
||||
шлюза. DoH/DoT также не распознаются как DNS, но проходят через общий TUN/VPN
|
||||
маршрут как обычный трафик.
|
||||
|
||||
Chrome QUIC parroting включён upstream default; поле
|
||||
`disable_chrome_parrot=false` не рендерится. Production acceptance обязан
|
||||
подтвердить совместимый RSA/ECDSA/ACME certificate, поскольку Ed25519 с этим
|
||||
режимом несовместим.
|
||||
|
||||
|
||||
+15
-7
@@ -17,19 +17,27 @@
|
||||
```
|
||||
|
||||
512/1200 совпадает с profile HY2XS и текущими upstream defaults, но рендерится
|
||||
явно. Hysteria отмечает Gecko как experimental; проект осознанно принимает его
|
||||
как единственный production obfs сервера HY2XS.
|
||||
явно. Hysteria отмечает Gecko как experimental; проект осознанно использует его
|
||||
по умолчанию для новых production-установок HY2XS. Salamander остаётся полностью
|
||||
поддержанным compatibility fallback при совпадающей серверной настройке.
|
||||
|
||||
### TUN DNS
|
||||
|
||||
Новый default `dns_mode=hijack` не используется. Renderer задаёт `disabled`,
|
||||
потому что проект уже имеет явную DNS route rule и отдельные bootstrap/DoH
|
||||
маршруты. Это решение должно подтверждаться privileged Linux acceptance.
|
||||
Renderer явно задаёт `dns_mode=hijack`. При `auto_redirect=true` sing-box
|
||||
перехватывает TCP/UDP 53 на platform level. `dns_address` намеренно отсутствует:
|
||||
sing-box выводит следующий адрес из TUN subnet (`172.19.0.2`) и автоматически
|
||||
передаёт обращения к нему в DNS module. Явная route rule `hijack-dns` поэтому
|
||||
удалена.
|
||||
|
||||
Bootstrap DNS остаётся UDP outbound с `bind_interface=eth0`, remote DoH — с
|
||||
`detour=hy2-out`. Отсутствие bootstrap loop и DNS leak является обязательным
|
||||
privileged Linux acceptance gate.
|
||||
|
||||
### Chrome QUIC parroting
|
||||
|
||||
Renderer явно задаёт `disable_chrome_parrot=false`. Серверный certificate должен
|
||||
быть RSA/ECDSA/ACME, не Ed25519.
|
||||
Renderer не задаёт `disable_chrome_parrot`: в exact 1.14.0 upstream default
|
||||
оставляет Chrome QUIC parroting включённым. Серверный certificate должен быть
|
||||
RSA/ECDSA/ACME, не Ed25519.
|
||||
|
||||
### Congestion control
|
||||
|
||||
|
||||
+31
-12
@@ -5,7 +5,8 @@
|
||||
1. Unit: URI, policy, version, renderer, redaction и metadata.
|
||||
2. Failure injection: candidate rejection, restart failure, URI/config restore,
|
||||
unmanaged config, corrupted/legacy last-good.
|
||||
3. Packaging: MPL-2.0, exact dependency, upgrade rejection и postinst guard.
|
||||
3. Packaging: MPL-2.0, exact dependency, cross-release rejection,
|
||||
same-release reinstall и postinst guard.
|
||||
4. Real binary: официальный sing-box 1.14.0 для Gecko и Salamander configs.
|
||||
5. Privileged Linux: TUN, systemd, nftables, routing и DNS.
|
||||
6. HY2XS acceptance: Gecko TCP/UDP/DNS, failures, reconnect и reboot.
|
||||
@@ -38,15 +39,33 @@ Windows — schema decoding через `format`, поскольку Windows bina
|
||||
|
||||
В disposable Incus/VM с двумя NIC проверить:
|
||||
|
||||
1. чистую установку и отказ in-place upgrade;
|
||||
2. Gecko config и отсутствие last-good после первого apply;
|
||||
3. table 2022, rule 9000/32768, marks и NFQUEUE 100;
|
||||
4. bootstrap DNS через `eth0`, DoH через `hy2-out`;
|
||||
5. TCP/UDP/DNS с workload за `eth1`;
|
||||
6. отсутствие прямого WAN при остановленном sing-box;
|
||||
7. credential failure и same-release rollback;
|
||||
8. отказ legacy/mismatched metadata;
|
||||
9. A-record change и reboot recovery.
|
||||
1. собрать package и выполнить чистую установку;
|
||||
2. повторно установить ровно 0.2.0 поверх согласованного состояния;
|
||||
3. подтвердить отказ переходов 0.1.x → 0.2.0 и другой release → 0.2.0;
|
||||
4. применить Gecko config и подтвердить отсутствие `last-good` после первого
|
||||
apply;
|
||||
5. проверить table 2022, rule 9000/32768, marks и NFQUEUE 100;
|
||||
6. до установления HY2 разрешить hostname endpoint через `bootstrap-dns` по
|
||||
`eth0`, без recursion в `172.19.0.2`;
|
||||
7. подтвердить DoH `remote-dns` через `hy2-out`;
|
||||
8. с хоста выполнить `getent hosts example.com`, `dig @8.8.8.8 example.com` и
|
||||
`dig +tcp @8.8.8.8 example.com`;
|
||||
9. повторить оба `dig` с workload за `eth1` и через `tcpdump`/nft counters
|
||||
подтвердить отсутствие реального TCP/UDP 53 через `eth0`;
|
||||
10. при наличии `systemd-resolved` проверить DNS и route-only domain `~.` для
|
||||
`tun-sb0`; отдельно повторить запуск без доступного `resolvectl`;
|
||||
11. проверить TCP/UDP и внешний VPN IP с workload за `eth1`;
|
||||
12. подтвердить отсутствие прямого WAN при остановленном sing-box;
|
||||
13. проверить credential failure и same-release rollback;
|
||||
14. проверить отказ legacy/mismatched metadata и unmanaged identical config;
|
||||
15. проверить смену A-record, reconnect, stop/start и reboot recovery;
|
||||
16. после stop убедиться в очистке sing-box nftables/resolved state, после start
|
||||
— в полном восстановлении.
|
||||
|
||||
Server-side Gecko E2E выполняет владелец HY2XS. Salamander проверяется только на
|
||||
disposable test server и не добавляется в production server configuration.
|
||||
Server-side Gecko E2E выполняется с production-compatible HY2XS. Salamander E2E
|
||||
выполняется на disposable server, явно настроенном в compatibility-режим; это не
|
||||
автоматический fallback и не default новой production-установки.
|
||||
|
||||
Platform hijack не обслуживает DNS, направленный на локальный LAN IP gateway.
|
||||
Такой сценарий допустим только после добавления отдельного DNS listener и не
|
||||
входит в acceptance версии 0.2.0.
|
||||
|
||||
+12
-3
@@ -12,17 +12,19 @@ vpn-egressctl status --json
|
||||
Установлена не exact `1.14.0` либо отсутствуют `with_quic`/`with_gvisor`.
|
||||
Конфигурация не изменялась. Не обходить version gate.
|
||||
|
||||
## In-place upgrades are not supported
|
||||
## Cross-release upgrades are not supported
|
||||
|
||||
Пакет 0.2.0 устанавливается поверх старого релиза. Старый package остаётся
|
||||
Пакет 0.2.0 устанавливается поверх другого релиза. Старый package остаётся
|
||||
установленным; выполнить clean-install runbook из `docs/migration.md`.
|
||||
Повторная установка ровно 0.2.0 поддерживается, если state и config согласованы.
|
||||
|
||||
## State from another release / unmanaged configuration
|
||||
|
||||
Обнаружены сохранённые файлы 0.1.0 или конфиг вне управления 0.2.0. Остановить
|
||||
sing-box, убедиться в наличии guard, переместить старые данные в защищённый архив
|
||||
и повторить `dpkg --configure vpn-egressctl`. Не редактировать старый state так,
|
||||
чтобы обойти проверку.
|
||||
чтобы обойти проверку. Даже побайтово идентичный candidate не делает unmanaged
|
||||
config принадлежащим этому релизу.
|
||||
|
||||
## sing-box rejected generated configuration
|
||||
|
||||
@@ -54,3 +56,10 @@ Backup и metadata рассогласованы или повреждены. А
|
||||
|
||||
Проверить DNS, Gecko password, auth, SNI, certificate, UDP path и nftables.
|
||||
Успешный локальный healthcheck не заменяет forwarding E2E с `eth1`.
|
||||
|
||||
## DNS hijack не обслуживает LAN IP шлюза
|
||||
|
||||
`dns_mode=hijack` перехватывает TCP/UDP 53 только к non-local destination. Если
|
||||
LAN-клиент обращается к адресу самого gateway, например `10.20.0.1:53`, нужен
|
||||
отдельный DNS listener/service. Не добавлять `dns_address` вручную и не
|
||||
возвращать route rule как workaround: это меняет проверенный контракт renderer.
|
||||
|
||||
@@ -47,3 +47,5 @@ Units устанавливаются без автоматического enabl
|
||||
|
||||
Переход выполняется только по clean-install runbook с runtime mask для
|
||||
`sing-box.service`; anti-leak guard сохраняется до завершения acceptance.
|
||||
Повторная установка ровно версии 0.2.0 разрешена только при согласованных state
|
||||
и generated config; `preinst` отклоняет все cross-release переходы.
|
||||
|
||||
@@ -27,7 +27,6 @@ def render_config(policy: Policy, endpoint: Hy2Endpoint) -> dict[str, Any]:
|
||||
"server_name": endpoint.sni,
|
||||
"insecure": endpoint.insecure,
|
||||
},
|
||||
"disable_chrome_parrot": False,
|
||||
"bind_interface": network.upstream_interface,
|
||||
"domain_resolver": {"server": "bootstrap-dns", "strategy": dns.strategy},
|
||||
}
|
||||
@@ -79,7 +78,7 @@ def render_config(policy: Policy, endpoint: Hy2Endpoint) -> dict[str, Any]:
|
||||
"interface_name": network.tun_name,
|
||||
"address": [network.tun_address],
|
||||
"mtu": network.mtu,
|
||||
"dns_mode": "disabled",
|
||||
"dns_mode": "hijack",
|
||||
"auto_route": True,
|
||||
"iproute2_table_index": network.iproute2_table_index,
|
||||
"iproute2_rule_index": network.iproute2_rule_index,
|
||||
@@ -102,13 +101,6 @@ def render_config(policy: Policy, endpoint: Hy2Endpoint) -> dict[str, Any]:
|
||||
"strategy": dns.strategy,
|
||||
},
|
||||
"final": "hy2-out",
|
||||
"rules": [
|
||||
{
|
||||
"network": ["tcp", "udp"],
|
||||
"port": 53,
|
||||
"action": "hijack-dns",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -317,18 +317,6 @@ class Controller:
|
||||
) -> bool:
|
||||
target = Path(self.policy.sing_box.config_path)
|
||||
previous = self._installed_bytes()
|
||||
if previous == candidate_data:
|
||||
candidate_path.unlink(missing_ok=True)
|
||||
self._write_state(
|
||||
status="ok",
|
||||
version=version,
|
||||
config_data=candidate_data,
|
||||
source_data=source,
|
||||
endpoint=endpoint,
|
||||
changed=False,
|
||||
)
|
||||
return False
|
||||
|
||||
if previous is not None:
|
||||
if not self._installed_is_managed(previous, version):
|
||||
candidate_path.unlink(missing_ok=True)
|
||||
@@ -336,6 +324,17 @@ class Controller:
|
||||
"Installed configuration is not managed by this release; "
|
||||
"a clean installation is required"
|
||||
)
|
||||
if previous == candidate_data:
|
||||
candidate_path.unlink(missing_ok=True)
|
||||
self._write_state(
|
||||
status="ok",
|
||||
version=version,
|
||||
config_data=candidate_data,
|
||||
source_data=source,
|
||||
endpoint=endpoint,
|
||||
changed=False,
|
||||
)
|
||||
return False
|
||||
try:
|
||||
self._save_backup(previous, version)
|
||||
except OSError as exc:
|
||||
|
||||
+2
-13
@@ -37,7 +37,7 @@
|
||||
"172.19.0.1/30"
|
||||
],
|
||||
"mtu": 1400,
|
||||
"dns_mode": "disabled",
|
||||
"dns_mode": "hijack",
|
||||
"auto_route": true,
|
||||
"iproute2_table_index": 2022,
|
||||
"iproute2_rule_index": 9000,
|
||||
@@ -69,7 +69,6 @@
|
||||
"server_name": "fi.api.withen.pro",
|
||||
"insecure": false
|
||||
},
|
||||
"disable_chrome_parrot": false,
|
||||
"bind_interface": "eth0",
|
||||
"domain_resolver": {
|
||||
"server": "bootstrap-dns",
|
||||
@@ -90,16 +89,6 @@
|
||||
"server": "bootstrap-dns",
|
||||
"strategy": "ipv4_only"
|
||||
},
|
||||
"final": "hy2-out",
|
||||
"rules": [
|
||||
{
|
||||
"network": [
|
||||
"tcp",
|
||||
"udp"
|
||||
],
|
||||
"port": 53,
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
]
|
||||
"final": "hy2-out"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -25,11 +27,31 @@ class PackagingTests(unittest.TestCase):
|
||||
control = (ROOT / "debian/control").read_text(encoding="utf-8")
|
||||
self.assertIn("sing-box (= 1.14.0)", control)
|
||||
|
||||
def test_in_place_upgrade_is_rejected(self) -> None:
|
||||
def test_cross_release_upgrade_is_rejected_and_same_release_reinstall_allowed(self) -> None:
|
||||
preinst = (ROOT / "debian/preinst").read_text(encoding="utf-8")
|
||||
self.assertIn('[ "$1" = upgrade ]', preinst)
|
||||
self.assertIn('[ "$1" = upgrade ] && [ "${2:-}" != "0.2.0" ]', preinst)
|
||||
self.assertIn("in-place upgrades from another release", preinst)
|
||||
self.assertIn("exit 1", preinst)
|
||||
|
||||
@unittest.skipUnless(os.name == "posix", "Debian maintainer scripts require POSIX sh")
|
||||
def test_preinst_lifecycle_exit_codes(self) -> None:
|
||||
preinst = ROOT / "debian/preinst"
|
||||
cases = (
|
||||
(("install",), 0),
|
||||
(("upgrade", "0.2.0", "0.2.0"), 0),
|
||||
(("upgrade", "0.1.0", "0.2.0"), 1),
|
||||
(("upgrade", "0.2.1", "0.2.0"), 1),
|
||||
)
|
||||
for arguments, expected in cases:
|
||||
with self.subTest(arguments=arguments):
|
||||
result = subprocess.run(
|
||||
["/bin/sh", str(preinst), *arguments],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, expected, result.stderr)
|
||||
|
||||
def test_postinst_checks_for_legacy_state(self) -> None:
|
||||
postinst = (ROOT / "debian/postinst").read_text(encoding="utf-8")
|
||||
self.assertIn("vpn_egressctl.installcheck", postinst)
|
||||
|
||||
@@ -38,7 +38,8 @@ class RendererTests(unittest.TestCase):
|
||||
self.assertEqual(tun["auto_redirect_input_mark"], "0x2023")
|
||||
self.assertEqual(tun["auto_redirect_output_mark"], "0x2024")
|
||||
self.assertEqual(tun["auto_redirect_reset_mark"], "0x2025")
|
||||
self.assertEqual(tun["dns_mode"], "disabled")
|
||||
self.assertEqual(tun["dns_mode"], "hijack")
|
||||
self.assertNotIn("dns_address", tun)
|
||||
outbound = config["outbounds"][0]
|
||||
self.assertEqual(outbound["server"], "fi.api.withen.pro")
|
||||
self.assertEqual(outbound["password"], "AUTH")
|
||||
@@ -46,12 +47,13 @@ class RendererTests(unittest.TestCase):
|
||||
self.assertEqual(outbound["obfs"]["type"], "gecko")
|
||||
self.assertEqual(outbound["obfs"]["min_packet_size"], 512)
|
||||
self.assertEqual(outbound["obfs"]["max_packet_size"], 1200)
|
||||
self.assertFalse(outbound["disable_chrome_parrot"])
|
||||
self.assertNotIn("disable_chrome_parrot", outbound)
|
||||
self.assertNotIn("bbr_profile", outbound)
|
||||
self.assertEqual(outbound["bind_interface"], "eth0")
|
||||
self.assertEqual([item["tag"] for item in config["outbounds"]], ["hy2-out"])
|
||||
self.assertEqual(config["dns"]["servers"][1]["detour"], "hy2-out")
|
||||
self.assertEqual(config["route"]["final"], "hy2-out")
|
||||
self.assertNotIn("rules", config["route"])
|
||||
self.assertNotIn("185.156.108.141", render_bytes(policy, endpoint).decode())
|
||||
|
||||
def test_salamander_fallback_does_not_render_gecko_fields(self) -> None:
|
||||
|
||||
@@ -119,11 +119,49 @@ class TransactionTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
controller, runner, policy = self.make(directory)
|
||||
endpoint = parse_hysteria2_uri(URI_NEW)
|
||||
Path(policy.sing_box.config_path).write_bytes(render_bytes(policy, endpoint))
|
||||
installed = render_bytes(policy, endpoint)
|
||||
Path(policy.sing_box.config_path).write_bytes(installed)
|
||||
self.mark_managed(controller, installed, URI_NEW)
|
||||
self.assertFalse(controller.sync())
|
||||
self.assertFalse(any("restart" in call for call in runner.calls))
|
||||
self.assertFalse(json.loads(controller.state_path.read_text())["changed"])
|
||||
|
||||
def test_identical_unmanaged_config_is_not_adopted(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
controller, runner, policy = self.make(directory)
|
||||
endpoint = parse_hysteria2_uri(URI_NEW)
|
||||
installed = render_bytes(policy, endpoint)
|
||||
target = Path(policy.sing_box.config_path)
|
||||
target.write_bytes(installed)
|
||||
|
||||
with self.assertRaisesRegex(ValidationError, "clean installation"):
|
||||
controller.sync()
|
||||
|
||||
self.assertEqual(target.read_bytes(), installed)
|
||||
self.assertFalse(controller.state_path.exists())
|
||||
self.assertFalse(controller.last_good_path.exists())
|
||||
self.assertFalse(any("restart" in call for call in runner.calls))
|
||||
self.assertEqual(list(target.parent.glob(f".{target.name}.candidate.*")), [])
|
||||
|
||||
def test_import_restores_uri_when_identical_config_is_unmanaged(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
controller, runner, policy = self.make(directory)
|
||||
uri_path = Path(policy.runtime.uri_path)
|
||||
uri_path.write_text(URI_OLD + "\n", encoding="utf-8")
|
||||
endpoint = parse_hysteria2_uri(URI_NEW)
|
||||
installed = render_bytes(policy, endpoint)
|
||||
target = Path(policy.sing_box.config_path)
|
||||
target.write_bytes(installed)
|
||||
|
||||
with self.assertRaisesRegex(ValidationError, "clean installation"):
|
||||
controller.import_uri(URI_NEW)
|
||||
|
||||
self.assertEqual(uri_path.read_text(encoding="utf-8").strip(), URI_OLD)
|
||||
self.assertEqual(target.read_bytes(), installed)
|
||||
self.assertFalse(controller.state_path.exists())
|
||||
self.assertFalse(any("restart" in call for call in runner.calls))
|
||||
self.assertEqual(list(target.parent.glob(f".{target.name}.candidate.*")), [])
|
||||
|
||||
def test_generated_config_rejection_is_non_mutating(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
controller, _, policy = self.make(directory, runner=FakeRunner(check_result=1))
|
||||
@@ -247,7 +285,9 @@ class TransactionTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
controller, _, policy = self.make(directory)
|
||||
endpoint = parse_hysteria2_uri(URI_NEW)
|
||||
Path(policy.sing_box.config_path).write_bytes(render_bytes(policy, endpoint))
|
||||
installed = render_bytes(policy, endpoint)
|
||||
Path(policy.sing_box.config_path).write_bytes(installed)
|
||||
self.mark_managed(controller, installed, URI_NEW)
|
||||
controller.sync()
|
||||
Path(policy.runtime.uri_path).write_text(URI_OLD + "\n", encoding="utf-8")
|
||||
status = controller.status()
|
||||
|
||||
Reference in New Issue
Block a user