From f9dd4d9f9b7aa26ee45e47fca5fa1ccf02e4b4ec Mon Sep 17 00:00:00 2001 From: h8suga Date: Tue, 14 Jul 2026 03:21:40 +0300 Subject: [PATCH] feat(notify): unified notification manager + webhooks + shell bell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships an end-to-end notification pipeline: ring buffer + persistence, HTTP CRUD + SSE stream, backend Push() calls at ~20 async/scheduled sites, Telegram+generic HTTP dispatcher, shell bell icon with sliding panel, and a Notifications settings section for webhook management. Backend (Agents U + V): - New pkg internal/services/notify — cap-500 ring, SSE broadcast via events.DefaultHub topic "notifications", MarkRead / MarkAllRead / Delete / Clear + persistence to /notifications.json with 500 ms debounced atomic write. - Webhook dispatcher: goroutine consumes notify.PushChan, fan-out to every matching hook. Filters all / errors_only / warn_and_above / success_and_above. Retry 3× with 1/2/4 s backoff, 10 s per-request timeout. Telegram POST {chat_id, text=title+body, parse_mode:HTML}; generic POST raw Notification JSON. LastError / LastSuccessAt stamped per attempt. - 11 Push sites: feeds.fetch / feeds.error, routing.apply / commit / rollback, awg.tunnel_up / tunnel_down / handshake_recovered, update.available / applied / failed, pihole.enabled / disabled / container_restarted, auth.login / auth.failed (now with client IP + User-Agent via new AuthClient meta), backup.import / export, panic.recovered (server.go recoverer middleware), ipmeta.reload. Frontend (Agents W + X): - lib/notifications.ts — module-level singleton store, useSyncExternalStore hook, refcounted SSE + initial GET, backoff+ jitter reconnect, optimistic UI actions, sonner-toast fallback for incoming errors when the panel is closed. - NotifyBell + NotifyPanel — bell now lives in the desktop sidebar HEADER next to the "NFQWS2" title (was previously footer-only per user request), still mounted in mobile top bar. Sheet panel groups by Today / Yesterday / Older, filter chips (All / Errors / Warnings / Unread), row = colored dot + title (bold if unread) + 2-line body + relative timestamp (Intl.RelativeTimeFormat) + source badge + hover dismiss X. Panel header pr-12 so the sheet's built-in close X no longer collides with "Clear all". - Notifications settings under System — WebhookTable + Add/Edit Dialog with kind-dependent labels (Telegram bot-token URL + chat_id vs generic URL), inline validation, Test button hitting the backend test endpoint. - All strings routed through dict.ts (en+ru) — 60+ new keys under notify.*. Wiring in server.go / app.go: RegisterNotifyAPI + RegisterNotifyWebhooksAPI + /api/notifications/stream SSE alias + notify.InitDispatcher on boot. Co-Authored-By: Claude Opus 4.7 --- .DS_Store | Bin 0 -> 10244 bytes 0 | 2 + AWG_ROADMAP.md | 183 +++++++++++++ bench-results/193.233.126.94.md | 83 ++++++ bench-results/2.26.224.64.md | 79 ++++++ bench-results/2.26.252.43.md | 66 +++++ bench-results/31.76.103.176.md | 91 +++++++ bench-results/SUMMARY.md | 79 ++++++ bench-results/SUPPORT-REPORT.md | 107 ++++++++ bench-server.sh | 65 +++++ full_info_old.txt | 222 +++++++++++++++ nfqws2-selector | 1 + recovery/be7000_recovery.py | 464 ++++++++++++++++++++++++++++++++ rules/rr | 247 +++++++++++++++++ 14 files changed, 1689 insertions(+) create mode 100644 .DS_Store create mode 100644 0 create mode 100644 AWG_ROADMAP.md create mode 100644 bench-results/193.233.126.94.md create mode 100644 bench-results/2.26.224.64.md create mode 100644 bench-results/2.26.252.43.md create mode 100644 bench-results/31.76.103.176.md create mode 100644 bench-results/SUMMARY.md create mode 100644 bench-results/SUPPORT-REPORT.md create mode 100755 bench-server.sh create mode 100644 full_info_old.txt create mode 160000 nfqws2-selector create mode 100644 recovery/be7000_recovery.py create mode 100644 rules/rr diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2a9580b13a07ea9d1ade0f8f386a4f30e29082f4 GIT binary patch literal 10244 zcmeHML2nyH6n^Wt@!C#WgFxZ}T0lYwm6Etk3*zF2M2G_d#T7l!%Jte#Vs*Wnc-L{G zD3Z_gPxQcn0}@w$3IBx?eDCc#nwgy%aVV<9%xd zQCYS&aIzG>&$U!4#g!dM4|t*jdO)`*q&{UOZFj*j;23ZWI0hU8j)B*K0nFK4{AMoQ z*)iZ4a12~A!1*CSW!Wj@BA1dnaFQheWEGdCppA8aV1hz+3c1LoK+$KudhiOUS1d8S z9FF?|t3!4QxyWS>CohMSS6KFn6^bX;feS>PyiP9N*)iZ4C^Nuo_XfRBU98;itLyh~ zqGS+8Nr?MAq<%xa^_LX1h?>;HaYE-bz-oU$$28(A2I{_nYKd+}V>**P0>*@t;K;CV zfZlYx_pX7z4j&)V*FYW6cWCd?w>WrsSJHn!h zX&Sz5Te)ZcR-xKf6!W+Wb=!s#CRd^E*ig)$_ld%v`CL0z6e4*g>aGpN9Iiy&v!cM^ zO4R386gbpS99L7s5$~$yBZfy@9>X(Op6;8+%&GcsKz~LNjvjC^ekEun_&tO@ z%SiE9*&^$=;1gXCiC57 z77tUwcJmv4-LLmr?_6AL?`+)++V^%YZw41%w9&qE`|jms-GBST&%Qk9J&h)D@)L3> zis$8$HVe5FoSFZDqqFWbj*^&XDDGt)?`ei89wIu7S&T=f#-ES3+6U*UJzgbhRKDvH z1??gxY;nf%qaxOUIEOdK@G92Fy+n?xyD7Pd=ieSGM^S1Y2^>XhA1G0@p&*wT!{Qys zkA|z`+hQNR@wdf3;!F?eGdq9wka;+=2~92KJ@!?V3KmudKGKyfw% z8)g19&6;S&=vV%g(Y1xM7Yb+VSK7!KhA#7@>ek^cfAu{>9oWV-N0x?I!#khQiQX^A zlcx11o-D?*B{IkLWKQ9{Jrkywp>h^PglT z6{cOEjseHOYr=q6-Rth{!kOiQ!}N>y+5^-ls9acYkxPN#B-`` перед `-j MARK` | +| `internal/services/awgroute/mode.go` (и `mode_test.go`) | покрыть тестом: одна зона с SourceIPs матчит только нужный IP | +| UI: `internal/server/web/index.html` | в форме зоны под доменами добавить поле «Источники (IP/CIDR)» + помощь «оставь пустым → для всех» | + +**Готов критерий**: одна зона с `source_ips: ["192.168.31.243"]` и `*` в доменах +гонит ровно `.243` через awg0, остальные устройства идут напрямую. + +## Phase 1 — geosite в zones + автообновление + +### 1a. Geo доступен в awgroute + +| Файл | Что | +|---|---| +| `internal/tools/geo/geo.go` | публичный метод `Enumerate(kind, category string) ([]string, error)` | +| `internal/app/app.go` | передать `*geo.Geo` в `awgroute.New(...)` | +| `internal/services/awgroute/awgroute.go` | хранить `geo *geo.Geo`, использовать на резолве | + +### 1b. Префиксы в zone-парсере + +В существующем поле «домены» зоны строки `geosite:CATEGORY`, `domain:foo.bar`, +`full:exact.bar` распознаются: + +- `domain:foo.bar` → суффиксная маска (как `*.foo.bar`) +- `full:foo.bar` → ровный матч +- `geosite:CN` → разворачивается через `geo.Enumerate("geosite","cn")` на лету + при `Apply()`. Если geosite.dat ещё не загружен → ошибка в `Apply`-ответ + «загрузите geosite.dat или включи автообновление». +- `geoip:CN` → раскрывается в CIDR-список в существующее поле IPs. +- `list:NAME` → читает `/opt/etc/nfqws2/lists/NAME.list` (те же файлы что nfqws2 + использует для hostlist/ipset DPI-фильтров: `user`, `exclude`, `auto`, + `ipset`, `ipset_exclude`). Распознавание IPs vs доменов по `isIPish()`. + +| Файл | Что | +|---|---| +| `internal/services/awgroute/sets_linux.go` (или где идёт `range zone.Domains`) | новый helper `expandDomains(zone, geo) (plain []string, ips []string, err error)` | +| `internal/services/awg/match.go` | парсер префиксов | + +### 1c. Фоновый фетчер + +| Файл | Что | +|---|---| +| `internal/services/awgroute/awgroute.go` | в `New(...)` стартует goroutine с тикером (по умолчанию 24h, выкл если URL пуст) | +| `internal/services/awgroute/geo_fetch.go` (новый) | `func fetchGeo(ctx, url, kind) ([]byte, error)`, кладёт в существующий `Geo` store через `geo.Upload(...)` | +| `internal/services/awg/config.go` | новый блок `GeoUpdate { Enabled bool; GeositeURL, GeoipURL string; IntervalHours int; LastFetchedAt int64; LastError string }` в `ServerConfig` | + +Дефолт URL: `https://github.com/v2fly/domain-list-community/releases/latest/download/geosite.dat` +и `https://github.com/v2fly/geoip/releases/latest/download/geoip.dat`. + +### 1d. UI: панель «Geo» + +В `index.html` (vite single-file, придётся `npm --prefix frontend run build`): + +- В Routing tab над списком зон секция «GeoSite / GeoIP»: + - текущая версия + `last_fetched_at` + - две URL-поля (geosite/geoip), интервал в часах, тогл «авто» + - кнопка «Обновить сейчас» → `POST /api/geo/fetch-now` + - последняя ошибка (если была) + +| Файл | Что | +|---|---| +| `internal/server/server.go` | новый `POST /api/geo/fetch-now`, ручка зовёт fetcher синхронно | +| `frontend/src/...` | компонент GeoPanel | +| `scripts/build.sh` | без изменений (vite-build уже есть) | + +## Phase 2 — Xray JSON importer + +### 2a. Endpoint + +`POST /api/awg2/routing/import-xray` принимает: +```json +{"json": "", "dry_run": true} +``` + +Парсит `routing.rules`: +- `outboundTag: "proxy"` → Include-зона (в туннель) +- `outboundTag: "direct"` → Exclude-зона (мимо туннеля) +- `outboundTag: "block"` → в новую отдельную Drop-зону (опционально; иначе игнор с предупреждением) + +Каждое правило раскидывается в `domains`/`ips` по типу: +- `domain` → строки `domain:`/`geosite:`/`regexp:` сохраняются с префиксом, парсер фазы 1 их расширит +- `ip` → CIDR в `ips`, `geoip:CN` сохраняется с префиксом +- `port`/`protocol`/`source`/`inboundTag` → не поддерживаем, в ответе + `dropped: [{rule_index, reason}]` + +### 2b. Конвертер regexp → wildcards + +`internal/services/awg/xray_regex.go` (новый): + +- `(^|\.)foo\.(com|ru)$` → `*.foo.com`, `*.foo.ru` +- `(^|\.)foo[a-z0-9-]*\.com$` → `*.foo*.com` (если поддержим), иначе сохранить как `regexp:` для фазы 3 +- Сложнее — оставить как `regexp:` (фаза 3) с пометкой `lossy` + +### 2c. UI: «Импорт Xray JSON» + +В Routing tab кнопка → модалка с textarea (paste JSON): +- сначала dry_run, показывает preview: + ``` + Зона proxy: 1247 доменов, 2 CIDR (выйдет в туннель) + Зона direct: 156 доменов, 2 CIDR (мимо туннеля) + Пропущено: 3 правила (port-based, inboundTag) + ``` +- кнопка «Применить» → второй POST без dry_run, заменяет существующие зоны + +| Файл | Что | +|---|---| +| `internal/server/server.go` | новый handler | +| `internal/services/awg/xray_import.go` (новый) | парсер + конвертер | +| `internal/services/awg/xray_regex.go` (новый) | regexp → wildcards | +| `frontend/src/...` | компонент XrayImportDialog | + +## Phase 3 — Regexp / keyword runtime + +`iptables --match-set` работает по IP, а не по домену. Domain-уровневый матч идёт +через `snisniff_linux.go` (читает SNI из TCP ClientHello). + +### 3a. Хранение скомпилированных матчеров + +| Файл | Что | +|---|---| +| `internal/services/awg/config.go` | в `Zone` новые поля: `Regexps []string`, `Keywords []string` | +| `internal/services/awgroute/sni.go` | при `Apply` компилировать regexp'ы один раз, держать кэш | + +### 3b. Матчер в SNI sniffer + +| Файл | Что | +|---|---| +| `internal/services/awgroute/snisniff_linux.go` | в hot-path callback'е (когда пришёл SNI): проверка по compiled regexp + substring keyword'ам. Матч → resolve(SNI host) → запихнуть IP в существующий ipset `awgSetSNI` | + +Текущая инфраструктура (MARK по ipset, ip rule, ip route, NAT) переиспользуется +как есть. + +### 3c. UI + +Тот же textarea зоны автоматически принимает строки: +``` +regexp:^(.*\.)?yandex\.(ru|com)$ +keyword:porn +``` + +Парсер 1b и 2b их раскидывает по `Regexps`/`Keywords` (не путать с `Domains`). + +## Сборка / деплой + +```sh +cd /tmp/nfqws2-sel +npm --prefix frontend install +npm --prefix frontend run build # для фаз 1d, 2c +GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' \ + -o /tmp/nfqws2-strategy ./cmd/nfqws2-strategy +scp /tmp/nfqws2-strategy root@192.168.31.1:/opt/usr/bin/nfqws2-strategy +ssh root@192.168.31.1 ' + kill $(pidof nfqws2-strategy) 2>/dev/null + ( /opt/usr/bin/nfqws2-strategy serve /opt/var/log/nfqws2-strategy/serve.log 2>&1 & ) +' +``` + +## Smoke-тесты после деплоя + +1. Фаза 0: зона `{source_ips:[".243"], domains:["*"]}` → с .243 `ipinfo.io` показывает `185.209.21.51`, с другого устройства — реальный IP. +2. Фаза 1: импортить `geosite.dat`, зона с `geosite:cn` → доменов > 1000 в ответе `apply`. +3. Фаза 2: paste минимального xray JSON, dry_run, preview корректный. +4. Фаза 3: добавить `regexp:^(.*\.)?example\.com$`, открыть `https://sub.example.com` — IP попадает в ipset, трафик идёт в awg0. + +## Что НЕ делаем (намеренно) + +- `keyword:` substring через iptables `-m string` — пакеты могут быть фрагментированы, ложные срабатывания, не делаем +- `inboundTag` xray-стиля — у нас нет input-tag концепции, пропускаем с предупреждением +- `domainStrategy: IPIfNonMatch` и подобные — резолвим всё через выбранный `DomainSource` +- `port`/`protocol` правила — селектор работает на L3, не L4-routing, пропускаем diff --git a/bench-results/193.233.126.94.md b/bench-results/193.233.126.94.md new file mode 100644 index 0000000..cbbcf79 --- /dev/null +++ b/bench-results/193.233.126.94.md @@ -0,0 +1,83 @@ +# Server bench: 193.233.126.94 (Hysteria2 host) + +**Дата**: 2026-07-09 +**Хостер**: dhost.su (VM `1083448745-iwqfmwg.dhost.su`, 1 vCPU, 2 GB RAM, kernel 6.8) +**Роль**: VPN endpoint (Hysteria2 UDP :443) +**Тестовый сегмент**: WAN, роутер `88.201.133.115` (ISP SkyNet SPB) ↔ VPS + +--- + +## 1. Reverse-path MTR: сервер → router WAN 88.201.133.115 + +``` +HOST: 193.233.126.94 Loss% Snt Last Avg Best Wrst StDev + 1. 94.103.1.1 0.0% 10 4.8 2.5 0.9 4.8 1.5 (AS209207 DHOST, RU) + 2. 146.19.191.175 0.0% 10 0.9 4.4 0.8 18.3 6.7 (AS49581 Tube-Hosting, DE) + 3. 185.0.44.28 0.0% 10 56.4 56.6 56.2 57.1 0.3 (transit) + 4. 185.37.128.156 0.0% 10 55.3 55.0 54.7 55.6 0.4 (AS35807 SkyNet SPB) + 5. 185.37.128.225 0.0% 10 54.6 54.6 54.5 55.2 0.2 (AS35807 SkyNet SPB) + 6. 185.37.128.189 0.0% 10 57.3 56.9 56.5 57.4 0.4 (AS35807 SkyNet SPB) + 7. 185.37.128.166 0.0% 10 57.5 70.2 57.4 115.1 19.0 (AS35807 SkyNet SPB) + 8. ??? 100.0 10 (silent — обычно target-firewall) +``` + +**Вердикт**: путь **чистый**. 0% loss на всём отвечающем сегменте. +Тот же SkyNet AS35807 что в старом кейсе 31.76.103.176 (где `185.37.128.156` был StDev 1192ms, 30% loss) — сейчас работает как надо (Avg 55ms, StDev 0.2-0.4). +Лёгкий скачок на hop 7 (StDev 19ms) — терпимо. + +--- + +## 2. Forward-path MTR: srv (192.168.31.243) → 193.233.126.94 + +``` +HOST: len-srv Loss% Snt Last Avg Best Wrst StDev + 1. 192.168.31.1 0.0% 10 1.2 1.5 1.1 2.5 0.5 (router) + 2. 88.201.128.1 0.0% 10 2.0 1.8 1.7 2.0 0.1 (AS35807 SkyNet SPB — ISP) + 3. 185.37.128.167 0.0% 10 1.9 2.0 1.8 2.3 0.1 (AS35807) + 4. 185.37.128.188 0.0% 10 3.3 3.1 2.2 4.1 0.5 (AS35807) + 5. ??? 100.0 10 (silent hop) + 6. 185.37.128.157 0.0% 10 2.1 2.4 1.9 3.3 0.4 (AS35807) + 7. 185.0.44.98 0.0% 10 54.3 56.1 53.1 75.5 6.8 (transit — RU→DE hop) + 8. 146.19.191.174 0.0% 10 68.1 61.0 56.6 68.5 4.9 (AS49581 Tube-Hosting, DE) + 9. 193.233.126.94 0.0% 10 57.0 56.9 55.1 57.9 0.7 (target) +``` + +**Вердикт**: чисто. 0% loss везде, target hop StDev 0.7ms. + +--- + +## 3. iperf3 direct WAN (роутер → сервер, оба BBR) + +| Test | Rate | Loss/Retr | +|---|---:|---| +| **UDP 300M UP** (router→server) | **298 Mbit/s** | 0/259004 packets (0%) | +| **UDP 300M DL** (server→router) | **299 Mbit/s** | 912/258982 packets (0.35%) | +| **TCP BBR UP** | **408 Mbit/s** | — | +| **TCP BBR DL** | **265 Mbit/s** | — | + +**Вердикт**: физический path clean, TCP BBR даёт ~410/265 Mbit, UDP ~300M без потерь. +Download чуть проседает под нагрузкой (UDP 0.35% loss на 300M) — типичный ISP-shape в reverse-направлении. + +--- + +## 4. Hysteria2 tunnel через этот сервер + +| Test | Rate | +|---|---:| +| iperf3 UP через hys tunnel | 328 Mbit/s | +| iperf3 DL через hys tunnel | 250 Mbit/s | +| curl bench UP (cloudflare) через hys | 156 Mbit/s | +| curl bench DL (cachefly) через hys | 210 Mbit/s | + +Overhead tunnel vs direct WAN: ~20% (QUIC + userspace TUN) + +--- + +## Как читать (общие критерии для нового сервера) + +- **Reverse-mtr — самое важное**. Server→router путь чаще всего убитый. + - Плохо: Avg > 100ms или StDev > 50ms или loss > 5% на любом хопе → single-TCP CUBIC загнётся в ~5 Mbit + - Хорошо: 0% loss везде, StDev < 10ms (как здесь) +- **Forward-mtr обычно чистый** — но всё равно надо иметь для сравнения +- **UDP iperf** = физический ceiling пути без TCP-CC шума +- **Delta UDP↔TCP** широкая (например UDP 400M / TCP 30M) = loss/jitter, CUBIC не тянет — нужен BBR либо Brutal diff --git a/bench-results/2.26.224.64.md b/bench-results/2.26.224.64.md new file mode 100644 index 0000000..832d0ff --- /dev/null +++ b/bench-results/2.26.224.64.md @@ -0,0 +1,79 @@ +# Server bench: 2.26.224.64 + +**Дата**: 2026-07-09 +**Хостер**: tihost.com (`my-server-25.tihost.com`, 1 vCPU, 2 GB RAM, kernel 6.8) +**Тестовый сегмент**: WAN, роутер `88.201.133.115` (ISP SkyNet SPB) ↔ VPS + +--- + +## 1. Reverse-path MTR: сервер → router WAN 88.201.133.115 + +``` +HOST: 2.26.224.64 Loss% Snt Last Avg Best Wrst StDev + 1. 10.0.0.1 0.0% 10 0.1 0.1 0.1 0.2 0.0 (private, VPS lan-gw) + 2. 2.59.253.41 0.0% 10 0.2 0.2 0.2 0.3 0.0 (AS215730 H2NEXUS, GB) + 3. 2.59.253.33 0.0% 10 0.2 0.2 0.2 0.3 0.0 (AS215730 H2NEXUS, GB) + 4. 144.31.4.50 0.0% 10 2.0 2.6 1.8 6.6 1.6 (AS215730 H2NEXUS, GB) + 5. 80.77.167.98 0.0% 10 2.5 3.6 1.8 11.1 3.1 (AS28917 Fiord Networks, LT) + 6. 80.77.167.152 0.0% 10 29.6 23.1 20.3 32.0 4.1 (AS28917 Fiord, LT — jitter) + 7. 62.140.245.32 0.0% 10 20.5 22.1 20.5 29.6 3.0 (AS51369 FIORD-RU, RU) + 8. 62.140.245.203 0.0% 10 28.4 34.1 28.4 85.4 18.0 (AS51369 FIORD-RU — StDev 18!) + 9. 93.191.9.59 0.0% 10 30.3 30.3 30.2 30.5 0.1 (AS51369 FIORD-RU) +10. ??? 100.0 10 (silent) +11. 185.37.128.225 0.0% 10 28.6 28.7 28.6 28.9 0.1 (AS35807 SkyNet SPB) +12. 185.37.128.189 0.0% 10 28.7 28.7 28.6 28.8 0.1 (AS35807 SkyNet SPB) +13. 185.37.128.166 0.0% 10 30.6 30.7 30.6 30.8 0.1 (AS35807 SkyNet SPB) +14. ??? 100.0 10 (silent — target FW) +``` + +**Вердикт**: путь **чистый** (0% loss везде). +- Hop 8 (62.140.245.203) StDev **18ms** — единственная точка с джиттером, но loss 0%. +- RTT ~30ms до target. +- Транзит: H2NEXUS (GB) → Fiord (LT) → FIORD-RU → SkyNet SPB. Международка, но без потерь. +- **Лучше чем 31.76 и 2.26.252.43** — 0% loss везде vs 10%. + +--- + +## 2. Forward-path MTR: srv → 2.26.224.64 + +``` +HOST: len-srv Loss% Snt Last Avg Best Wrst StDev + 1. 192.168.31.1 0.0% 10 1.2 1.4 1.1 1.8 0.2 (router) + 2. ??? 100.0 10 (silent) + 3. 2.26.224.64 0.0% 10 2.4 2.5 1.7 3.2 0.4 (target) +``` + +**Вердикт**: цель в 3 хопах, RTT 2.5ms — идёт через tunnel. + +--- + +## 3. iperf3 direct WAN (роутер → сервер, оба BBR) + +| Test | Rate | Loss/Retr | +|---|---:|---| +| **UDP 300M UP** (router→server) | **184 Mbit/s** | 102348/274044 packets (**37%**) | +| **UDP 300M DL** (server→router) | **178 Mbit/s** | 101325/263836 packets (**38%**) | +| **TCP BBR UP** | **341 Mbit/s** | — | +| **TCP BBR DL** | **179 Mbit/s** | — | + +**Вердикт**: TCP UP 341M ~ такой же ceiling как у SRV1 (тот же ISP-shape ~180M UDP). +TCP DL 179 Mbit — **в 1.6× выше чем у 2.26.252.43** (там 111M) — потому что reverse-mtr тут чистый vs 10% loss на 252.43. + +--- + +## Сравнение всех замеренных серверов + +| Сервер | RTT (rev) | Rev-mtr worst | TCP UP | TCP DL | UDP 300M loss | +|---|---:|---|---:|---:|---:| +| **193.233.126.94** (dhost.su) | 57 ms | 0% | **408** | 265 | 0% / 0.35% | +| **2.26.224.64** (tihost.com) | 30 ms | 0% (StDev 18ms) | 341 | 179 | 37% / 38% | +| **2.26.252.43** (tihost.com) | 41 ms | 10% loss | 332 | 111 | 37% / 39% | +| **31.76.103.176** (tihost.com) | 27 ms | 10% loss (Telia) | 89 ⚠ | 239 | 44% / 38% | + +**Итоги**: +- **193.233.126.94 — рекордсмен** (408 Mbit UP, 0% UDP loss, чистый reverse-path) +- **2.26.224.64 — второй** (341/179, 0% loss) +- **2.26.252.43 — DL слабый** (111 Mbit) из-за reverse-loss +- **31.76.103.176 — только для build** (89 UP) + +Все tihost.com имеют один ISP-cap ~180 Mbit UDP UP. dhost.su (193.233) выделяется полосой ×2. diff --git a/bench-results/2.26.252.43.md b/bench-results/2.26.252.43.md new file mode 100644 index 0000000..de30cbe --- /dev/null +++ b/bench-results/2.26.252.43.md @@ -0,0 +1,66 @@ +# Server bench: 2.26.252.43 + +**Дата**: 2026-07-09 +**Хостер**: tihost.com (`my-server-24.tihost.com`, 1 vCPU, 2 GB RAM, kernel 6.8) +**Тестовый сегмент**: WAN, роутер `88.201.133.115` (ISP SkyNet SPB) ↔ VPS + +--- + +## 1. Reverse-path MTR: сервер → router WAN 88.201.133.115 + +``` +HOST: 2.26.252.43 Loss% Snt Last Avg Best Wrst StDev + 1. 10.0.0.1 0.0% 10 0.2 0.2 0.1 0.2 0.0 (private, VPS lan-gw) + 2. ??? 100.0 10 (silent) + 3. ??? 100.0 10 (silent) + 4. ??? 100.0 10 (silent) + 5. ??? 100.0 10 (silent) + 6. ??? 100.0 10 (silent) + 7. ??? 100.0 10 (silent) + 8. 80.64.103.55 0.0% 10 41.1 41.0 40.7 41.4 0.2 (AS20764 RASCOM, RU) + 9. ??? 100.0 10 (silent) +10. 185.37.128.225 0.0% 10 41.1 41.0 40.8 41.4 0.2 (AS35807 SkyNet SPB) +11. ??? 100.0 10 (silent) +12. 185.37.128.166 10.0% 10 41.2 41.3 41.0 41.8 0.3 (AS35807 SkyNet SPB — 10% loss) +13. ??? 100.0 10 (silent — target FW) +``` + +**Вердикт**: путь **терпимый, но не блестящий**. +- Много silent-хопов (7 подряд) — операторы фильтруют ICMP-TTL — не проблема сама по себе. +- **hop 12 (SkyNet SPB) держит 10% loss** — та же точка что мучала 31.76 сервер (было там же). +- RTT ~41ms стабильный, StDev < 0.5ms. +- Транзит через RASCOM (RU AS20764) → сразу к SkyNet SPB, без международки. Короткий но с шумом. + +--- + +## 2. Forward-path MTR: srv → 2.26.252.43 + +``` +HOST: len-srv Loss% Snt Last Avg Best Wrst StDev + 1. 192.168.31.1 0.0% 10 2.8 2.3 1.4 2.8 0.4 (router) + 2. ??? 100.0 10 (silent) + 3. 2.26.252.43 0.0% 10 1.3 1.4 1.2 1.6 0.1 (target) +``` + +**Вердикт**: цель в 3 хопах, RTT 1.4ms — идёт через tunnel (не WAN). + +--- + +## 3. iperf3 direct WAN (роутер → сервер, оба BBR) + +| Test | Rate | Loss/Retr | +|---|---:|---| +| **UDP 300M UP** (router→server) | **183 Mbit/s** | 101710/274135 packets (**37%**) | +| **UDP 300M DL** (server→router) | **177 Mbit/s** | 103307/264662 packets (**39%**) | +| **TCP BBR UP** | **332 Mbit/s** | — | +| **TCP BBR DL** | **111 Mbit/s** | ⚠ низкий | + +**Вердикт**: TCP UP норм 332M, но **TCP DL всего 111 Mbit** — коррелирует с 10% loss на reverse-mtr hop 12. +UDP 300M target тонет на ~180 Mbit — потолок ISP. + +--- + +## Как читать + +- **UDP 300M ceiling ~180 Mbit** = тот же ISP-shape что у 31.76. +- **TCP DL 111 Mbit** = 10% loss + BBR не спасает. diff --git a/bench-results/31.76.103.176.md b/bench-results/31.76.103.176.md new file mode 100644 index 0000000..c239be0 --- /dev/null +++ b/bench-results/31.76.103.176.md @@ -0,0 +1,91 @@ +# Server bench: 31.76.103.176 (build-server, AWG endpoint `router-correct`) + +**Дата**: 2026-07-09 +**Хостер**: tihost.com (`my-server-1.tihost.com`, 1 vCPU, 2 GB RAM, kernel 6.8) +**Роль**: билд-сервер для amneziawg.ko / tcp_bbr.ko, также AWG endpoint (тоннель `awg-f1faca2af5a6`, DISABLED) +**Тестовый сегмент**: WAN, роутер `88.201.133.115` (ISP SkyNet SPB) ↔ VPS + +--- + +## 1. Reverse-path MTR: сервер → router WAN 88.201.133.115 + +``` +HOST: 31.76.103.176 Loss% Snt Last Avg Best Wrst StDev + 1. 10.0.0.1 0.0% 10 0.2 0.1 0.1 0.2 0.0 (private, VPS lan-gw) + 2. 10.0.102.254 0.0% 10 0.6 0.7 0.5 0.9 0.1 (private) + 3. ??? 100.0 10 (silent hop) + 4. 100.64.120.0 0.0% 10 0.9 0.9 0.8 1.3 0.2 (CGNAT) + 5. 62.115.199.250 0.0% 10 0.8 0.9 0.8 1.1 0.1 (AS1299 Arelion, SE) + 6. 62.115.137.119 0.0% 10 6.3 8.2 1.5 26.1 9.6 (AS1299 Arelion — jitter!) + 7. 62.115.122.34 0.0% 10 24.6 24.6 24.5 24.8 0.1 (AS1299 Arelion) + 8. 62.115.141.23 0.0% 10 23.3 23.7 23.2 25.0 0.5 (AS1299 Arelion) + 9. 62.115.135.109 10.0% 10 35.3 28.9 27.8 35.3 2.4 (AS1299 Arelion — Telia Moscow) +10. 87.245.255.42 0.0% 10 26.8 26.6 26.3 26.8 0.1 (AS9002 RETN, GB) +11. 139.45.243.65 0.0% 10 27.1 27.8 26.6 31.2 1.5 (AS57304 RETN, RU) +12. 139.45.238.85 0.0% 10 26.9 26.8 26.5 27.0 0.2 (AS57304 RETN, RU) +13. 185.37.128.156 0.0% 10 27.4 28.4 26.9 39.3 3.8 (AS35807 SkyNet SPB) +14. 185.37.128.225 0.0% 10 27.0 26.9 26.6 27.1 0.1 (AS35807 SkyNet SPB) +15. 185.37.128.189 0.0% 10 27.3 27.3 27.0 27.7 0.2 (AS35807 SkyNet SPB) +16. 185.37.128.166 0.0% 10 27.3 27.1 26.9 27.3 0.1 (AS35807 SkyNet SPB) +17. ??? 100.0 10 (silent — target firewall) +``` + +**Вердикт**: путь **очень хороший** сейчас. В прошлом здесь ловился Arelion→SkyNet-транзит с Avg 1002ms / StDev 1192ms / 30% loss (см. [[project-multitunnel-status]] и переписку). Сейчас: +- 62.115.135.109 (Telia Moscow) держит 10% loss — единственная точка, где что-то теряется. Было 30%, сейчас лучше. +- 185.37.128.156 (тот кейс с 1002ms) теперь Avg 28ms, StDev 3.8ms — грязный транзит прошёл. + +**Note**: транзит через Arelion (шведский AS1299), потом RETN (GB→RU), потом SkyNet (RU). 4-хопная международка. Уязвим к пиковым нагрузкам. + +--- + +## 2. Forward-path MTR: srv (192.168.31.243) → 31.76.103.176 + +``` +HOST: len-srv Loss% Snt Last Avg Best Wrst StDev + 1. 192.168.31.1 0.0% 10 1.4 1.4 1.2 1.6 0.1 (router) + 2. ??? 100.0 10 (silent) + 3. 31.76.103.176 0.0% 10 2.6 2.0 1.3 3.0 0.7 (target) +``` + +**Вердикт**: **3 хопа и Avg 2ms — идёт через tunnel** (не через WAN). Активный AWG main-tunnel сейчас = `amnezia_for_awg` → 185.209.21.51, но 31.76.103.176 сидит в том же DC/AS peering, поэтому tunnel-эндпоинт→31.76 = один hop. + +--- + +## 3. iperf3 direct WAN (роутер → 31.76.103.176, оба BBR) + +| Test | Rate | Loss/Retr | +|---|---:|---| +| **UDP 300M UP** (router→server) | **165 Mbit/s** | 123534/278129 packets (**44%**) | +| **UDP 300M DL** (server→router) | **184 Mbit/s** | 106127/277071 packets (**38%**) | +| **TCP BBR UP** | **89 Mbit/s** | — | +| **TCP BBR DL** | **239 Mbit/s** | — | + +**Вердикт**: **аплоад к этому серверу зашивается на ~180 Mbit**. При попытке 300M UDP 44% пакетов теряется — где-то на пути (скорее всего hop 9 Telia Moscow или дальше на пути Arelion→RETN→SkyNet) стоит рейт-лимит ~180 Mbit. + +TCP-UP всего 89 Mbit — при таких потерях даже BBR не спасает. + +--- + +## 4. Сравнение с 193.233.126.94 + +| Метрика | 31.76.103.176 | 193.233.126.94 | +|---|---:|---:| +| RTT (reverse-mtr avg) | ~27 ms | ~57 ms | +| Reverse-mtr loss worst hop | 10% (Telia) | 0% | +| UDP UP 300M actual | 165 Mbit (44% loss) | **298 Mbit (0% loss)** | +| UDP DL 300M actual | 184 Mbit (38% loss) | **299 Mbit (0.35% loss)** | +| TCP BBR UP | 89 Mbit | **408 Mbit** | +| TCP BBR DL | 239 Mbit | 265 Mbit | + +**193.233.126.94 в 4.5× быстрее по TCP UP** и без потерь в UDP. 31.76 годится только для build (компиляция ядра) — не для тоннеля. + +--- + +## Как читать (общие критерии) + +- **Reverse-mtr — самое важное**. Server→router путь чаще всего убитый. + - Плохо: Avg > 100ms или StDev > 50ms или loss > 5% на любом хопе → single-TCP CUBIC загнётся в ~5 Mbit + - Хорошо: 0% loss везде, StDev < 10ms +- **UDP iperf с высоким target = тест реальной физики**. Loss > 5% на 300M target = ISP или транзит режет. +- **Delta UDP↔TCP** широкая = потери / jitter → нужен BBR либо Brutal. +- **Международный транзит через много AS** (как тут Arelion→RETN→SkyNet) уязвим — может внезапно проседать. diff --git a/bench-results/SUMMARY.md b/bench-results/SUMMARY.md new file mode 100644 index 0000000..e9bfb8a --- /dev/null +++ b/bench-results/SUMMARY.md @@ -0,0 +1,79 @@ +# Сводная таблица VPS-серверов (по состоянию 2026-07-09) + +Тестовый сегмент: роутер `88.201.133.115` (Санкт-Петербург, SkyNet ISP) → VPS + +--- + +## RTT / пинги + +| Сервер | Хостер | Reverse-mtr Avg | Reverse-mtr StDev (worst hop) | Forward-mtr Avg (srv→target) | +|---|---|---:|---:|---:| +| **193.233.126.94** | dhost.su | ~57 ms | 19.0 ms (hop 7) | 57 ms | +| **2.26.224.64** | tihost.com | ~30 ms | **18.0 ms** (hop 8, FIORD-RU) | 2.5 ms | +| **2.26.252.43** | tihost.com | ~41 ms | 0.5 ms | 1.4 ms | +| **31.76.103.176** | tihost.com | ~27 ms | 9.6 ms (hop 6, Arelion) | 2.0 ms | + +*Reverse-mtr = server→router, самое важное для CUBIC single-flow* +*Forward-mtr короткий (2-3 хопа) значит цель достижима через активный тоннель, а не WAN* + +--- + +## Потери / джиттер (по iperf3 UDP 300 Mbit target) + +### UP (router → server) +| Сервер | Rate | Джиттер | Потери | +|---|---:|---:|---:| +| **193.233.126.94** | **298 Mbit** | **0.038 ms** | **0 / 259004 (0%)** | +| 2.26.224.64 | 184 Mbit | 0.085 ms | 102348 / 274044 (37%) | +| 2.26.252.43 | 183 Mbit | 0.079 ms | 101710 / 274135 (37%) | +| 31.76.103.176 | 165 Mbit | 0.108 ms | 123534 / 278129 (**44%**) | + +### DL (server → router) +| Сервер | Rate | Джиттер | Потери | +|---|---:|---:|---:| +| **193.233.126.94** | **299 Mbit** | 0.062 ms | 912 / 258982 (0.35%) | +| 2.26.224.64 | 178 Mbit | 0.100 ms | 101325 / 263836 (38%) | +| 2.26.252.43 | 177 Mbit | **8.718 ms** ⚠ | 103307 / 264662 (39%) | +| 31.76.103.176 | 184 Mbit | 0.090 ms | 106127 / 277071 (38%) | + +**Замечания**: +- 193.233 — единственный сервер, где 300 Mbit UDP реально держится (0% loss). У tihost'ов физический cap ~180 Mbit — при попытке 300M половина пакетов теряется. +- 2.26.252.43 DL джиттер **8.7 ms** (у всех остальных <0.1) — точечная проблема пути или очередь на промежуточном хопе. + +--- + +## TCP throughput (BBR обеих сторон) + +| Сервер | TCP UP | TCP DL | Ретрансмиты UP | Ретрансмиты DL | +|---|---:|---:|---:|---:| +| **193.233.126.94** | **408 Mbit** | 265 Mbit | низкие | 117 | +| 2.26.224.64 | 341 Mbit | 179 Mbit | — | — | +| 2.26.252.43 | 332 Mbit | **111 Mbit** ⚠ | — | — | +| 31.76.103.176 | **89 Mbit** ⚠ | 239 Mbit | — | — | + +**Замечания**: +- **193.233 в 1.2× быстрее** второго (2.26.224.64) по TCP UP, и в **4.5× быстрее** 31.76. +- 31.76 TCP UP всего 89 Mbit — там аплоад-cap где-то на пути (Arelion→Telia Moscow→RETN). +- 2.26.252.43 DL 111 Mbit — коррелирует с 10% loss на reverse-mtr hop 12. + +--- + +## Reverse-path критические хопы (что убивает CUBIC) + +| Сервер | Хоп-провал | Loss | StDev | AS | +|---|---|---:|---:|---| +| **193.233.126.94** | нет | 0% | 0.4 (hop 6) | — | +| 2.26.224.64 | 62.140.245.203 | 0% | **18 ms** | AS51369 FIORD-RU | +| 2.26.252.43 | 185.37.128.166 | **10%** | 0.3 | AS35807 SkyNet | +| 31.76.103.176 | 62.115.135.109 | **10%** | 2.4 | AS1299 Arelion (Telia Moscow) | + +--- + +## Итоговый рейтинг + +1. **193.233.126.94** — эталон. 0% loss, 0.04 ms джиттер, TCP UP 408 M. **Основной тоннель.** +2. **2.26.224.64** — резерв. Rev-mtr чистый по loss, но hop 8 StDev 18 ms → пиковые задержки. TCP UP 341M годится. +3. **2.26.252.43** — TCP DL всего 111 Mbit из-за 10% reverse-loss. Годится **только для UP**. +4. **31.76.103.176** — только для build (ядро/модули). Как VPN — не годится. + +**Общий паттерн**: у всех трёх tihost.com одинаковый ISP-cap ~180 Mbit UDP UP к моему WAN (SkyNet SPB). Отличие только в качестве reverse-path (loss/jitter). dhost.su на 193.233 сидит в другой сетке где этот cap не действует. diff --git a/bench-results/SUPPORT-REPORT.md b/bench-results/SUPPORT-REPORT.md new file mode 100644 index 0000000..e3d67ee --- /dev/null +++ b/bench-results/SUPPORT-REPORT.md @@ -0,0 +1,107 @@ +# Отчёт по производительности VPS + +**Дата тестирования:** 2026-07-09 +**Тестовый сегмент:** VPS ↔ клиентский роутер `88.201.133.115` (SPB, AS35807 SkyNet) +**Инструменты:** `mtr` (10 циклов), `iperf3` (10 сек, BBR обеих сторон) +**Сравнение:** 4 сервера, три из них у tihost.com (одинаковый ISP-пафс), один у dhost.su + +--- + +## Сводная таблица + +| Сервер | Хостер | RTT
(мс) | Reverse-path
MTR loss | Reverse-path
MTR max StDev | TCP UP
(Mbit/s) | TCP DL
(Mbit/s) | UDP 300 Mbit
UP loss | UDP 300 Mbit
DL loss | UDP
UP jitter (мс) | UDP
DL jitter (мс) | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 193.233.126.94 | dhost.su | 57 | 0% | 19.0 | **408** | 265 | 0% | 0.35% | 0.038 | 0.062 | +| 2.26.224.64 | tihost.com | 30 | 0% | 18.0 | 341 | 179 | 37% | 38% | 0.085 | 0.100 | +| 2.26.252.43 | tihost.com | 41 | 10% | 0.3 | 332 | **111** | 37% | 39% | 0.079 | **8.718** | +| 31.76.103.176 | tihost.com | 27 | 10% | 2.4 | **89** | 239 | 44% | 38% | 0.108 | 0.090 | + +Жирным выделены проблемные значения. + +--- + +## Наблюдение №1: у трёх серверов tihost.com одинаковый cap ~180 Mbit UDP + +При попытке залить 300 Mbit UDP в обе стороны все три tihost-сервера теряют 37-44% пакетов и держат стабильные 165-184 Mbit. У dhost-сервера (193.233.126.94) те же 300 Mbit проходят с 0-0.35% потерь на 298-299 Mbit. + +Это указывает на **shape/rate-limit ~180 Mbit** где-то на пути между tihost и клиентским ISP (AS35807 SkyNet SPB). При этом physical-link tihost'а явно шире — TCP BBR всё ещё вытягивает 341/332 Mbit там где UDP теряется. + +## Наблюдение №2: две точки с 10% packet loss в reverse-path + +MTR `mtr -c 10 -rn 88.201.133.115` с серверов: + +**2.26.252.43 → hop 12 (185.37.128.166 SkyNet SPB):** +``` +10. 185.37.128.225 0.0% loss Avg 41.0 ms StDev 0.2 +11. ??? (silent) +12. 185.37.128.166 10.0% loss Avg 41.3 ms StDev 0.3 +``` + +**31.76.103.176 → hop 9 (62.115.135.109 Arelion Telia Moscow):** +``` + 8. 62.115.141.23 0.0% loss Avg 23.7 ms StDev 0.5 + 9. 62.115.135.109 10.0% loss Avg 28.9 ms StDev 2.4 +10. 87.245.255.42 0.0% loss Avg 26.6 ms +``` + +Эти потери коррелируют с деградацией TCP-throughput: +- 2.26.252.43 TCP DL = 111 Mbit (у соседа 2.26.224.64 без loss = 179 Mbit) +- 31.76.103.176 TCP UP = 89 Mbit (у соседа 2.26.224.64 без loss = 341 Mbit) + +## Наблюдение №3: аномальный DL-джиттер на 2.26.252.43 + +UDP-джиттер измеренный iperf3: + +``` +2.26.252.43 DL jitter: 8.718 ms +2.26.224.64 DL jitter: 0.100 ms +193.233.126.94 DL jitter: 0.062 ms +31.76.103.176 DL jitter: 0.090 ms +``` + +8.7 мс vs 0.1 мс у других — межпакетная дрожь в 87× выше нормы для конкретно этого сервера в DL-направлении. При тех же ~40 мс RTT. + +## Наблюдение №4: peak-джиттер на hop 8 (2.26.224.64) + +Reverse-mtr с 2.26.224.64: +``` + 8. 62.140.245.203 0.0% loss Avg 34.1 ms StDev 18.0 Wrst 85.4 ms +``` + +Потерь нет, но конкретный пакет может опоздать на 85 мс при среднем 34 мс. Это AS51369 FIORD-RU. Не критично для usable throughput, но для low-latency (VoIP/games) неприятно. + +--- + +## Что просим у саппорта + +1. **Проверить рейт-лимит ~180 Mbit к AS35807 SkyNet SPB (88.201.133.115).** У трёх ваших серверов одинаковый potolок при том что физика гораздо шире (TCP пробивает выше). Похоже на настройку shape на исходящем пиринге. + +2. **10% loss на конкретных хопах:** + - `185.37.128.166` в pop-е SkyNet SPB (виден с 2.26.252.43) + - `62.115.135.109` (Telia Moscow, AS1299 Arelion) — виден с 31.76.103.176 + Оба стабильно теряют 1 из 10 пакетов при разных RTT/StDev — это не burst-loss. + +3. **Аномальный DL-джиттер 8.7 мс на 2.26.252.43** — не воспроизводится у соседа в том же ДЦ (`2.26.224.64` = 0.1 мс). Проверить очереди на исходящем интерфейсе именно этого узла. + +Готов предоставить сырые `mtr`/`iperf3` логи по запросу. + +--- + +## Методология (для воспроизведения) + +```bash +# reverse-path +ssh root@ 'mtr -c 10 -rn 88.201.133.115' + +# forward-path +ssh root@ 'mtr -c 10 -rn ' + +# iperf3 direct WAN (оба BBR) +ssh root@ 'iperf3 -s -D -p 5201' +ssh root@ 'iperf3 -c -p 5201 -t 10 -f m' # TCP UP +ssh root@ 'iperf3 -c -p 5201 -R -t 10 -f m' # TCP DL +ssh root@ 'iperf3 -c -p 5201 -u -b 300M -t 10' # UDP UP 300M +ssh root@ 'iperf3 -c -p 5201 -u -b 300M -R -t 10' # UDP DL 300M +``` + +Все VPS — Ubuntu 24.04, kernel 6.8, `sysctl net.ipv4.tcp_congestion_control=bbr`, `net.core.default_qdisc=fq`. diff --git a/bench-server.sh b/bench-server.sh new file mode 100755 index 0000000..eff59ba --- /dev/null +++ b/bench-server.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Прогон нового server-кандидата (VPN endpoint). +# Usage: ./bench-server.sh [ssh-user] [ssh-port] +# +# Даёт три вещи в связке: +# 1. Reverse-path MTR с сервера на роутер — ловит asymmetric-routing грязный transit +# (это уже раз убивало single-TCP до 5 Mbit — см. AS35807 SkyNet SPB кейс). +# 2. Forward-path MTR с srv/роутера — обычно чистый, но всё равно надо иметь. +# 3. iperf3 UDP направленный тест — показывает реальный ceiling пути без TCP CC-шума. + +set -e + +SRV="${1:?server_ip required}" +SUSER="${2:-root}" +SPORT="${3:-22}" + +# фиксированные для этой инфры +ROUTER_WAN="88.201.133.115" # твой публичный WAN, для reverse-mtr с сервера +ROUTER_LAN="192.168.31.1" # sshpass root:root +LAN_CLIENT="srv" # ssh alias — WSL Ubuntu за роутером + +echo "═══ Инспекция сервера $SRV ═══" +ssh -p "$SPORT" "$SUSER@$SRV" 'hostname; uname -r; nproc; free -m | head -2; ip -4 addr | grep inet' || { + echo "! SSH не пускает — нужен пароль. Задай sshpass -p '...' вручную." + exit 1 +} + +echo "" +echo "═══ 1. Reverse-path MTR: server → router WAN ($ROUTER_WAN) ═══" +echo " (высокий StDev/loss на конкретном hop = грязный transit — плохо для CUBIC single-flow)" +ssh -p "$SPORT" "$SUSER@$SRV" "which mtr || (apt-get install -qq -y mtr-tiny 2>/dev/null); mtr -c 10 -rn $ROUTER_WAN" + +echo "" +echo "═══ 2. Forward-path MTR: srv → server ═══" +ssh srv "which mtr || sudo apt-get install -qq -y mtr-tiny; mtr -c 10 -rn $SRV" + +echo "" +echo "═══ 3. Forward MTR: srv → 8.8.8.8 (baseline через текущий main tunnel) ═══" +ssh srv "mtr -c 10 -rn 8.8.8.8" + +echo "" +echo "═══ 4. iperf3 UDP 300 Mbit с роутера → сервер (сырой path без TCP CC) ═══" +echo " Устанавливаю iperf3 на сервере и запускаю в фоне..." +ssh -p "$SPORT" "$SUSER@$SRV" 'which iperf3 || apt-get install -qq -y iperf3 2>/dev/null; pkill -f "iperf3 -s" 2>/dev/null; iperf3 -s -D -p 5201' +sleep 2 +echo " UDP 300M UP (router → server):" +sshpass -p 'root' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$ROUTER_LAN \ + "/opt/bin/iperf3 -c $SRV -p 5201 -u -b 300M -t 10 -f m 2>&1 | grep receiver | tail -1" +echo " UDP 300M DL (server → router):" +sshpass -p 'root' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$ROUTER_LAN \ + "/opt/bin/iperf3 -c $SRV -p 5201 -u -b 300M -R -t 10 -f m 2>&1 | grep receiver | tail -1" + +echo "" +echo "═══ 5. iperf3 TCP (BBR both sides) ═══" +sshpass -p 'root' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$ROUTER_LAN \ + "echo ' TCP UP: ' && /opt/bin/iperf3 -c $SRV -p 5201 -t 10 -f m 2>&1 | grep receiver | tail -1 + echo ' TCP DL: ' && /opt/bin/iperf3 -c $SRV -p 5201 -R -t 10 -f m 2>&1 | grep receiver | tail -1" + +echo "" +echo "═══ Как читать ═══" +echo " • MTR reverse: любой hop с Avg > 100ms или StDev > 50ms или loss > 5% → путь с сервера уебан." +echo " Такой сервер уебёт single-TCP CUBIC до ~5 Mbit несмотря на любой раmax UDP." +echo " • UDP iperf: показывает physical-cap без TCP-CC-шума." +echo " • Delta UDP↔TCP: если UDP 400M а TCP 30M — потери / jitter (CUBIC не тянет), нужен BBR/Brutal." +echo " • Если reverse-mtr чистый (Avg <60ms, StDev<10ms, 0%loss) — сервер годится." diff --git a/full_info_old.txt b/full_info_old.txt new file mode 100644 index 0000000..8595b84 --- /dev/null +++ b/full_info_old.txt @@ -0,0 +1,222 @@ +_MTD_partitions_: + 0 > addr: 00000000 size: 00100000 ro: 0 name: "0:SBL1" + 1 > addr: 00100000 size: 00100000 ro: 0 name: "0:SBL1_1" + 2 > addr: 00200000 size: 00100000 ro: 0 name: "0:MIBIB" + 3 > addr: 00300000 size: 00080000 ro: 0 name: "0:BOOTCONFIG" + 4 > addr: 00380000 size: 00080000 ro: 0 name: "0:BOOTCONFIG1" + 5 > addr: 00400000 size: 00200000 ro: 0 name: "0:QSEE" + 6 > addr: 00600000 size: 00200000 ro: 0 name: "0:QSEE_1" + 7 > addr: 00800000 size: 00080000 ro: 0 name: "0:DEVCFG" + 8 > addr: 00880000 size: 00080000 ro: 0 name: "0:DEVCFG_1" + 9 > addr: 00900000 size: 00080000 ro: 0 name: "0:APDP" + 10 > addr: 00980000 size: 00080000 ro: 0 name: "0:APDP_1" + 11 > addr: 00A00000 size: 00080000 ro: 0 name: "0:TME" + 12 > addr: 00A80000 size: 00080000 ro: 0 name: "0:TME_1" + 13 > addr: 00B00000 size: 00080000 ro: 0 name: "0:RPM" + 14 > addr: 00B80000 size: 00080000 ro: 0 name: "0:RPM_1" + 15 > addr: 00C00000 size: 00080000 ro: 0 name: "0:CDT" + 16 > addr: 00C80000 size: 00080000 ro: 0 name: "0:CDT_1" + 17 > addr: 00D00000 size: 00080000 ro: 0 name: "0:APPSBLENV" + 18 > addr: 00D80000 size: 00100000 ro: 0 name: "0:APPSBL" + 19 > addr: 00E80000 size: 00100000 ro: 0 name: "0:APPSBL_1" + 20 > addr: 00F80000 size: 00100000 ro: 1 name: "0:ART" + 21 > addr: 01080000 size: 00080000 ro: 0 name: "0:TRAINING" + 22 > addr: 01100000 size: 00040000 ro: 0 name: "0:LICENSE" + 23 > addr: 01140000 size: 02800000 ro: 0 name: "rootfs" + 24 > addr: 03940000 size: 02800000 ro: 0 name: "rootfs_1" + 25 > addr: 06140000 size: 00080000 ro: 1 name: "bdata" + 26 > addr: 061C0000 size: 00080000 ro: 0 name: "crash" + 27 > addr: 06240000 size: 00080000 ro: 0 name: "crash_syslog" + 28 > addr: 062C0000 size: 01C00000 ro: 0 name: "overlay" + 29 > addr: 07EC0000 size: 00040000 ro: 0 name: "KF" + 30 > addr: FFFFFFFF size: 003AB914 ro: 0 name: "kernel" + 31 > addr: FFFFFFFF size: 02017000 ro: 0 name: "ubi_rootfs" + 32 > addr: FFFFFFFF size: 01838000 ro: 1 name: "cfg" + +_Base_info_: + Linux stamp: Linux XiaoQiang 5.4.164 #0 SMP PREEMPT Tue Jan 27 03:33:27 2026 aarch64 GNU/Linux + Linux version: 5.4.164 + CPU arch: arm64 + CPU name: ipq95xx + SPI rom: False + +_Kernel_command_line_: + ubi.mtd=rootfs + root=mtd:ubi_rootfs + rootfstype=squashfs + cnss2.bdf_pci2=0x02 + cnss2.enable_mlo_support=0 + rootwait + uart_en=1 + +_NVRam_params_: + CountryCode=CN + Router_unconfigured=0 + SN=44594/14YE00117 + boot_wait=on + bootargs=ubi.mtd=rootfs root=mtd:ubi_rootfs rootfstype=squashfs cnss2.bdf_pci2=0x02 cnss2.enable_mlo_support=0 rootwait + bootcmd=bootmiwifi + bootdelay=3 + bootmenu_delay=5 + color=100 + crash_write_count=2 + eth1addr=58:ea:1f:78:e1:ac + eth2addr=58:ea:1f:78:e1:ad + eth3addr=ee:31:75:9a:f3:b2 + eth4addr=ee:31:75:9a:f3:b2 + eth5addr=ee:31:75:9a:f3:b2 + ethaddr=58:ea:1f:4a:2b:bc + ethprime=eth1 + fdt_high=0x48500000 + fdtcontroladdr=4a4e4004 + flag_boot_rootfs=0 + flag_boot_success=1 + flag_boot_type=2 + flag_flash_permission=1 + flag_last_success=0 + flag_ota_reboot=0 + flag_show_upgrade_info=1 + flag_try_sys1_failed=0 + flag_try_sys2_failed=0 + flash_type=11 + fsbootargs=ubi.mtd=rootfs root=mtd:ubi_rootfs rootfstype=squashfs cnss2.bdf_pci2=0x02 cnss2.enable_mlo_support=0 + ipaddr=192.168.31.1 + machid=8050501 + mode=Router + model=RC06 + mtdids=nand0=nand0 + netmask=255.255.255.0 + no_wifi_dev_times=0 + rand_key=1v83pS+jQk44UMaY3U8YWrSapXcvNpzz3a2ualPGkDJ1I27tx4Sq7w== + rand_nonce=webhbGM9UQcONmqKea3lK7u5gz1B9T6RZ8NlZDTOF5Tv+7+KXOFXYltnvH+H/3aaRNJZoXWhfvg4vmfukKEun1Ehou+nPbpG4scJiiouXd98TiuMXLrkGc8pQLVh4nQK1DqvDnuUx50S4mwQPYwUnd/eEbbMOrcdKP35mniDD9lgUC8ZTT7v5RSfrM2EkQ6E5GUGCLbR+WaKk4JxO+Cc2SWJLbIQGRZfmxPhajukXvldooly3M2Z1wvp7SyPXd4zl82shzDzHZ1zwO9NPGq7RGihrYt029OGfk35TyyV1JlT03e7UN8P8q4vMcJWFFCTQcm7GHxy85TW7JBsMuW3USZ26v7pCiVgI0u3Z1qVUxEVeGuax0sv2QUeHopiFQCFF7DjlzpwvAjmYXM+MXTBLmOdKG0bj0DARrI7vgDPsP8FHZDgxDZX4nimdWw1SkqLFsJ+jE5+r82/QkRXBkBv33epFSPycY0QVAyb8tcqF7/lhrJEeXWqSvp6u2vZqmaRD6YJMJ+r33Zc8Waj4955ATPCtTBTsa6Cz6IQ7EhOm0d77DuBrKmsvv07rsSL47h/dYRXr/phEsAWk7de4QMkVOGG/vL/NdS+7cLHE7udcjqKlujG6K1JmbWdcLG+5xmf0Qe47Ok6Utvpf5xG1ct41J88mvLR7OS5lxi92yMwJ3W/AAO+Qep6NMZjKJOzZTScT4eyqZiQYORv/36gP1L7hoUonULjMg5tFyndy5OTlwmXRWu7Kiln+kGGvm7CFZfhQkNUNoAH9sctGNug43l/APh+xbgCeGRCLFUerRjrSMbuiOJ8A0SOrLm9HVbfozY2X/rj6g/dShtaKx6ihP9TPO0alGfRTktpSZALOgvpNQst/8Wk95eOdRch4PChDxJQpLW4Jjl0X4QpV6y+pRTqQOT8O1t/PjQIbOiFZjjB6WWq6TSxndxsmi+XuAh4pP7xmCYqAu601o03mddyjZ09ONhwNuB4ksfKRjCykm7LwUDctNYnRtm8eM3HG+qfqzjZFLvr7ZF3/XFNRHKAegk2tRuL13val1AbRFzj0EOQL5HgY6UPAPyCqk6uwfWKQtXV01ODxthcRdghtDkIDWdpl5zke/uO3FIEYqJZI3em/Q9GcXILwqIMvCnthW9VqcxJypK7kmnvNziJG0Z2CvKrn5y4Cl9J3npTo6kjPjlQUx5D1bm6/UipamPRCklWsBDXgFpVkPZ8kiPD3uuEiG8E4TqPeL4ZTPkuGXGENn+vUatQ52Dog5iTfIG02vUh7bxDGHtFdCzxwp9oOvs9kodqW5dSLbQOvGsEjibm9iEYYTMW56CfnyNTehR4SzS3tZuOyn2wErBTiAGgpa16GUh8eQ== + restore_defaults=0 + serverip=192.168.31.100 + soc_version_major=1 + soc_version_minor=1 + ssh_en=1 + stderr=serial@78B1000 + stdin=serial@78B1000 + stdout=serial@78B1000 + telnet_en=0 + uart_en=1 + wl0_radio=1 + wl0_ssid=Xiaomi_5G + wl1_radio=1 + wl1_ssid=Xiaomi + wl2_radio=1 + wl2_ssid=Xiaomi_5G-2 + +_RootFS_current_: + num = 0 + mtd_num = None + mtd_dev = "None" + partition = "rootfs" + +_Version_info_: + UBoot: None + OpenWrt: unknown + Firmware: 1.1.38 + Channel: release + BuildTime: Tue, 27 Jan 2026 04:23:05 +0000 + Hardware: RC06 + UBoot(2): 1.0.0 + +_Bootloader_info_: + 0:SBL1: + addr: 0x00000000 + size: 0x00100000 + image size: 12 bytes + type: None + 0:APPSBL: + addr: 0x00D80000 + size: 0x00100000 + image size: 177 bytes + type: None + +_ENV_info_: + 0:APPSBLENV: + addr: 0x00D00000 + size: 0x00010000 + len: 2638 bytes + prefix: b'\x8e\xd7\x93\x1d' + CountryCode=CN + Router_unconfigured=0 + SN=44594/14YE00117 + boot_wait=on + bootargs=ubi.mtd=rootfs root=mtd:ubi_rootfs rootfstype=squashfs cnss2.bdf_pci2=0x02 cnss2.enable_mlo_support=0 rootwait + bootcmd=bootmiwifi + bootdelay=3 + bootmenu_delay=5 + color=100 + crash_write_count=2 + eth1addr=58:ea:1f:78:e1:ac + eth2addr=58:ea:1f:78:e1:ad + eth3addr=ee:31:75:9a:f3:b2 + eth4addr=ee:31:75:9a:f3:b2 + eth5addr=ee:31:75:9a:f3:b2 + ethaddr=58:ea:1f:4a:2b:bc + ethprime=eth1 + fdt_high=0x48500000 + fdtcontroladdr=4a4e4004 + flag_boot_rootfs=0 + flag_boot_success=1 + flag_boot_type=2 + flag_flash_permission=1 + flag_last_success=0 + flag_ota_reboot=0 + flag_show_upgrade_info=1 + flag_try_sys1_failed=0 + flag_try_sys2_failed=0 + flash_type=11 + fsbootargs=ubi.mtd=rootfs root=mtd:ubi_rootfs rootfstype=squashfs cnss2.bdf_pci2=0x02 cnss2.enable_mlo_support=0 + ipaddr=192.168.31.1 + machid=8050501 + mode=Router + model=RC06 + mtdids=nand0=nand0 + netmask=255.255.255.0 + no_wifi_dev_times=0 + rand_key=1v83pS+jQk44UMaY3U8YWrSapXcvNpzz3a2ualPGkDJ1I27tx4Sq7w== + rand_nonce=webhbGM9UQcONmqKea3lK7u5gz1B9T6RZ8NlZDTOF5Tv+7+KXOFXYltnvH+H/3aaRNJZoXWhfvg4vmfukKEun1Ehou+nPbpG4scJiiouXd98TiuMXLrkGc8pQLVh4nQK1DqvDnuUx50S4mwQPYwUnd/eEbbMOrcdKP35mniDD9lgUC8ZTT7v5RSfrM2EkQ6E5GUGCLbR+WaKk4JxO+Cc2SWJLbIQGRZfmxPhajukXvldooly3M2Z1wvp7SyPXd4zl82shzDzHZ1zwO9NPGq7RGihrYt029OGfk35TyyV1JlT03e7UN8P8q4vMcJWFFCTQcm7GHxy85TW7JBsMuW3USZ26v7pCiVgI0u3Z1qVUxEVeGuax0sv2QUeHopiFQCFF7DjlzpwvAjmYXM+MXTBLmOdKG0bj0DARrI7vgDPsP8FHZDgxDZX4nimdWw1SkqLFsJ+jE5+r82/QkRXBkBv33epFSPycY0QVAyb8tcqF7/lhrJEeXWqSvp6u2vZqmaRD6YJMJ+r33Zc8Waj4955ATPCtTBTsa6Cz6IQ7EhOm0d77DuBrKmsvv07rsSL47h/dYRXr/phEsAWk7de4QMkVOGG/vL/NdS+7cLHE7udcjqKlujG6K1JmbWdcLG+5xmf0Qe47Ok6Utvpf5xG1ct41J88mvLR7OS5lxi92yMwJ3W/AAO+Qep6NMZjKJOzZTScT4eyqZiQYORv/36gP1L7hoUonULjMg5tFyndy5OTlwmXRWu7Kiln+kGGvm7CFZfhQkNUNoAH9sctGNug43l/APh+xbgCeGRCLFUerRjrSMbuiOJ8A0SOrLm9HVbfozY2X/rj6g/dShtaKx6ihP9TPO0alGfRTktpSZALOgvpNQst/8Wk95eOdRch4PChDxJQpLW4Jjl0X4QpV6y+pRTqQOT8O1t/PjQIbOiFZjjB6WWq6TSxndxsmi+XuAh4pP7xmCYqAu601o03mddyjZ09ONhwNuB4ksfKRjCykm7LwUDctNYnRtm8eM3HG+qfqzjZFLvr7ZF3/XFNRHKAegk2tRuL13val1AbRFzj0EOQL5HgY6UPAPyCqk6uwfWKQtXV01ODxthcRdghtDkIDWdpl5zke/uO3FIEYqJZI3em/Q9GcXILwqIMvCnthW9VqcxJypK7kmnvNziJG0Z2CvKrn5y4Cl9J3npTo6kjPjlQUx5D1bm6/UipamPRCklWsBDXgFpVkPZ8kiPD3uuEiG8E4TqPeL4ZTPkuGXGENn+vUatQ52Dog5iTfIG02vUh7bxDGHtFdCzxwp9oOvs9kodqW5dSLbQOvGsEjibm9iEYYTMW56CfnyNTehR4SzS3tZuOyn2wErBTiAGgpa16GUh8eQ== + restore_defaults=0 + serverip=192.168.31.100 + soc_version_major=1 + soc_version_minor=1 + ssh_en=1 + stderr=serial@78B1000 + stdin=serial@78B1000 + stdout=serial@78B1000 + telnet_en=0 + uart_en=1 + wl0_radio=1 + wl0_ssid=Xiaomi_5G + wl1_radio=1 + wl1_ssid=Xiaomi + wl2_radio=1 + wl2_ssid=Xiaomi_5G-2 + bdata: + addr: 0x06140000 + size: 0x00010000 + len: 1688 bytes + prefix: b'\xf5\x82\x90P' + rand_key=1v83pS+jQk44UMaY3U8YWrSapXcvNpzz3a2ualPGkDJ1I27tx4Sq7w== + rand_nonce=webhbGM9UQcONmqKea3lK7u5gz1B9T6RZ8NlZDTOF5Tv+7+KXOFXYltnvH+H/3aaRNJZoXWhfvg4vmfukKEun1Ehou+nPbpG4scJiiouXd98TiuMXLrkGc8pQLVh4nQK1DqvDnuUx50S4mwQPYwUnd/eEbbMOrcdKP35mniDD9lgUC8ZTT7v5RSfrM2EkQ6E5GUGCLbR+WaKk4JxO+Cc2SWJLbIQGRZfmxPhajukXvldooly3M2Z1wvp7SyPXd4zl82shzDzHZ1zwO9NPGq7RGihrYt029OGfk35TyyV1JlT03e7UN8P8q4vMcJWFFCTQcm7GHxy85TW7JBsMuW3USZ26v7pCiVgI0u3Z1qVUxEVeGuax0sv2QUeHopiFQCFF7DjlzpwvAjmYXM+MXTBLmOdKG0bj0DARrI7vgDPsP8FHZDgxDZX4nimdWw1SkqLFsJ+jE5+r82/QkRXBkBv33epFSPycY0QVAyb8tcqF7/lhrJEeXWqSvp6u2vZqmaRD6YJMJ+r33Zc8Waj4955ATPCtTBTsa6Cz6IQ7EhOm0d77DuBrKmsvv07rsSL47h/dYRXr/phEsAWk7de4QMkVOGG/vL/NdS+7cLHE7udcjqKlujG6K1JmbWdcLG+5xmf0Qe47Ok6Utvpf5xG1ct41J88mvLR7OS5lxi92yMwJ3W/AAO+Qep6NMZjKJOzZTScT4eyqZiQYORv/36gP1L7hoUonULjMg5tFyndy5OTlwmXRWu7Kiln+kGGvm7CFZfhQkNUNoAH9sctGNug43l/APh+xbgCeGRCLFUerRjrSMbuiOJ8A0SOrLm9HVbfozY2X/rj6g/dShtaKx6ihP9TPO0alGfRTktpSZALOgvpNQst/8Wk95eOdRch4PChDxJQpLW4Jjl0X4QpV6y+pRTqQOT8O1t/PjQIbOiFZjjB6WWq6TSxndxsmi+XuAh4pP7xmCYqAu601o03mddyjZ09ONhwNuB4ksfKRjCykm7LwUDctNYnRtm8eM3HG+qfqzjZFLvr7ZF3/XFNRHKAegk2tRuL13val1AbRFzj0EOQL5HgY6UPAPyCqk6uwfWKQtXV01ODxthcRdghtDkIDWdpl5zke/uO3FIEYqJZI3em/Q9GcXILwqIMvCnthW9VqcxJypK7kmnvNziJG0Z2CvKrn5y4Cl9J3npTo6kjPjlQUx5D1bm6/UipamPRCklWsBDXgFpVkPZ8kiPD3uuEiG8E4TqPeL4ZTPkuGXGENn+vUatQ52Dog5iTfIG02vUh7bxDGHtFdCzxwp9oOvs9kodqW5dSLbQOvGsEjibm9iEYYTMW56CfnyNTehR4SzS3tZuOyn2wErBTiAGgpa16GUh8eQ== + SN=44594/14YE00117 + color=100 + CountryCode=CN + model=RC06 + wl1_ssid=rc06_minet_2bbc + wl0_ssid=rc06_minet_2bbc + wl2_ssid=rc06_minet_2bbc + miot_did=7lwIXO/a946Op5P8NBvcoA== + miot_key=XtP5ihT1JyTEUg8kPT8QSw== + telnet_en=1 + ssh_en=1 + uart_en=1 + boot_wait=on + 0:SBL1: + addr: 0x00060000 + size: 0x000A0000 + len: 0 bytes + prefix: b'\xd1\xdcK\x84' + diff --git a/nfqws2-selector b/nfqws2-selector new file mode 160000 index 0000000..859aa17 --- /dev/null +++ b/nfqws2-selector @@ -0,0 +1 @@ +Subproject commit 859aa17f08d341d65e4b3dc04a571d822bb5b9ef diff --git a/recovery/be7000_recovery.py b/recovery/be7000_recovery.py new file mode 100644 index 0000000..3e5c384 --- /dev/null +++ b/recovery/be7000_recovery.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +BE7000 (RC06) recovery — DHCP + TFTP + прогресс как в MIWIFIRepairTool. + +Usage: + sudo python3 be7000_recovery.py + sudo python3 be7000_recovery.py --iface en5 miwifi_rc06.bin + +Что делает: + 1. Слушает UDP :67 (DHCP) и UDP :69 (TFTP RRQ) на указанном iface. + 2. На DHCP DISCOVER отвечает OFFER + на REQUEST → ACK. Раздаёт роутеру IP + 192.168.31.1, объявляет себя (192.168.31.100) TFTP-сервером и указывает + имя бутфайла (option 66 + 67 + BOOTP file field). + 3. На TFTP RRQ (независимо от имени файла) отдаёт указанную прошивку + блоками с честной поддержкой blksize/tsize/timeout OACK. + 4. Печатает статус на месте: state, MAC, IP, прогресс, скорость, + прогрессбар — как в MIWIFIRepairTool. + +Безопасно к соседям: отвечает ТОЛЬКО на MAC начинающиеся с 58:ea:1f (Xiaomi +OUI), другие устройства игнорирует. Bind — только на один iface с +192.168.31.100/24 (параметр --iface). + +Требует sudo для bind на 67/69. +""" + +import argparse +import ipaddress +import os +import socket +import struct +import sys +import time +import threading + +# ── defaults ──────────────────────────────────────────────────────────────── + +DEFAULT_IFACE = "en5" +SERVER_IP = "192.168.31.100" # this Mac +CLIENT_IP = "192.168.31.1" # what we hand to the router +NETMASK = "255.255.255.0" +XIAOMI_OUI_PREFIXES = (b"\x58\xea\x1f",) # only respond to Xiaomi MACs + +DHCP_MAGIC = b"\x63\x82\x53\x63" +TFTP_BLOCK_DEFAULT = 512 +TFTP_TIMEOUT_S = 5.0 + + +# ── shared status object (printed periodically) ───────────────────────────── + +class Status: + """Central state for the pretty status line — updated by DHCP/TFTP threads.""" + STATES = ("WAITING", "DHCP_OFFER", "DHCP_ACK", "TFTP_XFER", "COMPLETE", "ERROR") + + def __init__(self, fw_path, bootfile): + self.lock = threading.Lock() + self.state = "WAITING" + self.mac = "-" + self.ip = "-" + self.fw_path = fw_path + self.fw_size = os.path.getsize(fw_path) + self.bootfile = bootfile + self.sent_bytes = 0 + self.total_bytes = self.fw_size + self.speed_bps = 0 + self.xfer_started = None + self.xfer_done = None + self.error_msg = None + self.event_log = [] # tuples (time, tag, msg) — for scroll-back + + def event(self, tag, msg): + with self.lock: + self.event_log.append((time.time(), tag, msg)) + # trim so unbounded runs don't eat memory + if len(self.event_log) > 200: + self.event_log = self.event_log[-100:] + + def set(self, **kw): + with self.lock: + for k, v in kw.items(): + setattr(self, k, v) + + +def _fmt_bytes(n): + if n < 1024: + return f"{n} B" + for unit in ("KB", "MB", "GB"): + n /= 1024.0 + if n < 1024: + return f"{n:.1f} {unit}" + return f"{n:.1f} TB" + + +def _fmt_speed(bps): + if bps <= 0: + return "-" + return f"{_fmt_bytes(bps)}/s" + + +def _progress_bar(cur, total, width=32): + if total <= 0: + return "-" * width + pct = min(1.0, cur / total) + filled = int(pct * width) + return "█" * filled + "░" * (width - filled) + + +def status_printer(st, stop_flag): + """Print in-place status line every 250ms + scroll-back of recent events.""" + ansi_up = "\033[F" + ansi_clr = "\033[2K" + lines_out = 0 + printed_events = 0 + + print("\n" * 5, end="") # reserve 5 lines for the status block + lines_out = 5 + + while not stop_flag.is_set(): + with st.lock: + state = st.state + mac = st.mac + ip = st.ip + sent = st.sent_bytes + total = st.total_bytes + speed = st.speed_bps + fw = os.path.basename(st.fw_path) + bootfile = st.bootfile + new_events = st.event_log[printed_events:] + printed_events = len(st.event_log) + + # First: print any new events ABOVE the status block (scroll-back). + # Move cursor up over the status block, print events (they push status + # block down naturally when they scroll), then re-draw block. + sys.stdout.write(ansi_up * lines_out) + for ts, tag, msg in new_events: + ts_str = time.strftime("%H:%M:%S", time.localtime(ts)) + sys.stdout.write(ansi_clr + f"[{ts_str}] {tag:5s} {msg}\n") + + # Now the status block (5 lines): + pct = 100.0 * sent / total if total else 0 + bar = _progress_bar(sent, total) + state_col = { + "WAITING": "\033[90m", # dim + "DHCP_OFFER": "\033[33m", # yellow + "DHCP_ACK": "\033[36m", # cyan + "TFTP_XFER": "\033[32m", # green + "COMPLETE": "\033[1;32m", # bold green + "ERROR": "\033[1;31m", # bold red + }.get(state, "") + reset = "\033[0m" + + sys.stdout.write(ansi_clr + f"┌─ BE7000 recovery ──────────────────────────────────────────────┐\n") + sys.stdout.write(ansi_clr + f"│ state: {state_col}{state:<12}{reset} peer: {mac} ip: {ip}\n") + sys.stdout.write(ansi_clr + f"│ file: {fw} ({_fmt_bytes(total)}) bootfile-name: {bootfile}\n") + sys.stdout.write(ansi_clr + f"│ {bar} {pct:5.1f}% {_fmt_bytes(sent)}/{_fmt_bytes(total)} {_fmt_speed(speed)}\n") + sys.stdout.write(ansi_clr + f"└────────────────────────────────────────────────────────────────┘\n") + + lines_out = 5 + sys.stdout.flush() + time.sleep(0.25) + + +# ── helpers ───────────────────────────────────────────────────────────────── + +def ip4(s): + return socket.inet_aton(s) + + +def mac_str(b): + return ":".join(f"{x:02x}" for x in b) + + +# ── DHCP (minimal — DISCOVER→OFFER + REQUEST→ACK) ─────────────────────────── + +def parse_dhcp_options(payload): + i = 0 + while i < len(payload): + code = payload[i] + if code == 0xff: + return + if code == 0x00: + i += 1 + continue + length = payload[i + 1] + yield code, payload[i + 2 : i + 2 + length] + i += 2 + length + + +def build_dhcp_reply(req, msg_type, bootfile): + xid = req[4:8] + chaddr = req[28:44] + file_field = bootfile.encode()[:127].ljust(128, b"\x00") + sname_field = SERVER_IP.encode()[:63].ljust(64, b"\x00") + + pkt = bytearray(240) + pkt[0] = 0x02 + pkt[1] = 0x01 + pkt[2] = 0x06 + pkt[3] = 0x00 + pkt[4:8] = xid + pkt[8:10] = b"\x00\x00" + # Echo client's flags — Xiaomi U-Boot sends DISCOVER with flags=0x0000 + # and drops OFFERs whose flag bits don't match its request. Setting + # 0x8000 (broadcast) here silently breaks the handshake even though + # RFC 2131 says server is free to broadcast the reply. + pkt[10:12] = req[10:12] # mirror flags from request + pkt[12:16] = b"\x00\x00\x00\x00" + pkt[16:20] = ip4(CLIENT_IP) + pkt[20:24] = ip4(SERVER_IP) + pkt[24:28] = b"\x00\x00\x00\x00" + pkt[28:44] = chaddr + pkt += sname_field + pkt += file_field + pkt += DHCP_MAGIC + + def opt(code, value): + pkt.extend(bytes([code, len(value)]) + value) + + # Options that match what dnsmasq (which the router accepts) sends. + # Verified against a working recovery of a real BE7000: the router's + # U-Boot DHCP client wants EXACTLY this set — option 28 (broadcast + # addr) and options 58/59 (renewal timers) are what it looks for to + # know the ACK is complete. Missing 28 makes it silently retry. + subnet_bcast = str( + ipaddress.ip_network(f"{SERVER_IP}/{NETMASK}", strict=False).broadcast_address + ) + opt(53, bytes([msg_type])) # DHCP msg type + opt(54, ip4(SERVER_IP)) # server identifier + opt(51, struct.pack("!I", 3600)) # lease time (1h) + opt(58, struct.pack("!I", 1800)) # T1 renewal + opt(59, struct.pack("!I", 3150)) # T2 rebinding + opt(1, ip4(NETMASK)) # subnet mask + opt(28, ip4(subnet_bcast)) # broadcast address + opt(3, ip4(SERVER_IP)) # router + opt(66, SERVER_IP.encode()) # TFTP server (as string) + pkt.append(0xff) + return bytes(pkt) + + +def dhcp_worker(iface_ip, bootfile, status): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + try: + s.setsockopt(socket.SOL_SOCKET, socket.SO_DONTROUTE, 1) + except OSError: + pass + s.bind(("0.0.0.0", 67)) + status.event("DHCP", f"listening on :67, server={iface_ip}") + + while True: + try: + data, src = s.recvfrom(2048) + except Exception as e: + status.event("DHCP", f"recv err: {e}") + continue + try: + _dhcp_handle(data, s, iface_ip, bootfile, status) + except Exception as e: + status.event("DHCP", f"handler err: {e}") + + +def _dhcp_handle(data, sock, iface_ip, bootfile, status): + if len(data) < 240 or data[236:240] != DHCP_MAGIC: + return + chaddr = data[28:34] + if not any(chaddr.startswith(p) for p in XIAOMI_OUI_PREFIXES): + return + + msg_type = None + for code, val in parse_dhcp_options(data[240:]): + if code == 53 and len(val) == 1: + msg_type = val[0] + break + + if msg_type == 1: + status.event("DHCP", f"DISCOVER from {mac_str(chaddr)} → OFFER {CLIENT_IP}") + status.set(state="DHCP_OFFER", mac=mac_str(chaddr)) + reply = build_dhcp_reply(data, msg_type=2, bootfile=bootfile) + elif msg_type == 3: + status.event("DHCP", f"REQUEST from {mac_str(chaddr)} → ACK {CLIENT_IP}") + status.set(state="DHCP_ACK", mac=mac_str(chaddr), ip=CLIENT_IP) + reply = build_dhcp_reply(data, msg_type=5, bootfile=bootfile) + else: + return + + subnet_bcast = str( + ipaddress.ip_network(f"{iface_ip}/{NETMASK}", strict=False).broadcast_address + ) + for dst in (subnet_bcast, "255.255.255.255"): + try: + sock.sendto(reply, (dst, 68)) + break + except OSError as e: + status.event("DHCP", f"sendto {dst}:68 failed: {e}") + + +# ── TFTP server ───────────────────────────────────────────────────────────── + +def tftp_serve(client, firmware_path, options, status): + block_size = int(options.get("blksize", TFTP_BLOCK_DEFAULT)) + fw_size = os.path.getsize(firmware_path) + + conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + conn.settimeout(TFTP_TIMEOUT_S) + + if options: + ack_opts = {} + if "blksize" in options: ack_opts["blksize"] = str(block_size) + if "tsize" in options: ack_opts["tsize"] = str(fw_size) + if "timeout" in options: ack_opts["timeout"] = options["timeout"] + oack = b"\x00\x06" + for k, v in ack_opts.items(): + oack += k.encode() + b"\x00" + v.encode() + b"\x00" + status.event("TFTP", f"OACK {ack_opts} → {client}") + conn.sendto(oack, client) + try: + reply, _ = conn.recvfrom(1024) + except socket.timeout: + status.event("TFTP", "no OACK-ACK, continuing with default blocksize") + block_size = TFTP_BLOCK_DEFAULT + + status.set(state="TFTP_XFER", ip=client[0], + sent_bytes=0, total_bytes=fw_size, + xfer_started=time.time()) + t0 = time.time() + last_speed_calc = t0 + last_speed_sent = 0 + + with open(firmware_path, "rb") as f: + block_num = 1 + while True: + chunk = f.read(block_size) + pkt = b"\x00\x03" + struct.pack("!H", block_num & 0xFFFF) + chunk + got_ack = False + for retry in range(5): + conn.sendto(pkt, client) + try: + reply, _ = conn.recvfrom(1024) + except socket.timeout: + if retry == 4: + break + continue + if reply[:2] == b"\x00\x04": + ack_blk = struct.unpack("!H", reply[2:4])[0] + if ack_blk == (block_num & 0xFFFF): + got_ack = True + break + elif reply[:2] == b"\x00\x05": + err = reply[4:].split(b"\x00")[0].decode(errors="replace") + status.event("TFTP", f"client ERROR: {err}") + status.set(state="ERROR", error_msg=err) + conn.close() + return + if not got_ack: + status.event("TFTP", f"block {block_num} timeout after 5 retries") + status.set(state="ERROR", error_msg=f"block {block_num} lost") + conn.close() + return + + sent = min(block_num * block_size, fw_size) + now = time.time() + if now - last_speed_calc >= 0.3: + speed = int((sent - last_speed_sent) / (now - last_speed_calc)) + status.set(sent_bytes=sent, speed_bps=speed) + last_speed_calc = now + last_speed_sent = sent + + if len(chunk) < block_size: + status.set(state="COMPLETE", + sent_bytes=fw_size, + speed_bps=int(fw_size / max(time.time() - t0, 0.001)), + xfer_done=time.time()) + elapsed = time.time() - t0 + status.event( + "TFTP", + f"COMPLETE — {block_num} blocks, {fw_size} bytes, " + f"{elapsed:.1f}s, avg {_fmt_speed(int(fw_size / max(elapsed, 0.001)))}", + ) + conn.close() + return + block_num += 1 + + +def tftp_worker(firmware_path, status): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("0.0.0.0", 69)) + status.event("TFTP", "listening on :69 for RRQ") + + while True: + data, src = s.recvfrom(2048) + if len(data) < 4: + continue + opcode = struct.unpack("!H", data[:2])[0] + if opcode != 1: + continue + parts = data[2:].split(b"\x00") + parts = [p for p in parts if p] + if len(parts) < 2: + continue + filename = parts[0].decode(errors="replace") + mode = parts[1].decode(errors="replace").lower() + options = {} + i = 2 + while i + 1 < len(parts): + options[parts[i].decode().lower()] = parts[i + 1].decode() + i += 2 + status.event("TFTP", f"RRQ from {src[0]} name='{filename}' opts={options}") + threading.Thread( + target=tftp_serve, + args=(src, firmware_path, options, status), + daemon=True, + ).start() + + +# ── main ──────────────────────────────────────────────────────────────────── + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("firmware", help="Path to .bin firmware image to serve") + ap.add_argument("--iface", default=DEFAULT_IFACE, + help=f"Interface with {SERVER_IP} assigned (default {DEFAULT_IFACE})") + ap.add_argument("--bootfile", default=None, + help="Filename to advertise in BOOTP file field " + "(default: basename of the firmware file — this is " + "what BE7000 U-Boot wants, e.g. " + "miwifi_rc06_firmware_65d1d_1.1.38.bin)") + args = ap.parse_args() + if args.bootfile is None: + args.bootfile = os.path.basename(args.firmware) + + if os.geteuid() != 0: + print("Run with sudo — need :67 and :69.", file=sys.stderr) + sys.exit(1) + + if not os.path.exists(args.firmware): + print(f"Firmware not found: {args.firmware}", file=sys.stderr) + sys.exit(1) + + st = Status(args.firmware, args.bootfile) + stop = threading.Event() + + threading.Thread(target=dhcp_worker, + args=(SERVER_IP, args.bootfile, st), + daemon=True).start() + threading.Thread(target=tftp_worker, + args=(args.firmware, st), + daemon=True).start() + threading.Thread(target=status_printer, + args=(st, stop), + daemon=True).start() + + try: + while True: + time.sleep(1.0) + except KeyboardInterrupt: + stop.set() + time.sleep(0.3) + print("\nstopped") + + +if __name__ == "__main__": + main() diff --git a/rules/rr b/rules/rr new file mode 100644 index 0000000..71d79a6 --- /dev/null +++ b/rules/rr @@ -0,0 +1,247 @@ +"routing": { + "domainStrategy": "IPIfNonMatch", + "rules": [ + { + "ip": [ + "geoip:private" + ], + "outboundTag": "direct" + }, + { + "domain": [ + "geosite:private" + ], + "outboundTag": "direct" + }, + { + "domain": [ + "domain:happ.su" + ], + "outboundTag": "proxy" + }, + { + "domain": [ + "domain:24h.tv", + "domain:calls.okcdn.ru", + "domain:ifconfig.me", + "domain:api.ipify.org", + "domain:checkip.amazonaws.com", + "domain:2w.ru", + "domain:4c.ru", + "domain:5ka.ru", + "domain:5post.market", + "domain:abr.ru", + "domain:afafb.com", + "domain:afisha.ru", + "domain:amediateka.ru", + "domain:applovin.com", + "domain:auchan.ru", + "domain:av.ru", + "domain:avs.io", + "domain:avtovokzaly.ru", + "domain:bcs-bank.ru", + "domain:bcs.ru", + "domain:beeline.tv", + "domain:blablacar.ru", + "domain:boosty.to", + "domain:bristol.ru", + "domain:broker.ru", + "domain:capitalt.ru", + "domain:cardsmobile.ru", + "domain:cbr.ru", + "domain:cdek.ru", + "domain:cdn-tinkoff.ru", + "domain:cdn-vk.ru", + "domain:chizhik.club", + "domain:cian.ru", + "domain:cikrf.ru", + "domain:citydrive.ru", + "domain:cplsb.ru", + "domain:custody.ru", + "domain:data-tscbank.ru", + "domain:dbo-dengi.online", + "domain:deltacredit.ru", + "domain:depository.ru", + "domain:detmir.ru", + "domain:dixy.ru", + "domain:dolyame.ru", + "domain:donationalerts.com", + "domain:dostaevsky.ru", + "domain:e5.ru", + "domain:eaptechka.online", + "domain:eapteka.ru", + "domain:ebs.ru", + "domain:edu.ru", + "domain:electronicsignature.ru", + "domain:emias.info", + "domain:enterly.ru", + "domain:era-invest.ru", + "domain:etna.ai", + "domain:etradebook.ru", + "domain:fivepost.ru", + "domain:fix-price.com", + "domain:gainstech.ru", + "domain:geobasket.ru", + "domain:getsynapse.io", + "domain:gorodpay.ru", + "domain:gosdolgi.ru", + "domain:goskey.ru", + "domain:gosuslugi.ru", + "domain:gov.ru", + "domain:government.ru", + "domain:gpb.ru", + "domain:grfc.ru", + "domain:gu-st.ru", + "domain:imgsmail.ru", + "domain:ivi.ru", + "domain:jfn.su", + "domain:kinopoisk.ru", + "domain:kinorium.com", + "domain:koshelek.app", + "domain:krasnoeibeloe.ru", + "domain:kremlin.ru", + "domain:kuper.ru", + "domain:lifestyle-tec.ru", + "domain:magnit.ru", + "domain:mail.ru", + "domain:malotogo.ru", + "domain:max-tcs.ru", + "domain:max.ru", + "domain:megamarket.ru", + "domain:megamarket.tech", + "domain:memealerts.com", + "domain:meteum.ai", + "domain:metro-cc.ru", + "domain:mfk-t-finance.ru", + "domain:mgfoms.ru", + "domain:mkb.ru", + "domain:mos.ru", + "domain:mosreg.ru", + "domain:mp.world", + "domain:mradx.net", + "domain:mtsdengi.ru", + "domain:mvk.com", + "domain:my-tbank.ru", + "domain:myapelsin.ru", + "domain:mycdn.me", + "domain:myspar.ru", + "domain:naydex.net", + "domain:neon.click", + "domain:nspk.ru", + "domain:ocourier.ru", + "domain:ok.ru", + "domain:okcdn.ru", + "domain:okolo.app", + "domain:oneme.ru", + "domain:onlyjobs.ru", + "domain:ostrovok.ru", + "domain:otello.ru", + "domain:payment.ru", + "domain:phoenix-ca.ru", + "domain:pochtabank.ru", + "domain:premier.one", + "domain:private-banking.ru", + "domain:prodcontest.com", + "domain:psk", + "domain:restream-media.net", + "domain:rnd.ru", + "domain:rrtp.ru", + "domain:rshb.ru", + "domain:rtbcdn.ru", + "domain:russoutdoor.ru", + "domain:safeliner.tech", + "domain:sage-observability.tech", + "domain:samokat.ru", + "domain:sbrf.ru", + "domain:sgcheck.ru", + "domain:smotreshka.tv", + "domain:spb.ru", + "domain:start.ru", + "domain:t.finance", + "domain:tamtam.chat", + "domain:tb-esim.ru", + "domain:tbank-online.com", + "domain:tbank.ru", + "domain:tconf.ru", + "domain:tcsbank.ru", + "domain:telega.info", + "domain:telega.me", + "domain:time-messenger.ru", + "domain:tmiaf.ru", + "domain:tpension.ru", + "domain:travel-tec.ru", + "domain:trbcdn.net", + "domain:turbopages.org", + "domain:tutu.ru", + "domain:tvigle.ru", + "domain:userapi.com", + "domain:vamzakaz.ru", + "domain:viju.ru", + "domain:vkusnoitochka.ru", + "domain:vkusvill.ru", + "domain:vsemrabota.ru", + "domain:vtb.ru", + "domain:vungle.com", + "domain:warehouse.domains", + "domain:winelab.ru", + "domain:wink.ru", + "domain:yads.tech", + "domain:yccdn.ru", + "domain:yektanet.com", + "domain:youla-web-static.mrgcdn.ru", + "domain:2ip.to", + "domain:icloud.com", + "regexp:(^|\\.)2gis\\.(ae|am|az|by|com|com.cy|cz|ge|kg|kz|ru|tj|ua|uz)$", + "regexp:(^|\\.)alfa[a-z0-9-]*\\.(oavdo.amc|ru|st)$", + "regexp:(^|\\.)atomyze\\.(ru|tech)$", + "regexp:(^|\\.)aviasales\\.(com|ru)$", + "regexp:(^|\\.)avito\\.(ru|st)$", + "regexp:(^|\\.)dodo[a-z0-9-]*\\.(com|io|net|ru)$", + "regexp:(^|\\.)dzen[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)gazprombank\\.(com|investments|ru)$", + "regexp:(^|\\.)it-[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)jivo[a-z0-9-]*\\.(chat|co.in|co.za|com|com.br|com.co|com.pe|com.tr|de|es|ng|ru)$", + "regexp:(^|\\.)lenta\\.(com|tech)$", + "regexp:(^|\\.)mosmetro\\.(ru|tech)$", + "regexp:(^|\\.)o3[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)okko\\.(sport|tv)$", + "regexp:(^|\\.)ozon[a-z0-9-]*\\.(by|com|pro|ru)$", + "regexp:(^|\\.)ozon\\.(by|com|dev|express|kz|li|ru|tech|tm|travel)$", + "regexp:(^|\\.)ozonru\\.(co.il|com|eu|me)$", + "regexp:(^|\\.)p(ay|lati)ecom\\.(ru)$", + "regexp:(^|\\.)perekrestok\\.(com|ru)$", + "regexp:(^|\\.)psb[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)rb(collect|credit)\\.(ru)$", + "regexp:(^|\\.)rosban[a-z0-9-]*\\.(com|ru)$", + "regexp:(^|\\.)rusfinance[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)rutube[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)rwb[a-z0-9-]*\\.(ru|tech)$", + "regexp:(^|\\.)rzd[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)sber[a-z0-9-]*\\.(by|com|pro|ru|tech)$", + "regexp:(^|\\.)spasib[a-z0-9-]*\\.(digital|market|ru)$", + "regexp:(^|\\.)t-[a-z0-9-]*\\.(by|group|ru|su)$", + "regexp:(^|\\.)t-tech\\.(dev|ru|team)$", + "regexp:(^|\\.)taxi[a-z0-9-]*\\.(org)$", + "regexp:(^|\\.)tink[a-z0-9-]*\\.(ai|com|ru)$", + "regexp:(^|\\.)tinsurance[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)tochka[a-z0-9-]*\\.(com)$", + "regexp:(^|\\.)vk[^u][a-z0-9-]*\\.(app|com|me|net|ru)$", + "regexp:(^|\\.)vk\\.(cc|com|company|design|link|me|ru|team)$", + "regexp:(^|\\.)vkuser[a-z0-9-]*\\.(com|net|ru)$", + "regexp:(^|\\.)w[aeiou]{0,2}l[a-z0-9-]*\\.(ru)$", + "regexp:(^|\\.)wb[a-z0-9-]*\\.(by|net|ru|tech)$", + "regexp:(^|\\.)wb\\.(am|kg|kz|ru)$", + "regexp:(^|\\.)webvisor\\.(com|org)$", + "regexp:(^|\\.)wibes\\.(com|ru)$", + "regexp:(^|\\.)wildberries\\.(am|by|co.il|es|eu|ge|kg|kz|lt|ru|tj|tm|uz)$", + "regexp:(^|\\.)x5[a-z0-9-]*\\.(net|ru)$", + "regexp:(^|\\.)x5\\.(ai|com|digital|group|media|ru|team|tech)$", + "regexp:(^|\\.)ya\\.(cc|ru)$", + "regexp:(^|\\.)yand[a-z0-9-]*\\.(com|net|org|ru)$", + "regexp:(^|\\.)yandex\\.(aero|asia|az|by|cloud|co.il|com|com.am|com.ge|com.ru|com.tr|com.ua|de|ee|eu|fi|fr|jobs|kg|kz|lt|lv|md|mobi|net|org|pl|ru|st|sx|tj|tm|tr|ua|uz)$", + "regexp:(^|\\.)yang[a-z0-9-]*\\.(com)$", + "regexp:(^|\\.)yango\\.(africa|ao|bh|co.il|com|fi|qa|taxi|tech)$", + "regexp:(^|\\.)yast[a-z0-9-]*\\.(net|ru)$", + "regexp:(^|\\.)zvuk[a-z0-9-]*\\.(com)$", + "regexp:wb\\.(com|ru)$", + "regexp:youla\\.(io|ru)$" \ No newline at end of file