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
+}