From b25a1495741c40542811bd0011c3dccc6b6394b3 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:37:06 +0200 Subject: [PATCH] Ke zmerene adrese i rozsah, ktery ji pokryje cely Povolit jednu adresu nema smysl: docker prideluje z bloku a pri prekresleni site nebo redeployi se cisla meni, takze povoleni vydrzi do prvniho restartu. Portal proto k namerene adrese dopocita CIDR rozsah - 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10. U 172.16-31 schvalne /12 a ne /16 toho konkretniho bridge: docker si smi vzit kterykoliv podblok a nikdo nezaruci, ze zustane u toho dnesniho. Verejna adresa zadny rozsah nedostane, tam nabizet blok nema smysl. Taky opravena popiska u remoteAddress. Je to sama proxy, ne volajici - k nam uz to jde od ni. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/12-sluzby-a-konektory.md | 7 ++++ src/data/egressIp.ts | 57 ++++++++++++++++++++++++-- src/openapi.ts | 17 +++++++- web/src/pages/dashboard/Connectors.tsx | 10 ++++- web/src/types/dashboard.ts | 3 ++ 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/documentation/12-sluzby-a-konektory.md b/documentation/12-sluzby-a-konektory.md index fbb0075..fc0ac79 100644 --- a/documentation/12-sluzby-a-konektory.md +++ b/documentation/12-sluzby-a-konektory.md @@ -211,6 +211,13 @@ Samotny kod odpovedi nikoho nikam nedovede. Hlaska proto nese tri veci: `/whoami` je zamerne bez prihlaseni: vraci volajicimu jeho vlastni adresu, tedy nic, co by uz nevedel. + K namerene adrese se dopocita **rozsah, ktery ji pokryje cely** + (`suggestedRange`). Do seznamu povolenych patri on, ne jedna adresa: docker + prideluje z bloku a pri prekresleni site nebo redeployi se cisla meni, + takze povoleni jedne adresy vydrzi do prvniho restartu. U `172.16-31.x.x` + se schvalne vraci `/12`, ne `/16` toho konkretniho bridge - docker si smi + vzit kterykoliv podblok. + Co se **nedela**: sonda na `/health` vedle overeni. `/health` povoleni IP adresy nevyzaduje, takze z toho, ze projde, se o IP nic nedozvis - byla by to veta, ktera zni jako zaver a zadny neni. diff --git a/src/data/egressIp.ts b/src/data/egressIp.ts index 06ed274..bd6c792 100644 --- a/src/data/egressIp.ts +++ b/src/data/egressIp.ts @@ -35,17 +35,64 @@ export interface EgressIp { } export interface ProxyView { - /** Adresa po `trust proxy`, tedy z X-Forwarded-For. */ + /** + * **Tohle je to cislo.** Adresa po `trust proxy`, tedy z X-Forwarded-For - + * proxy tam zapsala, s jakou adresou k ni nase volani doslo. + */ ip: string | null; /** Surova hlavicka, at je videt i to, co proxy nepripsala. */ forwardedFor: string | null; - /** Adresa druheho konce spojeni, jak ji vidi nas server. */ + /** + * Adresa druheho konce spojeni. Pozor, to je **sama proxy**, ne volajici - + * k nam uz to jde od ni. Je to tu na to, aby bylo poznat, ze se volani + * opravdu tocilo pres ni a ne primo. + */ remoteAddress: string | null; + /** + * Rozsah, ktery tuhle adresu pokryje cely. Docker prideluje z bloku, + * takze povolit jednu adresu vydrzi do prvniho prekresleni site - patri + * tam cely rozsah. null = adresa je verejna, zadny blok se nenabizi. + */ + suggestedRange: string | null; /** Kam se volalo. Pro uzivatele, at vi, co se vlastne merilo. */ url: string; error?: string; } +/** + * Rozsah, ktery danou adresu pokryje cely. + * + * U dockeru nema smysl povolovat jednu adresu: bridge se pri prekresleni site + * nebo pri redeployi muze precislovat a povoleni vydrzi do prvniho restartu. + * Proto cely blok, ze ktereho adresa pochazi. + * + * U 172.16-31 se schvalne vraci `/12`, ne `/16` toho konkretniho bridge. + * Docker si smi vzit kterykoliv podblok a nikdo nezaruci, ze zustane u toho + * dnesniho. + */ +export function suggestRange(raw: string | null): string | null { + if (raw === null) return null; + // Node casto vraci IPv4 zabalenou do IPv6 jako `::ffff:172.17.0.1`. + const ip = raw.trim().replace(/^::ffff:/i, '').toLowerCase(); + if (ip === '') return null; + + if (ip === '::1') return '::1/128'; + if (/^f[cd]/.test(ip) && ip.includes(':')) return 'fc00::/7'; + if (/^fe80:/.test(ip)) return 'fe80::/10'; + + const parts = ip.split('.'); + if (parts.length !== 4) return null; + const [first, second] = parts.map((part) => Number(part)); + if (!Number.isInteger(first) || !Number.isInteger(second)) return null; + + if (first === 127) return '127.0.0.0/8'; + if (first === 10) return '10.0.0.0/8'; + if (first === 192 && second === 168) return '192.168.0.0/16'; + if (first === 172 && second >= 16 && second <= 31) return '172.16.0.0/12'; + if (first === 169 && second === 254) return '169.254.0.0/16'; + return null; +} + let cached: EgressIp | null = null; let cachedAt = 0; /** Rozdelane volani. Deset soubeznych dotazu nema delat deset volani ven. */ @@ -98,16 +145,19 @@ async function askProxy(): Promise { ip: null, forwardedFor: null, remoteAddress: null, + suggestedRange: null, url, error: `Vlastní adresa vrátila HTTP ${response.status}.`, }; } const body = (await response.json()) as Partial; + const ip = typeof body.ip === 'string' ? body.ip : null; return { - ip: typeof body.ip === 'string' ? body.ip : null, + ip, forwardedFor: typeof body.forwardedFor === 'string' ? body.forwardedFor : null, remoteAddress: typeof body.remoteAddress === 'string' ? body.remoteAddress : null, + suggestedRange: suggestRange(ip), url, }; } catch (err) { @@ -116,6 +166,7 @@ async function askProxy(): Promise { ip: null, forwardedFor: null, remoteAddress: null, + suggestedRange: null, url, error: `Vlastní adresu se nepodařilo zavolat: ${message}`, }; diff --git a/src/openapi.ts b/src/openapi.ts index 42585fe..f3003b0 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -1718,7 +1718,22 @@ export function buildOpenApiDocument() { properties: { ip: { type: 'string', nullable: true }, forwardedFor: { type: 'string', nullable: true }, - remoteAddress: { type: 'string', nullable: true }, + remoteAddress: { + type: 'string', + nullable: true, + description: + 'Sama proxy, ne volajici - k nam uz to jde od ni. Je to tu na to, ' + + 'aby bylo poznat, ze se volani opravdu tocilo pres ni.', + }, + suggestedRange: { + type: 'string', + nullable: true, + description: + 'CIDR rozsah, ktery tu adresu pokryje cely (napr. 172.16.0.0/12 ' + + 'nebo 127.0.0.0/8). Do seznamu povolenych patri on, ne jedna ' + + 'adresa: docker prideluje z bloku a pri prekresleni site se cisla ' + + 'meni. null = adresa je verejna, zadny blok se nenabizi.', + }, url: { type: 'string' }, error: { type: 'string' }, }, diff --git a/web/src/pages/dashboard/Connectors.tsx b/web/src/pages/dashboard/Connectors.tsx index 18edd67..e453136 100644 --- a/web/src/pages/dashboard/Connectors.tsx +++ b/web/src/pages/dashboard/Connectors.tsx @@ -268,9 +268,17 @@ function EgressNotice({ egress }: { egress: EgressIp }) { {egress.viaProxy.error ?? 'Tohle je adresa, kterou porovnává seznam povolených IP u služeb za toutéž proxy. Když se liší od té veřejné, volání se točí přes vnitřní síť a na seznamu musí být ona.'} + {egress.viaProxy.suggestedRange && ( + + Na seznam povolených patří celý rozsah{' '} + {egress.viaProxy.suggestedRange}, ne + tahle jedna adresa. Docker přiděluje z bloku a při překreslení sítě se čísla + mění, takže povolení jedné adresy vydrží do prvního redeploye. + + )} {(egress.viaProxy.forwardedFor || egress.viaProxy.remoteAddress) && ( - x-forwarded-for: {egress.viaProxy.forwardedFor ?? '(nic)'} / spojení z:{' '} + x-forwarded-for: {egress.viaProxy.forwardedFor ?? '(nic)'} / proxy sama:{' '} {egress.viaProxy.remoteAddress ?? '(nic)'} )} diff --git a/web/src/types/dashboard.ts b/web/src/types/dashboard.ts index acf4a9d..813a042 100644 --- a/web/src/types/dashboard.ts +++ b/web/src/types/dashboard.ts @@ -437,7 +437,10 @@ export interface EgressIp { viaProxy: { ip: string | null; forwardedFor: string | null; + /** Sama proxy, ne volajici - k nam uz to jde od ni. */ remoteAddress: string | null; + /** Rozsah, ktery tu adresu pokryje cely. null = adresa je verejna. */ + suggestedRange: string | null; url: string; error?: string; } | null;