Robert Važan

Subdomény na localhoste

Programovanie webových aplikácií a prevádzka lokálnych webových aplikácií zvyčajne vyžaduje vysoké čísla portov, napríklad :8080, ktoré sa ťažko pamätajú a sú náchylné na konflikty. Pozrime sa preto na lepší prístup: využitie subdomén localhostu namiesto čísel portov.

Žiadne štandardné riešenie

Neexistuje žiadne systémové API na pridelenie subdomén localhostu tak, ako je to pri portoch. V minulosti som namapoval subdoménu mojej verejnej domény (*.test.machinezoo.com) na 127.0.0.1, ale toto riešenie má niekoľko nevýhod: nie je dostupné pre programátorov bez vlastnej domény, je potenciálne nebezpečné, nefunguje pri výpadku internetu a stále vyžaduje porty v URL adresách.

Lepším riešením je spustiť reverzný proxy server na porte 80, ktorý presmeruje virtuálne domény na konkrétne porty. Toto riešenie funguje dobre nielen pre vývoj webových aplikácií, ale aj pre bežné lokálne webové aplikácie, ako sú Syncthing a Open WebUI.

Konfigurácia DNS

Na mojom systéme (Fedora) subdomény ako app1.localhost smerujú na rovnakú adresu ako samotný localhost (::1 pre IPv6 a 127.0.0.1 pre IPv4), čo je zrejme vďaka systemd-resolved. Rýchly test ukazuje, že to funguje tak s ping, ako aj vo Firefoxe. Hoci to nemusí fungovať automaticky na všetkých operačných systémoch, zvyčajne je to jednoduché nakonfigurovať.

Ak nastavujete systémový resolver domén manuálne, môžete na localhost nasmerovať akúkoľvek rezervovanú doménu. Hoci .test je dobrá voľba a .local sa často (aj keď nesprávne) používa na tento účel, ja preferujem .localhost doménu, pretože je určená na tento účel a názov sa hodí pre akúkoľvek aplikáciu.

Výber reverzného proxy servera

Pokiaľ ide o reverzné proxy servery, sú tu traja hlavní kandidáti: Caddy, Traefik a Nginx. Nginx som vylúčil, pretože je písaný v C (potenciálne bezpečnostné riziko), je prepojený na Rusko prostredníctvom niektorých jeho najaktívnejších vývojárov a niektoré kľúčové funkcie sú k dispozícii len pre platiacich zákazníkov.

Z dvojice Caddy a Traefik som uprednostnil Caddy. Konfigurácia Traefiku je podstatne rozsiahlejšia a zložitejšia než Caddyfile. Caddy je navrhnutý tak, aby sa dal ľahko konfigurovať, pričom lokálne servery sú jedným z jeho zamýšľaných využití. Hoci Caddy má menej funkcií než Traefik, pre naše potreby je viac než postačujúci.

Stratégia výberu portov

Aj pri použití subdomén stále každá webová aplikácia potrebuje port, na ktorý môže reverzný proxy server preposielať dotazy. Namiesto konfigurovateľných portov, z čoho by bola extra práca pre programátorov a starosti pre používateľov, odporúčam zvoliť si pevný port náhodne v rozsahu medzi 1 024 a 32 000. Privilegovaným portom pod 1 024 a dynamickým portom nad 32 000 je lepšie sa vyhnúť. Konflikty s inými aplikáciami sú nepravdepodobné, ale ak ich niekto nahlási, proste zvoľte iný náhodný port. Porty by mali byť konfigurovateľné iba v populárnych lokálnych serveroroch, kde je to opodstatnené.

Konfigurácia Caddy

Caddy používa jediný konfiguračný súbor s názvom Caddyfile. Vďaka rozumným predvoľbám môže konfigurácia zostať pomerne stručná. Tu je základný príklad:

{
    default_bind 127.0.0.1
    admin off
}

http://app1.localhost {
    reverse_proxy localhost:8080
}

http://*.app2.localhost {
    reverse_proxy localhost:8081
}

Nie som si istý, či je default_bind striktne potrebný (Caddy by to mohol odvodiť zo zakončenia domény na .localhost), ale je lepšie špecifikovať to explicitne. Vypol som admin API, aby som nemusel analyzovať jeho dopad na bezpečnosť, aj keď niektorí používatelia by ho mohli chcieť, aby mohli znovu načítať Caddyfile bez reštartovania Caddy. Predpona http:// v adrese zaručuje, že Caddy počúva iba na porte 80 a nesnaží sa získať TLS certifikáty. Caddy podporuje long-poll a websocket automaticky bez dodatočnej konfigurácie.

Caddyfile môže byť trochu repetitívny. Aj keď Caddy podporuje šablóny (snippets) pre stručnú definíciu viacerých domén, manipulácia s parametrami šablón mi príde trochu ťažkopádna. Namiesto toho používam krátky Python skript na vygenerovanie zoznamu domén z dvojíc subdoména-port:

import textwrap

mappings = {
    8080: "app1",
    8081: "*.app2",
}

print(textwrap.dedent('''
    {
        default_bind 127.0.0.1
        admin off
    }
''').strip())

for port, subdomain in mappings.items():
    print(textwrap.dedent(f'''
        http://{subdomain}.localhost {{
            reverse_proxy localhost:{port}
        }}
    ''').rstrip())

Spúšťanie Caddy servera

Hoci Caddy môže bežať priamo ako systémová služba (Caddyfile vyššie je na to navrhnutý), kontajnerizácia poskytuje čistejšie riešenie so zníženým bezpečnostným rizikom. Takto ho spustíte s Podmanom:

sudo podman run -d --name caddy \
    --replace \
    --pull=always \
    --restart=always \
    --network=host \
    --stop-signal=SIGKILL \
    -v /path/to/Caddyfile:/etc/caddy/Caddyfile:z \
    docker.io/caddy
sudo systemctl enable podman-restart

Parameter --network=host je nevyhnutný, aby mohol Caddy preposlielať dotazy na lokálne porty. Kontajner možno aktualizovať jednoducho opätovným spustením tohoto príkazu a podman-restart zabezpečí, že Caddy sa spustí po štarte systému. Keďže admin API je vypnuté a TLS sa nepoužíva, nie je potrebné pripojiť dátový ani konfiguračný zväzok. Stop signál SIGKILL zaručuje rýchle vypnutie/reštart, čím sa vyhneme predvolenému správaniu SIGTERM, kde Caddy čaká na ukončenie spojení (čo môže trvať dlho pri aktívnych long-poll dotazoch).

Stojí to za to?

Nastavenie subdomén na localhoste má zmysel v niekoľkých prípadoch:

Pre občasné lokálne testovanie webových aplikácií je však toto riešenie zbytočne zložité.