|
|
@@ -42,6 +42,7 @@ const (
|
|
|
MAX_OBFUSCATED_QUIC_IPV6_PACKET_SIZE = 1352
|
|
|
MAX_PADDING = 64
|
|
|
NONCE_SIZE = 12
|
|
|
+ RANDOM_STREAM_LIMIT = 1 << 38
|
|
|
)
|
|
|
|
|
|
// ObfuscatedPacketConn wraps a QUIC net.PacketConn with an obfuscation layer
|
|
|
@@ -70,6 +71,7 @@ type ObfuscatedPacketConn struct {
|
|
|
|
|
|
randomStreamMutex sync.Mutex
|
|
|
randomStream *chacha20.Cipher
|
|
|
+ randomStreamCount int64
|
|
|
}
|
|
|
|
|
|
type peerMode struct {
|
|
|
@@ -82,15 +84,16 @@ func (p *peerMode) isStale() bool {
|
|
|
}
|
|
|
|
|
|
// NewObfuscatedPacketConnPacketConn creates a new ObfuscatedPacketConn.
|
|
|
-func NewObfuscatedPacketConnPacketConn(
|
|
|
+func NewObfuscatedPacketConn(
|
|
|
conn net.PacketConn,
|
|
|
isServer bool,
|
|
|
obfuscationKey string) (*ObfuscatedPacketConn, error) {
|
|
|
|
|
|
packetConn := &ObfuscatedPacketConn{
|
|
|
- PacketConn: conn,
|
|
|
- isServer: isServer,
|
|
|
- peerModes: make(map[string]*peerMode),
|
|
|
+ PacketConn: conn,
|
|
|
+ isServer: isServer,
|
|
|
+ peerModes: make(map[string]*peerMode),
|
|
|
+ randomStreamCount: RANDOM_STREAM_LIMIT,
|
|
|
}
|
|
|
|
|
|
secret := []byte(obfuscationKey)
|
|
|
@@ -101,26 +104,6 @@ func NewObfuscatedPacketConnPacketConn(
|
|
|
return nil, common.ContextError(err)
|
|
|
}
|
|
|
|
|
|
- // Use a stream cipher to generate randomness for padding. This mitigates
|
|
|
- // issues using a high volume (multiple per packet) of crypto/rand.Read
|
|
|
- // calls, which use getrandom via a syscall; under high load with many
|
|
|
- // clients, we observed very long syscall durations, perhaps due to lock
|
|
|
- // contention. Using a userspace random stream avoids frequent syscall
|
|
|
- // context switches as well as spinlock overhead.
|
|
|
-
|
|
|
- var randomStreamKey [32]byte
|
|
|
- _, err = rand.Read(randomStreamKey[:])
|
|
|
- if err != nil {
|
|
|
- return nil, common.ContextError(err)
|
|
|
- }
|
|
|
- var randomKeyNonce [NONCE_SIZE]byte
|
|
|
- packetConn.randomStream, err = chacha20.NewCipher(
|
|
|
- randomStreamKey[:],
|
|
|
- randomKeyNonce[:])
|
|
|
- if err != nil {
|
|
|
- return nil, common.ContextError(err)
|
|
|
- }
|
|
|
-
|
|
|
if isServer {
|
|
|
|
|
|
packetConn.runWaitGroup = new(sync.WaitGroup)
|
|
|
@@ -318,9 +301,10 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
|
|
|
|
|
|
nonce := buffer[0:NONCE_SIZE]
|
|
|
for {
|
|
|
- conn.randomStreamMutex.Lock()
|
|
|
- conn.randomStream.KeyStream(nonce)
|
|
|
- conn.randomStreamMutex.Unlock()
|
|
|
+ err := conn.getRandomBytes(nonce)
|
|
|
+ if err != nil {
|
|
|
+ return 0, common.ContextError(err)
|
|
|
+ }
|
|
|
|
|
|
// Don't use a random nonce that looks like QUIC, or the
|
|
|
// peer will not treat this packet as obfuscated.
|
|
|
@@ -340,14 +324,18 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
|
|
|
maxPaddingLen = MAX_PADDING
|
|
|
}
|
|
|
|
|
|
- paddingLen := conn.getPaddingLen(maxPaddingLen)
|
|
|
+ paddingLen, err := conn.getRandomPaddingLen(maxPaddingLen)
|
|
|
+ if err != nil {
|
|
|
+ return 0, common.ContextError(err)
|
|
|
+ }
|
|
|
|
|
|
buffer[NONCE_SIZE] = uint8(paddingLen)
|
|
|
|
|
|
padding := buffer[(NONCE_SIZE + 1) : (NONCE_SIZE+1)+paddingLen]
|
|
|
- conn.randomStreamMutex.Lock()
|
|
|
- conn.randomStream.KeyStream(padding)
|
|
|
- conn.randomStreamMutex.Unlock()
|
|
|
+ err = conn.getRandomBytes(padding)
|
|
|
+ if err != nil {
|
|
|
+ return 0, common.ContextError(err)
|
|
|
+ }
|
|
|
|
|
|
copy(buffer[(NONCE_SIZE+1)+paddingLen:], p)
|
|
|
dataLen := (NONCE_SIZE + 1) + paddingLen + n
|
|
|
@@ -367,7 +355,46 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
|
|
|
return n, err
|
|
|
}
|
|
|
|
|
|
-func (conn *ObfuscatedPacketConn) getPaddingLen(maxPadding int) int {
|
|
|
+func (conn *ObfuscatedPacketConn) getRandomBytes(b []byte) error {
|
|
|
+ conn.randomStreamMutex.Lock()
|
|
|
+ defer conn.randomStreamMutex.Unlock()
|
|
|
+
|
|
|
+ // Use a stream cipher to generate randomness for padding. This mitigates
|
|
|
+ // issues using a high volume (multiple per packet) of crypto/rand.Read
|
|
|
+ // calls, which use getrandom via a syscall; under high load with many
|
|
|
+ // clients, we observed very long syscall durations, perhaps due to lock
|
|
|
+ // contention. Using a userspace random stream avoids frequent syscall
|
|
|
+ // context switches as well as spinlock overhead.
|
|
|
+
|
|
|
+ if conn.randomStreamCount+int64(len(b)) >= RANDOM_STREAM_LIMIT {
|
|
|
+
|
|
|
+ // Re-key before reaching the 2^38 chacha20 key stream limit.
|
|
|
+
|
|
|
+ var randomStreamKey [32]byte
|
|
|
+ _, err := rand.Read(randomStreamKey[:])
|
|
|
+ if err != nil {
|
|
|
+ return common.ContextError(err)
|
|
|
+ }
|
|
|
+ var randomKeyNonce [NONCE_SIZE]byte
|
|
|
+
|
|
|
+ conn.randomStream, err = chacha20.NewCipher(
|
|
|
+ randomStreamKey[:],
|
|
|
+ randomKeyNonce[:])
|
|
|
+ if err != nil {
|
|
|
+ return common.ContextError(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ conn.randomStreamCount = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ conn.randomStream.KeyStream(b)
|
|
|
+
|
|
|
+ conn.randomStreamCount += int64(len(b))
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (conn *ObfuscatedPacketConn) getRandomPaddingLen(maxPadding int) (int, error) {
|
|
|
|
|
|
// Selects uniformly from [0, maxPadding], using the ObfuscatedPacketConn's
|
|
|
// random stream.
|
|
|
@@ -375,11 +402,11 @@ func (conn *ObfuscatedPacketConn) getPaddingLen(maxPadding int) int {
|
|
|
maxRand := 255
|
|
|
|
|
|
if maxPadding < 0 || maxPadding > maxRand {
|
|
|
- panic(fmt.Sprintf("unexpected max padding: %d", maxPadding))
|
|
|
+ return 0, common.ContextError(fmt.Errorf("unexpected max padding: %d", maxPadding))
|
|
|
}
|
|
|
|
|
|
if maxPadding == 0 {
|
|
|
- return 0
|
|
|
+ return 0, nil
|
|
|
}
|
|
|
|
|
|
upperBound := maxPadding
|
|
|
@@ -389,13 +416,14 @@ func (conn *ObfuscatedPacketConn) getPaddingLen(maxPadding int) int {
|
|
|
|
|
|
for {
|
|
|
var value [1]byte
|
|
|
- conn.randomStreamMutex.Lock()
|
|
|
- conn.randomStream.KeyStream(value[:])
|
|
|
- conn.randomStreamMutex.Unlock()
|
|
|
+ err := conn.getRandomBytes(value[:])
|
|
|
+ if err != nil {
|
|
|
+ return 0, common.ContextError(err)
|
|
|
+ }
|
|
|
|
|
|
padding := int(value[0])
|
|
|
if padding <= upperBound {
|
|
|
- return padding % (maxPadding + 1)
|
|
|
+ return padding % (maxPadding + 1), nil
|
|
|
}
|
|
|
}
|
|
|
}
|