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) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-25 07:37:06 +02:00
co-authored by Claude Opus 5
parent 6a2e0dc424
commit b25a149574
5 changed files with 89 additions and 5 deletions
+7
View File
@@ -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.
+54 -3
View File
@@ -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<ProxyView | null> {
ip: null,
forwardedFor: null,
remoteAddress: null,
suggestedRange: null,
url,
error: `Vlastní adresa vrátila HTTP ${response.status}.`,
};
}
const body = (await response.json()) as Partial<ProxyView>;
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<ProxyView | null> {
ip: null,
forwardedFor: null,
remoteAddress: null,
suggestedRange: null,
url,
error: `Vlastní adresu se nepodařilo zavolat: ${message}`,
};
+16 -1
View File
@@ -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' },
},
+9 -1
View File
@@ -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.'}
</span>
{egress.viaProxy.suggestedRange && (
<span className="mt-1 block text-xs text-white/50">
Na seznam povolených patří celý rozsah{' '}
<span className="font-mono text-white">{egress.viaProxy.suggestedRange}</span>, 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.
</span>
)}
{(egress.viaProxy.forwardedFor || egress.viaProxy.remoteAddress) && (
<span className="mt-1 block font-mono text-xs break-all text-white/35">
x-forwarded-for: {egress.viaProxy.forwardedFor ?? '(nic)'} / spojení z:{' '}
x-forwarded-for: {egress.viaProxy.forwardedFor ?? '(nic)'} / proxy sama:{' '}
{egress.viaProxy.remoteAddress ?? '(nic)'}
</span>
)}
+3
View File
@@ -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;