Skip to content
This repository was archived by the owner on Jun 12, 2022. It is now read-only.

Commit b99d77b

Browse files
committed
🪴 Shadowsocks 2022 UDP protocol change
Change: Server messages are now assigned their own server session IDs, and no longer reuse client session IDs. A new `client session ID` field is added to server message header, between timestamp and padding length. Motivation: UDP sessions are initiated by clients. When a server restarts during a UDP session, the session state on the server, notably the packet ID, is lost. When the restarted server responds to that session, the packet ID starts again at 0 or 1 << 63. These packets will be treated as replay by the client. For 2022-blake3-aes-256-gcm, the situation is even worse. The separate headers now repeat the same pattern as before, and the AEAD part is effectively doing nonce reuse. This method is completely broken. This change mitigates the issue by using separate session IDs for client and server messages in a session. The client initiates sessions, and maintains the changing server sessions. In this implementation, the client maintains two server sessions: old and current. When it receives a server message, the server session ID is checked against the current server session. If the server session ID is different from the current one: - If the server session ID equals the old session, then the packet is processed using the old session state, and the last seen time of the old session is updated. - If the client has never seen the server session ID, and the old session's last seen time is less than 60 seconds ago, then the packet is dropped, and an error is returned, indicating that the server session is changing too fast for the client to handle. - If the client has never seen the server session ID, but the old session's last seen time is over 60 seconds ago, then the new session is accepted after packet validation. The current session is now the old session. This is the recommended way of handling changing server sessions. It simplifies session management, leaves no room for an adversary to replay packets from the last session, and properly handles out-of-order packets from the old session.
1 parent c499c64 commit b99d77b

File tree

5 files changed

+292
-156
lines changed

5 files changed

+292
-156
lines changed

client/client.go

Lines changed: 196 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package client
22

33
import (
4+
"bytes"
45
"crypto/cipher"
6+
"encoding/binary"
57
"errors"
8+
"fmt"
69
"io"
10+
"math"
711
"math/rand"
812
"net"
913
"time"
@@ -30,7 +34,8 @@ const (
3034
// was ~1 ms.) If no client payload is received by this time, we connect without it.
3135
helloWait = 100 * time.Millisecond
3236

33-
ShadowsocksPacketConnFrontReserve = 24 + 8 + 8 + 1 + 8 + 2 + ss.MaxPaddingLength + socks.MaxAddrLen
37+
// Defines the space to reserve in front of a slice for making an outgoing Shadowsocks client message.
38+
ShadowsocksPacketConnFrontReserve = 24 + ss.UDPClientMessageHeaderFixedLength + ss.MaxPaddingLength + socks.MaxAddrLen
3439
)
3540

3641
var (
@@ -229,8 +234,8 @@ type ShadowsocksPacketConn interface {
229234
// WriteToZeroCopy minimizes copying by requiring that enough space is reserved in b.
230235
// The socks address is still being copied into the buffer.
231236
//
232-
// You should reserve 24 + 8 + 8 + 1 + 8 + 2 + ss.MaxPaddingLength + socks.MaxAddrLen in the beginning,
233-
// and cipher.TagSize() in the end.
237+
// You should reserve 24 + ss.UDPClientMessageHeaderFixedLength + ss.MaxPaddingLength + socks.MaxAddrLen
238+
// in the beginning, and cipher.TagSize() in the end.
234239
//
235240
// start points to where the actual payload (excluding header) starts.
236241
// length is payload length.
@@ -241,41 +246,64 @@ type packetConn struct {
241246
*net.UDPConn
242247
cipher *ss.Cipher
243248

244-
// sid stores session ID for Shadowsocks 2022 Edition methods.
245-
sid []byte
249+
// csid stores client session ID for Shadowsocks 2022 Edition methods.
250+
csid []byte
246251

247-
// pid stores packet ID for Shadowsocks 2022 Edition methods.
248-
pid uint64
252+
// cpid stores client packet ID for Shadowsocks 2022 Edition methods.
253+
cpid uint64
249254

250-
// Provides sliding window replay protection.
251-
filter *wgreplay.Filter
255+
// caead is the client session's AEAD cipher.
256+
// Only used by 2022-blake3-aes-256-gcm.
257+
// Initialized with client session subkey.
258+
caead cipher.AEAD
259+
260+
// cssid stores the current server session ID for Shadowsocks 2022 Edition methods.
261+
cssid []byte
262+
263+
// csaead is the current server session's AEAD cipher.
264+
// Only used by 2022-blake3-aes-256-gcm.
265+
// Initialized with the current server session subkey.
266+
csaead cipher.AEAD
252267

268+
// csfilter is the current server session's sliding window filter.
269+
csfilter *wgreplay.Filter
270+
271+
// ossid stores the old server session ID for Shadowsocks 2022 Edition methods.
272+
ossid []byte
273+
274+
// osaead is the oldl server session's AEAD cipher.
253275
// Only used by 2022-blake3-aes-256-gcm.
254-
// Initialized with session subkey.
255-
aead cipher.AEAD
276+
// Initialized with the old server session subkey.
277+
osaead cipher.AEAD
278+
279+
// osfilter is the old server session's sliding window filter.
280+
osfilter *wgreplay.Filter
281+
282+
// osLastSeenTime is the last time when we received a packet from the old server session.
283+
osLastSeenTime time.Time
256284
}
257285

258286
func newPacketConn(proxyConn *net.UDPConn, c *ss.Cipher) (*packetConn, error) {
259287
// Random session ID
260-
sid := make([]byte, 8)
261-
err := ss.Blake3KeyedHashSaltGenerator.GetSalt(sid)
288+
csid := make([]byte, 8)
289+
err := ss.Blake3KeyedHashSaltGenerator.GetSalt(csid)
262290
if err != nil {
263291
return nil, err
264292
}
265293

266-
// Separate header AEAD
267-
var aead cipher.AEAD
268-
aead, err = c.NewAEAD(sid)
294+
// Separate header client AEAD
295+
var caead cipher.AEAD
296+
caead, err = c.NewAEAD(csid)
269297
if err != nil {
270298
return nil, err
271299
}
272300

273301
return &packetConn{
274-
UDPConn: proxyConn,
275-
cipher: c,
276-
sid: sid,
277-
filter: &wgreplay.Filter{},
278-
aead: aead,
302+
UDPConn: proxyConn,
303+
cipher: c,
304+
csid: csid,
305+
csfilter: &wgreplay.Filter{},
306+
caead: caead,
279307
}, nil
280308
}
281309

@@ -303,12 +331,12 @@ func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) {
303331
}
304332

305333
// Write header
306-
n, err := ss.WriteClientUDPHeader(cipherBuf[headerStart:], cipherConfig, c.sid, c.pid, addr, 1452)
334+
n, err := ss.WriteClientUDPHeader(cipherBuf[headerStart:], cipherConfig, c.csid, c.cpid, addr, 1452)
307335
if err != nil {
308336
return 0, err
309337
}
310338
if cipherConfig.IsSpec2022 {
311-
c.pid++
339+
c.cpid++
312340
}
313341

314342
// Copy payload
@@ -319,7 +347,7 @@ func (c *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) {
319347

320348
switch {
321349
case cipherConfig.UDPHasSeparateHeader:
322-
buf, err = ss.PackAesWithSeparateHeader(cipherBuf, cipherBuf[headerStart:headerStart+plaintextLen], c.cipher, c.aead)
350+
buf, err = ss.PackAesWithSeparateHeader(cipherBuf, cipherBuf[headerStart:headerStart+plaintextLen], c.cipher, c.caead)
323351
default:
324352
buf, err = ss.Pack(cipherBuf, cipherBuf[headerStart:headerStart+plaintextLen], c.cipher)
325353
}
@@ -347,10 +375,10 @@ func (c *packetConn) WriteToZeroCopy(b []byte, start, length int, socksAddr []by
347375

348376
switch {
349377
case cipherConfig.UDPHasSeparateHeader:
350-
headerStart = socksAddrStart - paddingLen - 2 - 8 - 1 - 8 - 8
378+
headerStart = socksAddrStart - paddingLen - ss.UDPClientMessageHeaderFixedLength
351379
packetStart = headerStart
352380
case cipherConfig.IsSpec2022:
353-
headerStart = socksAddrStart - paddingLen - 2 - 8 - 1 - 8 - 8
381+
headerStart = socksAddrStart - paddingLen - ss.UDPClientMessageHeaderFixedLength
354382
packetStart = headerStart - 24
355383
default:
356384
headerStart = socksAddrStart
@@ -360,16 +388,16 @@ func (c *packetConn) WriteToZeroCopy(b []byte, start, length int, socksAddr []by
360388
// Write header
361389
switch {
362390
case cipherConfig.IsSpec2022:
363-
ss.WriteUDPHeader(b[headerStart:], ss.HeaderTypeClientPacket, c.sid, c.pid, nil, socksAddr, paddingLen)
364-
c.pid++
391+
ss.WriteUDPHeader(b[headerStart:], ss.HeaderTypeClientPacket, c.csid, c.cpid, nil, nil, socksAddr, paddingLen)
392+
c.cpid++
365393
default:
366394
copy(b[headerStart:], socksAddr)
367395
}
368396

369397
var buf []byte
370398
switch {
371399
case cipherConfig.UDPHasSeparateHeader:
372-
buf, err = ss.PackAesWithSeparateHeader(b[packetStart:], b[headerStart:start+length], c.cipher, c.aead)
400+
buf, err = ss.PackAesWithSeparateHeader(b[packetStart:], b[headerStart:start+length], c.cipher, c.caead)
373401
default:
374402
buf, err = ss.Pack(b[packetStart:], b[headerStart:start+length], c.cipher)
375403
}
@@ -394,7 +422,7 @@ func (c *packetConn) ReadFrom(b []byte) (int, net.Addr, error) {
394422
}
395423

396424
// Decrypt in-place.
397-
_, socksAddr, payload, err := ss.UnpackAndValidatePacket(c.cipher, c.aead, c.filter, c.sid, nil, cipherBuf[:n])
425+
_, socksAddr, payload, err := c.unpackAndValidatePacket(nil, cipherBuf[:n])
398426
if err != nil {
399427
return 0, nil, err
400428
}
@@ -418,8 +446,145 @@ func (c *packetConn) ReadFromZeroCopy(b []byte) (socksAddrStart, payloadStart, p
418446
return
419447
}
420448

421-
socksAddrStart, socksAddr, payload, err := ss.UnpackAndValidatePacket(c.cipher, c.aead, c.filter, c.sid, nil, b[:n])
449+
socksAddrStart, socksAddr, payload, err := c.unpackAndValidatePacket(nil, b[:n])
422450
payloadStart = socksAddrStart + len(socksAddr)
423451
payloadLength = len(payload)
424452
return
425453
}
454+
455+
// unpackAndValidatePacket unpacks an encrypted packet, validates the packet,
456+
// and returns the payload (without header) and the address.
457+
func (c *packetConn) unpackAndValidatePacket(dst, src []byte) (socksAddrStart int, socksAddr socks.Addr, payload []byte, err error) {
458+
cipherConfig := c.cipher.Config()
459+
var plaintextStart int
460+
var buf []byte
461+
462+
const (
463+
currentServerSession = iota
464+
oldServerSession
465+
newServerSession
466+
)
467+
468+
var sessionStatus int
469+
var ssid []byte
470+
var saead cipher.AEAD
471+
var sfilter *wgreplay.Filter
472+
473+
switch {
474+
case cipherConfig.UDPHasSeparateHeader:
475+
if dst == nil {
476+
dst = src
477+
}
478+
479+
// Decrypt separate header
480+
err = ss.DecryptSeparateHeader(c.cipher, dst, src)
481+
if err != nil {
482+
return
483+
}
484+
485+
// Check server session id
486+
switch {
487+
case bytes.Equal(c.cssid, dst[:8]): // is current server session
488+
sessionStatus = currentServerSession
489+
ssid = c.cssid
490+
saead = c.csaead
491+
sfilter = c.csfilter
492+
case bytes.Equal(c.ossid, dst[:8]): // is old server session
493+
sessionStatus = oldServerSession
494+
ssid = c.ossid
495+
saead = c.osaead
496+
sfilter = c.osfilter
497+
default: // is a new server session
498+
// When a server session changes, there's a replay window of less than 60 seconds,
499+
// during which an adversary can replay packets with a valid timestamp from the old session.
500+
// To protect against such attacks, and to simplify implementation and save resources,
501+
// we only save information for one previous session.
502+
//
503+
// In an unlikely event where the server session changed more than once within 60s,
504+
// we simply drop new server sessions.
505+
if time.Since(c.osLastSeenTime) < 60*time.Second {
506+
err = ss.ErrTooManyServerSessions
507+
return
508+
}
509+
sessionStatus = newServerSession
510+
ssid = dst[:8]
511+
saead, err = c.cipher.NewAEAD(ssid)
512+
if err != nil {
513+
return
514+
}
515+
// Delay sfilter creation after validation to avoid a possibly unnecessary allocation.
516+
}
517+
518+
// Unpack
519+
buf, err = ss.UnpackAesWithSeparateHeader(dst, src, nil, c.cipher, saead)
520+
if err != nil {
521+
return
522+
}
523+
524+
case cipherConfig.IsSpec2022:
525+
plaintextStart, buf, err = ss.Unpack(dst, src, c.cipher)
526+
if err != nil {
527+
return
528+
}
529+
530+
// Check server session id
531+
switch {
532+
case bytes.Equal(c.cssid, buf[:8]): // is current server session
533+
sessionStatus = currentServerSession
534+
ssid = c.cssid
535+
sfilter = c.csfilter
536+
case bytes.Equal(c.ossid, buf[:8]): // is old server session
537+
sessionStatus = oldServerSession
538+
ssid = c.ossid
539+
sfilter = c.osfilter
540+
default: // is a new server session
541+
if time.Since(c.osLastSeenTime) < 60*time.Second {
542+
err = ss.ErrTooManyServerSessions
543+
return
544+
}
545+
sessionStatus = newServerSession
546+
ssid = buf[:8]
547+
// Delay sfilter creation after validation to avoid a possibly unnecessary allocation.
548+
}
549+
550+
default:
551+
plaintextStart, buf, err = ss.Unpack(dst, src, c.cipher)
552+
if err != nil {
553+
return
554+
}
555+
}
556+
557+
socksAddrStart, socksAddr, payload, err = ss.ParseUDPHeader(buf, ss.HeaderTypeServerPacket, c.csid, cipherConfig)
558+
if err != nil {
559+
return
560+
}
561+
socksAddrStart += plaintextStart
562+
563+
if cipherConfig.IsSpec2022 {
564+
pid := binary.BigEndian.Uint64(buf[8:])
565+
if sessionStatus == newServerSession {
566+
sfilter = &wgreplay.Filter{}
567+
}
568+
if !sfilter.ValidateCounter(pid, math.MaxUint64) {
569+
err = fmt.Errorf("detected replay packet, server session id %v, packet id %d", ssid, pid)
570+
return
571+
}
572+
switch sessionStatus {
573+
case oldServerSession:
574+
// Update old session's last seen time.
575+
c.osLastSeenTime = time.Now()
576+
case newServerSession:
577+
// Move current to old.
578+
c.ossid = c.cssid
579+
c.osaead = c.csaead
580+
c.osfilter = c.csfilter
581+
c.osLastSeenTime = time.Now()
582+
// Save temporary vars to current.
583+
c.cssid = ssid
584+
c.csaead = saead
585+
c.csfilter = sfilter
586+
}
587+
}
588+
589+
return
590+
}

0 commit comments

Comments
 (0)