From f14578dc65c203f5c520241cf6f85d8fbe122585 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 24 May 2026 05:56:54 +0200 Subject: [PATCH] feat(dns): party-DNS records generator + online/airgap modes DNS is now your own party server. tooling/dns-records.sh prints the records (online = service names; airgap = + Mojang/launcher/NeoForge spoofs) in several formats. build-stack --up online|airgap selects the mirror layer and prints the matching records. dnsmasq moved to an optional override (docker-compose.dnsmasq.yml). Default domain switched to .lan (.local is hijacked by mDNS on unicast DNS). Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 13 +++++-- docker-compose.dnsmasq.yml | 20 +++++++++++ docker-compose.yml | 24 +++---------- tooling/build-stack.sh | 61 +++++++++++++++++++------------- tooling/dns-records.sh | 72 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 46 deletions(-) create mode 100644 docker-compose.dnsmasq.yml create mode 100755 tooling/dns-records.sh diff --git a/.env.example b/.env.example index 73717a7..71d3531 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,19 @@ # Copy to .env and fill in real values # Single base domain for the whole stack; every service is a subdomain of this. -BASE_DOMAIN=ulicraft.local -# The Docker host's LAN IP — dnsmasq points *.BASE_DOMAIN here for guest laptops. +# Use .lan (NOT .local) — .local is intercepted by mDNS on macOS/Linux and +# won't resolve through a normal unicast DNS server. +BASE_DOMAIN=ulicraft.lan +# The Docker host's LAN IP — every service name resolves here. HOST_LAN_IP=192.168.1.10 +# mDNS toggle. true + BASE_DOMAIN=*.local → an Avahi container publishes the +# service names over mDNS (zero-config for macOS/Linux guests; Windows needs +# Bonjour). false → use your own party DNS (records: tooling/dns-records.sh). +# NOTE: mDNS resolves ONLY *.local; it cannot serve the air-gap upstream-host +# spoofs — those always need a real DNS server. +ENABLE_MDNS=false + RCON_PASSWORD=change-me-to-something-random # Only needed if using CurseForge-exclusive mods: # CF_API_KEY=your-curseforge-api-key diff --git a/docker-compose.dnsmasq.yml b/docker-compose.dnsmasq.yml new file mode 100644 index 0000000..99005c1 --- /dev/null +++ b/docker-compose.dnsmasq.yml @@ -0,0 +1,20 @@ +# Optional turnkey DNS — only if you DON'T run your own party DNS server. +# Layer on: `docker compose -f docker-compose.yml -f docker-compose.dnsmasq.yml ... up -d`. +# Serves dnsmasq/dnsmasq.conf (rendered from the template, includes the airgap +# spoof records). If you have your own DNS, skip this and use the records from +# `tooling/dns-records.sh` instead. +services: + dnsmasq: + image: jpillora/dnsmasq + container_name: dnsmasq + restart: unless-stopped + ports: + # Bind DNS to the host LAN IP so it doesn't clash with systemd-resolved on :53. + - "${HOST_LAN_IP}:53:53/udp" + - "${HOST_LAN_IP}:53:53/tcp" + volumes: + - ./dnsmasq/dnsmasq.conf:/etc/dnsmasq.conf:ro + networks: + - mcnet + # webproc UI (:8080) can be exposed behind Caddy as dns.${BASE_DOMAIN}; + # guard with HTTP_USER / HTTP_PASS. diff --git a/docker-compose.yml b/docker-compose.yml index ddf54c2..269904e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,10 @@ # ────────────────────────────────────────────────────────────────── # Ulicraft LAN-party stack — Minecraft 1.21.1 NeoForge + Drasl auth # ────────────────────────────────────────────────────────────────── -# Topology (single source of config: BASE_DOMAIN + HOST_LAN_IP in .env): -# - dnsmasq resolves *.${BASE_DOMAIN} -> HOST_LAN_IP for guest laptops +# Single source of config: BASE_DOMAIN + HOST_LAN_IP in .env. +# DNS is provided by YOUR party DNS server — see tooling/dns-records.sh for the +# records to add (online vs airgap). A turnkey dnsmasq is available as an +# optional layer: docker-compose.dnsmasq.yml. # - caddy is the only HTTP ingress; holds mcnet aliases so the stack # resolves auth.${BASE_DOMAIN} / packwiz.${BASE_DOMAIN} internally too # - drasl handles auth (password login; NO Keycloak/OIDC in this stack) @@ -11,24 +13,6 @@ # ────────────────────────────────────────────────────────────────── services: - dnsmasq: - image: jpillora/dnsmasq - container_name: dnsmasq - restart: unless-stopped - ports: - # Bind DNS to the host LAN IP so it doesn't clash with systemd-resolved on :53. - - "${HOST_LAN_IP}:53:53/udp" - - "${HOST_LAN_IP}:53:53/tcp" - volumes: - - ./dnsmasq/dnsmasq.conf:/etc/dnsmasq.conf:ro - networks: - - mcnet - # The webproc UI (:8080) can be exposed later behind Caddy as dns.${BASE_DOMAIN}; - # guard it with HTTP_USER / HTTP_PASS env vars. - # environment: - # HTTP_USER: "admin" - # HTTP_PASS: "change-me" - # Core ingress. Static site + air-gap mirror are layered in via override # files (docker-compose.static.yml / .mirror.yml) — see build-stack.sh --up. caddy: diff --git a/tooling/build-stack.sh b/tooling/build-stack.sh index 5512b3b..9eba664 100755 --- a/tooling/build-stack.sh +++ b/tooling/build-stack.sh @@ -83,6 +83,11 @@ prep() { echo " downloaded $(basename "$ai_url")" fi + if [ "${ENABLE_MDNS:-false}" = true ]; then + log "build avahi mDNS image (cache it for offline use)" + "${COMPOSE[@]}" -f docker-compose.yml -f docker-compose.avahi.yml build avahi + fi + log "pre-bake server volume (TYPE=NEOFORGE install, one-shot, online)" # Bring minecraft up once so itzg installs NeoForge + mods into the volume, # then stop it. The party then runs offline against the baked volume. @@ -90,23 +95,29 @@ prep() { warn "watch logs until 'Done' then re-run with --up; or automate a readiness probe" } -# ---- offline bring-up ------------------------------------------------------ -# Modes select which optional layers come up (override files): -# core base only (dnsmasq, drasl, minecraft, mc-backup, caddy auth+packwiz) -# static + apex landing page + launcher downloads -# mirror + full air-gap upstream mirror (tls internal on :443) -# full static + mirror (default) +# ---- bring-up -------------------------------------------------------------- +# Modes (DNS is YOUR party server — see tooling/dns-records.sh for the records): +# online base + landing/launcher; internet present, no mirror +# airgap online + full upstream mirror (tls internal :443) for no-internet +# core base only (no landing, no mirror) — debugging up() { - local mode="${1:-full}" + local mode="${1:-airgap}" local -a files=(-f docker-compose.yml) case "$mode" in core) ;; - static) files+=(-f docker-compose.static.yml) ;; - mirror) files+=(-f docker-compose.mirror.yml) ;; - full) files+=(-f docker-compose.static.yml -f docker-compose.mirror.yml) ;; - *) die "unknown up mode: '$mode' (use core|static|mirror|full)" ;; + online) files+=(-f docker-compose.static.yml) ;; + airgap) files+=(-f docker-compose.static.yml -f docker-compose.mirror.yml) ;; + *) die "unknown up mode: '$mode' (use online|airgap|core)" ;; esac - log "PHASE up (mode=$mode)" + # Optional mDNS (.local) responder — see ENABLE_MDNS in .env. + if [ "${ENABLE_MDNS:-false}" = true ]; then + files+=(-f docker-compose.avahi.yml) + case "$BASE_DOMAIN" in + *.local) ;; + *) warn "ENABLE_MDNS=true but BASE_DOMAIN=$BASE_DOMAIN is not *.local — mDNS won't resolve it" ;; + esac + fi + log "PHASE up (mode=$mode${ENABLE_MDNS:+, mdns=$ENABLE_MDNS})" have_script render-config.sh tooling/render-config.sh # cheap; ensures configs match current .env @@ -114,7 +125,7 @@ up() { # Pre-create it (empty) so Docker doesn't make a directory at that path; the # real cert is exported below once Caddy's local CA exists. case "$mode" in - static|full) [ -e caddy/caddy-root-ca.crt ] || : > caddy/caddy-root-ca.crt ;; + online|airgap) [ -e caddy/caddy-root-ca.crt ] || : > caddy/caddy-root-ca.crt ;; esac "${COMPOSE[@]}" "${files[@]}" up -d @@ -122,7 +133,7 @@ up() { # Mirror/full provision Caddy's local CA — export it so guests can trust the # HTTPS asset mirror (served at http://${BASE_DOMAIN}/ca.crt in static/full). case "$mode" in - mirror|full) + airgap) local i for i in 1 2 3 4 5; do docker exec caddy cat /data/caddy/pki/authorities/local/root.crt \ @@ -136,26 +147,28 @@ up() { esac log "stack up (mode=$mode). apex: http://${BASE_DOMAIN} auth: http://auth.${BASE_DOMAIN}" + echo + log "Add these to your party DNS (mode=$mode):" + [ -x tooling/dns-records.sh ] && tooling/dns-records.sh "$mode" | sed -n '/## Plain/,/^$/p' } # ---- dispatch -------------------------------------------------------------- preflight case "${1:-}" in --prep) prep ;; - --up) up "${2:-full}" ;; + --up) up "${2:-airgap}" ;; "") cat <&2; exit 1 ;; +esac + +echo "# DNS records for mode=${mode} -> ${HOST_LAN_IP}" +echo "# Configure these on your party DNS server (all A records to the host)." +echo + +echo "## Plain (name -> ip)" +for n in "${names[@]}"; do printf '%-36s %s\n' "$n" "$HOST_LAN_IP"; done +echo + +echo "## hosts file (/etc/hosts style)" +for n in "${names[@]}"; do printf '%s %s\n' "$HOST_LAN_IP" "$n"; done +echo + +echo "## BIND zone (A records)" +for n in "${names[@]}"; do printf '%-36s IN A %s\n' "${n}." "$HOST_LAN_IP"; done +echo + +echo "## dnsmasq (address=)" +echo "address=/${BASE_DOMAIN}/${HOST_LAN_IP} # wildcard covers all *.${BASE_DOMAIN}" +if [ "$mode" = airgap ]; then + for n in "${spoof[@]}"; do printf 'address=/%s/%s\n' "$n" "$HOST_LAN_IP"; done +fi +echo + +echo "## Minecraft (no port needed)" +echo "# Clients connect to mc.${BASE_DOMAIN} (defaults to :25565)." +echo "# Optional SRV if your DNS supports it:" +echo "_minecraft._tcp.mc.${BASE_DOMAIN}. IN SRV 0 0 25565 mc.${BASE_DOMAIN}."