Skip to content

feat: Device Discovery Service — mDNS/Bonjour, RFC 2136 DNS UPDATE, Docker deployment#137

Merged
fifthsegment merged 26 commits into
fifthsegment:masterfrom
jbarwick:feature/1-device-discovery
May 17, 2026
Merged

feat: Device Discovery Service — mDNS/Bonjour, RFC 2136 DNS UPDATE, Docker deployment#137
fifthsegment merged 26 commits into
fifthsegment:masterfrom
jbarwick:feature/1-device-discovery

Conversation

@jbarwick
Copy link
Copy Markdown
Contributor

@jbarwick jbarwick commented Feb 9, 2026

Summary

This PR adds a Device Discovery Service to Gatesentry, enabling automatic detection and tracking of network devices via passive DNS monitoring, mDNS/Bonjour browsing, and RFC 2136 Dynamic DNS UPDATE support. It also includes Docker deployment improvements and admin UI consolidation.

⚠️ Dependency: This PR depends on PR #135 (feature/dns-env-config-and-testing) and must be merged AFTER #135.

The branches are cumulative — this branch builds directly on top of #135. Please merge #135 first, then this PR will merge cleanly into master.

Merge Sequence

  1. First: Merge PR Fix DNS server concurrency bug and add TCP support #135 (feature/dns-env-config-and-testing) — DNS concurrency fixes, TCP support, IPv6, env-configurable resolver
  2. Then: Merge this PR (feature/1-device-discovery) — Device discovery, mDNS, DNS UPDATE, Docker deployment

What's Included

Phase 1–2: Device Discovery Foundation

  • Device data model with hostname, IP, MAC, vendor, and source tracking
  • Thread-safe record store with TTL-based expiry
  • Passive discovery from DNS query logging
  • 30+ unit tests for the device store

Phase 3: mDNS/Bonjour Browser + Multi-Zone DNS

  • mDNS/Bonjour service browser for LAN device detection
  • Multi-zone DNS support for internal record management

Phase 4: RFC 2136 Dynamic DNS UPDATE Handler

  • Full RFC 2136 support for dynamic DNS record updates
  • TSIG authentication support

Phase 5: Docker Deployment + Admin Consolidation

  • Configurable base path via GATESENTRY_BASE_PATH
  • Docker Compose deployment configuration
  • Admin port consolidation (single port for proxy admin + web UI)

Phase 6: Devices Page + API

  • /api/devices REST endpoint
  • Admin UI devices page for viewing discovered devices
  • Dev tooling fixes

Testing

All existing tests continue to pass. New tests added for device store, DNS handler integration, and mDNS browser components.

Related

…tests (#1)

Add the core data structures and store for the device discovery system:

- Device type: hostname-centric identity model (not IP-centric)
  Supports multiple hostnames, mDNS names, MACs per device.
  Tracks source (ddns, lease, mdns, passive, manual).
  Manual names override auto-derived names.

- DnsRecord type: auto-derived A, AAAA, PTR records from device inventory.
  ToRR() converts to miekg/dns resource records for direct use in responses.

- DeviceStore: thread-safe (RWMutex) device inventory with lookup indexes.
  LookupName() / LookupReverse() for DNS query answering.
  FindDevice by hostname, MAC, or IP for discovery correlation.
  UpsertDevice() merges identity across discovery sources.
  UpdateDeviceIP() regenerates DNS records on DHCP renewal.
  ImportLegacyRecords() for backward compat with existing DNSCustomEntry.
  Bare hostname lookup ("macmini" matches "macmini.local").

- SanitizeDNSName: hostname → valid DNS label (RFC 952/1123).
- reverseIPv4/reverseIPv6: address → PTR name conversion.

- 30 tests covering: types, sanitization, reverse DNS, store CRUD,
  merge behavior, IP updates, offline detection, legacy import,
  concurrent read/write safety.

- DEVICE_DISCOVERY_SERVICE_PLAN.md: full technical plan documenting
  the 5-tier discovery architecture and implementation phases.

Refs #1
Document how the device discovery system enables per-device filtering
policies without implementing any filtering logic on this branch.

Key design decisions:
- Category stays as string (evolves to Groups []string later)
- Owner maps to existing Rule.Users in the rule engine
- FindDeviceByIP() is the hot path for future per-device filtering
- Store has zero filtering logic — policy decisions belong elsewhere
- Migration path documented for future per-group parental controls

No functional changes — comments and plan document only.

Refs #1
Phase 1 completion + Phase 2:

handleDNSRequest upgrades:
- Device store lookup runs BEFORE legacy internalRecords (priority)
- Supports A, AAAA, and PTR query types from device store
- Reverse DNS lookups (in-addr.arpa, ip6.arpa) via LookupReverse
- Backward compatible: legacy internalRecords still work as fallback
- Blocked domains still work (checked after device store)

Passive discovery (Phase 2):
- Extracts client IP from w.RemoteAddr() on every DNS query
- Creates new device entries for unknown IPs (fire-and-forget goroutine)
- Touches LastSeen for known devices (zero-latency fast path)
- MAC correlation via /proc/net/arp when device has new IP
- Skips loopback addresses (127.0.0.1, ::1)

Pre-existing test fix:
- Removed root setup_test.go (duplicate of main_test.go declarations)
- Root package tests now compile (broken since upstream commit 3209c1b)
- tests/ package (Makefile integration suite) unaffected

New test files:
- dns/discovery/passive.go + passive_test.go (12 tests)
- dns/server/server_test.go (12 integration tests with mock ResponseWriter)

Total: 54 tests passing (30 store + 12 passive + 12 server)
See TEST_CHANGES.md for full documentation.
- New: dns/discovery/mdns.go — MDNSBrowser with periodic scanning,
  27 default service types, IP/hostname/instance correlation,
  passive device enrichment, link-local IPv6 handling, ARP lookup
- New: dns/discovery/mdns_test.go — 22 tests covering processEntry,
  enrichment, dedup, IPv4/IPv6 preservation, GUA preference, lifecycle
- Modified: dns/discovery/store.go — multi-zone support:
  zones []string replaces single zone string, NewDeviceStoreMultiZone(),
  SetZones(), AddZone(), Zones(). rebuildIndexes() generates A/AAAA for
  ALL zones, PTR targets primary zone only (RFC 1033). UpsertDevice
  preserves IPs when new values are empty.
- Modified: dns/discovery/store_test.go — 15 multi-zone tests +
  6 PTR round-trip tests verifying forward→reverse→forward integrity
- Modified: dns/server/server.go — comma-separated dns_local_zone
  parsing, mDNS browser wiring (start/stop), GetMDNSBrowser() accessor
- All tests passing (discovery + server + webserver)
- New: dns/server/ddns.go — complete DDNS UPDATE handler:
  ddnsMsgAcceptFunc overrides default to accept OpcodeUpdate,
  handleDDNSUpdate with TSIG validation (required/optional/absent),
  zone authorization, RFC 2136 §2.5 update parsing (ClassINET=add,
  ClassANY=delete-all, ClassNONE=delete-specific), device store
  integration with hostname/IP matching, ARP enrichment,
  orphan cleanup for delete-then-add lease renewals
- New: dns/server/ddns_test.go — 20 tests:
  extractHostname, isAuthorizedZone, parseDDNSUpdates (adds/deletes/mixed),
  ddnsMsgAcceptFunc (query/update/notify), handleDDNSUpdate integration
  (AddA, AddAAAA, AddDualStack, DeleteByName, DeleteSpecific,
  DeleteThenAdd lease renewal, WrongZone, Disabled, EmptyZone,
  EnrichPassive, MultiZone primary+secondary, TSIG valid/invalid/
  missing-required/optional-absent/optional-present-invalid,
  UPDATE routing via handleDNSRequest, StandardQueryNotAffected,
  PersistentDeviceSurvivesDelete, DeleteNonexistent)
- Modified: dns/discovery/store.go — new ClearDeviceAddress() method
  for direct IP clearing without UpsertDevice merge interference
- Modified: dns/server/server.go — OpcodeUpdate dispatch in
  handleDNSRequest, DDNS settings parsing (ddns_enabled,
  ddns_tsig_required, ddns_tsig_key_name/secret/algorithm),
  MsgAcceptFunc + TsigSecret on both UDP and TCP servers
- Modified: dns/server/server_test.go — save/restore DDNS vars
- Settings: ddns_enabled, ddns_tsig_required, ddns_tsig_key_name,
  ddns_tsig_key_secret, ddns_tsig_algorithm
- All tests passing (discovery + server + webserver)
…tion

BREAKING CHANGES — Read carefully before merging.

This commit restructures how the web admin UI is served, moving from
a hardcoded root-path setup on port 10786 to a configurable base path
(default /gatesentry) on port 80. It also adds Docker support and
cleans up stale build artifacts from git tracking.

=== WHY THESE CHANGES WERE MADE ===

1. REVERSE PROXY SUPPORT: GateSentry needs to run behind reverse proxies
   (Nginx, Traefik, NAS built-in proxies) at paths like /gatesentry/.
   Previously all routes were hardcoded at root (/), making this impossible.

2. PORT 80 FOR PRODUCTION: The admin UI was on port 10786 — a non-standard
   port that users had to remember. Port 80 is the standard HTTP port
   and what users expect when typing http://gatesentry.local in a browser.

3. DOCKER DEPLOYMENT: GateSentry is designed for home networks (Raspberry Pi,
   NUC, etc.) and needs a simple Docker deployment story. The existing build
   had no Docker support at all.

4. BUILD ARTIFACTS IN GIT: The old React build output (bundle.js, material.css)
   and the Vite dist/ output were committed to git. These are generated files
   that bloat the repo and cause merge conflicts.

=== WHAT CHANGED ===

--- Go Backend (the big architectural change) ---

main.go:
  - Default admin port changed: 10786 → 80
  - Added GS_ADMIN_PORT env var to override the port
  - Added GS_BASE_PATH env var (default: /gatesentry)
  - Calls application.SetBasePath() to configure routing

application/runtime.go:
  - Added GSBASEPATH global + SetBasePath()/GetBasePath() with normalization

application/webserver/api.go (GsWeb router — CORE CHANGE):
  - GsWeb now has root router + subrouter architecture
  - NewGsWeb(basePath) creates a mux subrouter at the base path
  - All API/page routes are registered on the subrouter, not root
  - Root "/" redirects to basePath + "/" when basePath != "/"
  - All HTTP methods (Get/Post/Put/Delete) route through g.sub

application/webserver/webserver.go:
  - RegisterEndpointsStartServer() now accepts basePath parameter
  - makeIndexHandler(basePath) injects base path into HTML at serve time
  - Static file serving fixed: only strips basePath prefix (not /fs),
    so /gatesentry/fs/bundle.js correctly maps to fs/bundle.js in the
    embedded filesystem (this was a bug with the original StripPrefix)
  - Added SPA routes: /rules, /logs, /blockedkeywords, /blockedfiletypes,
    /excludeurls, /blockedurls, /excludehosts, /services, /ai

application/webserver/frontend/frontend.go:
  - Added GetIndexHtmlWithBasePath() — injects <base href> and
    window.__GS_BASE_PATH__ script tag into index.html at runtime
  - Changed //go:embed files → //go:embed all:files (includes dotfiles)

application/bonjour.go:
  - Now advertises _http._tcp on port 80 (so http://gatesentry.local works)
  - Kept _gatesentry_proxy._tcp on port 10413

application/webserver.go:
  - Passes basePath to RegisterEndpointsStartServer()
  - Log message now includes base path

--- Svelte Frontend ---

ui/src/lib/navigate.ts (NEW):
  - getBasePath() reads window.__GS_BASE_PATH__ injected by Go server
  - gsNavigate() prepends base path to all client-side navigation

ui/src/lib/api.ts:
  - API base URL now respects base path: basePath + "/api"
  - No longer hardcodes "/api"

ui/src/App.svelte:
  - <Router> now uses basepath={getBasePath()}
  - Uses gsNavigate() instead of raw navigate()

ui/src/components/{headermenu,sidenavmenu,headerrightnav}.svelte:
  - All navigation calls changed from navigate() → gsNavigate()

ui/src/routes/login/login.svelte:
  - Uses gsNavigate() for post-login redirect

ui/vite.config.ts:
  - Added base: "./" for relative asset paths (required for base path)
  - Added /gatesentry/api proxy for dev server
  - Dev proxy target changed from localhost:10786 → localhost:80

--- Build & Deployment ---

build.sh:
  - Now builds Svelte UI automatically (npm run build in ui/)
  - Copies dist/ into Go embed directory, preserving .gitkeep
  - Uses CGO_ENABLED=0 + stripped ldflags for static binary

Dockerfile (NEW):
  - Runtime-only Alpine image (~30MB), no build tools
  - Copies pre-built binary from bin/gatesentrybin
  - Exposes 53/udp, 53/tcp, 80, 10413

docker-compose.yml:
  - Updated for new deployment model
  - Uses network_mode: host (required for DNS + device discovery)
  - Volume mount for persistent data

.dockerignore (NEW):
  - Only sends bin/gatesentrybin + Dockerfile to Docker build context

DOCKER_DEPLOYMENT.md (NEW):
  - Comprehensive deployment guide: quick start, reverse proxy config,
    DHCP/DDNS integration (pfSense, ISC DHCP, Kea, dnsmasq),
    mDNS/Bonjour, troubleshooting

--- Cleanup ---

Deleted application/dns/http/http-server.go:
  - Removed unused block page HTTP server (was never called)

Removed from git tracking (still generated by build):
  - application/webserver/frontend/files/* (old React build output)
  - ui/dist/* (Vite build output)
  - Added .gitkeep to keep the embed directory in git
  - Updated .gitignore for both directories

Deleted resume.txt:
  - Personal file, should not be in repository

--- Tests ---

main_test.go:
  - Sets GS_ADMIN_PORT=10786 so tests run without root (port 80 needs root)
  - Computes endpoint URL with base path: localhost:10786/gatesentry/api
  - Added readiness loop — waits for server before running tests

tests/setup_test.go:
  - Updated for base path in endpoint URLs
  - Added graceful skip: if external server not running, exits 0 (not hang)

Makefile:
  - Health check URL updated to /gatesentry/api/health

run.sh:
  - Added GS_ADMIN_PORT=8080 default for local dev (avoids needing root)

=== ENVIRONMENT VARIABLES ===

  GS_ADMIN_PORT  — Override admin UI listen port (default: 80)
  GS_BASE_PATH   — URL prefix for all routes (default: /gatesentry)

=== URL ROUTING (default config) ===

  /                        → 302 redirect to /gatesentry/
  /gatesentry/             → Admin UI (Svelte SPA with injected base path)
  /gatesentry/api/...      → REST API endpoints
  /gatesentry/fs/...       → Static assets (bundle.js, style.css)
  /gatesentry/login        → SPA login route
  /gatesentry/stats        → SPA stats route
  ...etc

All tests pass: ok gatesentrybin 44.9s, ok gatesentrybin/tests 30.0s
Phase 6 (Device Discovery Service Plan — COMPLETE):
- New Svelte Devices page with Carbon DataTable, status indicators,
  search, auto-refresh, click-to-name, and device detail modal
- Go API endpoints: GET/DELETE /api/devices/{id}, POST /api/devices/{id}/name
- Side nav menu entry and SPA route for /devices

Bug fixes:
- Fix rules page API 404 (hardcoded /api/ path missing base path)
- Fix vite.config.ts proxy: rewrite /api → /gatesentry/api for dev server
- Fix vite base path from './gatesentry/' to './' (was doubling prefix)

Dev tooling:
- run.sh kills existing gatesentry processes before rebuild

Files added:
  application/webserver/endpoints/handler_devices.go
  ui/src/routes/devices/devices.svelte
  ui/src/routes/devices/devicelist.svelte
  ui/src/routes/devices/devicedetail.svelte

Files modified:
  DEVICE_DISCOVERY_SERVICE_PLAN.md (Phase 6 marked complete)
  application/webserver/webserver.go (device API routes + SPA route)
  ui/src/App.svelte (Devices route)
  ui/src/menu.ts (Devices nav entry)
  ui/src/routes/rules/rulelist.svelte (base path fix)
  ui/vite.config.ts (proxy rewrite fix)
  run.sh (kill stale processes)
Copilot AI review requested due to automatic review settings February 9, 2026 09:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Device Discovery Service and related infrastructure across the DNS server, web API, and Svelte admin UI, plus deployment changes (Docker/host networking) and reverse-proxy base-path support.

Changes:

  • Add a device inventory model/store with passive DNS observation, mDNS discovery, and RFC 2136 DDNS handling.
  • Add /api/devices endpoints and a new Admin UI “Devices” page, with base-path-aware routing/navigation.
  • Update deployment defaults/config (Docker host networking, admin port/base path), and adjust tests/build scripts accordingly.

Reviewed changes

Copilot reviewed 57 out of 68 changed files in this pull request and generated no comments.

Show a summary per file
File Description
application/dns/discovery/* New device model + store, passive discovery, and mDNS browser with unit tests.
application/dns/server/* Device-store-backed DNS answers, DDNS UPDATE handling, env-configurable listener/resolver, TCP server support.
application/webserver/endpoints/handler_devices.go New REST endpoints for device inventory CRUD/name assignment.
application/webserver/api.go, application/webserver/webserver.go, application/webserver/frontend/frontend.go Base-path-aware routing + index.html base-path injection + static asset routing changes.
ui/src/* + ui/vite.config.ts Base-path-aware navigation/API calls; new devices page + detail modal; dev proxy rewrite.
Dockerfile, docker-compose.yml, DOCKER_DEPLOYMENT.md, build.sh, run.sh New container workflow and host-network deployment guidance; build embeds UI into Go binary.
Makefile, tests/setup_test.go, main_test.go Test harness updated for base path and new admin port assumptions.
README.md, .gitignore, .dockerignore Docs + ignore rules updated for new build/deploy approach.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented May 17, 2026

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 17, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 67.33032% with 361 lines in your changes missing coverage. Please review.
✅ Project coverage is 16.25%. Comparing base (0fd5370) to head (1c3d47a).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
application/webserver/endpoints/handler_devices.go 0.00% 75 Missing ⚠️
application/dns/discovery/store.go 85.22% 44 Missing and 16 partials ⚠️
application/dns/server/server.go 35.29% 55 Missing ⚠️
application/webserver/webserver.go 2.12% 45 Missing and 1 partial ⚠️
application/dns/discovery/mdns.go 80.72% 26 Missing and 6 partials ⚠️
application/dns/server/ddns.go 85.11% 17 Missing and 8 partials ⚠️
application/dns/discovery/passive.go 61.01% 17 Missing and 6 partials ⚠️
application/webserver/api.go 0.00% 21 Missing ⚠️
application/dns/discovery/types.go 81.81% 12 Missing ⚠️
application/webserver/frontend/frontend.go 0.00% 12 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #137       +/-   ##
===========================================
+ Coverage    0.36%   16.25%   +15.89%     
===========================================
  Files          66       71        +5     
  Lines        3876     4904     +1028     
===========================================
+ Hits           14      797      +783     
- Misses       3860     4062      +202     
- Partials        2       45       +43     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fifthsegment fifthsegment force-pushed the feature/1-device-discovery branch from 773971d to 000d4a4 Compare May 17, 2026 09:14
@fifthsegment fifthsegment force-pushed the feature/1-device-discovery branch from 000d4a4 to b3cff6e Compare May 17, 2026 09:14
@fifthsegment fifthsegment merged commit d65a611 into fifthsegment:master May 17, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants