13 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Polityka rodziny IP egress (IPv4/IPv6) | 3.8.40 | 2026-06-28 |
Polityka rodziny IP egress (IPv4/IPv6)
Przypnij ruch wychodzący do jednej rodziny IP —
auto,ipv4lubipv6— per proxy, żeby egress tylko-IPv6 nigdy cicho nie wyciekał z powrotem na IPv4.
Source of truth:
open-sse/utils/proxyFamily.ts,open-sse/utils/proxyDispatcher.ts,open-sse/utils/proxyFetch.ts,open-sse/utils/socksConnectorWithFamily.ts,open-sse/utils/proxyFamilyResolve.ts,src/shared/validation/schemas.ts,src/lib/db/proxies.ts,src/lib/db/upstreamProxy.ts,src/lib/db/migrations/099_proxy_family.sql
OmniRoute pozwala każdemu proxy nieść dyrektywę egress rodziny adresów. Domyślnie system operacyjny wybiera IPv4 lub IPv6 (dual-stack, „Happy Eyeballs”). Gdy ustawisz dyrektywę na ipv4 lub ipv6, OmniRoute przypina każde połączenie przez to proxy do wybranej rodziny i fails closed (odmawia), zamiast robić fallback na drugą rodzinę.
Ta strona dokumentuje, czym jest dyrektywa, po co istnieje, gdzie się ją konfiguruje i jak runtime ją rozwiązuje.
Spis treści
- Czym to jest
- Dlaczego istnieje
- Trzy wartości
- Jak to skonfigurować
- Jak rozwiązuje się
auto - Jak egzekwowane są
ipv4/ipv6 - Kompatybilność SOCKS5
- Zachowanie fail-closed
- Model danych
- Powiązana dokumentacja
Czym to jest
Każde proxy w rejestrze ma pole family z trzema możliwymi wartościami, walidowanymi przez enum Zod:
// src/shared/validation/schemas.ts
family: z.enum(["auto", "ipv4", "ipv6"]).optional().default("auto"),
Pole domyślnie ma wartość "auto", co zachowuje wcześniejsze zachowanie dual-stack. Ustawienie na ipv4 lub ipv6 przypina rodzinę połączenia (connect family) dla tego proxy.
Dyrektywa jest normalizowana wszędzie przez jeden helper, więc dowolna nieznana wartość zwija się do auto:
// open-sse/utils/proxyFamily.ts
export type ProxyFamily = "auto" | "ipv4" | "ipv6";
export function parseProxyFamily(value: unknown): ProxyFamily {
return value === "ipv4" || value === "ipv6" ? value : "auto";
}
Dlaczego istnieje
Wprowadzone w PR #3777. Problemy motywujące:
| Problem | Co naprawia dyrektywa |
|---|---|
| Egress tylko-IPv6 wyciekający na IPv4 | Gdy host proxy ma zarówno rekordy A, jak i AAAA (albo OS preferuje IPv4), Happy Eyeballs może nawiązać połączenie po IPv4 nawet gdy zamierzasz ścieżkę tylko-IPv6. Przypięcie ipv6 usuwa ten wyciek. |
| Revocation przy anomalii shared-egress | Rotujące providery (codex/openai) unieważniają tokeny, gdy wiele kont wychodzi przez ten sam IP przy wysokim wolumenie. Kontrola rodziny egress jest częścią utrzymania kont na odrębnych, przewidywalnych ścieżkach egress (zob. src/lib/proxyEgress.ts pod kątem diagnostyki egress-IP, która idzie w parze z tym). |
| Deterministyczny egress na compliance/testy | Gdy musisz zagwarantować, że ruch wychodzi określoną rodziną, auto nie wystarcza. |
Dyrektywa jest celowo per-proxy, a nie globalna — różne proxy w puli mogą mieć różne polityki.
Trzy wartości
| Wartość | Etykieta UI | Zachowanie |
|---|---|---|
auto |
Auto (dual-stack) |
OS wybiera rodzinę. Dla hosta proxy będącego literałem IP rodzina wynika z literału; dla hostname obie rodziny są dopuszczalne. To domyślna wartość. |
ipv4 |
IPv4 only |
Przypina połączenie do IPv4. Fails closed, jeśli host proxy nie ma rekordu IPv4 (A). |
ipv6 |
IPv6 only |
Przypina połączenie do IPv6. Fails closed, jeśli host proxy nie ma rekordu IPv6 (AAAA). |
Stringi UI są w src/i18n/messages/en.json (labelFamily, familyAuto, familyIpv4, familyIpv6, familyHint).
Jak to skonfigurować
Dashboard
Selektor jest w formularzu proxy zakładki Proxy Pool:
- Otwórz Dashboard → Settings → Proxy → Proxy Pool
- Dodaj lub edytuj proxy
- Ustaw dropdown IP family na
Auto (dual-stack),IPv4 onlylubIPv6 only - Zapisz
Kontrolka jest renderowana przez ProxyRegistryManager.tsx (montowany w proxy/ProxyPoolTab.tsx).
API
Pole family jest częścią payloadów create/update rejestru proxy, walidowanych przez createProxyRegistrySchema / updateProxyRegistrySchema (src/shared/validation/schemas.ts) i obsługiwanych przez POST / PATCH /api/v1/management/proxies:
# Create an IPv6-only proxy
curl -X POST http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{
"name": "IPv6 egress",
"type": "socks5",
"host": "proxy.example.com",
"port": 1080,
"family": "ipv6"
}'
# Change an existing proxy to IPv4-only
curl -X PATCH http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{ "id": "proxy-uuid-here", "family": "ipv4" }'
To samo pole jest też akceptowane przez inline obiekt konfiguracji proxy używany przy wpisach upstream-proxy (upstream_proxy_config.family, zob. Model danych).
Reszta API CRUD/assignment proxy: PROXY_GUIDE.md.
Jak rozwiązuje się auto
Gdy family to auto, OmniRoute nie dokleja żadnej dyrektywy — URL proxy jest używany as-is, a rodzina połączenia wynika wewnętrznie.
W czasie budowy URL (proxyConfigToUrl / normalizeProxyUrl w open-sse/utils/proxyDispatcher.ts) proxy auto daje zwykły URL bez markera:
// open-sse/utils/proxyDispatcher.ts
const fam = parseProxyFamily(config.family);
const normalized = normalizeProxyUrl(proxyUrlStr, "context proxy", { allowSocks5 });
return fam === "auto" ? normalized : `${normalized}?family=${fam}`;
W czasie dispatchu (resolveDispatcherFamily) auto rozwiązuje się do wewnętrznej rodziny hosta będącego literałem IP albo do null (decyduje OS) dla hostname:
// open-sse/utils/proxyDispatcher.ts
function resolveDispatcherFamily(parsed: URL): 4 | 6 | null {
const directive = parseProxyFamily(parsed.searchParams.get("family") ?? undefined);
const literal = detectIpLiteralFamily(parsed.hostname);
if (directive === "auto") return literal; // null for a hostname → OS picks
// ...
}
Zatem:
auto+ host literał IP (192.0.2.1/[2001:db8::1]) → rodzina tego literału.auto+ hostname →null→ standardowa dual-stack rezolucja OS.
Jak egzekwowane są ipv4 / ipv6
Dyrektywa inna niż auto podróżuje jako jeden syntetyczny marker query — ?family=ipv4 lub ?family=ipv6 — doklejany raz do znormalizowanego URL proxy. normalizeProxyUrl ostrożnie usuwa i ponownie dokleja ten marker dokładnie raz, żeby nigdy nie psuć parsowania portu.
Gdy budowany jest dispatcher, marker jest odczytywany i zamieniany na konkretną rodzinę połączenia. Jeśli host to literał IP przeciwnej rodziny, OmniRoute rzuca wyjątek (sprzeczność = fail-closed):
// open-sse/utils/proxyDispatcher.ts
const want = directive === "ipv6" ? 6 : 4;
if (literal !== null && literal !== want) {
throw new Error(
`[ProxyDispatcher] Proxy family directive ${directive} contradicts ${literal === 6 ? "IPv6" : "IPv4"} literal host`
);
}
Konkretna rodzina jest potem przypinana na connectorze:
- Proxy HTTP/HTTPS (
ProxyAgent):proxyTls: { family, autoSelectFamily: false }— wyłącza Happy Eyeballs, więc wybierana jest wyłącznie wskazana rodzina. - Proxy SOCKS5: niestandardowy connector przekazuje
socket_options: { family, autoSelectFamily: false }do klienta SOCKS (zob. Kompatybilność SOCKS5).
Kompatybilność SOCKS5
Pin rodziny działa z proxy SOCKS5, ale stockowe fetch-socks nie udostępnia opcji socket potrzebnych do przypięcia rodziny hopu proxy. OmniRoute dostarcza własny connector do tego:
// open-sse/utils/socksConnectorWithFamily.ts
export function buildSocksFamilySocketOptions(family: 4 | 6 | null): Record<string, unknown> {
if (family === 6) return { family: 6, autoSelectFamily: false };
if (family === 4) return { family: 4, autoSelectFamily: false };
return {};
}
createProxyDispatcher wybiera connector w zależności od tego, czy rodzina jest przypięta:
family === null(czyliautonad hostname) → stockowesocksDispatcherzfetch-socks.family === 4 | 6→createSocksDispatcherWithFamily, które przekazujesocket_optionsdoSocksClient.createConnection, żeby Happy Eyeballs nie wybrał IPv4 przy polityce egress tylko-IPv6.
Sam support SOCKS5 jest domyślnie włączony (opt-out przez ENABLE_SOCKS5_PROXY=false); zob. PROXY_GUIDE.md → Environment Variables.
Zachowanie fail-closed
Cały sens dyrektywy to odmowa, a nie cichy fallback na złą rodzinę. Egzekwują to dwa guardy:
-
Sprzeczność literału — dyrektywa sprzeczna z hostem będącym literałem IP rzuca wyjątek przy budowie dispatchera (
resolveDispatcherFamily, pokazane wyżej). -
Pre-flight DNS hostname — dla proxy z hostname i przypiętą rodziną
proxyFetch.tsweryfikuje, że hostname faktycznie ma rekord w wymaganej rodzinie przed egressem, przezassertHostnameSupportsFamily:// open-sse/utils/proxyFamilyResolve.ts const hasFamily = records.some((r) => r.family === family); if (!hasFamily) { throw new Error( `[ProxyFamily] Proxy host ${host} has no ${family === 6 ? "IPv6 (AAAA)" : "IPv4 (A)"} record; ` + `refusing ${family === 6 ? "IPv6" : "IPv4"}-only egress (fail-closed)` ); }Przy niepowodzeniu
proxyFetch.tsoznacza błąd jakocode = "PROXY_FAMILY_UNAVAILABLE"istatusCode = 503. Niepowodzenie rezolucji DNS jest również traktowane jako fail-closed (odmowa egressu).
Hosty będące literałami IP to no-op dla pre-flight DNS — ich rodzina wynika z literału i nie wymaga lookupu.
Model danych
Kolumna family została dodana migracją 099_proxy_family.sql do dwóch tabel:
-- src/lib/db/migrations/099_proxy_family.sql
ALTER TABLE proxy_registry ADD COLUMN family TEXT NOT NULL DEFAULT 'auto';
ALTER TABLE upstream_proxy_config ADD COLUMN family TEXT NOT NULL DEFAULT 'auto';
proxy_registry.family— dyrektywa per-proxy dla wpisów rejestru (src/lib/db/proxies.ts). Zapytania rezolucji wybierająfamilyobok pozostałych kolumn proxy, a brakująca/nie-stringowa wartość jest coercowana do"auto".upstream_proxy_config.family— dyrektywa dla wpisów upstream-proxy (src/lib/db/upstreamProxy.ts), z tym samym domyślnym"auto".
Gdy rozwiązany obiekt proxy niesie nie-auto family, proxyConfigToUrl dokleja marker ?family=, żeby pin przetrwał aż do dispatchera.
Powiązana dokumentacja
📖 Powiązana dokumentacja:
- Proxy Guide — pełny system proxy: CRUD rejestru, 4-poziomowa rezolucja, rotacja, health checking, referencja API
- Stealth Guide — warstwy fingerprint TLS i CLI fingerprint jadące na wierzchu proxy
- Route Guard Tiers — egzekwowanie loopback dla tras tylko-lokalnych