Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ and [Semantic Versioning](https://semver.org/).
## [Unreleased]

## [v0.6.0-rc1] - 2026-10-05
### Added

- **The gateway now serves time to its own clients, before authentication
(#627).** A client on the open portal joins with whatever clock it boots
with — the ws3915i fleet sat months off — and a wrong clock cannot pay
honestly: Cashu proofs carry timestamps, keysets expire, sessions are
time-boxed, and a downstream TollGate in reseller mode needs accurate time
before it can pay *us*. `setup_ntp_server` enables busybox sysntpd's
listener (`system.ntp.enable_server='1'`, idempotently, creating the
section when the image shipped none), the pre-auth allow list gains
`allow udp port 123` beside the portal and the payment API, and a RUNNING
router gets exactly one cheap restart of the stateless UDP responder —
never a fresh boot, where procd starts sysntpd after uci-defaults with the
config already committed. Bench-side verified on the fleet bring-up
(79b1 answers NTP on the portal face; clients sync from the gateway).
([#648](https://github.com/OpenTollGate/tollgate-module-basic-go/pull/648))

### Fixed

Expand Down
40 changes: 38 additions & 2 deletions packaging/files/etc/uci-defaults/99-tollgate-setup
Original file line number Diff line number Diff line change
Expand Up @@ -1454,8 +1454,12 @@ setup_hostname() {
# allow list the previous install left behind.
#
# The list says what a PRE-AUTHENTICATION client may reach ON THE ROUTER, and
# nothing else belongs on it: the portal itself (:2050 NDS gateway, :2051 SPA)
# and the backend API (:2121). Every ADMIN surface — the :8090 board and LuCI's
# nothing else belongs on it: the portal itself (:2050 NDS gateway, :2051 SPA),
# the backend API (:2121) and the gateway's own NTP service (udp/123 — see
# setup_ntp_server below; a client with a wrong clock cannot pay honestly:
# Cashu proofs carry timestamps, keysets expire, sessions are time-boxed, and a
# downstream TollGate in reseller mode joins the open portal with whatever
# clock it boots with). Every ADMIN surface — the :8090 board and LuCI's
# :8080/:443 — is deliberately absent and actively removed below; a guest is
# answered by the portal, never by a login form.
#
Expand All @@ -1482,6 +1486,12 @@ assert_nodogsplash_allow_entries() {
if ! uci_list_has_port 2051; then
uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 2051'
fi
# The gateway's own time service: the one thing a pre-auth client needs
# that is not the portal or the payment API. sysntpd must be told to
# listen (setup_ntp_server below) for this rule to answer anything.
if ! uci_list_has_port 123; then
uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow udp port 123'
fi
# LuCI (:8080 plain HTTP, and the :443 TLS listener uhttpd.main opens
# whenever a cert/key pair exists) is an ADMIN surface, the same class as the
# :8090 board below: /www/index.html meta-refreshes into /cgi-bin/luci, i.e.
Expand Down Expand Up @@ -1564,6 +1574,31 @@ uci_list_has_port() {
printf '%s\n' "$nds_users" | grep -qE "(^|[[:space:]'])port $1([[:space:]]|'|\$)"
}

# The gateway serves time to its own clients, pre-authentication (#627).
# OpenWrt's sysntpd (busybox ntpd) listens when system.ntp.enable_server is
# set — that one option is the whole server half; the client half (polling
# upstream servers) keeps whatever the image shipped. Bench-verified on the
# ws3915i fleet bring-up: units sat months off, and nothing on the open
# portal face could correct them before payment.
#
# Restart discipline follows the script's rule (see converge_nodogsplash_
# runtime): a FRESH boot never restarts anything — procd starts sysntpd
# after uci-defaults, with this config already committed — and a RUNNING
# router gets the one cheap restart, because sysntpd reads its config once
# at start. Unlike the wireless or firewall services, bouncing sysntpd
# drops no customer: it is a stateless UDP responder.
setup_ntp_server() {
if ! uci -q get system.ntp >/dev/null 2>&1; then
uci set system.ntp=timeserver
fi
if [ "$(uci -q get system.ntp.enable_server 2>/dev/null || echo 0)" != "1" ]; then
uci set system.ntp.enable_server='1'
fi
if pidof sysntpd >/dev/null 2>&1; then
/etc/init.d/sysntpd restart >/dev/null 2>&1 || true
fi
}

setup_nodogsplash() {
# The section, its pre-auth allow list and the whole-list repair live in
# one place so the same-version path can call exactly the same writer.
Expand Down Expand Up @@ -2497,6 +2532,7 @@ setup_dns_dnsmasq
setup_hostname
detect_band_radios
setup_public_wifi # consumes DEVICE_SSID; exports GATEWAY_NAME
setup_ntp_server # serves time pre-auth (udp/123, issue #627)
setup_nodogsplash
setup_tollgate_firewall_rules
setup_profile_hook
Expand Down
2 changes: 1 addition & 1 deletion tests/uci-defaults-nodogsplash-convergence_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ mkdir -p "$TMP/bin" "$TMP/init.d"
# configured list must not".
KEY="nodogsplash.@nodogsplash[0].users_to_router"
# Entries are `proto/port` pairs, the shape the convergence step compares.
CORE_ENTRIES="tcp/2121 tcp/2050 tcp/2051"
CORE_ENTRIES="tcp/2121 tcp/2050 tcp/2051 udp/123"
STALE_ENTRY="tcp/8090"

# ---------------------------------------------------------------- fake apk
Expand Down
146 changes: 146 additions & 0 deletions tests/uci-defaults-ntp-preauth_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# Offline tests for the pre-auth NTP server half of issue #627
# (packaging/files/etc/uci-defaults/99-tollgate-setup: setup_ntp_server).
#
# The allowlist half (udp/123 joining users_to_router) is covered by
# uci-defaults-nodogsplash-convergence_test.sh, whose CORE_ENTRIES now
# carries it through the full script run. This file covers the other half:
# busybox sysntpd must be told to LISTEN (system.ntp.enable_server='1'),
# idempotently, with the section created when the image shipped none — and
# a RUNNING router gets exactly one cheap restart of a stateless UDP
# responder, never a fresh boot (procd starts sysntpd after uci-defaults
# with this config already committed).
#
# No router, SDK or network needed: the function is extracted from the
# setup script (function-boundary extraction, the same style as the band
# tests), its absolute /etc/init.d/sysntpd path is rewritten by sed, and it
# runs against a fake uci and pidof.
#
# Usage: bash tests/uci-defaults-ntp-preauth_test.sh
set -uo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

PASS=0
FAIL=0
ok() { PASS=$((PASS + 1)); printf 'ok %s\n' "$1"; }
bad() { FAIL=$((FAIL + 1)); printf 'FAIL %s\n' "$1"; }

SCRIPT="packaging/files/etc/uci-defaults/99-tollgate-setup"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# ---- extract setup_ntp_server by function boundary, rewriting the init path
awk '/^setup_ntp_server\(\) \{/,/^\}/' "$SCRIPT" \
| sed "s|/etc/init.d/sysntpd|$TMP/init.d/sysntpd|g" \
> "$TMP/ntp_fragment.sh"
grep -q 'enable_server' "$TMP/ntp_fragment.sh" \
|| { bad "extraction failed — setup_ntp_server not found in $SCRIPT"; exit 1; }

mkdir -p "$TMP/bin" "$TMP/init.d"

# ---- fake uci: flat-file state, the subset the function uses
cat > "$TMP/bin/uci" <<'SHIM'
#!/usr/bin/env bash
set -uo pipefail
STATE="${UCI_STATE:?UCI_STATE not set}"
quiet=0
[ "${1:-}" = "-q" ] && { quiet=1; shift; }
cmd="${1:-}"; shift || true
case "$cmd" in
get)
v="$(grep -F -- "$1=" "$STATE" | head -n1 | cut -d= -f2- || true)"
[ -n "$v" ] || exit 1
printf '%s\n' "$v"
;;
set)
key="${1%%=*}"; val="${1#*=}"
if grep -qF -- "$key=" "$STATE"; then
sed -i "s|^$(printf '%s' "$key" | sed 's/[.[\*^$]/\\&/g')=.*|$key=$val|" "$STATE"
else
printf '%s=%s\n' "$key" "$val" >> "$STATE"
fi
;;
esac
SHIM
chmod +x "$TMP/bin/uci"

# ---- fake pidof: controlled by PIDOF_SYSNTPD (empty = not running)
cat > "$TMP/bin/pidof" <<'SHIM'
#!/usr/bin/env bash
[ "$1" = "sysntpd" ] || exit 1
[ -n "${PIDOF_SYSNTPD:-}" ] || exit 1
printf '%s\n' "$PIDOF_SYSNTPD"
SHIM
chmod +x "$TMP/bin/pidof"

# ---- fake init script: records restarts
cat > "$TMP/init.d/sysntpd" <<'SHIM'
#!/usr/bin/env bash
printf 'restart\n' >> "${RESTART_LOG:?}" 2>/dev/null || true
SHIM
chmod +x "$TMP/init.d/sysntpd"

run_ntp_setup() {
# The fragment path travels by environment, not by sh -c positionals:
# "$2" inside a -c string is unset (the earlier form silently sourced
# nothing and every state assertion failed for the wrong reason).
PATH="$TMP/bin:$PATH" UCI_STATE="$1" NTP_FRAGMENT="$TMP/ntp_fragment.sh" \
sh -c '. "$NTP_FRAGMENT" >/dev/null 2>&1; setup_ntp_server'
}

# ---- case 1: fresh image, no system.ntp section, sysntpd not running
S1="$TMP/state1"; : > "$S1"
run_ntp_setup "$S1"
if grep -q '^system.ntp=timeserver$' "$S1"; then
ok "the timeserver section is created when the image shipped none"
else
bad "system.ntp=timeserver missing after setup: $(cat "$S1")"
fi
if grep -q '^system.ntp.enable_server=1$' "$S1"; then
ok "enable_server=1 is set on a fresh image"
else
bad "enable_server not set: $(cat "$S1")"
fi

# ---- case 2: idempotency — a second run changes nothing
cp "$S1" "$S1.before"
run_ntp_setup "$S1"
if diff -q "$S1.before" "$S1" >/dev/null; then
ok "a second run is a no-op (idempotent)"
else
bad "second run mutated state: $(diff "$S1.before" "$S1")"
fi

# ---- case 3: a router whose operator disabled the server is RE-ENABLED
S3="$TMP/state3"
printf 'system.ntp=timeserver\nsystem.ntp.enable_server=0\n' > "$S3"
run_ntp_setup "$S3"
if grep -q '^system.ntp.enable_server=1$' "$S3"; then
ok "an explicitly disabled server is re-enabled (policy, not preference)"
else
bad "enable_server=0 survived: $(cat "$S3")"
fi

# ---- case 4: a RUNNING sysntpd is restarted exactly once
S4="$TMP/state4"; : > "$S4"
RESTART_LOG="$TMP/restarts" PIDOF_SYSNTPD=1234 run_ntp_setup "$S4"
if [ "$(wc -l < "$TMP/restarts" 2>/dev/null || echo 0)" = "1" ]; then
ok "a running sysntpd is restarted exactly once"
else
bad "restart count = $(wc -l < "$TMP/restarts" 2>/dev/null || echo 0), want 1"
fi

# ---- case 5: a fresh boot (sysntpd not yet started) never touches the init
S5="$TMP/state5"; : > "$S5"
rm -f "$TMP/restarts"
run_ntp_setup "$S5"
if [ ! -e "$TMP/restarts" ]; then
ok "a fresh boot leaves the init script untouched (procd starts it later)"
else
bad "fresh boot restarted sysntpd: $(cat "$TMP/restarts")"
fi

printf 'ntp-preauth: %d passed, %d failed\n' "$PASS" "$FAIL"
[ "$FAIL" = 0 ]