Skip to content

[RFC] v2: Per-user rule-based filtering, Domain List system, proxy hardening, and UI overhaul#141

Open
jbarwick wants to merge 41 commits intofifthsegment:masterfrom
jbarwick:v2
Open

[RFC] v2: Per-user rule-based filtering, Domain List system, proxy hardening, and UI overhaul#141
jbarwick wants to merge 41 commits intofifthsegment:masterfrom
jbarwick:v2

Conversation

@jbarwick
Copy link

PR Title

[RFC] v2: Per-user rule-based filtering, Domain List system, proxy hardening, and UI overhaul

PR Description

Hi @fifthsegment — I've been working on a substantial set of improvements in my fork and wanted to share them for feedback. This is not a "please merge now" PR — it's more of an RFC / showcase. The branch is v2 and represents about 40 commits / ~44K insertions across 169 files.

Architecture Shift: Global → Per-User Rule-Based Filtering

The most significant change in v2 is moving away from global filter lists toward a fully rule-based filtering system.

In v1, proxy rules could already be defined for a user, and the rule system provided per-user targeting — that foundation was solid. However, the primary filtering mechanisms — blocked sites, exception lists, keyword filters, and DNS blocklists — operated as global pipelines that applied to everyone on the network identically. The per-user rules and the global filters were essentially two separate systems. Additionally, rules in v1 were strictly block rules — they could only deny access, not explicitly grant it.

In v2, those global filtering pipelines have been consolidated into the per-user rule system. There are no longer separate global blocked-site lists or global keyword filters running outside of rules. Instead, every filtering decision flows through the rule pipeline:

  • Everything is a rule now — Blocked domains, URL patterns, content-type filtering, and keyword scanning are all configured as properties of individual rules rather than global lists. What was previously a global "blocked sites" list becomes a rule referencing a Domain List with action "block."
  • Rules are match + action, not just block lists — Rules are no longer strictly "block" rules. Each rule is a matching rule with a configurable action: Allow or Block. Combined with priority ordering (lower number = higher priority, first match wins), this enables patterns that weren't possible before. For example, a high-priority "Allow" rule can grant a specific user access to educational-site.com even when a lower-priority "Block" rule blocks the entire category for everyone else. Exception lists become simply higher-priority Allow rules.
  • Per-user filtering becomes natural — Since all filtering is rule-scoped, targeting specific users is just a field on the rule. A "kids" rule can block adult content while an "adults" rule allows it. An empty user list means "all users."
  • DNS filtering is now optional — Domain blocking no longer requires the DNS server. All filtering can be handled entirely by proxy rules. DNS blocking is still available for users who want network-wide domain-level filtering, but it's no longer the only mechanism.
  • Reusable Domain Lists — Both DNS filtering and proxy rules share the same DomainListManager and in-memory index. A single curated list (StevenBlack, Hagezi, etc.) can be referenced by multiple rules and by DNS simultaneously.

8-Step Rule Pipeline

Each proxy request is evaluated through a well-defined pipeline, rule by rule in priority order. The first rule that fully matches is applied — subsequent rules are skipped:

  1. Rule status — Enabled/disabled, active hours schedule
  2. User match — Does the requesting user match the rule's user list?
  3. Domain match — Hostname checked against domain patterns (glob wildcards) and domain lists (by list ID)
  4. MITM resolution — Per-rule MITM setting: enable, disable, or default (fall back to global setting). Determines whether steps 5–7 can inspect HTTPS traffic.
  5. URL regex — Match against the full request URL (requires MITM for HTTPS)
  6. Content-type — Match against response Content-Type header (requires MITM for HTTPS)
  7. Keyword filter — Scan response body for blocked keywords with score watermark (requires MITM for HTTPS)
  8. ActionAllow or Block

If no rule matches after evaluating all rules, the request is allowed (default-allow). Because Allow rules participate in the same priority-ordered pipeline as Block rules, administrators can build layered policies: a broad Block rule at priority 100 blocking a category, with targeted Allow exceptions at priority 10 for specific users or domains.

Domain List System

A new DomainListManager provides CRUD operations, an O(1) in-memory index (map[domain] → set of list IDs), and support for both local lists and URL-sourced blocklists (StevenBlack, Hagezi, AdGuard, Firebog, etc.). DNS and proxy filtering share the same index. Includes automatic migration from the old blocklist format. Full API endpoints and a management UI at /domainlists.

Device Discovery (Foundation)

A new device discovery system using mDNS/Bonjour browsing and passive DNS observation lays the groundwork for per-device filtering in a future release. Devices are identified, tracked, and exposed via API. The data model and record store are in place (with 30+ tests), and the DNS handler already performs passive discovery. The next step would be allowing rules to target devices in addition to users.

Server-Side Rule Tester

Two new API endpoints (POST /api/test/rule-match, POST /api/test/domain-lookup) simulate the full 8-step pipeline against a rule definition without affecting live traffic. The rule editor UI has a built-in tester panel with optional live fetch to verify that a rule behaves as expected before saving.

Real-Time Stats and Logs via SSE

The stats page now uses Server-Sent Events for near-real-time updates, replacing the previous 5-second polling approach. DNS request events are emitted from the handler and streamed to the browser as they happen.

Proxy Hardening

  • Streaming 3-path content router (small buffer / scan / stream-through) with configurable GS_MAX_SCAN_SIZE_MB
  • Graceful shutdown with in-flight request draining
  • Proxy loop detection (Via header + X-GateSentry-Loop + XFF depth limit)
  • SSRF protection blocking proxy requests to the admin UI
  • HTTPS block page delivery (MITM-signed error page for blocked HTTPS requests)
  • WebSocket tunnel support
  • TRACE method blocking, response header sanitization

DNS Improvements

  • Sharded response cache with configurable TTL and negative caching
  • TCP query support (large responses > 512 bytes)
  • RFC 2136 dynamic DNS update handler
  • WPAD/PAC auto-configuration endpoints
  • IPv6 listener support
  • All settings configurable via environment variables

UI Overhaul

  • Complete Svelte/Carbon rewrite of the rule form matching the 8-step pipeline layout
  • New Domain Lists management page (/domainlists)
  • DNS page with allow/block domain list assignment sections
  • Configurable base path support for reverse proxy deployments
  • Docker deployment support with docker-compose.yml and publish script

Test Coverage

  • 19 rule-tester endpoint unit tests, 19 domain list unit tests
  • Comprehensive proxy deep test script (scripts/proxy_deep_tests.sh) covering MITM, content filtering, keyword scanning, loop detection, and more

Breaking Changes from v1

  • Admin UI default port: 107868080 (configurable via GS_ADMIN_PORT)
  • Rule struct expanded: domain_patterns, domain_lists, mitm_action, url_regex_patterns, blocked_content_types, keyword_filter_enabled, active_hours, users
  • Settings keys added: dns_domain_lists, dns_whitelist_domain_lists
  • Old global blockedDomains map replaced by shared DomainListIndex

How to Try It

git remote add jbarwick https://github.com/jbarwick/Gatesentry.git
git fetch jbarwick v2
git checkout jbarwick/v2
./build.sh && cd bin && ../run.sh
# Admin UI at http://localhost:8080

Happy to answer questions, break this into smaller PRs, or adjust anything based on your feedback. I'm continuing to evaluate and test the software and plan to add parental controls in the near future — per-user and per-device content filtering — so additional changes will be coming.

Also — AI filtering is an interesting idea you have, and I'd like to help out on that front. I have some ideas that may or may not work, but I'd be happy to discuss them with you.

Great project — I've really enjoyed working on it.

Bug Fixes:
- Changed sync.Mutex to sync.RWMutex for concurrent DNS query handling
- Fixed race condition in filter initialization (map pointer reassignment)
- Release mutex before external DNS forwarding (was blocking all queries)

Enhancements:
- Added TCP protocol support for large DNS queries (>512 bytes)
- Environment variable support (GATESENTRY_DNS_ADDR, PORT, RESOLVER)
- Environment variable now overrides stored settings for containerized deployments
- Added normalizeResolver() to auto-append :53 port suffix

Scripts:
- Enhanced run.sh with environment variable exports for local development
- Improved build.sh with better output and error handling
- Added comprehensive DNS test suite (scripts/dns_deep_test.sh)

Test Results: 85/85 tests passed (100% pass rate)
- Fix writer starvation in InitializeBlockedDomains: Download all blocklists
  first without holding lock, then apply with single write lock acquisition.
  This prevents DNS queries from being blocked while blocklists are loading.

- Fix IPv6 resolver address handling: Use net.SplitHostPort/JoinHostPort
  instead of strings.Contains(':') to properly detect port presence.
  IPv6 addresses like '2001:4860:4860::8888' now correctly get formatted
  as '[2001:4860:4860::8888]:53'.

Testing shows 50 concurrent queries now complete successfully during
blocklist loading, vs previous behavior where all queries would hang.
fmt.Sprintf("%s:%s", addr, port) produces invalid addresses for IPv6
(e.g., '::1:53' instead of '[::1]:53'). net.JoinHostPort handles this.
Reading a Go map (even len()) concurrently with writes is a data race.
Moved the log statement after RLock acquisition and capture len() while
holding the lock.
serverRunning was read in handleDNSRequest and written in Start/StopDNSServer
without synchronization. Changed from bool to sync/atomic.Bool with proper
Load()/Store() calls for thread-safe access.
- Add set -euo pipefail for better error handling
- Remove explicit $? check (now handled by set -e)
- Add platform detection (Linux, macOS, BSD)
- Add portable time functions (get_time_ns, get_time_ms) using python/perl
  fallback for macOS which lacks date +%s%N
- Add portable grep helpers (extract_dns_status, extract_key_value) with
  sed fallback when GNU grep -oP is unavailable
- Detect GNU grep PCRE support and use sed fallbacks when needed
- Update dependency check with platform-specific guidance for macOS
- Document platform requirements in header comments
- Detect if client connected via TCP and preserve protocol for forwarding
- When response is truncated (>512 bytes), automatically retry over TCP
- Gracefully fall back to truncated response if TCP retry fails
Server-side fixes (server.go):
- Return SERVFAIL response when forwardDNSRequest fails instead of
  silently returning without writing a reply. The missing response
  caused clients to hang until their own timeout expired, which was
  the root cause of concurrent query failures under load.
- Add explicit 3-second timeout on dns.Client to prevent indefinite
  hangs when the upstream resolver is slow or unreachable.

Test script fixes (dns_deep_test.sh):
- Replace bare 'wait' with PID-specific waits in concurrent query
  test and security flood test. The bare 'wait' blocked on ALL
  background jobs including the GateSentry server process itself,
  which never exits — causing the test to lock up indefinitely.
- Change dns_query_validated to return 0 on errors (error details
  are communicated via VALIDATION_ERROR variable). Returning 1
  under set -e caused the script to silently terminate mid-run.
- Add ${val:-0} fallback in get_query_time and get_msg_size for
  the non-PCRE sed branch, preventing empty-string arithmetic
  errors on platforms without GNU grep.
- Rewrite case-insensitivity test to verify all case variants
  resolve successfully with consistent record counts, instead of
  comparing exact IP sets which differ due to DNS round-robin.
- Change P95 latency threshold from FAIL to WARNING since transient
  spikes (blocklist reloads, network hiccups) are expected and do
  not indicate a server defect.

Test results: 84/84 passed (100% pass rate)
…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)
- Add docker-publish.sh: builds, tags, and pushes to Docker Hub or Nexus
  - Supports DOCKERHUB_TOKEN (PAT) with DOCKERHUB_PASSWORD fallback
  - Uses --password-stdin for secure authentication
  - Auto-detects version from git tags
  - Pushes repository description/README via Docker Hub API after image push
  - Supports --nexus, --no-build, --no-latest, --dry-run options
- Add DOCKERHUB_README.md: standalone README for the Docker Hub repo page
  - Quick start with docker run and docker-compose examples
  - Environment variables, ports, and volumes reference
  - Reverse proxy and DDNS integration docs
  - Links to fork source repo (jbarwick/Gatesentry)
- Update Dockerfile: minor refinements for runtime image
- Update docker-compose.yml: improved port mappings and comments
Implemented:
- sanitizeResponseHeaders() — validates Content-Length conflicts,
  negative values, null bytes, CRLF injection (response splitting defense)
- Via: 1.1 gatesentry header on all proxied responses (RFC 7230 §5.7.1)
- Via-based loop detection in ServeHTTP() — returns 508 Loop Detected
- X-Content-Type-Options: nosniff on all proxied responses
- Content-Length lifecycle fix — set after body processing, not before
- RoundTrip error handling fix — transport failures return 502 Bad Gateway
  instead of block page (block pages reserved for intentional filter blocks)

Test results: 81 PASS, 2 FAIL, 13 KNOWN, 1 SKIP (97 total)
Improvements: §3.1 Via header, §3.6 Content-Length, §7.4 loop detection
  all moved from KNOWN/FAIL → PASS

Also adds:
- Comprehensive 97-test benchmark suite (tests/proxy_benchmark_suite.sh)
- Adversarial echo server with 41+ hostile endpoints (tests/testbed/)
- TLS test fixtures for local HTTPS testbed
- PROXY_SERVICE_UPDATE_PLAN.md — 5-phase hardening roadmap
Implemented:
- dialer.Resolver wired to GateSentry DNS (127.0.0.1:10053) so all
  proxy hostname resolution goes through GateSentry filtering
- DNS port configurable via GATESENTRY_DNS_PORT env var
- Admin port isolation: ServeHTTP() blocks proxy requests to admin
  port (8080) on loopback/LAN/localhost addresses (HTTP 403)
- safeDialContext(): prevents DNS rebinding SSRF to admin port —
  blocks when a hostname resolves to loopback/link-local AND targets
  the admin port. All other connections allowed (GateSentry DNS is
  trusted as the resolver)
- ConnectDirect() and all HTTP transports now use safeDialContext()
- extractPort() helper in utils.go

Design decisions:
- Only admin-port rebinding is blocked at dial level. Full RFC 1918
  blocking deferred to PAC file endpoint (clients already configure
  'bypass proxy for LAN' in their proxy settings)
- IP-literal requests to non-admin ports allowed through — the proxy
  trusts GateSentry DNS resolution for hostnames

Test results: 84 PASS, 2 FAIL, 10 KNOWN, 1 SKIP (97 total)
Phase 2 fixes: §8.1 DNS resolution, §7.1 SSRF admin, §7.2 SSRF localhost
  all moved from KNOWN → PASS. CONNECT tunnels (§5.1, §5.2) confirmed
  no regression.
Replace buffer-everything architecture with a 3-path response router
that only buffers content that actually needs scanning:

  Path A (Stream): JS, CSS, fonts, JSON, binary, downloads — zero
    buffering, io.Copy + http.Flusher for progressive delivery
  Path B (Peek+Stream): images, video, audio — read first 4KB for
    filetype detection + content filter, then stream remainder
  Path C (Buffer+Scan): text/html only — preserves existing
    ScanMedia/ScanText full-body scanning behaviour

Key changes:
- Add classifyContentType(), streamWithFlusher(), decompressResponseBody()
- DisableCompression: true on transports (end-to-end compression passthrough)
- Accept-Encoding normalized to gzip-only (was unconditionally stripped)
- HEAD requests routed to Path A (no body to scan)
- Content-Length set before WriteHeader() in Path A
- Drip test threshold adjusted (2000ms lower bound)

Test results: 86 PASS · 0 FAIL · 9 KNOWN · 1 SKIP
Fixed: §3.6 (Content-Length), §11.2 (10MB download), §12.3 (drip streaming)
…v var

Path C (HTML-only) no longer needs 10MB — 2MB is plenty for even the
largest SSR pages. JS/CSS/images/binary all go through Path A (stream)
and never touch the scan buffer.

- GS_MAX_SCAN_SIZE_MB env var for runtime tuning
- Wired into run.sh (default 2) and docker-compose.yml
- Parsed in init() with validation
- websocket.go: Full bidirectional WebSocket tunnel replacing stub
  - Hijacks client conn, dials upstream via safeDialContext (SSRF-safe)
  - Forwards upgrade request, relays 101 response
  - Bidirectional io.Copy with graceful half-close (TCPConn.CloseWrite)
  - 5s drain deadline for orderly shutdown

- application/dns/server/server.go: DNS response cache
  - In-memory cache keyed by (qname, qtype) with TTL expiration
  - Deep-copy on read with TTL adjustment for elapsed time
  - Max 10,000 entries with simple eviction (clear-all at limit)
  - Min 5s / max 1h TTL bounds, 60s default for negative responses

- application/dns/server/server.go: NXDOMAIN rcode preservation
  - Propagate upstream rcode (was always NOERROR via SetReply default)
  - Copy authority section (Ns) for SOA in negative responses

- tests/proxy_benchmark_suite.sh: Test assertion fixes
  - §6.1: Fix curl exit-code corruption (101 + timeout → 101000)
  - §2.1: Relax cache threshold (dig overhead ~10ms makes <3ms impossible)
  - §14.1: Reflect actual MaxContentScanSize (2MB, tunable via env var)

Test results: 90 PASS, 0 FAIL, 5 KNOWN, 1 SKIP
Fixed: §6.1 (WebSocket), §2.1 (DNS cache), §1.5 (NXDOMAIN), §14.1 (buffer)
… code removal

- Replace outgoing Via header with private X-GateSentry-Loop for loop detection;
  fixes nginx gzip_proxied=off killing compression for default-configured servers
- Block TRACE method at proxy (405) per RFC 9110 §9.3.8 — XST mitigation
- Remove ~100 lines of dead commented-out LazyLoad JS from contentscanner.go
- Add Path A proxy-side gzip compression fallback for uncompressed upstream
- Fix HEAD body-skip in Path A (prevents io.Copy blocking on empty body)
- Update §3.7, §15.13, §15.19, §15.29, §15.31 tests with correct assertions
- Add concurrency test suite (proxy_concurrency_test.sh)

Score: 94 PASS, 0 FAIL, 1 KNOWN (DNS caching), 1 SKIP (SSE)
- Delete application/proxy/ directory (4 files: certs.go, session.go,
  structures.go, ext/html.go) — expired 2018 CA cert + private key in source
- Remove gatesentry2proxy import and Proxy field from runtime.go
- Remove goproxy import and dead ConditionalMitm var from filters.go
- Clean commented-out proxy references from start.go
- Remove gopkg.in/elazarl/goproxy.v1 and github.com/abourget/goproxy
  from go.mod (root and application)

Section 14 of PROXY_SERVICE_UPDATE_PLAN.md — dead code cleanup.
PR Review Fixes (fifthsegment#139):

Security:
- Remove committed private key + certs from repo (GitGuardian finding)
- Add tests/fixtures/gen_test_certs.sh for ephemeral cert generation
- Add .gitignore rules for generated certs and Zone.Identifier files
- Fix docker-publish.sh: use --password-stdin for Nexus login (no -p flag)
- Fix basePath HTML injection: escape with html.EscapeString + json.Marshal

WebSocket:
- Filter hop-by-hop headers (Proxy-*, Connection, Keep-Alive, TE, Trailer,
  Transfer-Encoding) before forwarding to upstream (RFC 7230 §6.1)
- Ensure Host header is consistent with dialed upstream target

DNS Cache:
- Fix cache key collision: fall back to numeric qtype for unknown types
- Replace full cache clear with incremental eviction (expired first,
  then 10% random if still >90% capacity) to avoid latency spikes

Portability:
- Fix hardcoded 192.168.1.105 in proxy_concurrency_test.sh → 127.0.0.1
- Add NO_COLOR / ASCII_MODE env var support to both test scripts
  (see https://no-color.org/) for CI terminals without UTF-8/emoji
- Fix tests/setup_test.go: honor GS_ADMIN_PORT env (default 8080)
- Fix bonjour.go: use GS_ADMIN_PORT + GS_BASE_PATH instead of hardcoded 80
- Auto-generate test certs in testbed setup.sh and benchmark suite
New dns/cache package replaces the old inline cache in dns/server:

Cache (cache.go):
- 16-shard concurrent map with FNV-1a distribution
- TTL-aware with configurable min/max TTL clamping
- Negative caching per RFC 2308 §5 (SOA minimum field)
- Bounded entries with smart eviction (expired first, then nearest-to-expire)
- Background reaper goroutine for expired entry cleanup
- Atomic hit/miss/insert/eviction counters with memory estimation
- Deep-copy on Get() prevents mutation of cached data
- Tuneable via GS_DNS_CACHE_MAX environment variable
- Pi-safe: default 10,000 entries ≈ 40-80 MB

Event Bus (events.go):
- Fan-out pub/sub for real-time DNS cache events
- Event types: query, insert, evict, expire, flush, reaper
- Non-blocking sends — DNS server never blocks on slow consumers
- Auto-enable/disable based on subscriber count
- ~50 KB per SSE client (256-event buffer × ~200 bytes)

SSE Endpoint (handler_dns_cache.go):
- GET /api/dns/cache/stats — JSON cache statistics snapshot
- POST /api/dns/cache/flush — flush all cache entries
- GET /api/dns/events — Server-Sent Events stream for real-time monitoring

Server Integration (server.go):
- Replaced ~120 lines of inline cache with dnscache package
- Cache initialised in StartDNSServer(), stopped in StopDNSServer()
- Nil guards on all cache access for graceful degradation

Test Suite (cache_test.go):
- 33 tests covering all cache operations, eviction, concurrency,
  event bus, TTL behaviour, negative caching, and JSON serialisation
- All passing (5.0s)
Add EventRequest type to the event bus so SSE consumers can build the
same stats view (top domains, blocked counts, request rates) that the
/stats page currently polls every 5 seconds — but in real-time.

events.go:
- Add EventRequest type and RequestEvent() constructor
- Add ResponseType and Blocked fields to Event struct
- ResponseType values: blocked, cached, forwarded, device, exception,
  internal, error

server.go:
- Add emitRequestEvent() helper with nil-guard
- Emit at every DNS resolution path:
  · device store hit → "device"
  · exception domain → "exception"
  · internal record → "internal"
  · blocked domain → "blocked" (blocked=true)
  · cache hit → "cached"
  · forward success → "forwarded"
  · forward error → "error"

cache_test.go:
- TestRequestEvent: field values and constructor
- TestRequestEventJSON: serialisation and omitempty
- TestRequestEventViaEventBus: end-to-end through event bus

36 tests passing (5.0s).
Stats page now subscribes to the /api/dns/events SSE stream for
near-real-time chart and table updates instead of polling /stats/byUrl
every 5 seconds.

stats.svelte:
- One-shot fetch of historical 7-day data on mount (no more setInterval)
- EventSource subscription to /api/dns/events?token=JWT for live events
- Real-time accumulation: in-memory Maps bucketed by time scale
- Throttled UI refresh (500ms) so even 50 QPS doesn't thrash the DOM
- Time-scale dropdown: Past 7 days / Past 24 hours / Past hour
  Each scale changes the bucket granularity (day / hour / minute)
- Locale-aware x-axis labels via Intl (navigator.language)
- Weekday names on 7-day view, HH:MM on 24h/1h views
- Green 'Live' / gray 'Connecting' tag shows SSE connection status
- Event counter shows total events received in this session
- Top-5 tables update live as events arrive (merged with historical)
- Carbon AreaChart with curveMonotoneX, colour-coded series
- Proper cleanup: EventSource.close() + chart.destroy() on onDestroy

webserver.go:
- Auth middleware now accepts ?token= query param as fallback for
  SSE/EventSource connections (which cannot set custom headers)
- Existing Authorization header flow unchanged

Full build (build.sh): frontend + backend pass.
Major Features:
- DNS cache hit rate improved 13% → 13.7% with per-minute statistics
- Fixed critical filter reload bug (filters now update correctly)
- Added WPAD/PAC auto-configuration support
- One-click CA certificate generation
- Comprehensive keyword blocking test (end-to-end coverage)

Performance:
- Cache statistics recorder with BuntDB persistence
- Real-time SSE event streaming (cache + traffic)
- Time-scale support for stats API (1h/24h/7d views)
- Local-time bucket keys fix UTC timezone issues

Bug Fixes:
- Filter reload: R.Init() now reuses slice instead of creating new one
- Port 8080 default for non-root testing
- Certificate name: 'GateSentry CA' for auto-generated certs
- Status API: detectLanIP() fixes 127.0.1.1 reporting

API Changes:
- POST /api/certificate/generate - Generate new CA cert
- GET /api/dns/cache/stats/history - Per-minute cache snapshots
- GET /api/wpad/info - WPAD configuration info
- GET /wpad.dat, /proxy.pac - PAC file serving (unauthenticated)
- GET /api/stats/byUrl?seconds=X&group=Y - Time-scale support

UI Improvements:
- DNS Cache tab in Stats view with live metrics
- WPAD configuration section in Settings
- Certificate management overhaul with status badges
- Logs view shows DNS response types with tags
- Home view displays separate DNS/proxy ports

Build & Test:
- All 8 test suites passing (100% pass rate)
- New test: keyword_content_blocking_test.go (310 lines)
- build.sh preserves data files during rebuild
- restart.sh for graceful server restarts
- CORS middleware for multi-hostname access

Version: 1.20.6.2
DNS & Proxy improvements (from v1.20.6.3 work):
- DNS server returns A record with localIp for blocked domains
- Block page middleware serves HTML for DNS-blocked domains
- DNS Filtering toggle (enable/disable without stopping server)
- Proxy DNS resolver auto-switches when DNS server starts/stops
- Boot-time sync ensures proxy uses correct resolver on startup
- PAC file: removed dnsResolve(), hostname-pattern matching for bypass

UI & Build:
- GateSentry shield SVG logo and multi-resolution favicon.ico
- RootFileHandler for serving favicon.ico/gatesentry.svg at root
- Vite code splitting (1.1MB -> 259KB initial bundle)
- Clipboard fallback for non-HTTPS contexts
- Docker publish script NEXUS_SERVER format fix

Planning:
- Domain List & Rules Enhancement Plan (DOMAIN_LIST_RULES_PLAN.md)
- Architectural rework: unified Domain Lists, DNS whitelist support,
  Rule expansion for multi-pattern/multi-list domain matching,
  content filtering by domain list, filter system retirement

Version bumped to 2.0.0-alpha.1
…ine expansion

ARCHITECTURE — Admin UI (Svelte 4 + Carbon Components)
======================================================
Rewrote every page from legacy Carbon Grid/Row/Column/Breadcrumb layout
to a modern card-based design system with consistent gs-* utility classes
(gs-section, gs-card, gs-tabs, gs-tab, gs-page-title, gs-row-list, etc.).
All pages are now mobile-first with responsive breakpoints at 671px and 960px.

Pages rewritten:
- Home: Status bar with custom NetworkIcon SVG, expandable setup cards
  (computer/phone) with PAC URL copy-to-clipboard, 3-column info grid
- Login: Centered card layout with Security icon, responsive form
- DNS: Tabbed interface (Filters/Server), domain list picker modals,
  stats row with human-readable timestamps, A record CRUD via modals
- Rules: List/detail navigation pattern with drag-to-reorder (mouse +
  touch), inline toggle for enable/disable, detail form with domain
  patterns, domain lists, content type ComboBox, user ComboBox
- Users: Row-based list with inline toggle for access control, icon
  buttons for edit/delete, data consumption display
- Logs: SSE live streaming replaces polling, pause/resume, desktop
  table + mobile card layout, search filter, time-ago labels
- Stats: Custom gs-tabs replacing Carbon Tabs, DNS cache tiles
- Settings: Grouped cards (Logging, MITM/CA, WPAD) with section icons
- Filter: Simplified keyword editor with strictness setting
- Services: Page title with Power icon, info notices
- Domain Lists: New page — card grid with CRUD, sort, URL/local source
  types, refresh for URL lists, category tags, entry counts

Custom SVG Icons:
- LaptopIcon.svelte, MobileIcon.svelte (restored from Carbon b/w)
- NetworkIcon.svelte (new — network switch with status LEDs, ports,
  signal arcs, matching dark gray/blue/green design language)

Shared Components Updated:
- connectedCertificateComposed: Skeleton shimmer loading state,
  text-only status tags (removed misaligned icons from Carbon Tags)
- globalheader, sidenavmenu: Menu restructured (removed old Block List
  and Exception Hostnames items, added Domain Lists)
- wpadSettings: PAC URL builder logic
- filtereditor, httpsToggle, modal, toggle: Style alignment
- app.css: New gs-* utility classes for consistent spacing/cards/tabs

BACKEND — Domain List System (Phase 1-4)
=========================================
New application/domainlist/ package:
- types.go: DomainList struct (id, name, source, url, category, etc.)
- manager.go: DomainListManager with CRUD operations
- index.go: DomainListIndex for fast domain membership lookups
- loader.go: URL and local domain list loading/parsing
- migrate.go: Migration from legacy blocklist format
- domainlist_test.go: 19 passing tests

DNS Server Migration (Phase 2):
- application/dns/server: Uses shared DomainListIndex instead of
  private blockedDomains map
- application/dns/filter: Shared domain filtering through index
- application/dns/scheduler: Updated for domain list refresh cycles

Rule Engine Expansion (Phase 3-4):
- application/types/rule.go: Added DomainPatterns (plural wildcards),
  DomainLists (list IDs), ContentDomainLists for domain matching
- application/rules.go: Rule evaluation uses domain patterns + lists
- application/rules_test.go: 18 passing tests
- gatesentryproxy: Content filtering by domain list membership for
  MITM inspection (blocks embedded resources from listed domains)

API Endpoints:
- handler_domainlists.go: Full REST API for domain lists
  (GET/POST/PUT/DELETE /api/domainlists, /domains, /refresh)
- handler_settings.go: Added dns_domain_lists and
  dns_whitelist_domain_lists to GET/POST whitelists
- handler_rules.go: Updated for expanded rule fields

Other Backend:
- application/webserver.go: blockedDomainMiddleware for Host checking
- Removed application/filters/filter-blocked-mimes.go (consolidated)
- AGENTS.md: Agent context documentation for AI-assisted development
- main.go, application/start.go, runtime.go: Initialization updates
…est suite

Replace legacy block_type enum with auto-derived per-rule filtering. All content
filtering (keyword scanning, content-type blocking, URL regex matching) is now
scoped to individual rules based on populated fields, not a global pipeline.

Proxy Engine (gatesentryproxy/):
- Add per-rule content-type blocking after response headers are read
- Make keyword scanning conditional via isKeywordFilterEnabled() per rule
- Simplify URL regex matching: remove content-domain-list handler entirely
- Serve proper HTML block page for rule-based domain blocks (RuleBlockPageHandler)
- Fix XFF loop detection to count comma-separated IPs, not just header lines
- Allow explicit rules to override LAN address MITM bypass
- Reduce noisy debug logging behind DebugLogging flag

Rule Engine (application/):
- Remove block_type enum dependency from MatchRule(); auto-derive filters from
  populated rule fields (BlockedContentTypes, URLRegexPatterns, KeywordFilterEnabled)
- Remove CheckContentDomainBlocked() and ContentDomainBlockHandler (unused)
- Add KeywordFilterEnabled field to Rule struct and RuleMatch
- Remove 8 obsolete content-domain-list tests

UI (ui/src/routes/rules/):
- Restructure rule form: name field at top, description after priority
- Remove block_type dropdown and conditional content filter visibility
- Add keyword_filter_enabled toggle to rule form
- Add 'All rules are evaluated in sequence' info text to rule list page
- Slim down block page CSS (responder/assets.go) from full MDL to minimal

Infrastructure:
- Add scripts/proxy_deep_tests.sh: 64-test comprehensive proxy test suite
  covering connectivity, RFC compliance, MITM, passthrough, domain patterns,
  domain lists, keyword filtering, content-type blocking, URL regex matching,
  block pages, rule priority, loop detection, SSRF, error handling, headers,
  and performance. Uses IPv6 echo server with trusted JVJCA-signed certs.
- Rewrite tests/fixtures/gen_test_certs.sh to reuse existing trusted CA
- Unset proxy env vars in run.sh/restart.sh to prevent self-routing loops
- Update AGENTS.md with proxy rule architecture docs and test instructions
- Add server-side rule test API (POST /api/test/rule-match, /api/test/domain-lookup)
  with full 8-step pipeline evaluation in handler_test_rule.go
- Add 19 unit tests for rule test endpoints (handler_test_rule_test.go)
- Fix domain list lookup for URL-sourced lists: add IsDomainInList/IsDomainInAnyList
  methods to DomainListManager and interface, using in-memory index instead of
  GetDomainsForList() which returns nil for URL-sourced lists
- Update rform.svelte: replace client-side rule tester with server API calls,
  add live test toggle, remove unused CSS selectors
- Enhanced proxy deep tests with comprehensive filtering, MITM, and content pipeline tests
- Proxy improvements: graceful shutdown, HTTPS block page fixes, performance fixes,
  loop detection, security hardening
- CSS cleanup and UI polish across rule form
Copilot AI review requested due to automatic review settings February 14, 2026 12:30
Copy link

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 RFC PR introduces a v2 architecture with per-user rule-based filtering, a reusable Domain List system shared across DNS/proxy, and several hardening/operability improvements (WPAD, cert generation/reload, SSE, device discovery, base-path support).

Changes:

  • Added Domain List CRUD + shared in-memory index and migrated DNS filtering to consume it.
  • Added new admin endpoints for WPAD/PAC, DNS cache stats/history/events (SSE), device inventory, and CA generation/reload.
  • Refactored routing to support a configurable base path and removed legacy frontend/proxy assets.

Reviewed changes

Copilot reviewed 75 out of 169 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
application/webserver/frontend/files/material-mini.css Removed legacy Material CSS asset
application/webserver/frontend/files/index.html Removed legacy static index.html (likely replaced by new build pipeline)
application/webserver/frontend/files/bundle.js.LICENSE.txt Removed legacy bundle license artifact
application/webserver/endpoints/handler_wpad.go Added WPAD/PAC generation + info endpoints
application/webserver/endpoints/handler_status.go Expanded status response (DNS/proxy URLs) + LAN IP detection
application/webserver/endpoints/handler_stats.go Added query parsing and time-bucketed stats support
application/webserver/endpoints/handler_settings.go Whitelisted new DNS/WPAD settings + hot-reload behaviors
application/webserver/endpoints/handler_rules.go Relaxed legacy “Domain required” validation to support new rule fields
application/webserver/endpoints/handler_domainlists.go Added Domain List CRUD + lookup endpoints
application/webserver/endpoints/handler_dns_cache.go Added DNS cache admin APIs + SSE event stream
application/webserver/endpoints/handler_devices.go Added device inventory endpoints backed by discovery store
application/webserver/endpoints/handler_certificate.go Added CA generation + proxy cert reload helpers
application/webserver/endpoints/handler_blocked.go Added DNS-level blocked page handler
application/webserver/api.go Added basePath-aware router/subrouter + global CORS middleware
application/webserver.go Wired rule manager + domain list manager + base path into server registration
application/types/rule.go Expanded rule schema for domain patterns/lists and keyword flag
application/start.go Initialized shared DomainListManager + migration + async list load
application/rules_test.go Added tests for domain globbing, domain list matching, block types
application/rules.go Implemented glob matching + domain-list-aware rule matching + MITM “default” behavior
application/responder/responder.go Updated proxy block page HTML template/layout
application/proxy/structures.go Removed legacy proxy wrapper types
application/proxy/session.go Removed legacy goproxy session glue
application/proxy/ext/html.go Removed legacy goproxy HTML handler extension
application/proxy/certs.go Removed hardcoded CA cert/key constants
application/logger/logger.go Added SSE-style subscribers + log grouping-by-format + DB maintenance
application/go.mod Removed old goproxy dependencies
application/filters/loader.go Removed blocked MIME filter registration (migrating to rules)
application/filters/filter-blocked-mimes.go Removed blocked MIME filter implementation
application/filters.go Removed goproxy ConditionalMitm hook
application/domainlist/types.go Added DomainList and summary types
application/domainlist/migrate.go Added migration from legacy DNS list config to Domain Lists
application/domainlist/manager.go Added DomainListManager CRUD + index refresh/load
application/domainlist/loader.go Added URL/local domain parsing + blocklist downloader
application/domainlist/index.go Added O(1) DomainListIndex with thread-safe ops
application/domainlist/domainlist_test.go Added tests for index/manager/loader/migration helpers
application/dns/utils/network.go Improved local IP selection logic
application/dns/server/ddns.go Added RFC 2136 DDNS update handling
application/dns/scheduler/scheduler.go Scheduler now refreshes domain lists via DomainListManager
application/dns/http/http-server.go Removed legacy DNS HTTP/HTTPS server
application/dns/filter/internal-records.go Switched filter mutex to RWMutex
application/dns/filter/exception-records.go Switched filter mutex to RWMutex
application/dns/filter/domains.go Removed legacy blocklist download path; now initializes internal/exception only
application/dns/discovery/types.go Added device + record model for discovery
application/dns/discovery/passive_test.go Added passive discovery unit tests
application/dns/discovery/passive.go Added passive discovery via ARP correlation
application/dns/discovery/mdns.go Added Bonjour/mDNS browser feeding device store
application/dns/cache/recorder_test.go Added cache recorder tests
application/dns/cache/recorder.go Added per-minute cache stats recorder in BuntDB
application/dns/cache/events.go Added cache EventBus for SSE
application/dns.go DNS server thread now accepts DomainListManager
application/consumptionupdater.go Removed unused ticker goroutine stub
application/bonjour.go Advertised admin UI + proxy via Bonjour with base path/port
README.md Updated documented ports and access URL
Makefile Updated integration test harness to use port 8080 and new health URL
Dockerfile Added runtime-only container packaging (prebuilt binary)
DOCKERHUB_README.md Added Docker Hub-focused documentation
DNS_CACHE_FEATURE.md Added DNS cache feature documentation
AGENTS.md Added AI-agent/project operational notes
.dockerignore Restricted Docker build context to prebuilt binary + Dockerfile
Comments suppressed due to low confidence (1)

application/domainlist/manager.go:1

  • Deduplication uses the raw stored dl.Domains values as map keys, but incoming domains are normalized (lowercased, trailing dot stripped). If existing entries are not normalized (e.g., legacy data), duplicates can slip through (Example.com vs example.com). Normalize existing domains when building the existing map so dedup is consistent.
package domainlist

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

Comment on lines +81 to +83
for _, dl := range lists {
if dl.ID == id {
return &dl, nil
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

This returns the address of the range loop variable (dl), which is a copy that gets reused each iteration. Callers can receive a pointer to a value that is not the actual slice element. Fix by iterating by index and returning &lists[i], or by assigning to a new variable and returning a pointer that doesn’t escape the loop variable.

Suggested change
for _, dl := range lists {
if dl.ID == id {
return &dl, nil
for i := range lists {
if lists[i].ID == id {
return &lists[i], nil

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +353
// We need access to the index — get it from the concrete manager.
// The interface doesn't expose the index directly, but we can check
// by trying to get the list and checking membership.
dl, err := domainListManager.GetList(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if dl == nil {
http.Error(w, "Domain list not found", http.StatusNotFound)
return
}

// For local lists, check the domains array directly
found := false
if dl.Source == "local" {
domain = normalizeForCheck(domain)
for _, d := range dl.Domains {
if normalizeForCheck(d) == domain {
found = true
break
}
}
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"domain": domain,
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

GSApiDomainListCheck only checks membership for Source == \"local\", so it will always report found=false for URL-sourced lists (even though those are the main large lists). Since the manager already exposes IsDomainInList, use that (with the same normalization) so both local and URL lists are supported and the check uses the shared in-memory index.

Suggested change
// We need access to the index — get it from the concrete manager.
// The interface doesn't expose the index directly, but we can check
// by trying to get the list and checking membership.
dl, err := domainListManager.GetList(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if dl == nil {
http.Error(w, "Domain list not found", http.StatusNotFound)
return
}
// For local lists, check the domains array directly
found := false
if dl.Source == "local" {
domain = normalizeForCheck(domain)
for _, d := range dl.Domains {
if normalizeForCheck(d) == domain {
found = true
break
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"domain": domain,
// Normalize the domain before checking membership to match how domains are stored.
normalizedDomain := normalizeForCheck(domain)
// Use the manager's shared index so both local and URL-sourced lists are supported.
found := domainListManager.IsDomainInList(normalizedDomain, id)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"domain": normalizedDomain,

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +13
func BlockedPageHTML(host string) string {
return fmt.Sprintf(`<!DOCTYPE html>
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The block page interpolates host (derived from r.Host) directly into HTML without escaping, which can allow HTML injection/XSS if a client supplies a crafted Host header. Escape the host with html.EscapeString(host) (from html or html/template) before inserting it into the response.

Copilot uses AI. Check for mistakes.
<div class="blocked-icon">🛡️</div>
<h1>Access Blocked</h1>
<div class="domain">
<strong>%s</strong>
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The block page interpolates host (derived from r.Host) directly into HTML without escaping, which can allow HTML injection/XSS if a client supplies a crafted Host header. Escape the host with html.EscapeString(host) (from html or html/template) before inserting it into the response.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +105
</html>`, host)
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The block page interpolates host (derived from r.Host) directly into HTML without escaping, which can allow HTML injection/XSS if a client supplies a crafted Host header. Escape the host with html.EscapeString(host) (from html or html/template) before inserting it into the response.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +29
if basePath == "/" {
sub = root
} else {
sub = root.PathPrefix(basePath).Subrouter()
// Redirect bare root to the base path
root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, basePath+"/", http.StatusFound)
})
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

If basePath is configured with a trailing slash (e.g., "/gatesentry/"), basePath+\"/\" produces a double-slash redirect target. Normalize basePath (ensure it starts with a single leading '/', and trim any trailing '/') before building the redirect and PathPrefix.

Copilot uses AI. Check for mistakes.
| 10413 | For proxy (explicit mode) |
| 10414 | For proxy (transparent mode, optional) |
| 10786 | For the web based administration panel |
| 80 | For the web based administration panel |
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The README now states the admin UI runs on port 80, but other repo docs in this PR (e.g., AGENTS.md and Makefile) reference 8080 as the default admin port. Please reconcile the documented defaults across files to avoid confusing users, and also fix the missing space after Note: (Note: Ensure ...).

Copilot uses AI. Check for mistakes.

Open a modern web browser of your choice.
Enter the following URL in the address bar: http://localhost:10786
Enter the following URL in the address bar: http://localhost
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The README now states the admin UI runs on port 80, but other repo docs in this PR (e.g., AGENTS.md and Makefile) reference 8080 as the default admin port. Please reconcile the documented defaults across files to avoid confusing users, and also fix the missing space after Note: (Note: Ensure ...).

Copilot uses AI. Check for mistakes.
Use the above credentials to log in to the Gatesentry system for the first time. For security reasons, it is highly recommended to change the default password after the initial login.

Note:Ensure your systems firewall and security settings allow traffic on ports 10413 and 10786 to ensure seamless operation and access to the Gatesentry server and user interface.
Note:Ensure your system's firewall and security settings allow traffic on ports 53, 80, and 10413 to ensure seamless operation and access to the Gatesentry server and user interface.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The README now states the admin UI runs on port 80, but other repo docs in this PR (e.g., AGENTS.md and Makefile) reference 8080 as the default admin port. Please reconcile the documented defaults across files to avoid confusing users, and also fix the missing space after Note: (Note: Ensure ...).

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +19
http.Error(w, `{"error":"DNS cache not initialized — DNS server may not be running"}`, http.StatusServiceUnavailable)
return nil
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

http.Error will set Content-Type: text/plain; charset=utf-8, but the body is JSON. For API consistency, write a JSON response with Content-Type: application/json (and ideally a struct) so clients don’t need to special-case error parsing.

Copilot uses AI. Check for mistakes.
Comprehensive Prometheus /metrics endpoint with zero hot-path impact:
- ProxyMetrics: 30+ atomic counters for requests, blocks, errors,
  active connections, pipeline paths, cert cache, bytes transferred,
  and latency histograms (gatesentryproxy/metrics.go)
- DNSMetrics: 10 atomic counters for query results + latency
  histograms (application/dns/server/metrics.go)
- Custom Prometheus collector reads all atomics on scrape only — no
  background goroutines, no lock contention (metrics/metrics.go)
- 71 metric families: DNS, cache, proxy, SSE, devices, rules,
  domain index, Go runtime, and process stats
- METRICS.md: full reference with PromQL troubleshooting examples

Debug and profiling endpoints (JWT-protected):
- GET /api/debug/runtime — goroutines, memory, GC, SSE subscribers
- /api/debug/pprof/* — standard Go pprof endpoints

SSE connection hardening (all SSE endpoints):
- 30s heartbeat to detect dead TCP connections / zombie goroutines
- 4-hour max duration with server-initiated reconnect event
- Client-side exponential backoff on auth errors (1s → 30s)
- Reconnect event listener for graceful server-requested reconnect

Instrumented hot paths:
- dns/server/server.go: 10 points (query timing, result counters, upstream RTT)
- gatesentryproxy/proxy.go: ~15 points (request lifecycle, block reasons, pipeline)
- gatesentryproxy/ssl.go: 7 points (MITM/direct gauges, cert cache, TLS errors)

Other:
- Logger SubscriberCount() for SSE subscriber metrics
- Version bump to 2.0.0-alpha.7
- V2_PR.md: RFC pull request description
- Removed noisy 'Logged in with username' log line from auth middleware
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.

1 participant