Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39602afe5 | ||
|
|
a4f961d609 | ||
|
|
5f47e12f18 | ||
|
|
2e6d535242 | ||
|
|
420fa2ba2a |
No files matched your search
+134
@@ -433,3 +433,137 @@ Tickets DC-046 through DC-049 implement pluggable auth + email magic link. Sami
|
||||
- **prerequisite:** None. `caddy-upstream-watcher.js` already provides equivalent monitoring.
|
||||
- **result:** Shipped GLM-pending (Codex quota dead). Before/after on DNS2 (`journalctl -u caddy --since "5 minutes ago" | grep health_checker.active | wc -l`): **before = ~30 entries / 5min** (active probe every 10s, all failing); **after = 0 entries / 5min**. Live-verified: `caddy validate` succeeded (after removing `.bak` file that caused `ambiguous site definition`), Caddy reloaded via `caddy-apply`, route `arch.sami → 100.120.159.34:5000` still active in admin API (verified via `curl http://localhost:2019/config/apps/http/servers/srv0/routes` — `health_uri: None, health_interval: None` confirms the block is gone). Container `dashcaddy-api Up About an hour (healthy)` (no restart needed — only Caddyfile changed, not container). Live HTTP smoke all green: `https://status.sami=200`, `https://dashcaddy.net=200`, `https://ca.sami=200`, `https://status.sami/api/health=401` (auth-gated, expected). Watcher state for `100.120.159.34:5000`: `consecutiveFailures: 1905`, `lastError: "probe timeout"`, `status: down`, `dead: true` — correctly tracked in `/opt/dashcaddy/dashcaddy-api/data/caddy-upstreams.json`. Backup deleted (would have caused site-definition ambiguity on next Caddy reload). Git: change lives only in DNS2's `/etc/caddy/sites/arch.sami` (the `/etc/caddy` git repo `.gitignore` excludes `sites/` per design — only the main `Caddyfile` is tracked). The dashcaddy source repo (`/root/dashcaddy`) carries only this BACKLOG.md documentation update on branch `dc/DC-084-arch-sami-caddy-healthcheck-removal`.
|
||||
- **Tests:** No source code change; existing `__tests__/caddy-upstream-watcher.test.js` 26/26 pass (baseline preserved). 2465/2465 repo tests pass (4 pre-existing billing test suites fail with `Cannot find module pdfkit` — unrelated to this change).
|
||||
# P7 — Private Network lane (DC-140 … DC-152)
|
||||
|
||||
> Spec: `specs/PRIVATE-NETWORK-SPEC.md`. Parent feature: DashCaddy Private Network
|
||||
> (Tailscale mode v1 + WireGuard self-hosted mode). Ordered by dependency, not by
|
||||
> value. All commits judge-gated (min B).
|
||||
|
||||
### DC-140: Network provider abstraction (`src/network/`)
|
||||
- **status:** todo
|
||||
- **owner:** (fleet)
|
||||
- **details:** `provider.js` (contract + JSDoc), `registry.js` (active provider +
|
||||
capabilities), `errors.js` (ProviderError with code/retryable). No feature code
|
||||
may import a provider directly — registry only. Provider contract methods:
|
||||
getStatus, listDevices, listPeople, createInvite, revokeInvite, blockPerson,
|
||||
unblockPerson, removePerson, revokeDevice, syncAccess, whois, configureDns.
|
||||
Tailscale provider wraps existing tailscale-coord.js + tailscale-manager.js.
|
||||
Registry defaults to inactive until the wizard connects one. Tests: registry
|
||||
resolution, capability flags, error mapping. Files: `src/network/*.js`,
|
||||
`__tests__/network-registry.test.js`, `__tests__/network-provider-tailscale.test.js`.
|
||||
|
||||
### DC-141: People store + routes (`routes/network-people.js`)
|
||||
- **status:** todo
|
||||
- **details:** people.json atomic store (invite-store pattern), CRUD routes under
|
||||
`/api/v1/network/people`, admin-protection (`protectedAt`), last-admin guard
|
||||
`assertNotLastAdmin()` exported for reuse. Sync endpoint merging provider users
|
||||
into people. Tests: store CRUD, route contract, guard (cannot block/remove
|
||||
last admin), sync merge idempotency.
|
||||
|
||||
### DC-142: Tailscale invites (link-first) via provider
|
||||
- **status:** todo
|
||||
- **details:** POST/GET/DELETE `/api/v1/network/invites` calling provider
|
||||
createInvite/revokeInvite. Tailscale: user-invites API, email optional —
|
||||
inviteUrl is the artifact (share-text pre-formed sentence, copy button,
|
||||
navigator.share, opt-in email checkbox; never log raw invite URLs to console).
|
||||
Reuse DC-085 share pattern. Tests: route contract with mocked provider,
|
||||
no-raw-token-in-logs assertion.
|
||||
|
||||
### DC-143: Block / unblock / remove person + revoke device
|
||||
- **status:** todo
|
||||
- **details:** POST `/network/people/:id/block|unblock|remove`,
|
||||
`/network/devices/:id/revoke`. Orchestrates: provider calls, session kill
|
||||
(existing auth store), access-map purge/restore, audit log entries. Block
|
||||
snapshots pre-block grants; unblock restores the AUTHORIZATION state (grants,
|
||||
status, sessions) — Tailscale device keys, once expired, are irreversible and
|
||||
require re-authentication (reauthRequired=true + user note); WireGuard peers
|
||||
restore immediately. Remove deletes devices + person record. Tests: orchestration order (mocked provider), honest unblock
|
||||
semantics (Tailscale: reauthRequired true + note; WireGuard: immediate
|
||||
restore), last-admin guard integration, audit entries written.
|
||||
|
||||
**Note (not a ticket):** the access map module itself is DC-145.
|
||||
|
||||
### DC-144: Groups (DashCaddy-side) + resources
|
||||
- **status:** todo
|
||||
- **details:** groups in people-store (array on person), group CRUD routes,
|
||||
resource model (services registry projection + manual entries), per-resource
|
||||
`sensitive` flag. No Tailscale-group mapping — ever (free plan caps 3; compiled
|
||||
rules are per-user). Tests: group CRUD, resource projection, sensitive flag
|
||||
validation.
|
||||
|
||||
### DC-145: Access map store + grid API
|
||||
- **status:** todo
|
||||
- **details:** `network-access.json` grants store, bulk GET/PUT
|
||||
`/network/access` (whole-grid save, atomic), effective-access computation
|
||||
(group grants + person overrides), change events for compiler subscription.
|
||||
This file is the single source of truth. Tests: store, effective access
|
||||
resolution (group + override precedence), atomic whole-grid save.
|
||||
|
||||
### DC-146: Technitium integration
|
||||
- **status:** todo
|
||||
- **details:** Technitium HTTP API client (credential via credential-manager),
|
||||
per-resource/per-device record upserts (idempotent), split-DNS configure when
|
||||
provider capability present, verify-by-resolve after write, wizard check step.
|
||||
Tests: API client with mocked HTTP, idempotent upsert, failure paths.
|
||||
|
||||
### DC-147: CA naming + name constraint + install page hardening
|
||||
- **status:** todo
|
||||
- issued from existing DashCA (routes/ca.js): operator-named root (setup-time,
|
||||
rename = guided re-trust), critical nameConstraints (permitted DNS subtree =
|
||||
private TLD), leaf regression tests (SAN + serverAuth EKU + ≤398d), branded
|
||||
install page with per-OS steps incl. iOS enable-trust second step + fingerprint
|
||||
+ Android native-app caveat, encrypted root-key backup step (passphrase +
|
||||
checksum). Tests: parse issued cert and assert constraint/SAN/EKU/validity;
|
||||
install-page content checks.
|
||||
|
||||
### DC-148: Tailscale ACL compiler + lockout guard
|
||||
- **status:** todo
|
||||
- **details:** access map → per-user HuJSON policy compiler. Hard rules:
|
||||
last-active-admin must keep access to the DashCaddy host resource; host device
|
||||
keeps egress; dry-run (ACL preview/test) before every push; last-known-good
|
||||
policy retained for rollback; post-push hash stored, pre-push hash compare →
|
||||
drift banner on mismatch. Provider.syncAccess(tailscale) uses this. Tests:
|
||||
golden-policy cases, lockout adversarial cases (each rule), drift detection,
|
||||
preview-failure refusal to push.
|
||||
|
||||
### DC-149: WireGuard provider — hub management
|
||||
- **status:** todo
|
||||
- **details:** `providers/wireguard.js`: wg-quick iface `dcnet0` at 10.53.0.1/16
|
||||
(client pool 10.53.0.0/16, one /32 per device, stable assignment), HUB keypair
|
||||
generated host-side, peer store (pubkeys only), systemd unit if missing,
|
||||
net.ipv4.ip_forward=1 for hub-and-spoke, no NAT on dcnet0. All execFile-mocked
|
||||
in tests. Tests: pool assignment stability, peer add/remove, unit generation,
|
||||
error paths (missing wg binary).
|
||||
|
||||
### DC-150: WireGuard invites (QR) + client config
|
||||
- **status:** todo
|
||||
- **details:** one-time token (7d TTL, single-use) → redemption page. Client
|
||||
keypair generated IN THE BROWSER via WebCrypto X25519 ONLY — NO fallback
|
||||
algorithm; browsers lacking X25519 get an explicit unsupported-browser
|
||||
failure. Server receives the public key only, registers the peer /32,
|
||||
returns config
|
||||
(Address, DNS=10.53.0.1, hub PublicKey, AllowedIPs=10.53.0.0/16 — a CIDR,
|
||||
never a DNS zone). Config + QR rendered client-side; private key never
|
||||
transits or rests on the server. Tests: token lifecycle, single-use
|
||||
enforcement, expiry, server-rejects-private-key-posts, config template
|
||||
correctness.
|
||||
|
||||
### DC-151: WireGuard firewall matrix (nftables)
|
||||
- **status:** todo
|
||||
- **details:** per-resource nft sets (net_dc_res_<name>), peer IPs per grant,
|
||||
transactional apply (build temp ruleset → `nft -c` → `nft -f` → verify →
|
||||
rollback file), three-zone baseline: external-interface UDP listen-port accept
|
||||
(NOT on dcnet0); dcnet0 local input = ICMP + admin ports from admin set only +
|
||||
resource ports; dcnet0 forwarding = per-resource sets, peer-to-peer default
|
||||
drop. Tests: ruleset
|
||||
golden text, transactional failure injection (nft -c fails → no apply),
|
||||
verify-step failure, admin-port isolation.
|
||||
|
||||
### DC-152: Setup wizard + status page
|
||||
- **status:** todo
|
||||
- **details:** mode pick → credentials/endpoint → private domain pick
|
||||
(.internal default, home.arpa, custom+warning) → provider connect + sync →
|
||||
Technitium check → CA naming (locked-in warning) → first admin person.
|
||||
Status page: who's online, recent connections (provider status + DashCaddy
|
||||
connection log). Reuse monitoring widget patterns. Tests: wizard step
|
||||
contracts, status aggregation with mocked providers.
|
||||
@@ -0,0 +1,257 @@
|
||||
# DashCaddy Private Network — Product Spec (v1)
|
||||
|
||||
> Source: feature brief draft v4 (Sami, 2026-10-03) + technical review (same thread).
|
||||
> Status: **approved for build** — "Do it and then build the non-tailscale version as well."
|
||||
> Tickets: DC-140 … DC-152 (see BACKLOG.md "P7 — Private Network lane").
|
||||
> Judge gate: every commit in this lane is graded via codex-as-judge, min B.
|
||||
|
||||
## 1. What we're building
|
||||
|
||||
DashCaddy turns a tailnet (or a self-hosted WireGuard hub) into a **managed private
|
||||
network**: people, devices, per-app access, private DNS names, and a personal
|
||||
certificate authority — all from one panel.
|
||||
|
||||
Two network modes, one product:
|
||||
|
||||
| | Tailscale mode (v1 default) | WireGuard mode (self-hosted) |
|
||||
|---|---|---|
|
||||
| Data plane | Tailscale (direct, NAT traversal) | WireGuard hub run by DashCaddy on the host |
|
||||
| Control plane | api.tailscale.com (OAuth client preferred; API key fallback) | DashCaddy itself (host wg-quick + key store) |
|
||||
| Invites | Tailscale user-invites API, link-first (no email required) | DashCaddy invite link + QR config |
|
||||
| Block/remove | API device expire/delete + ACL deny | Remove peer from hub + firewall rule |
|
||||
| Unblock | Re-allow at ACL level; expired device keys must re-authenticate (see §5) | Re-add peer from stored record (immediate) |
|
||||
| Rules | Tailnet policy ACLs compiled by DashCaddy | Hub firewall (nftables set-based) compiled by DashCaddy |
|
||||
| Outside dependency | Yes | None |
|
||||
|
||||
**Everything above the provider is shared**: people, groups, resources, the access
|
||||
map, forward_auth enforcement, Technitium names, the CA, the UI. This is enforced
|
||||
by the provider abstraction (DC-140) — no feature module may call a provider
|
||||
directly.
|
||||
|
||||
## 2. Core concepts and stores
|
||||
|
||||
- **Person** — a human. `people.json` (atomic JSON store, same pattern as
|
||||
invite-store/user-store). Fields: `id`, `displayName`, `providerUserId`
|
||||
(Tailscale user id or WG pubkey identity), `groups[]`, `status:
|
||||
active|blocked|removed`, `devices[]`, `createdAt`, `protectedAt` (admin guard).
|
||||
- **Device** — one machine. Belongs to a person. `keyId` (TS device id / WG peer
|
||||
pubkey), `name`, `ip` (tailnet IP), `online`, `lastSeen`, `os`.
|
||||
- **Group** — named bundle of people, DashCaddy-side only (UI concept). Free
|
||||
Tailscale plans cap ACL groups at 3 — therefore **DashCaddy groups never map
|
||||
to Tailscale groups**; compiled ACLs reference users directly.
|
||||
- **Resource** — an app or device reachable by name. `plex.internal`, `nas.internal`.
|
||||
Derived from the existing services registry + manual entries.
|
||||
- **Access map** — groups/people × resources grid. Single source of truth.
|
||||
`network-access.json`: `{ grants: [{ principal: {type:'person'|'group', id}, resource: 'plex', allowed: bool }] }`.
|
||||
This file, not Tailscale state, is authoritative.
|
||||
|
||||
## 3. Provider abstraction (DC-140)
|
||||
|
||||
`src/network/` package:
|
||||
|
||||
```
|
||||
src/network/provider.js — NetworkProvider interface (documented contract)
|
||||
src/network/registry.js — active provider resolution + capability flags
|
||||
src/network/errors.js — ProviderError { code, retryable }
|
||||
src/network/providers/tailscale.js
|
||||
src/network/providers/wireguard.js
|
||||
```
|
||||
|
||||
Contract (all async, all return plain JSON, all validated):
|
||||
|
||||
| Method | Tailscale impl | WireGuard impl |
|
||||
|---|---|---|
|
||||
| `id()`, `capabilities()` | `'tailscale'` | `'wireguard'` |
|
||||
| `getStatus()` | tailscale status --json (cached, existing manager) | wg show dump + iface up/down |
|
||||
| `listDevices()` | API + local status merge | peer store read |
|
||||
| `listPeople()` | API users → people projection | people.json (self-owned) |
|
||||
| `createInvite({role, email?})` | POST user-invites (email optional → returns inviteUrl) | mint one-time invite token in store |
|
||||
| `revokeInvite(id)` | DELETE user-invite | delete token |
|
||||
| `blockPerson(userId)` | expire all device keys + ACL deny | drop peer + firewall deny set |
|
||||
| `unblockPerson(userId)` | re-allow at ACL level; expired device keys stay expired (re-auth required, §5) | re-add stored peer (immediate) |
|
||||
| `removePerson(userId)` | DELETE devices, revoke invites | remove peer + person record |
|
||||
| `revokeDevice(deviceId)` | expire/delete device key | remove single peer |
|
||||
| `syncAccess(map, resources)` | compile ACLs → dry-run → push (hash-guarded) | compile nftables → apply → verify |
|
||||
| `whois(ip)` | local tailscale whois | peer table lookup |
|
||||
| `configureDns({zone, server})` | split-DNS API | not supported (Technitium direct) |
|
||||
|
||||
Capabilities: `{ invites, splitDnsApi, perPeerFirewall, selfHosted }`. Feature
|
||||
modules check capabilities and degrade cleanly (e.g. WireGuard mode skips the
|
||||
split-DNS API step and writes Technitium directly).
|
||||
|
||||
**Existing routes untouched.** `routes/tailscale.js` / `tailscale-admin.js` keep
|
||||
working as-is; the new `routes/network.js` + `src/network/` are additive.
|
||||
|
||||
## 4. Enforcement — two layers
|
||||
|
||||
**Layer 1: network.** The access map compiles to provider rules.
|
||||
- Tailscale: HuJSON policy with per-user src entries (never groups), `dst` as
|
||||
`host:port` per resource. DashCaddy host always keeps an explicit allow for
|
||||
the admin. **Lockout guard** (hard rule, tested): refuse to push any policy
|
||||
where (a) the last active admin person would lose access to the DashCaddy
|
||||
host resource, or (b) the DashCaddy host device would lose egress. Use the
|
||||
ACL preview/dry-run endpoint before every push; keep the last-known-good
|
||||
policy body for one-click rollback. Store the policy hash after each push and
|
||||
compare before the next — outside edits ⇒ drift banner, not silent overwrite.
|
||||
- WireGuard: hub-and-spoke means everyone can reach the hub; per-resource
|
||||
enforcement is a hub nftables matrix. One nft set per resource
|
||||
(`net_dc_res_plex`), peer IPs added per grant. **Apply is transactional**:
|
||||
build the full ruleset to a temp file, `nft -c` (check), swap atomically via
|
||||
`nft -f`, verify with `nft list set`, roll back file kept.
|
||||
|
||||
**Layer 2: app (forward_auth).** Shared across providers.
|
||||
- Whois path: forward_auth asks DashCaddy which person owns the connecting
|
||||
device (provider.whois), then checks the access map. Tailnet users get in
|
||||
without a separate login. **Identity follows the device** — a shared device
|
||||
grants the owner's access; documented in the UI.
|
||||
- Step-up: resources flagged `sensitive` additionally require the existing
|
||||
DashCaddy login + TOTP (reuse sso-gate).
|
||||
|
||||
## 5. People lifecycle (DC-141, DC-144)
|
||||
|
||||
- Invite = link-first (DC-085 pattern): inviteUrl is the artifact, email is an
|
||||
opt-in checkbox, never logged raw to console.
|
||||
- Block: network-layer deny (ACL / firewall) + expire Tailscale device keys + kill
|
||||
DashCaddy sessions + mark person blocked. **Unblock honestly:** the deny, the
|
||||
status, and the session policy reverse — but expired Tailscale device keys are
|
||||
NOT un-expired; those devices must re-authenticate with Tailscale on next
|
||||
connect. The UI says "blocked devices will need to sign in again" rather than
|
||||
pretending instant reversal. WireGuard peers restore immediately on re-add
|
||||
(peer config is re-generated server-side from the stored pubkey).
|
||||
- Remove: block semantics + delete provider devices + delete person record +
|
||||
purge from access map. The last active admin can never be blocked/removed
|
||||
(same guard as the ACL compiler — one shared `assertNotLastAdmin()`).
|
||||
|
||||
## 6. Names — Technitium (DC-146)
|
||||
|
||||
- Per-resource + per-device A/AAAA records → tailnet IPs, written via
|
||||
Technitium HTTP API (credential in credential-manager, existing pattern).
|
||||
- Split DNS: Tailscale mode uses the API when available; check + repair wizard
|
||||
step. WireGuard mode: devices get Technitium's hub IP as their DNS entry in
|
||||
the generated client config.
|
||||
- Record writes are idempotent (upsert by name+type) and verified by resolve.
|
||||
|
||||
## 7. Certificate authority (DC-147)
|
||||
|
||||
Builds on the existing DashCA pipeline (routes/ca.js, DC-076). Changes:
|
||||
- Root CN = operator-chosen name ("Ahmed Family Certificate Authority"),
|
||||
captured at first setup, baked in; rename ⇒ guided re-trust flow (warn at
|
||||
naming time, not after).
|
||||
- Root gets a **critical nameConstraints extension** (permittedSubtrees
|
||||
DNS:<private-tld>). Mark critical so non-understanding clients fail closed.
|
||||
- Leaves ≤ 398 days, SANs always set, serverAuth EKU (iOS 13+ requirements) —
|
||||
the existing openssl leaf path already complies; add regression tests that
|
||||
parse the issued cert and assert all three.
|
||||
- Branded install page at ca.<tld>: per-OS steps including **iOS's second
|
||||
step** (enable trust in Settings → General → About → Certificate Trust
|
||||
Settings), SHA-256 fingerprint for verification, and an Android note that
|
||||
native apps ignore user CAs (browsers fine).
|
||||
- Root key never leaves the host except through an explicit wizard step:
|
||||
encrypted backup download ( passphrase-wrapped ) + checksum.
|
||||
|
||||
## 8. WireGuard provider (DC-149–151)
|
||||
|
||||
### 8.1 Addressing and routing (exact prefixes)
|
||||
|
||||
- Hub interface `dcnet0`: 10.53.0.1/16, listen port chosen at setup (default 51820,
|
||||
UDP). Endpoint = host's reachable address (setup captures it; wizard warns if
|
||||
it's behind NAT without a forwarded port).
|
||||
- Client address pool: 10.53.0.0/16, one /32 per device (10.53.x.y/32 assigned
|
||||
on device add, stable, stored in people.json devices[]).
|
||||
- **AllowedIPs = 10.53.0.0/16 only** (a CIDR, never a DNS zone). Split tunnel:
|
||||
client's other traffic is untouched. Resource names resolve via Technitium
|
||||
(8.3) to IPs inside 10.53.0.0/16, which routes through the tunnel.
|
||||
- Hub forwarding: `net.ipv4.ip_forward=1` is required for hub-and-spoke
|
||||
peer→peer traffic; peer→hub-resource traffic is local. No NAT/masquerade on
|
||||
dcnet0 — all resource access is to hub-local services on 10.53.0.1 or to
|
||||
other peers' /32s, so addresses stay routable and firewall rules match peers.
|
||||
- Firewall baseline — three distinct zones, never conflated:
|
||||
(a) **External input**: the WireGuard UDP listen port arrives on the EXTERNAL
|
||||
interface (e.g. eth0), not dcnet0 — its accept rule is scoped to the external
|
||||
interface name (captured at setup).
|
||||
(b) **dcnet0 local input** (iifname dcnet0): ICMP from 10.53.0.0/16, admin
|
||||
ports (API/SSH on 10.53.0.1) from admin peers only, per-resource ports to
|
||||
10.53.0.1; drop other new inbound to 10.53.0.1.
|
||||
(c) **dcnet0 forwarding** (ip_forward=1): peer→resource gated by per-resource
|
||||
sets; peer→peer default DROP (both iifname and oifname dcnet0 tails) unless a
|
||||
grant implies it.
|
||||
|
||||
### 8.2 Key lifecycle (no server-side private-key storage)
|
||||
|
||||
- **Client keypairs are generated in the invitee's browser** using WebCrypto
|
||||
**X25519** — WireGuard requires Curve25519 key material; there is NO fallback
|
||||
algorithm (a browser lacking X25519 support gets an explicit "browser not
|
||||
supported" error, not a degraded key). Serialization (browser-export
|
||||
reality): `exportKey('raw', publicKey)` yields the raw 32-byte public key →
|
||||
base64. `exportKey('pkcs8', privateKey)` yields a PKCS#8 wrapper — the page
|
||||
strips the fixed 16-byte PKCS#8 X25519 prefix (`302e020100300506032b656e0422
|
||||
0420`) to extract the raw 32-byte private key → base64 (`wg genkey`-format;
|
||||
the extraction is pure byte-slicing of a fixed-prefix wrapper, verified by
|
||||
length/constant-prefix assertions before use). The server receives the base64
|
||||
public key (44 chars) and validates length/charset before use.
|
||||
- The redemption page posts ONLY the public key to the server; the server
|
||||
registers it as a WireGuard peer (10.53.x.y/32) and returns the client
|
||||
config (Address, DNS, PublicKey of hub, AllowedIPs = 10.53.0.0/16).
|
||||
- The private key NEVER transits or rests on the server: it lives in the
|
||||
browser page for the session, rendered into the config + QR client-side.
|
||||
- Server stores: peer public keys, assigned /32s, names — nothing else.
|
||||
- Revocation = remove peer (pubkey) from the hub + firewall-set update; the
|
||||
client's private key becomes useless without hub acceptance.
|
||||
|
||||
### 8.3 DNS
|
||||
|
||||
- Technitium serves the private zone on the hub IP (10.53.0.1). Client config
|
||||
DNS = 10.53.0.1. Per-resource/per-device records resolve to 10.53.x.y /32s
|
||||
(per DC-146). In Tailscale mode, the same records hold tailnet IPs instead.
|
||||
|
||||
### 8.4 Hub management
|
||||
|
||||
- `wg-quick` interface `dcnet0` managed by DashCaddy (systemd unit
|
||||
`dcnet-hub.service` if absent), keygen for the HUB keypair on the host
|
||||
(hub key is host-local and backed up with it).
|
||||
|
||||
### 8.5 Invites
|
||||
|
||||
- Invite redemption: one-time token (7-day TTL, single-use) → redemption page
|
||||
(browser keygen per 8.2) → config + QR rendered client-side, page
|
||||
auto-expires after redemption.
|
||||
|
||||
- Firewall: nftables matrix (section 4). Server resources listen on the hub IP;
|
||||
per-resource sets gate peer access. The host's own admin ports are NOT
|
||||
exposed on dcnet0 except to admins.
|
||||
|
||||
WireGuard mode dependency note: "no third party" still depends on host
|
||||
networking — a reachable UDP endpoint, wireguard-tools, nftables, and Technitium
|
||||
on the DashCaddy host.
|
||||
|
||||
## 9. Setup wizard (DC-152)
|
||||
|
||||
Single flow, mode pick first:
|
||||
1. Mode: Tailscale (paste OAuth client secret or API key — surface expiry
|
||||
warnings for API keys, ≤90-day; recommend OAuth client `tskey-client-`) or
|
||||
WireGuard (port, endpoint hostname/IP).
|
||||
2. Private domain pick (default `.internal`, `home.arpa` offered, custom with
|
||||
"could become real later" warning).
|
||||
3. Provider connect + health check + first sync.
|
||||
4. Technitium zone + split-DNS check.
|
||||
5. CA naming (locked-in warning) + root generation + install page link.
|
||||
6. First person (the admin — protected).
|
||||
|
||||
## 10. Non-goals (explicitly out of scope for this lane)
|
||||
|
||||
- Headscale support. Subnet routers / LAN access. Time-limited access.
|
||||
- Multi-hub HA. Client auto-updaters. Migration between modes with data
|
||||
carry-over (documented manual path only — switching re-runs the wizard).
|
||||
|
||||
## 11. Test strategy
|
||||
|
||||
- Every new module: Jest unit tests with the same mock rigor as
|
||||
tailscale-manager.test.js (no live network in tests).
|
||||
- ACL compiler: golden-policy tests (map JSON → expected HuJSON), lockout-guard
|
||||
adversarial cases (last-admin removal, host egress cut, drift detection).
|
||||
- WG provider: execFile-mocked `wg`/`nft` binaries; ruleset built to temp and
|
||||
compared as text; transactional-apply failure injection.
|
||||
- forward_auth: route tests against the running supertest app (existing
|
||||
__tests__/routes pattern), including shared-device semantics.
|
||||
- After merge + deploy to the dev rig: live-curl every new route (the DC-048
|
||||
discipline — hermetic green is not production proof).
|
||||
Reference in new issue
Block a user