fix lint error, implement zerolog
Kim committed
Mar 6, 2026 at 12:50 UTC
9bfb760945eb96dd09559d2c38af97407549350b
23 files changed
+468
-273
cmd/demo-app/main.go
+23
-3
@@ -5,6 +5,7 @@ import (
5
"embed"
6
"encoding/base64"
7
"encoding/json"
8
+ "errors"
9
"flag"
10
"fmt"
11
"io/fs"
@@ -15,6 +16,8 @@ import (
16
"syscall"
17
"time"
18
19
+ "github.com/rs/zerolog"
20
+ "github.com/rs/zerolog/log"
21
"golang.org/x/net/websocket"
22
23
"gosuda.org/portal/sdk"
@@ -37,6 +40,10 @@ var (
40
)
41
42
func main() {
43
+ zerolog.TimeFieldFormat = time.RFC3339
44
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
45
+ logger := log.With().Str("component", "demo-app").Logger()
46
+
47
flag.StringVar(&flagServerURL, "server-url", "https://localhost:4017", "relay API URL (https only)")
48
flag.IntVar(&flagPort, "port", 8092, "local demo HTTP port")
49
flag.StringVar(&flagName, "name", "demo-app", "backend display name")
@@ -47,12 +54,14 @@ func main() {
54
flag.Parse()
55
56
if err := runDemo(); err != nil {
50
- fmt.Fprintf(os.Stderr, "demo command failed: %v\n", err)
57
+ logger.Error().Err(err).Msg("demo command failed")
58
os.Exit(1)
59
}
60
}
61
62
func runDemo() error {
63
+ logger := log.With().Str("component", "demo-app").Logger()
64
+
65
sdkClient, err := sdk.NewClient(sdk.ClientConfig{RelayURL: flagServerURL})
66
if err != nil {
67
return fmt.Errorf("new client: %w", err)
@@ -79,6 +88,12 @@ func runDemo() error {
88
}
89
defer listener.Close()
90
91
+ logger.Info().
92
+ Str("lease_id", listener.LeaseID()).
93
+ Strs("public_urls", listener.PublicURLs()).
94
+ Int("local_port", flagPort).
95
+ Msg("demo app registered with relay")
96
+
97
mux := http.NewServeMux()
98
99
staticFS, err := fs.Sub(staticFiles, "static")
@@ -131,7 +146,10 @@ func runDemo() error {
146
Handler: mux,
147
ReadHeaderTimeout: 5 * time.Second,
148
}
134
- _ = localSrv.ListenAndServe()
149
+ logger.Info().Str("addr", localAddr).Msg("demo app listening locally")
150
+ if err := localSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
151
+ logger.Error().Err(err).Str("addr", localAddr).Msg("demo local server stopped")
152
+ }
153
}()
154
155
relaySrv := &http.Server{
@@ -149,12 +167,14 @@ func runDemo() error {
167
168
select {
169
case <-sig:
170
+ logger.Info().Msg("demo app shutting down")
171
case err := <-errCh:
153
- if err != nil && err != http.ErrServerClosed {
172
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
173
return err
174
}
175
}
176
177
+ logger.Info().Msg("demo app shutdown complete")
178
return nil
179
}
180
cmd/demo-app/static/index.html
+126
-52
@@ -1,57 +1,131 @@
1
-<!doctype html>
1
+<!DOCTYPE html>
2
<html lang="en">
3
+
4
<head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Portal Demo App</title>
7
- <link rel="stylesheet" href="/style.css">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="mobile-web-app-capable" content="yes">
9
+ <title>Portal Demo Connectivity</title>
10
+ <link rel="stylesheet" href="style.css">
11
</head>
12
+
13
<body>
10
- <main class="shell">
11
- <header>
12
- <p class="eyebrow">Portal Demo</p>
13
- <h1>Relay-backed demo application</h1>
14
- <p>Use this page to verify static assets, cookies, fetches, and websocket echo.</p>
15
- </header>
16
-
17
- <section class="card">
18
- <h2>HTTP</h2>
19
- <button id="ping">Fetch /api/ping</button>
20
- <button id="cookies">Set cookies</button>
21
- <pre id="output">Ready.</pre>
22
- </section>
23
-
24
- <section class="card">
25
- <h2>WebSocket</h2>
26
- <input id="ws-input" value="hello portal">
27
- <button id="ws-send">Send</button>
28
- <pre id="ws-output">Disconnected.</pre>
29
- </section>
30
- </main>
31
-
32
- <script>
33
- const output = document.getElementById('output');
34
- const wsOutput = document.getElementById('ws-output');
35
-
36
- document.getElementById('ping').addEventListener('click', async () => {
37
- const res = await fetch('/api/ping');
38
- output.textContent = JSON.stringify(await res.json(), null, 2);
39
- });
40
-
41
- document.getElementById('cookies').addEventListener('click', async () => {
42
- const res = await fetch('/api/test-cookies');
43
- output.textContent = JSON.stringify(await res.json(), null, 2);
44
- });
45
-
46
- const proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
47
- const ws = new WebSocket(proto + location.host + '/ws');
48
- ws.onopen = () => { wsOutput.textContent = 'Connected.'; };
49
- ws.onmessage = (event) => { wsOutput.textContent = event.data; };
50
- ws.onclose = () => { wsOutput.textContent = 'Disconnected.'; };
51
-
52
- document.getElementById('ws-send').addEventListener('click', () => {
53
- ws.send(document.getElementById('ws-input').value);
54
- });
55
- </script>
14
+ <div class="container">
15
+ <h1>Portal Demo Connectivity</h1>
16
+
17
+ <div class="toolbar">
18
+ <button id="httpPingBtn">HTTP Ping</button>
19
+ <button id="testCookiesBtn">Test Cookies</button>
20
+ <button id="wsConnectBtn">WS Connect</button>
21
+ <button id="wsSendBtn" disabled>WS Send "hello"</button>
22
+ </div>
23
+
24
+ <div class="canvas-wrapper">
25
+ <pre id="log" class="log"></pre>
26
+ </div>
27
+
28
+ <div id="status" class="status">Idle</div>
29
+ </div>
30
+
31
+ <script>
32
+ const statusEl = document.getElementById('status');
33
+ const logEl = document.getElementById('log');
34
+ const httpPingBtn = document.getElementById('httpPingBtn');
35
+ const wsConnectBtn = document.getElementById('wsConnectBtn');
36
+ const wsSendBtn = document.getElementById('wsSendBtn');
37
+
38
+ let ws = null;
39
+
40
+ function log(message) {
41
+ const time = new Date().toISOString();
42
+ logEl.textContent += `[${time}] ${message}\n`;
43
+ logEl.scrollTop = logEl.scrollHeight;
44
+ }
45
+
46
+ httpPingBtn.addEventListener('click', async () => {
47
+ statusEl.textContent = 'HTTP: Pinging...';
48
+ try {
49
+ const res = await fetch('/api/ping');
50
+ const json = await res.json();
51
+ statusEl.textContent = 'HTTP: OK';
52
+ log(`HTTP /api/ping -> ${JSON.stringify(json)}`);
53
+ } catch (err) {
54
+ statusEl.textContent = 'HTTP: Error';
55
+ log(`HTTP error: ${err}`);
56
+ }
57
+ });
58
+
59
+ document.getElementById('testCookiesBtn').addEventListener('click', async () => {
60
+ statusEl.textContent = 'Cookies: Testing...';
61
+ try {
62
+ const res = await fetch('/api/test-cookies');
63
+ const json = await res.json();
64
+ statusEl.textContent = 'Cookies: OK';
65
+ log(`HTTP /api/test-cookies -> ${JSON.stringify(json)}`);
66
+
67
+ // Log response headers
68
+ log(`Response headers:`);
69
+ for (const [key, value] of res.headers.entries()) {
70
+ log(` ${key}: ${value}`);
71
+ }
72
+
73
+ // Log current cookies
74
+ log(`Current document.cookie: ${document.cookie || '(empty)'}`);
75
+ } catch (err) {
76
+ statusEl.textContent = 'Cookies: Error';
77
+ log(`HTTP error: ${err}`);
78
+ }
79
+ });
80
+
81
+ wsConnectBtn.addEventListener('click', () => {
82
+ if (ws && ws.readyState === WebSocket.OPEN) {
83
+ ws.close();
84
+ return;
85
+ }
86
+
87
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
88
+ const basePath = location.pathname.endsWith('/') ? location.pathname : (location.pathname + '/');
89
+ const url = protocol + '//' + window.location.host + basePath + 'ws';
90
+
91
+ statusEl.textContent = 'WS: Connecting...';
92
+ log(`WS connecting to ${url}`);
93
+
94
+ ws = new WebSocket(url);
95
+
96
+ ws.onopen = () => {
97
+ statusEl.textContent = 'WS: Connected';
98
+ log('WS connected');
99
+ wsSendBtn.disabled = false;
100
+ wsConnectBtn.textContent = 'WS Disconnect';
101
+ };
102
+
103
+ ws.onclose = () => {
104
+ statusEl.textContent = 'WS: Disconnected';
105
+ log('WS disconnected');
106
+ wsSendBtn.disabled = true;
107
+ wsConnectBtn.textContent = 'WS Connect';
108
+ };
109
+
110
+ ws.onerror = (err) => {
111
+ log(`WS error: ${err.message || err}`);
112
+ };
113
+
114
+ ws.onmessage = (event) => {
115
+ log(`WS recv: ${event.data}`);
116
+ };
117
+ });
118
+
119
+ wsSendBtn.addEventListener('click', () => {
120
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
121
+ log('WS send skipped: not connected');
122
+ return;
123
+ }
124
+ const msg = 'hello';
125
+ ws.send(msg);
126
+ log(`WS send: ${msg}`);
127
+ });
128
+ </script>
129
</body>
57
-</html>
130
+
131
+</html>
\ No newline at end of file
cmd/demo-app/static/style.css
+70
-31
@@ -1,46 +1,85 @@
1
-:root {
2
- color-scheme: light;
3
- font-family: "Georgia", serif;
4
- background: #f6f1e8;
5
- color: #1f1a17;
1
+* {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
}
6
7
body {
9
- margin: 0;
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9
+ background: #f5f5f5;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ padding: 16px;
15
}
16
12
-.shell {
13
- max-width: 960px;
14
- margin: 0 auto;
15
- padding: 48px 20px 80px;
17
+.container {
18
+ width: 100%;
19
+ max-width: 520px;
20
+ background: #ffffff;
21
+ border-radius: 8px;
22
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
23
+ padding: 16px;
24
+ display: flex;
25
+ flex-direction: column;
26
}
27
18
-.eyebrow {
19
- text-transform: uppercase;
20
- letter-spacing: 0.18em;
21
- font-size: 12px;
22
- color: #8f5e3b;
28
+h1 {
29
+ font-size: 18px;
30
+ margin-bottom: 12px;
31
+ color: #333333;
32
+ text-align: center;
33
+}
34
+
35
+.toolbar {
36
+ display: flex;
37
+ flex-wrap: wrap;
38
+ gap: 8px;
39
+ margin-bottom: 12px;
40
+ justify-content: center;
41
}
42
25
-.card {
26
- background: #fff;
27
- border: 1px solid #dccdb8;
28
- padding: 20px;
29
- margin-top: 18px;
30
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.06);
43
+.toolbar button {
44
+ padding: 6px 12px;
45
+ font-size: 13px;
46
+ border-radius: 4px;
47
+ border: 1px solid #d0d7de;
48
+ background: #ffffff;
49
+ cursor: pointer;
50
+ transition: background 0.15s ease, border-color 0.15s ease;
51
}
52
33
-button,
34
-input {
35
- font: inherit;
36
- margin-right: 8px;
37
- margin-bottom: 8px;
38
- padding: 10px 12px;
53
+.toolbar button:hover {
54
+ background: #f3f4f6;
55
+ border-color: #c3ccd6;
56
}
57
41
-pre {
42
- background: #201a17;
43
- color: #f7efe4;
44
- padding: 14px;
58
+.canvas-wrapper {
59
+ border-radius: 4px;
60
+ border: 1px solid #e1e4e8;
61
+ background: #111827;
62
+ padding: 8px;
63
+ height: 220px;
64
overflow: auto;
65
}
66
+
67
+.log {
68
+ width: 100%;
69
+ height: 100%;
70
+ border: none;
71
+ background: transparent;
72
+ color: #e5e7eb;
73
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
74
+ "Liberation Mono", "Courier New", monospace;
75
+ font-size: 12px;
76
+ white-space: pre-wrap;
77
+ word-break: break-all;
78
+}
79
+
80
+.status {
81
+ margin-top: 10px;
82
+ font-size: 13px;
83
+ color: #4b5563;
84
+ text-align: right;
85
+}
\ No newline at end of file
cmd/portal-tunnel/main.go
+33
-25
@@ -6,7 +6,6 @@ import (
6
"flag"
7
"fmt"
8
"io"
9
- "log"
9
"net"
10
"os"
11
"os/signal"
@@ -15,6 +14,9 @@ import (
14
"syscall"
15
"time"
16
17
+ "github.com/rs/zerolog"
18
+ "github.com/rs/zerolog/log"
19
+
20
"gosuda.org/portal/portal"
21
"gosuda.org/portal/sdk"
22
)
@@ -31,6 +33,10 @@ var (
33
)
34
35
func main() {
36
+ zerolog.TimeFieldFormat = time.RFC3339
37
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
38
+ logger := log.With().Str("component", "portal-tunnel").Logger()
39
+
40
defaultRelayURLs := os.Getenv("RELAYS")
41
if defaultRelayURLs == "" {
42
defaultRelayURLs = "https://localhost:4017"
@@ -47,12 +53,14 @@ func main() {
53
flag.Parse()
54
55
if err := runTunnel(); err != nil {
50
- log.Printf("Exited with error: %v", err)
56
+ logger.Error().Err(err).Msg("portal tunnel exited with error")
57
os.Exit(1)
58
}
59
}
60
61
func runTunnel() error {
62
+ logger := log.With().Str("component", "portal-tunnel").Logger()
63
+
64
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
65
defer stop()
66
@@ -65,12 +73,12 @@ func runTunnel() error {
73
return err
74
}
75
68
- log.Printf("Local service is reachable at %s", flagHost)
69
- log.Printf("Starting Portal Tunnel...")
70
- log.Printf(" Local: %s", flagHost)
71
- log.Printf(" Relays: %s", strings.Join(relayURLs, ", "))
76
+ logger.Info().
77
+ Str("local", flagHost).
78
+ Strs("relays", relayURLs).
79
+ Msg("starting portal tunnel")
80
if len(relayURLs) > 1 {
73
- log.Printf(" Note: current runtime uses the first relay URL only: %s", relayURL)
81
+ logger.Warn().Str("selected_relay", relayURL).Msg("multiple relays configured; using first relay only")
82
}
83
84
sdkClient, err := sdk.NewClient(sdk.ClientConfig{RelayURL: relayURL})
@@ -99,14 +107,11 @@ func runTunnel() error {
107
_ = listener.Close()
108
}()
109
102
- log.Printf("")
103
- log.Printf("Access via:")
104
- log.Printf("- Relay: %s", relayURL)
105
- log.Printf("- Lease ID: %s", listener.LeaseID())
106
- for _, publicURL := range listener.PublicURLs() {
107
- log.Printf("- Public: %s", publicURL)
108
- }
109
- log.Printf("")
110
+ logger.Info().
111
+ Str("relay", relayURL).
112
+ Str("lease_id", listener.LeaseID()).
113
+ Strs("public_urls", listener.PublicURLs()).
114
+ Msg("tunnel ready")
115
116
connCount := 0
117
var connWG sync.WaitGroup
@@ -115,7 +120,7 @@ loop:
120
for {
121
select {
122
case <-ctx.Done():
118
- log.Printf("[tunnel] shutting down...")
123
+ logger.Info().Msg("tunnel shutting down")
124
break loop
125
default:
126
}
@@ -123,29 +128,32 @@ loop:
128
relayConn, err := listener.Accept()
129
if err != nil {
130
if errors.Is(err, net.ErrClosed) {
126
- log.Printf("[tunnel] listener closed")
131
+ logger.Info().Msg("tunnel listener closed")
132
break loop
133
}
134
select {
135
case <-ctx.Done():
136
break loop
137
default:
133
- log.Printf("Failed to accept connection: %v", err)
138
+ logger.Error().Err(err).Msg("failed to accept relay connection")
139
continue
140
}
141
}
142
143
connCount++
139
- log.Printf("[#%d] New connection from %s", connCount, relayConn.RemoteAddr())
144
+ logger.Info().
145
+ Int("conn_id", connCount).
146
+ Str("remote_addr", relayConn.RemoteAddr().String()).
147
+ Msg("accepted relay connection")
148
149
connWG.Add(1)
142
- go func(relayConn net.Conn) {
150
+ go func(connID int, relayConn net.Conn) {
151
defer connWG.Done()
152
if err := proxyConnection(ctx, flagHost, relayConn); err != nil {
145
- log.Printf("Proxy error: %v", err)
153
+ logger.Error().Err(err).Int("conn_id", connID).Msg("proxy connection failed")
154
}
147
- log.Printf("Connection closed")
148
- }(relayConn)
155
+ logger.Info().Int("conn_id", connID).Msg("proxy connection closed")
156
+ }(connCount, relayConn)
157
}
158
159
done := make(chan struct{})
@@ -157,10 +165,10 @@ loop:
165
select {
166
case <-done:
167
case <-time.After(5 * time.Second):
160
- log.Printf("[tunnel] shutdown timeout, some connections still active")
168
+ logger.Warn().Msg("tunnel shutdown timeout; connections still active")
169
}
170
163
- log.Printf("[tunnel] shutdown complete")
171
+ logger.Info().Msg("tunnel shutdown complete")
172
return nil
173
}
174
cmd/relay-server/admin.go
+2
-2
@@ -10,10 +10,10 @@ import (
10
)
11
12
type Admin struct {
13
- secret string
14
- trustProxy bool
13
frontend *Frontend
14
server *portal.Server
15
+ secret string
16
+ trustProxy bool
17
}
18
19
func NewAdmin(secret string, trustProxy bool, frontend *Frontend) *Admin {
cmd/relay-server/http_helpers.go
-18
@@ -3,7 +3,6 @@ package main
3
import (
4
"crypto/subtle"
5
"encoding/json"
6
- "html"
6
"mime"
7
"net/http"
8
"strings"
@@ -15,19 +14,6 @@ func writeJSON(w http.ResponseWriter, status int, data any) {
14
_ = json.NewEncoder(w).Encode(data)
15
}
16
18
-func isSecureRequestWithPolicy(r *http.Request, trustProxyHeaders bool) bool {
19
- if r == nil {
20
- return false
21
- }
22
- if r.TLS != nil {
23
- return true
24
- }
25
- if !trustProxyHeaders {
26
- return false
27
- }
28
- return strings.EqualFold(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")), "https")
29
-}
30
-
17
func hasPathPrefix(path, prefix string) bool {
18
return strings.HasPrefix(strings.TrimSpace(path), prefix)
19
}
@@ -43,10 +29,6 @@ func subtleValueMatch(left, right string) bool {
29
return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1
30
}
31
46
-func escapeHTML(value string) string {
47
- return html.EscapeString(value)
48
-}
49
-
32
func getContentType(ext string) string {
33
ext = strings.TrimSpace(ext)
34
if ext == "" {
cmd/relay-server/lease_rows.go
+9
-9
@@ -10,15 +10,15 @@ import (
10
)
11
12
type leaseRow struct {
13
- TTL string `json:"ttl"`
14
- Metadata string `json:"metadata"`
15
- Kind string `json:"kind"`
16
- DNS string `json:"dns"`
17
- Name string `json:"name"`
18
- Peer string `json:"peer"`
19
- Link string `json:"link"`
20
- Hide bool `json:"hide"`
21
- Connected bool `json:"connected"`
13
+ TTL string
14
+ Metadata string
15
+ Kind string
16
+ DNS string
17
+ Name string
18
+ Peer string
19
+ Link string
20
+ Hide bool
21
+ Connected bool
22
}
23
24
func convertLeaseEntriesToRows(serv *portal.Server, includeHidden bool, portalURL string) []leaseRow {
cmd/relay-server/main.go
+13
-4
@@ -3,9 +3,12 @@ package main
3
import (
4
"flag"
5
"fmt"
6
- "log"
6
"os"
7
"strings"
8
+ "time"
9
+
10
+ "github.com/rs/zerolog"
11
+ "github.com/rs/zerolog/log"
12
)
13
14
const (
@@ -28,6 +31,10 @@ type relayServerConfig struct {
31
}
32
33
func main() {
34
+ zerolog.TimeFieldFormat = time.RFC3339
35
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
36
+ logger := log.With().Str("component", "relay-server").Logger()
37
+
38
cfg := relayServerConfig{}
39
40
portalURL := strings.TrimSuffix(trimmedEnv("PORTAL_URL"), "/")
@@ -67,11 +74,13 @@ func main() {
74
cfg.PortalURL = cfg.Bootstraps[0]
75
}
76
70
- log.Printf("[server] portal base url %s", cfg.PortalURL)
71
- log.Printf("[server] bootstraps %s", strings.Join(cfg.Bootstraps, ", "))
77
+ logger.Info().
78
+ Str("portal_url", cfg.PortalURL).
79
+ Strs("bootstraps", cfg.Bootstraps).
80
+ Msg("configured relay server")
81
82
if err := runServer(cfg); err != nil {
74
- log.Fatalf("execute root command: %v", err)
83
+ logger.Fatal().Err(err).Msg("execute root command")
84
}
85
}
86
cmd/relay-server/serve.go
+12
-6
@@ -3,7 +3,6 @@ package main
3
import (
4
"context"
5
"fmt"
6
- "log"
6
"net"
7
"net/http"
8
"os"
@@ -11,11 +10,15 @@ import (
10
"strings"
11
"syscall"
12
13
+ "github.com/rs/zerolog/log"
14
+
15
"gosuda.org/portal/portal"
16
"gosuda.org/portal/portal/acme"
17
)
18
19
func runServer(cfg relayServerConfig) error {
20
+ logger := log.With().Str("component", "relay-server").Logger()
21
+
22
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
23
defer stop()
24
@@ -68,9 +71,12 @@ func runServer(cfg relayServerConfig) error {
71
acmeManager.Start(ctx)
72
defer acmeManager.Stop()
73
71
- log.Printf("[server] https api enabled via ACME/self-signed on %s", loopbackAddr(server.APIAddr()))
72
- log.Printf("[server] sni router listening on %s", server.SNIAddr())
73
- log.Printf("[server] root host %s", rootHost)
74
+ logger.Info().
75
+ Str("api_addr", loopbackAddr(server.APIAddr())).
76
+ Str("sni_addr", server.SNIAddr()).
77
+ Str("root_host", rootHost).
78
+ Bool("acme_enabled", !strings.HasSuffix(rootHost, "localhost") && rootHost != "127.0.0.1" && rootHost != "::1").
79
+ Msg("relay server started")
80
81
return server.Wait()
82
}
@@ -134,11 +140,11 @@ func loopbackAddr(addr string) string {
140
141
func mustRead(path string) []byte {
142
if path == "" {
137
- log.Fatal("missing required PEM path")
143
+ log.Fatal().Msg("missing required PEM path")
144
}
145
data, err := os.ReadFile(path)
146
if err != nil {
141
- log.Fatalf("read %s: %v", path, err)
147
+ log.Fatal().Err(err).Str("path", path).Msg("read pem file")
148
}
149
return data
150
}
go.mod
+3
@@ -5,6 +5,7 @@ go 1.26.0
5
require (
6
github.com/go-acme/lego/v4 v4.32.0
7
github.com/gosuda/keyless_tls v0.0.1-0.20260304212324-7733f8366abc
8
+ github.com/rs/zerolog v1.34.0
9
golang.org/x/net v0.50.0
10
golang.org/x/sync v0.19.0
11
)
@@ -12,6 +13,8 @@ require (
13
require (
14
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
15
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
16
+ github.com/mattn/go-colorable v0.1.13 // indirect
17
+ github.com/mattn/go-isatty v0.0.20 // indirect
18
github.com/miekg/dns v1.1.72 // indirect
19
golang.org/x/crypto v0.48.0 // indirect
20
golang.org/x/mod v0.32.0 // indirect
go.sum
+15
@@ -1,19 +1,31 @@
1
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
2
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
3
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
5
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
7
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
8
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
9
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
10
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
11
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
12
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
13
github.com/gosuda/keyless_tls v0.0.1-0.20260304212324-7733f8366abc h1:aS9LQ35x6EtrGKCmOWRj6Y9aQ2l5hP8dVva4oxB9VEg=
14
github.com/gosuda/keyless_tls v0.0.1-0.20260304212324-7733f8366abc/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
15
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
16
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
17
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
18
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
19
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
20
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
21
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
22
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
23
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
24
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
25
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
27
+github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
28
+github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
29
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
30
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
31
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -24,6 +36,9 @@ golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
36
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
37
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
38
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
39
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
43
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
44
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
portal/acme/acme.go
+9
-6
@@ -41,13 +41,12 @@ type Config struct {
41
}
42
43
type Manager struct {
44
- cfg Config
45
-
44
+ stopCh chan struct{}
45
+ cfg Config
46
+ wg sync.WaitGroup
47
mu sync.RWMutex
48
startOnce sync.Once
49
stopOnce sync.Once
49
- stopCh chan struct{}
50
- wg sync.WaitGroup
50
}
51
52
type provisionConfig struct {
@@ -262,18 +261,22 @@ func certCoversDomains(certFile string, domains []string) (bool, error) {
261
}
262
for _, domain := range domains {
263
if wildcardDomain, ok := strings.CutPrefix(domain, "*."); ok {
265
- if err := cert.VerifyHostname("probe." + wildcardDomain); err != nil {
264
+ if !certificateCoversHostname(cert, "probe."+wildcardDomain) {
265
return false, nil
266
}
267
continue
268
}
270
- if err := cert.VerifyHostname(domain); err != nil {
269
+ if !certificateCoversHostname(cert, domain) {
270
return false, nil
271
}
272
}
273
return true, nil
274
}
275
276
+func certificateCoversHostname(cert *x509.Certificate, hostname string) bool {
277
+ return cert != nil && cert.VerifyHostname(hostname) == nil
278
+}
279
+
280
func newClient(cfg provisionConfig) (*lego.Client, *acmeUser, error) {
281
accountKey, err := loadOrCreateAccountKey(cfg.AccountKeyFile)
282
if err != nil {
portal/acme/dnsrecord.go
+2
-2
@@ -51,8 +51,8 @@ type cfRecordsResult struct {
51
}
52
53
type cfRecordResult struct {
54
- Errors []cfError `json:"errors"`
54
Result cfDNSRecord `json:"result"`
55
+ Errors []cfError `json:"errors"`
56
Success bool `json:"success"`
57
}
58
@@ -113,7 +113,7 @@ func detectPublicIP(ctx context.Context) (string, error) {
113
114
func findZoneID(ctx context.Context, token, domain string) (string, error) {
115
parts := strings.Split(domain, ".")
116
- for i := 0; i < len(parts)-1; i++ {
116
+ for i := range len(parts) - 1 {
117
candidate := strings.Join(parts[i:], ".")
118
zones, err := cfListZones(ctx, token, candidate)
119
if err != nil {
portal/api.go
+7
-7
@@ -13,9 +13,9 @@ const (
13
)
14
15
type APIEnvelope struct {
16
- OK bool `json:"ok"`
16
Data any `json:"data,omitempty"`
17
Error *APIError `json:"error,omitempty"`
18
+ OK bool `json:"ok"`
19
}
20
21
type APIError struct {
@@ -25,27 +25,27 @@ type APIError struct {
25
26
type LeaseMetadata struct {
27
Description string `json:"description,omitempty"`
28
- Tags []string `json:"tags,omitempty"`
28
Owner string `json:"owner,omitempty"`
29
Thumbnail string `json:"thumbnail,omitempty"`
30
+ Tags []string `json:"tags,omitempty"`
31
Hide bool `json:"hide,omitempty"`
32
}
33
34
type RegisterRequest struct {
35
Name string `json:"name"`
36
+ ReverseToken string `json:"reverse_token"`
37
Hostnames []string `json:"hostnames,omitempty"`
38
Metadata LeaseMetadata `json:"metadata,omitempty"`
38
- ReverseToken string `json:"reverse_token"`
39
- TLS bool `json:"tls"`
39
TTLSeconds int `json:"ttl_seconds,omitempty"`
40
+ TLS bool `json:"tls"`
41
}
42
43
type RegisterResponse struct {
44
+ ExpiresAt time.Time `json:"expires_at"`
45
LeaseID string `json:"lease_id"`
46
+ ConnectURL string `json:"connect_url"`
47
Hostnames []string `json:"hostnames"`
48
Metadata LeaseMetadata `json:"metadata,omitempty"`
47
- ExpiresAt time.Time `json:"expires_at"`
48
- ConnectURL string `json:"connect_url"`
49
}
50
51
type RenewRequest struct {
@@ -55,8 +55,8 @@ type RenewRequest struct {
55
}
56
57
type RenewResponse struct {
58
- LeaseID string `json:"lease_id"`
58
ExpiresAt time.Time `json:"expires_at"`
59
+ LeaseID string `json:"lease_id"`
60
}
61
62
type UnregisterRequest struct {
portal/broker.go
+9
-11
@@ -24,14 +24,13 @@ const (
24
)
25
26
type leaseBroker struct {
27
+ notify chan struct{}
28
leaseID string
29
+ ready []*reverseSession
30
idleInterval time.Duration
31
readyLimit int
30
-
31
- mu sync.Mutex
32
- ready []*reverseSession
33
- state brokerState
34
- notify chan struct{}
32
+ state brokerState
33
+ mu sync.Mutex
34
}
35
36
func newLeaseBroker(leaseID string, idleInterval time.Duration, readyLimit int) *leaseBroker {
@@ -171,15 +170,14 @@ const (
170
)
171
172
type reverseSession struct {
174
- conn net.Conn
175
- idleInterval time.Duration
176
-
177
- mu sync.Mutex
178
- state reverseSessionState
173
+ conn net.Conn
174
keepaliveStop chan struct{}
175
keepaliveDone chan struct{}
176
done chan struct{}
177
+ idleInterval time.Duration
178
+ state reverseSessionState
179
closeOnce sync.Once
180
+ mu sync.Mutex
181
}
182
183
func newReverseSession(conn net.Conn, idleInterval time.Duration) *reverseSession {
@@ -254,7 +252,7 @@ func (s *reverseSession) Activate() error {
252
_, err := s.conn.Write([]byte{MarkerTLSStart})
253
_ = s.conn.SetWriteDeadline(time.Time{})
254
if err != nil {
257
- go s.Close()
255
+ _ = s.Close()
256
}
257
return err
258
}
portal/helpers.go
+1
-17
@@ -16,7 +16,7 @@ const (
16
defaultIdleKeepalive = 15 * time.Second
17
defaultReadyQueueLimit = 8
18
defaultClientHelloWait = 2 * time.Second
19
- defaultControlBodyLimit = 32 << 10
19
+ defaultControlBodyLimit = 4 << 20
20
defaultSessionWriteLimit = 5 * time.Second
21
)
22
@@ -86,18 +86,6 @@ func suggestHostname(name, rootHost string) string {
86
return label + "." + rootHost
87
}
88
89
-func hostnameMatchesWildcard(pattern, host string) bool {
90
- if !strings.HasPrefix(pattern, "*.") {
91
- return false
92
- }
93
- suffix := strings.TrimPrefix(pattern, "*.")
94
- parts := strings.Split(host, ".")
95
- if len(parts) < 2 {
96
- return false
97
- }
98
- return normalizeHostname("*."+strings.Join(parts[1:], ".")) == normalizeHostname(pattern) && strings.Count(suffix, ".")+1 == len(parts)-1
99
-}
100
-
89
func randomID(prefix string) string {
90
buf := make([]byte, 8)
91
if _, err := rand.Read(buf); err != nil {
@@ -106,10 +94,6 @@ func randomID(prefix string) string {
94
return prefix + hex.EncodeToString(buf)
95
}
96
109
-func randomToken() string {
110
- return randomID("tok_")
111
-}
112
-
97
func durationOrDefault(v, fallback time.Duration) time.Duration {
98
if v > 0 {
99
return v
portal/routing.go
+1
-1
@@ -6,8 +6,8 @@ import (
6
)
7
8
type routeTable struct {
9
- mu sync.RWMutex
9
exact map[string]string
10
+ mu sync.RWMutex
11
}
12
13
func newRouteTable() *routeTable {
portal/server.go
+59
-32
@@ -14,61 +14,58 @@ import (
14
"sync"
15
"time"
16
17
- "github.com/gosuda/keyless_tls/relay/l4"
17
"golang.org/x/sync/errgroup"
18
+
19
+ "github.com/gosuda/keyless_tls/relay/l4"
20
)
21
22
type ServerConfig struct {
23
+ APIHandlerWrapper func(http.Handler) http.Handler
24
PortalURL string
25
APIListenAddr string
26
SNIListenAddr string
27
RootHost string
28
RootFallbackAddr string
29
+ APITLS TLSMaterialConfig
30
LeaseTTL time.Duration
31
ClaimTimeout time.Duration
32
IdleKeepaliveInterval time.Duration
33
ReadyQueueLimit int
34
ClientHelloTimeout time.Duration
32
- APITLS TLSMaterialConfig
33
- APIHandlerWrapper func(http.Handler) http.Handler
35
}
36
37
type Server struct {
37
- cfg ServerConfig
38
-
39
- apiServer *http.Server
40
- apiListener net.Listener
41
- sniListener net.Listener
42
- apiTLSClose io.Closer
43
-
44
- ctx context.Context
45
- cancel context.CancelFunc
46
- group *errgroup.Group
47
-
48
- routes *routeTable
49
-
50
- mu sync.RWMutex
51
- leases map[string]*leaseRecord
52
-
38
+ sniListener net.Listener
39
+ apiTLSClose io.Closer
40
+ apiListener net.Listener
41
+ apiServer *http.Server
42
+ ctxDone <-chan struct{}
43
+ baseContext func() context.Context
44
+ cancel context.CancelFunc
45
+ group *errgroup.Group
46
+ routes *routeTable
47
+ leases map[string]*leaseRecord
48
+ cfg ServerConfig
49
+ mu sync.RWMutex
50
shutdownOnce sync.Once
51
}
52
53
type leaseRecord struct {
54
+ ExpiresAt time.Time
55
+ Broker *leaseBroker
56
ID string
57
Name string
58
+ ReverseToken string
59
Hostnames []string
60
Metadata LeaseMetadata
61
- ReverseToken string
62
- ExpiresAt time.Time
63
- Broker *leaseBroker
61
}
62
63
type LeaseSnapshot struct {
64
+ ExpiresAt time.Time
65
ID string
66
Name string
67
Hostnames []string
68
Metadata LeaseMetadata
71
- ExpiresAt time.Time
69
Ready int
70
}
71
@@ -109,17 +106,21 @@ func (s *Server) Start(ctx context.Context) error {
106
return errors.New("server already started")
107
}
108
112
- apiListener, err := net.Listen("tcp", s.cfg.APIListenAddr)
109
+ serverCtx, cancel := context.WithCancel(ctx)
110
+ var listenConfig net.ListenConfig
111
+
112
+ apiListener, err := listenConfig.Listen(serverCtx, "tcp", s.cfg.APIListenAddr)
113
if err != nil {
114
+ cancel()
115
return fmt.Errorf("listen api: %w", err)
116
}
116
- sniListener, err := net.Listen("tcp", s.cfg.SNIListenAddr)
117
+ sniListener, err := listenConfig.Listen(serverCtx, "tcp", s.cfg.SNIListenAddr)
118
if err != nil {
119
_ = apiListener.Close()
120
+ cancel()
121
return fmt.Errorf("listen sni: %w", err)
122
}
123
122
- serverCtx, cancel := context.WithCancel(ctx)
124
group, groupCtx := errgroup.WithContext(serverCtx)
125
126
apiServer := &http.Server{
@@ -139,7 +140,8 @@ func (s *Server) Start(ctx context.Context) error {
140
s.sniListener = sniListener
141
s.apiServer = apiServer
142
s.apiTLSClose = apiCloser
142
- s.ctx = groupCtx
143
+ s.baseContext = func() context.Context { return groupCtx }
144
+ s.ctxDone = groupCtx.Done()
145
s.cancel = cancel
146
s.group = group
147
@@ -527,7 +529,7 @@ func (s *Server) runSNIListener() error {
529
for {
530
conn, err := s.sniListener.Accept()
531
if err != nil {
530
- if s.ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
532
+ if errors.Is(err, net.ErrClosed) || s.isClosed() {
533
return nil
534
}
535
return err
@@ -568,7 +570,7 @@ func (s *Server) handleSNIConn(conn net.Conn) {
570
return
571
}
572
571
- claimCtx, cancel := context.WithTimeout(s.ctx, s.cfg.ClaimTimeout)
573
+ claimCtx, cancel := context.WithTimeout(s.context(), s.cfg.ClaimTimeout)
574
defer cancel()
575
576
session, err := record.Broker.Claim(claimCtx)
@@ -581,7 +583,8 @@ func (s *Server) handleSNIConn(conn net.Conn) {
583
}
584
585
func (s *Server) bridgeToFallback(conn net.Conn) {
584
- upstream, err := net.DialTimeout("tcp", hostPortOrLoopback(s.cfg.RootFallbackAddr), 5*time.Second)
586
+ dialer := &net.Dialer{Timeout: 5 * time.Second}
587
+ upstream, err := dialer.DialContext(s.context(), "tcp", hostPortOrLoopback(s.cfg.RootFallbackAddr))
588
if err != nil {
589
_ = conn.Close()
590
return
@@ -595,7 +598,7 @@ func (s *Server) runLeaseJanitor() error {
598
599
for {
600
select {
598
- case <-s.ctx.Done():
601
+ case <-s.ctxDone:
602
return nil
603
case <-ticker.C:
604
s.cleanupExpiredLeases()
@@ -623,7 +626,10 @@ func (s *Server) cleanupExpiredLeases() {
626
}
627
628
func (s *Server) watchContext() error {
626
- <-s.ctx.Done()
629
+ if s.ctxDone == nil {
630
+ return nil
631
+ }
632
+ <-s.ctxDone
633
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
634
defer cancel()
635
return s.Shutdown(shutdownCtx)
@@ -706,3 +712,24 @@ func closeWrite(conn net.Conn) {
712
_ = cw.CloseWrite()
713
}
714
}
715
+
716
+func (s *Server) context() context.Context {
717
+ if s.baseContext != nil {
718
+ if ctx := s.baseContext(); ctx != nil {
719
+ return ctx
720
+ }
721
+ }
722
+ return context.Background()
723
+}
724
+
725
+func (s *Server) isClosed() bool {
726
+ if s.ctxDone == nil {
727
+ return false
728
+ }
729
+ select {
730
+ case <-s.ctxDone:
731
+ return true
732
+ default:
733
+ return false
734
+ }
735
+}
portal/tls.go
+3
-2
@@ -2,6 +2,7 @@ package portal
2
3
import (
4
"crypto/tls"
5
+ "errors"
6
"fmt"
7
"io"
8
"net/http"
@@ -10,9 +11,9 @@ import (
11
)
12
13
type TLSMaterialConfig struct {
14
+ Keyless *RemoteSignerConfig
15
CertPEM []byte
16
KeyPEM []byte
15
- Keyless *RemoteSignerConfig
17
}
18
19
type RemoteSignerConfig struct {
@@ -26,7 +27,7 @@ type RemoteSignerConfig struct {
27
28
func attachAPITLS(server *http.Server, cfg TLSMaterialConfig) (io.Closer, error) {
29
if server == nil {
29
- return nil, fmt.Errorf("http server is required")
30
+ return nil, errors.New("http server is required")
31
}
32
if cfg.Keyless != nil {
33
remoteSigner, err := keylesslib.AttachToHTTPServer(server, keylesslib.HTTPServerAttachConfig{
sdk/client.go
+9
-7
@@ -9,6 +9,7 @@ import (
9
"crypto/x509"
10
"encoding/hex"
11
"encoding/json"
12
+ "errors"
13
"fmt"
14
"io"
15
"net"
@@ -104,7 +105,7 @@ func (c *Client) Close() {
105
106
func (c *Client) Listen(ctx context.Context, req ListenRequest) (*Listener, error) {
107
if strings.TrimSpace(req.Name) == "" {
107
- return nil, fmt.Errorf("listener name is required")
108
+ return nil, errors.New("listener name is required")
109
}
110
111
reverseToken := strings.TrimSpace(req.ReverseToken)
@@ -151,7 +152,8 @@ func (c *Client) Listen(ctx context.Context, req ListenRequest) (*Listener, erro
152
listenerCtx, cancel := context.WithCancel(ctx)
153
l := &Listener{
154
client: c,
154
- ctx: listenerCtx,
155
+ baseContext: func() context.Context { return listenerCtx },
156
+ ctxDone: listenerCtx.Done(),
157
cancel: cancel,
158
leaseID: registerResp.LeaseID,
159
hostnames: append([]string(nil), registerResp.Hostnames...),
@@ -253,9 +255,9 @@ func (c *Client) openReverseSession(ctx context.Context, leaseID, reverseToken s
255
req.Header.Set(portal.HeaderReverseToken, reverseToken)
256
req.Header.Set("Connection", "keep-alive")
257
256
- if err := req.Write(conn); err != nil {
258
+ if writeErr := req.Write(conn); writeErr != nil {
259
_ = conn.Close()
258
- return nil, err
260
+ return nil, writeErr
261
}
262
263
reader := bufio.NewReader(conn)
@@ -281,9 +283,9 @@ func (c *Client) resolve(path string) string {
283
}
284
285
type apiEnvelope struct {
284
- OK bool `json:"ok"`
285
- Data json.RawMessage `json:"data"`
286
Error *portal.APIError `json:"error"`
287
+ Data json.RawMessage `json:"data"`
288
+ OK bool `json:"ok"`
289
}
290
291
func buildRootCAs(rootCAPEM []byte) (*x509.CertPool, error) {
@@ -292,7 +294,7 @@ func buildRootCAs(rootCAPEM []byte) (*x509.CertPool, error) {
294
}
295
pool := x509.NewCertPool()
296
if !pool.AppendCertsFromPEM(rootCAPEM) {
295
- return nil, fmt.Errorf("failed to parse relay root ca")
297
+ return nil, errors.New("failed to parse relay root ca")
298
}
299
return pool, nil
300
}
sdk/listener.go
+50
-29
@@ -17,38 +17,37 @@ type LeaseMetadata = portal.LeaseMetadata
17
18
type ListenRequest struct {
19
Name string
20
+ ReverseToken string
21
+ TLS portal.TLSMaterialConfig
22
Hostnames []string
23
Metadata LeaseMetadata
22
- ReverseToken string
24
ReadyTarget int
25
LeaseTTL time.Duration
25
- TLS portal.TLSMaterialConfig
26
}
27
28
type Listener struct {
29
- client *Client
30
- ctx context.Context
31
- cancel context.CancelFunc
32
- leaseID string
33
- hostnames []string
34
- metadata LeaseMetadata
35
- reverseToken string
36
- leaseTTL time.Duration
37
- readyTarget int
38
- tlsConfig *tls.Config
39
- tlsCloser io.Closer
40
-
41
- accepted chan net.Conn
42
- signal chan struct{}
43
-
44
- mu sync.Mutex
29
+ tlsCloser io.Closer
30
+ tlsConfig *tls.Config
31
+ baseContext func() context.Context
32
+ ctxDone <-chan struct{}
33
+ cancel context.CancelFunc
34
+ client *Client
35
+ signal chan struct{}
36
+ accepted chan net.Conn
37
+ leaseID string
38
+ reverseToken string
39
+ hostnames []string
40
+ metadata LeaseMetadata
41
+ readyTarget int
42
+ leaseTTL time.Duration
43
activeSessions int
44
closeOnce sync.Once
45
+ mu sync.Mutex
46
}
47
48
func (l *Listener) Accept() (net.Conn, error) {
49
select {
51
- case <-l.ctx.Done():
50
+ case <-l.ctxDone:
51
return nil, net.ErrClosed
52
case conn := <-l.accepted:
53
if conn == nil {
@@ -102,7 +101,7 @@ func (l *Listener) PublicURLs() []string {
101
func (l *Listener) runSupervisor() {
102
for {
103
select {
105
- case <-l.ctx.Done():
104
+ case <-l.ctxDone:
105
return
106
case <-l.signal:
107
}
@@ -130,10 +129,10 @@ func (l *Listener) runRenewLoop() {
129
130
for {
131
select {
133
- case <-l.ctx.Done():
132
+ case <-l.ctxDone:
133
return
134
case <-ticker.C:
136
- ctx, cancel := context.WithTimeout(l.ctx, 10*time.Second)
135
+ ctx, cancel := context.WithTimeout(l.context(), 10*time.Second)
136
_ = l.client.renewLease(ctx, l.leaseID, l.reverseToken, l.leaseTTL)
137
cancel()
138
}
@@ -143,16 +142,17 @@ func (l *Listener) runRenewLoop() {
142
func (l *Listener) runSession() {
143
defer l.releaseSessionSlot()
144
146
- conn, err := l.client.openReverseSession(l.ctx, l.leaseID, l.reverseToken)
145
+ sessionCtx := l.context()
146
+ conn, err := l.client.openReverseSession(sessionCtx, l.leaseID, l.reverseToken)
147
if err != nil {
148
- sleepOrDone(l.ctx, time.Second)
148
+ sleepOrDone(sessionCtx, time.Second)
149
return
150
}
151
152
if err := l.awaitActivation(conn); err != nil {
153
_ = conn.Close()
154
if !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
155
- sleepOrDone(l.ctx, time.Second)
155
+ sleepOrDone(sessionCtx, time.Second)
156
}
157
}
158
}
@@ -179,16 +179,16 @@ func (l *Listener) awaitActivation(conn net.Conn) error {
179
180
func (l *Listener) activate(conn net.Conn) error {
181
tlsConn := tls.Server(conn, l.tlsConfig.Clone())
182
- handshakeCtx, cancel := context.WithTimeout(l.ctx, l.client.handshakeTimeout)
182
+ handshakeCtx, cancel := context.WithTimeout(l.context(), l.client.handshakeTimeout)
183
defer cancel()
184
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
185
return err
186
}
187
188
select {
189
- case <-l.ctx.Done():
189
+ case <-l.ctxDone:
190
_ = tlsConn.Close()
191
- return l.ctx.Err()
191
+ return l.context().Err()
192
case l.accepted <- tlsConn:
193
return nil
194
}
@@ -197,7 +197,7 @@ func (l *Listener) activate(conn net.Conn) error {
197
func (l *Listener) reserveSessionSlot() bool {
198
l.mu.Lock()
199
defer l.mu.Unlock()
200
- if l.ctx.Err() != nil {
200
+ if l.isClosed() {
201
return false
202
}
203
if l.activeSessions >= l.readyTarget {
@@ -234,3 +234,24 @@ type listenerAddr string
234
235
func (a listenerAddr) Network() string { return "portal" }
236
func (a listenerAddr) String() string { return string(a) }
237
+
238
+func (l *Listener) context() context.Context {
239
+ if l.baseContext != nil {
240
+ if ctx := l.baseContext(); ctx != nil {
241
+ return ctx
242
+ }
243
+ }
244
+ return context.Background()
245
+}
246
+
247
+func (l *Listener) isClosed() bool {
248
+ if l.ctxDone == nil {
249
+ return false
250
+ }
251
+ select {
252
+ case <-l.ctxDone:
253
+ return true
254
+ default:
255
+ return false
256
+ }
257
+}
sdk/listener_test.go
+10
-8
@@ -47,8 +47,9 @@ func TestListenerEndToEndTLSHTTP(t *testing.T) {
47
48
ctx, cancel := context.WithCancel(context.Background())
49
defer cancel()
50
- if err := relay.Start(ctx); err != nil {
51
- t.Fatalf("Start() error = %v", err)
50
+ startErr := relay.Start(ctx)
51
+ if startErr != nil {
52
+ t.Fatalf("Start() error = %v", startErr)
53
}
54
t.Cleanup(func() {
55
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -121,7 +122,7 @@ func TestListenerEndToEndTLSHTTP(t *testing.T) {
122
_ = server.Shutdown(shutdownCtx)
123
select {
124
case err := <-httpDone:
124
- if err != nil && err != http.ErrServerClosed && !errors.Is(err, net.ErrClosed) {
125
+ if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) {
126
t.Fatalf("server.Serve() error = %v", err)
127
}
128
case <-time.After(2 * time.Second):
@@ -170,8 +171,9 @@ func TestListenerEndToEndTLSHTTP_AutoSelfSigned(t *testing.T) {
171
172
ctx, cancel := context.WithCancel(context.Background())
173
defer cancel()
173
- if err := relay.Start(ctx); err != nil {
174
- t.Fatalf("Start() error = %v", err)
174
+ startErr := relay.Start(ctx)
175
+ if startErr != nil {
176
+ t.Fatalf("Start() error = %v", startErr)
177
}
178
t.Cleanup(func() {
179
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -215,7 +217,7 @@ func TestListenerEndToEndTLSHTTP_AutoSelfSigned(t *testing.T) {
217
_ = server.Shutdown(shutdownCtx)
218
select {
219
case err := <-httpDone:
218
- if err != nil && err != http.ErrServerClosed && !errors.Is(err, net.ErrClosed) {
220
+ if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) {
221
t.Fatalf("server.Serve() error = %v", err)
222
}
223
case <-time.After(2 * time.Second):
@@ -249,8 +251,8 @@ func doTenantRequest(addr, host, path string) (string, error) {
251
}
252
defer conn.Close()
253
252
- if _, err := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path, host); err != nil {
253
- return "", err
254
+ if _, writeErr := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path, host); writeErr != nil {
255
+ return "", writeErr
256
}
257
258
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodGet})
sdk/tls.go
+2
-1
@@ -8,6 +8,7 @@ import (
8
"crypto/x509"
9
"crypto/x509/pkix"
10
"encoding/pem"
11
+ "errors"
12
"fmt"
13
"io"
14
"math/big"
@@ -22,7 +23,7 @@ import (
23
24
func buildTenantTLSConfig(cfg portal.TLSMaterialConfig) (*tls.Config, io.Closer, error) {
25
if len(cfg.CertPEM) == 0 {
25
- return nil, nil, fmt.Errorf("tenant certificate is required")
26
+ return nil, nil, errors.New("tenant certificate is required")
27
}
28
if cfg.Keyless != nil {
29
remoteSigner, err := keylesslib.NewRemoteSigner(keylesslib.RemoteSignerConfig{