feat: enable HTTP/2 support for admin/API traffic and update related configurations
Kim committed
May 27, 2026 at 16:03 UTC
31a5f5349d9de90ad3ffd4ee9f492ef8b3d89852
9 files changed
+12
-20
docs/src/routes/architecture/+page.md
+1
-1
@@ -173,7 +173,7 @@ UDP client
173
- `/sdk/connect`, `/sdk/renew`, and `/sdk/unregister` are authorized by lease existence plus a relay-issued lease access token.
174
- `/sdk/register` is authenticated by a SIWE challenge/response flow using the SDK identity secp256k1 key. On success, the relay issues a lease-scoped ES256K JWT access token signed by the relay identity key and used for the rest of the lease lifecycle.
175
- Relay URLs must use `https://`.
176
-- HTTP/2 stays disabled on the admin/API TLS listener because `/sdk/connect` depends on HTTP/1.1 hijacking semantics.
176
+- HTTP/2 is allowed on the admin/API TLS listener for ordinary API traffic; `/sdk/connect` remains HTTP/1.1-only because it depends on hijacking semantics.
177
- WireGuard, when enabled, is relay-to-relay overlay transport only. It carries multi-hop relay forwarding and overlay discovery, but it is not used for direct tenant TLS termination, public UDP ingress, or `/sdk/*` control-plane traffic.
178
179
### Reverse Session Protocol
docs/static/examples/nginx-proxy/nginx.conf
+4
-4
@@ -53,9 +53,9 @@ stream {
53
# nginx terminates TLS for the root domain only, then proxies HTTP/1.1 to
54
# the portal admin/API listener on port 4017.
55
#
56
-# HTTP/2 is intentionally disabled on this listener.
57
-# /sdk/connect depends on HTTP/1.1 connection hijacking semantics.
58
-# Do NOT add 'http2' to the listen directives below.
56
+# HTTP/2 may be enabled on this listener for ordinary admin/API traffic.
57
+# /sdk/connect clients still negotiate HTTP/1.1 and the proxy must use
58
+# HTTP/1.1 to the portal admin/API backend for connection hijacking.
59
http {
60
sendfile on;
61
tcp_nopush on;
@@ -74,7 +74,7 @@ http {
74
# ── Root domain: admin/API/frontend ──────────────────────────────────────
75
server {
76
# Internal L7 listener. Receives traffic from the L4 stream block.
77
- # Do NOT add 'http2' — /sdk/connect requires HTTP/1.1 hijacking.
77
+ # Add 'http2' if desired; /sdk/connect clients still use HTTP/1.1.
78
listen 8443 ssl;
79
server_name portal.example.com;
80
server_tokens off;
portal/api_server.go
-1
@@ -69,7 +69,6 @@ func (s *Server) newAPIServer(listener net.Listener, apiMux *http.ServeMux, apiT
69
apiServer := &http.Server{
70
Handler: s.apiHandler(apiMux, keylessSignerHandler),
71
ReadHeaderTimeout: 10 * time.Second,
72
- TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
72
}
73
74
apiCloser, err := keyless.AttachToHTTPServer(apiServer, apiTLS)
portal/discovery/refresher.go
+1
-2
@@ -38,9 +38,8 @@ func NewRefresher(relaySet *RelaySet, overlay OverlayRuntime) *Refresher {
38
httpClient: utils.NewHTTPClient(
39
utils.WithHTTPTLSConfig(&tls.Config{
40
MinVersion: tls.VersionTLS12,
41
- NextProtos: []string{"http/1.1"},
41
+ NextProtos: []string{"h2", "http/1.1"},
42
}),
43
- utils.WithoutHTTP2(),
43
utils.WithHTTPTimeout(defaultRequestTimeout),
44
),
45
overlay: overlay,
portal/keyless/tls.go
+2
-2
@@ -42,7 +42,7 @@ func AttachToHTTPServer(server *http.Server, cfg TLSMaterialConfig) (io.Closer,
42
ClientKeyPEM: cfg.Keyless.ClientKeyPEM,
43
RootCAPEM: cfg.Keyless.RootCAPEM,
44
},
45
- NextProtos: []string{"http/1.1"},
45
+ NextProtos: []string{"h2", "http/1.1"},
46
MinTLSVersion: minVersion,
47
EncryptedClientHelloKeys: cfg.EncryptedClientHelloKeys,
48
})
@@ -60,7 +60,7 @@ func AttachToHTTPServer(server *http.Server, cfg TLSMaterialConfig) (io.Closer,
60
minVersion := MinTLSVersion(len(cfg.EncryptedClientHelloKeys) > 0)
61
server.TLSConfig = &tls.Config{
62
MinVersion: minVersion,
63
- NextProtos: []string{"http/1.1"},
63
+ NextProtos: []string{"h2", "http/1.1"},
64
Certificates: []tls.Certificate{cert},
65
EncryptedClientHelloKeys: cfg.EncryptedClientHelloKeys,
66
}
portal/overlay/overlay.go
-1
@@ -143,7 +143,6 @@ func NewOverlay(cfg Config, handler http.Handler, streamHandler StreamHandler) (
143
utils.WithHTTPIdleConnTimeout(90*time.Second),
144
utils.WithHTTPResponseHeaderTimeout(30*time.Second),
145
utils.WithHTTPExpectContinueTimeout(1*time.Second),
146
- utils.WithoutHTTP2(),
146
)
147
148
publicCfg := cfg.Copy()
sdk/listener.go
+3
-1
@@ -575,9 +575,11 @@ func (l *listener) openReverseSession(ctx context.Context) (net.Conn, error) {
575
return nil, errors.New("relay tls config is unavailable")
576
}
577
578
+ reverseTLSConfig := l.tlsConfig.Clone()
579
+ reverseTLSConfig.NextProtos = []string{"http/1.1"}
580
dialer := &tls.Dialer{
581
NetDialer: &net.Dialer{Timeout: l.dialTimeout},
580
- Config: l.tlsConfig.Clone(),
582
+ Config: reverseTLSConfig,
583
}
584
585
conn, err := dialer.DialContext(ctx, "tcp", utils.EnsurePort(l.relayURL.Host))
utils/http.go
-6
@@ -57,12 +57,6 @@ func WithHTTPDialContext(dialContext func(context.Context, string, string) (net.
57
}
58
}
59
60
-func WithoutHTTP2() HTTPClientOption {
61
- return func(c *http.Client) {
62
- mustTransportOf(c).ForceAttemptHTTP2 = false
63
- }
64
-}
65
-
60
func WithHTTPResponseHeaderTimeout(timeout time.Duration) HTTPClientOption {
61
return func(c *http.Client) {
62
mustTransportOf(c).ResponseHeaderTimeout = timeout
utils/tls.go
+1
-2
@@ -43,11 +43,10 @@ func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, timeout time.Durat
43
MinVersion: tls.VersionTLS12,
44
ServerName: serverName,
45
RootCAs: rootCAs,
46
- NextProtos: []string{"http/1.1"},
46
+ NextProtos: []string{"h2", "http/1.1"},
47
}
48
httpClient := NewHTTPClient(
49
WithHTTPTLSConfig(rawTLSConfig), // will be cloned internally
50
- WithoutHTTP2(),
50
WithHTTPTimeout(timeout),
51
)
52
return rawTLSConfig, httpClient, mustTransportOf(httpClient), nil