fix ip filter and env files

Kim committed Mar 6, 2026 at 16:44 UTC b76e74256517845703f0e1e926173b4f54ecf51e
12 files changed +94 -41
.env.example
+14 -8
@@ -1,15 +1,21 @@
1 -# Public base URL for relay/admin
2 -PORTAL_URL=http://localhost:4017
1 +# Public routing
2 +PORTAL_URL=https://localhost:4017
3 +BOOTSTRAP_URIS=https://localhost:4017
4
4 -# Admin login secret
5 -ADMIN_SECRET_KEY=
6 -
7 -# SNI router port
5 +# Listener ports
6 +API_PORT=4017
7 SNI_PORT=443
8
10 -# Directory for keyless/ACME materials
11 -KEYLESS_DIR=/etc/portal/keyless
9 +# TLS/ACME and keyless materials
10 +KEYLESS_DIR=.portal-certs
11
12 # Cloudflare API token
13 # Leave empty for local/no-TLS usage
14 CLOUDFLARE_TOKEN=
15 +
16 +# Admin/auth configuration
17 +ADMIN_SECRET_KEY=
18 +# Enable when the relay is behind nginx/ingress/load balancers and should trust forwarded client IP headers.
19 +# Optionally restrict which proxy source ranges may supply those headers; leave empty for default private/loopback proxy ranges.
20 +TRUST_PROXY_HEADERS=false
21 +TRUSTED_PROXY_CIDRS=
.golangci.yml
+2
@@ -75,6 +75,8 @@ linters:
75
76 govet:
77 enable-all: true
78 + disable:
79 + - fieldalignment
80
81 perfsprint:
82 strconcat: true
Dockerfile
+1 -1
@@ -42,7 +42,7 @@ ENV PORTAL_URL=https://localhost:4017
42 ENV BOOTSTRAP_URIS=https://localhost:4017
43 ENV ADMIN_SECRET_KEY=
44 ENV SNI_PORT=:443
45 -ENV KEYLESS_DIR=/etc/portal/keyless
45 +ENV KEYLESS_DIR=
46 ENV CLOUDFLARE_TOKEN=
47 ENV TZ=UTC
48
cmd/portal-tunnel/relays.go
+5 -3
@@ -18,15 +18,15 @@ import (
18 )
19
20 type relayRuntime struct {
21 - relayURL string
21 client *sdk.Client
22 listener *sdk.Listener
23 + relayURL string
24 }
25
26 type relayLoopResult struct {
27 + err error
28 leaseID string
29 relayURL string
29 - err error
30 }
31
32 func (r *relayRuntime) run(ctx context.Context, localAddr string, connWG *sync.WaitGroup, connCount *atomic.Int64, done chan<- relayLoopResult) {
@@ -144,8 +144,10 @@ func waitForRelayLoops(ctx context.Context, done <-chan relayLoopResult, relayCo
144 }
145 }
146
147 - if ctx.Err() != nil {
147 + select {
148 + case <-ctx.Done():
149 return nil
150 + default:
151 }
152 return errors.New("all relay listeners stopped")
153 }
cmd/relay-server/main.go
+15 -12
@@ -19,15 +19,15 @@ const (
19 )
20
21 type relayServerConfig struct {
22 - AdminSecretKey string
22 PortalURL string
24 - TrustedProxyCIDRs string
25 - KeylessDir string
26 - CloudflareToken string
23 Bootstraps []string
28 - AdminPort int
24 + APIPort int
25 SNIPort int
26 + AdminSecretKey string
27 TrustProxyHeaders bool
28 + TrustedProxyCIDRs string
29 + KeylessDir string
30 + CloudflareToken string
31 }
32
33 func main() {
@@ -44,23 +44,26 @@ func main() {
44 if bootstrapsCSV == "" {
45 bootstrapsCSV = portalURL
46 }
47 + apiPort := parsePortNumber(os.Getenv("API_PORT"), defaultAPIPort)
48 sniPort := parsePortNumber(os.Getenv("SNI_PORT"), defaultSNIPort)
49 + adminSecretKey := trimmedEnv("ADMIN_SECRET_KEY")
50 + trustProxyHeaders := parseBoolEnv("TRUST_PROXY_HEADERS")
51 + trustedProxyCIDRs := trimmedEnv("TRUSTED_PROXY_CIDRS")
52 keylessDir := trimmedEnv("KEYLESS_DIR")
53 if keylessDir == "" {
54 keylessDir = defaultKeylessDir
55 }
52 - adminSecretKey := trimmedEnv("ADMIN_SECRET_KEY")
56 cloudflareToken := trimmedEnv("CLOUDFLARE_TOKEN")
54 - trustProxyHeaders := parseBoolEnv("TRUST_PROXY_HEADERS")
55 - trustedProxyCIDRs := trimmedEnv("TRUSTED_PROXY_CIDRS")
57
57 - flag.IntVar(&cfg.AdminPort, "adminport", defaultAPIPort, "Admin/HTTP server port")
58 - flag.StringVar(&cfg.AdminSecretKey, "admin-secret-key", adminSecretKey, "admin auth secret (env: ADMIN_SECRET_KEY)")
58 flag.StringVar(&cfg.PortalURL, "portal-url", portalURL, "portal base URL (env: PORTAL_URL)")
60 - flag.BoolVar(&cfg.TrustProxyHeaders, "trust-proxy-headers", trustProxyHeaders, "trust X-Forwarded-* and X-Real-IP headers (env: TRUST_PROXY_HEADERS)")
61 - flag.StringVar(&cfg.TrustedProxyCIDRs, "trusted-proxy-cidrs", trustedProxyCIDRs, "trusted proxy CIDR allowlist for forwarded headers, comma-separated (env: TRUSTED_PROXY_CIDRS)")
59 flag.StringVar(&bootstrapsCSV, "bootstraps", bootstrapsCSV, "bootstrap URIs, comma-separated (env: BOOTSTRAP_URIS)")
60 + flag.IntVar(&cfg.APIPort, "api-port", apiPort, "Admin/API server port (env: API_PORT)")
61 flag.IntVar(&cfg.SNIPort, "sni-port", sniPort, "SNI router port number (env: SNI_PORT)")
62 +
63 + flag.StringVar(&cfg.AdminSecretKey, "admin-secret-key", adminSecretKey, "admin auth secret (env: ADMIN_SECRET_KEY)")
64 + flag.BoolVar(&cfg.TrustProxyHeaders, "trust-proxy-headers", trustProxyHeaders, "trust X-Forwarded-* and X-Real-IP headers from trusted proxies (env: TRUST_PROXY_HEADERS)")
65 + flag.StringVar(&cfg.TrustedProxyCIDRs, "trusted-proxy-cidrs", trustedProxyCIDRs, "trusted proxy CIDR allowlist for forwarded headers, comma-separated; defaults to private/loopback proxy ranges when trust-proxy-headers is enabled (env: TRUSTED_PROXY_CIDRS)")
66 +
67 flag.StringVar(&cfg.KeylessDir, "keyless-dir", keylessDir, "directory path for relay keyless materials (env: KEYLESS_DIR)")
68 flag.StringVar(&cfg.CloudflareToken, "cloudflare-token", cloudflareToken, "Cloudflare DNS API token (Zone:Read + DNS:Edit) (env: CLOUDFLARE_TOKEN)")
69 flag.Parse()
cmd/relay-server/serve.go
+1 -1
@@ -29,7 +29,7 @@ func runServer(cfg relayServerConfig) error {
29 cfg.PortalURL = cfg.Bootstraps[0]
30 }
31 rootHost := portal.PortalRootHost(cfg.PortalURL)
32 - apiListenAddr := fmt.Sprintf(":%d", cfg.AdminPort)
32 + apiListenAddr := fmt.Sprintf(":%d", cfg.APIPort)
33 sniListenAddr := fmt.Sprintf(":%d", cfg.SNIPort)
34 trustedProxyCIDRs, err := policy.ParseTrustedProxyCIDRs(cfg.TrustedProxyCIDRs)
35 if err != nil {
docker-compose.yml
+15 -11
@@ -4,22 +4,26 @@ services:
4 build:
5 context: .
6 dockerfile: Dockerfile
7 - command:
8 - - "--adminport"
9 - - "${ADMIN_PORT:-4017}"
7 environment:
11 - # Core configuration
12 - PORTAL_URL: ${PORTAL_URL:-https://localhost:${ADMIN_PORT:-4017}}
13 - BOOTSTRAP_URIS: ${BOOTSTRAP_URIS:-https://localhost:${ADMIN_PORT:-4017}}
14 - ADMIN_SECRET_KEY: ${ADMIN_SECRET_KEY:-}
8 + # Public routing
9 + PORTAL_URL: ${PORTAL_URL:-https://localhost:${API_PORT:-4017}}
10 + BOOTSTRAP_URIS: ${BOOTSTRAP_URIS:-https://localhost:${API_PORT:-4017}}
11
16 - # TLS/SNI and keyless configuration
12 + # Listener ports
13 + API_PORT: ${API_PORT:-4017}
14 SNI_PORT: ${SNI_PORT:-443}
18 - KEYLESS_DIR: ${KEYLESS_DIR:-/etc/portal/keyless}
15 +
16 + # Admin/auth configuration
17 + ADMIN_SECRET_KEY: ${ADMIN_SECRET_KEY:-}
18 + TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-false}
19 + TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-}
20 +
21 + # TLS/ACME and keyless materials
22 + KEYLESS_DIR: ${KEYLESS_DIR:-.portal-certs}
23 CLOUDFLARE_TOKEN: ${CLOUDFLARE_TOKEN:-}
24 ports:
21 - - "${ADMIN_PORT:-4017}:${ADMIN_PORT:-4017}"
25 + - "${API_PORT:-4017}:${API_PORT:-4017}"
26 - "443:443"
27 volumes:
24 - - ./data/keyless:/etc/portal/keyless
28 + - .portal-certs:/.portal-certs
29 restart: unless-stopped
docs/architecture.md
+1 -1
@@ -36,7 +36,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
36
37 ### Relay Server (`cmd/relay-server`)
38
39 -- Admin/API TLS listener on `--adminport` (default `:4017`)
39 +- Admin/API TLS listener on `--api-port` (default `:4017`)
40 - SNI listener on `--sni-port` (default `:443`)
41 - Public frontend routes under `/`, `/app`, `/assets/*`
42 - Minimal admin surface at `/admin` and `/admin/leases`
docs/deployment.md
+10 -1
@@ -95,13 +95,22 @@ PORTAL_URL=https://example.com
95 BOOTSTRAP_URIS=https://example.com
96 SNI_PORT=443
97 ADMIN_SECRET_KEY=your-admin-secret
98 -KEYLESS_DIR=/etc/portal/keyless
98 +KEYLESS_DIR=.portal-certs
99 CLOUDFLARE_TOKEN=cf_xxxxxxxxxxxxxxxxx
100 ```
101
102 For non-apex deployments, set `PORTAL_URL` and `BOOTSTRAP_URIS` to the same non-apex host value (for example, `https://portal.example.com:8443`).
103 `PORTAL_URL` path/query segments are ignored for route derivation; only the host component is used.
104
105 +If the relay sits behind a reverse proxy or ingress and you want admin/auth and lease IP tracking to use the original client IP, set:
106 +
107 +```bash
108 +TRUST_PROXY_HEADERS=true
109 +```
110 +
111 +By default, forwarded headers are accepted from private, loopback, and link-local proxy source ranges.
112 +If your proxy source addresses are public or you want a stricter allowlist, also set `TRUSTED_PROXY_CIDRS`.
113 +
114 ### 4.2 Start Relay
115
116 ```bash
frontend/README.md
+1 -1
@@ -152,7 +152,7 @@ npm run build
152
153 # Run relay server (embeds dist/ at compile time)
154 cd ..
155 -go run ./cmd/relay-server/*.go -adminport 4017
155 +go run ./cmd/relay-server/*.go
156 ```
157
158 Or use the combined script:
frontend/package.json
+1 -1
@@ -14,7 +14,7 @@
14 "test:coverage": "vitest run --coverage",
15 "preview": "vite preview",
16 "build:go": "cd .. && CGO_ENABLED=0 go build -o bin/relay-server ./cmd/relay-server/*.go",
17 - "serve": "npm run build && npm run build:go && STATIC_DIR=./cmd/relay-server/dist ../bin/relay-server -adminport 4017"
17 + "serve": "npm run build && npm run build:go && STATIC_DIR=./cmd/relay-server/dist ../bin/relay-server"
18 },
19 "dependencies": {
20 "@radix-ui/react-dialog": "^1.1.15",
portal/policy/ip_filter.go
+28 -1
@@ -25,6 +25,17 @@ type IPFilter struct {
25 var (
26 trustedProxyMu sync.RWMutex
27 trustedProxyCIDRs []*net.IPNet
28 + defaultProxyCIDRs = mustParseProxyCIDRs(
29 + "127.0.0.0/8",
30 + "10.0.0.0/8",
31 + "172.16.0.0/12",
32 + "192.168.0.0/16",
33 + "169.254.0.0/16",
34 + "100.64.0.0/10",
35 + "::1/128",
36 + "fc00::/7",
37 + "fe80::/10",
38 + )
39 )
40
41 func NewIPFilter() *IPFilter {
@@ -81,7 +92,11 @@ func IsTrustedProxyRemoteAddr(remoteAddr string) bool {
92
93 trustedProxyMu.RLock()
94 defer trustedProxyMu.RUnlock()
84 - for _, network := range trustedProxyCIDRs {
95 + networks := trustedProxyCIDRs
96 + if len(networks) == 0 {
97 + networks = defaultProxyCIDRs
98 + }
99 + for _, network := range networks {
100 if network != nil && network.Contains(remoteIP) {
101 return true
102 }
@@ -262,3 +277,15 @@ func parseRemoteAddrIP(remoteAddr string) net.IP {
277 }
278 return net.ParseIP(strings.TrimSpace(host))
279 }
280 +
281 +func mustParseProxyCIDRs(values ...string) []*net.IPNet {
282 + cidrs := make([]*net.IPNet, 0, len(values))
283 + for _, value := range values {
284 + _, network, err := net.ParseCIDR(value)
285 + if err != nil {
286 + panic(err)
287 + }
288 + cidrs = append(cidrs, network)
289 + }
290 + return cidrs
291 +}