-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsquic.go
More file actions
373 lines (324 loc) · 10.9 KB
/
squic.go
File metadata and controls
373 lines (324 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// Package squic provides Shielded QUIC — a silent-server wrapper around quic-go.
//
// sQUIC adds two features on top of standard QUIC:
// - Silent server: the server is invisible to port scanners. Only clients
// that possess the server's public key can elicit a response.
// - No CA/PKI: identity is a pinned public key, not a certificate chain.
// - Optional client key whitelisting with full silence for non-whitelisted clients.
//
// Usage:
//
// // Server
// ln, _ := squic.Listen("udp", ":4433", serverCert, serverPubKey, nil)
// conn, _ := ln.Accept(ctx)
//
// // Client
// conn, _ := squic.Dial(ctx, "server:4433", serverPubKey, nil)
package squic
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"encoding/hex"
"fmt"
"net"
"time"
"github.com/quic-go/quic-go"
"golang.org/x/crypto/curve25519"
)
// Config holds optional sQUIC configuration.
type Config struct {
// MaxIdleTimeout is the maximum time a connection can be idle.
// Default: 30 seconds.
MaxIdleTimeout time.Duration
// MaxIncomingStreams is the maximum number of concurrent incoming streams.
// Default: 100.
MaxIncomingStreams int64
// NextProtos overrides the TLS ALPN protocol list.
// Default: ["squic"]. Set to ["h3"] for HTTP/3.
NextProtos []string
// AllowedKeys is an optional whitelist of client X25519 public keys (32 bytes each).
// When set on the server, only clients whose X25519 public keys appear in this
// list can connect. Non-whitelisted clients are silently dropped (no response).
// When nil, any client that knows the server's public key can connect.
AllowedKeys [][]byte
// KeepAlive sends periodic keep-alive packets to prevent idle timeout.
// Default: disabled (zero value).
KeepAlive time.Duration
// HandshakeTimeout is the maximum time for the TLS handshake to complete.
// Default: 10 seconds.
HandshakeTimeout time.Duration
// MaxStreamReceiveWindow is the maximum per-stream flow control window.
// Default: 6 MB.
MaxStreamReceiveWindow uint64
// MaxConnectionReceiveWindow is the maximum connection-level flow control window.
// Default: 15 MB.
MaxConnectionReceiveWindow uint64
// InitialMTU sets the initial UDP payload size. Range: 1200-65000.
// Default: 1200.
InitialMTU uint16
// DisableMTUDiscovery disables RFC 8899 path MTU discovery.
// Default: false (discovery enabled).
DisableMTUDiscovery bool
// EnableDatagrams enables RFC 9221 QUIC datagram support.
// Default: false.
EnableDatagrams bool
// Enable0RTT allows 0-RTT resumption. Has replay attack implications.
// Default: false.
Enable0RTT bool
// ClientKey is an optional hex-encoded Ed25519 private key seed (64 hex chars).
// When set, Dial() uses this persistent identity instead of generating an ephemeral one.
// The client's X25519 public key is derived from this for MAC1 and whitelist matching.
ClientKey string
// LoadThreshold is the DH operations per second before the server enters
// under-load mode and requires MAC2 (cookie proof-of-address).
// Default: 1000. Set to 0 to disable MAC2 protection.
LoadThreshold int64
// QuicConfig allows passing additional quic-go configuration.
// If nil, sensible defaults are used. Overrides all other fields.
QuicConfig *quic.Config
}
func (c *Config) quicConfig() *quic.Config {
if c != nil && c.QuicConfig != nil {
return c.QuicConfig.Clone()
}
timeout := 30 * time.Second
maxStreams := int64(100)
if c != nil {
if c.MaxIdleTimeout > 0 {
timeout = c.MaxIdleTimeout
}
if c.MaxIncomingStreams > 0 {
maxStreams = c.MaxIncomingStreams
}
}
qc := &quic.Config{
MaxIdleTimeout: timeout,
MaxIncomingStreams: maxStreams,
MaxIncomingUniStreams: maxStreams,
InitialStreamReceiveWindow: 1 << 20, // 1MB
InitialConnectionReceiveWindow: 10 << 20, // 10MB
}
if c != nil {
if c.KeepAlive > 0 {
qc.KeepAlivePeriod = c.KeepAlive
}
if c.HandshakeTimeout > 0 {
qc.HandshakeIdleTimeout = c.HandshakeTimeout
}
if c.MaxStreamReceiveWindow > 0 {
qc.MaxStreamReceiveWindow = c.MaxStreamReceiveWindow
}
if c.MaxConnectionReceiveWindow > 0 {
qc.MaxConnectionReceiveWindow = c.MaxConnectionReceiveWindow
}
if c.InitialMTU > 0 {
qc.InitialPacketSize = c.InitialMTU
}
if c.DisableMTUDiscovery {
qc.DisablePathMTUDiscovery = true
}
if c.EnableDatagrams {
qc.EnableDatagrams = true
}
if c.Enable0RTT {
qc.Allow0RTT = true
}
}
return qc
}
func (c *Config) allowedKeys() [][]byte {
if c == nil {
return nil
}
return c.AllowedKeys
}
func (c *Config) loadThreshold() int64 {
if c == nil || c.LoadThreshold <= 0 {
return 1000
}
return c.LoadThreshold
}
func (c *Config) nextProtos() []string {
if c != nil && len(c.NextProtos) > 0 {
return c.NextProtos
}
return nil
}
// ServerListener wraps a quic.Listener with silent-server support.
type ServerListener struct {
*quic.Listener
conn net.PacketConn
sc *serverConn
}
// Listen creates a sQUIC listener on the given address.
// serverCert is the TLS certificate (from GenerateKeyPair or LoadKeyPair).
// serverPubKey is the raw Ed25519 public key bytes (distributed to clients out-of-band).
func Listen(network, addr string, serverCert tls.Certificate, serverPubKey []byte, config *Config) (*ServerListener, error) {
udpAddr, err := net.ResolveUDPAddr(network, addr)
if err != nil {
return nil, fmt.Errorf("squic: resolve addr: %w", err)
}
rawConn, err := net.ListenUDP(network, udpAddr)
if err != nil {
return nil, fmt.Errorf("squic: listen: %w", err)
}
// Convert server Ed25519 private key to X25519 for DH-based MAC1
edPriv, ok := serverCert.PrivateKey.(ed25519.PrivateKey)
if !ok {
rawConn.Close()
return nil, fmt.Errorf("squic: server certificate must use Ed25519 key")
}
serverX25519Priv := Ed25519PrivateToX25519(edPriv)
// Wrap with DH MAC1 validation — silent server
wrappedConn := newServerConn(rawConn, serverX25519Priv, config.allowedKeys(), config.loadThreshold())
tlsConf := ServerTLSConfig(serverCert)
if protos := config.nextProtos(); protos != nil {
tlsConf.NextProtos = protos
}
quicConf := config.quicConfig()
// StatelessResetKey left nil — disables stateless reset for silent server
tr := &quic.Transport{Conn: wrappedConn}
ln, err := tr.Listen(tlsConf, quicConf)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: quic listen: %w", err)
}
return &ServerListener{Listener: ln, conn: rawConn, sc: wrappedConn}, nil
}
// AllowKey adds a client X25519 public key to the whitelist at runtime.
// If whitelisting is not enabled, this implicitly enables it.
// The key must be exactly 32 bytes.
func (sl *ServerListener) AllowKey(pubKey []byte) error {
if len(pubKey) != 32 {
return fmt.Errorf("squic: key must be 32 bytes, got %d", len(pubKey))
}
var key [32]byte
copy(key[:], pubKey)
sl.sc.addKey(key)
return nil
}
// RemoveKey removes a client X25519 public key from the whitelist at runtime.
// The key must be exactly 32 bytes.
func (sl *ServerListener) RemoveKey(pubKey []byte) error {
if len(pubKey) != 32 {
return fmt.Errorf("squic: key must be 32 bytes, got %d", len(pubKey))
}
var key [32]byte
copy(key[:], pubKey)
sl.sc.removeKey(key)
return nil
}
// HasKey checks if a client X25519 public key is in the whitelist.
func (sl *ServerListener) HasKey(pubKey []byte) bool {
if len(pubKey) != 32 {
return false
}
var key [32]byte
copy(key[:], pubKey)
return sl.sc.hasKey(key)
}
// AllowedKeys returns a copy of all whitelisted client X25519 public keys.
// Returns nil if whitelisting is not enabled.
func (sl *ServerListener) AllowedKeys() [][]byte {
keys := sl.sc.allKeys()
if keys == nil {
return nil
}
result := make([][]byte, len(keys))
for i, k := range keys {
result[i] = k[:]
}
return result
}
// EnableWhitelist activates the client key whitelist, optionally pre-populated with keys.
// Once enabled, only clients whose X25519 public keys are in the whitelist can connect.
// If no keys are provided, the whitelist starts empty (blocks all new connections).
func (sl *ServerListener) EnableWhitelist(keys ...[]byte) {
var fixed [][32]byte
for _, k := range keys {
if len(k) == 32 {
var key [32]byte
copy(key[:], k)
fixed = append(fixed, key)
}
}
sl.sc.enableWhitelist(fixed)
}
// DisableWhitelist removes the whitelist entirely.
// Any client with a valid MAC1 (knowing the server's public key) can connect.
func (sl *ServerListener) DisableWhitelist() {
sl.sc.disableWhitelist()
}
// Close closes the listener and the underlying connection.
func (l *ServerListener) Close() error {
err := l.Listener.Close()
l.conn.Close()
return err
}
// Dial connects to a sQUIC server at the given address.
// serverPubKey is the server's raw Ed25519 public key (known out-of-band).
// The client generates an ephemeral X25519 key pair for DH-based MAC1.
func Dial(ctx context.Context, addr string, serverPubKey []byte, config *Config) (*quic.Conn, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, fmt.Errorf("squic: resolve addr: %w", err)
}
rawConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return nil, fmt.Errorf("squic: listen: %w", err)
}
// Convert server Ed25519 pubkey to X25519 for DH
serverX25519Pub, err := Ed25519PublicToX25519(serverPubKey)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: convert server key: %w", err)
}
// Derive or generate X25519 key pair for this connection
var clientPriv [32]byte
if config != nil && config.ClientKey != "" {
// Persistent client identity: derive X25519 from Ed25519 seed
ed25519Pub, err := hex.DecodeString(config.ClientKey)
if err != nil || len(ed25519Pub) != ed25519.SeedSize {
rawConn.Close()
return nil, fmt.Errorf("squic: invalid ClientKey (expected %d hex chars)", ed25519.SeedSize*2)
}
priv := ed25519.NewKeyFromSeed(ed25519Pub)
pub := priv.Public().(ed25519.PublicKey)
x25519Priv := Ed25519PrivateToX25519(priv)
copy(clientPriv[:], x25519Priv)
_ = pub // Ed25519 public key available if needed for TLS cert
} else {
// Ephemeral: random X25519 key pair
if _, err := rand.Read(clientPriv[:]); err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: generate client key: %w", err)
}
}
clientPub, err := curve25519.X25519(clientPriv[:], curve25519.Basepoint)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: derive client pubkey: %w", err)
}
// Compute DH shared secret
shared, err := X25519(clientPriv[:], serverX25519Pub)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: DH key exchange: %w", err)
}
// Wrap with DH MAC1 appending
wrappedConn := newClientConn(rawConn, shared, clientPub)
tlsConf := ClientTLSConfig(serverPubKey)
if protos := config.nextProtos(); protos != nil {
tlsConf.NextProtos = protos
}
quicConf := config.quicConfig()
tr := &quic.Transport{Conn: wrappedConn}
conn, err := tr.Dial(ctx, udpAddr, tlsConf, quicConf)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("squic: dial: %w", err)
}
return conn, nil
}