Explorar o código

Add enhanced blocking resistance for QUICv1 versions

- New obfuscated QUIC padding scheme

- New decoy packet mechanism

- Anti-probing mechanism for all QUICv1 versions

- Enforce minimum initial packet size in ObfuscatedPacketConn: to support
  additional message injection, to follow
Rod Hynes %!s(int64=4) %!d(string=hai) anos
pai
achega
2c60eac02e
Modificáronse 24 ficheiros con 891 adicións e 92 borrados
  1. 16 6
      psiphon/common/obfuscator/passthrough.go
  2. 5 1
      psiphon/common/protocol/protocol.go
  3. 256 53
      psiphon/common/quic/obfuscator.go
  4. 138 7
      psiphon/common/quic/quic.go
  5. 80 5
      psiphon/common/quic/quic_test.go
  6. 5 0
      psiphon/server/tunnelServer.go
  7. 7 0
      vendor/github.com/Psiphon-Labs/qtls-go1-15/common.go
  8. 21 0
      vendor/github.com/Psiphon-Labs/qtls-go1-15/conn.go
  9. 18 3
      vendor/github.com/Psiphon-Labs/qtls-go1-15/handshake_client.go
  10. 7 0
      vendor/github.com/Psiphon-Labs/qtls-go1-16/common.go
  11. 21 0
      vendor/github.com/Psiphon-Labs/qtls-go1-16/conn.go
  12. 18 3
      vendor/github.com/Psiphon-Labs/qtls-go1-16/handshake_client.go
  13. 7 0
      vendor/github.com/Psiphon-Labs/qtls-go1-17/common.go
  14. 21 0
      vendor/github.com/Psiphon-Labs/qtls-go1-17/conn.go
  15. 18 3
      vendor/github.com/Psiphon-Labs/qtls-go1-17/handshake_client.go
  16. 5 1
      vendor/github.com/Psiphon-Labs/quic-go/config.go
  17. 30 0
      vendor/github.com/Psiphon-Labs/quic-go/interface.go
  18. 2 0
      vendor/github.com/Psiphon-Labs/quic-go/internal/handshake/crypto_setup.go
  19. 8 2
      vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go115.go
  20. 8 2
      vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go116.go
  21. 7 0
      vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go117.go
  22. 14 2
      vendor/github.com/Psiphon-Labs/quic-go/packet_packer.go
  23. 141 1
      vendor/github.com/Psiphon-Labs/quic-go/server.go
  24. 38 3
      vendor/github.com/Psiphon-Labs/quic-go/session.go

+ 16 - 6
psiphon/common/obfuscator/passthrough.go

@@ -51,11 +51,14 @@ const (
 func MakeTLSPassthroughMessage(
 func MakeTLSPassthroughMessage(
 	useTimeFactor bool, obfuscatedKey string) ([]byte, error) {
 	useTimeFactor bool, obfuscatedKey string) ([]byte, error) {
 
 
-	passthroughKey := derivePassthroughKey(useTimeFactor, obfuscatedKey)
+	passthroughKey, err := derivePassthroughKey(useTimeFactor, obfuscatedKey)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
 
 
 	message := make([]byte, TLS_PASSTHROUGH_MESSAGE_SIZE)
 	message := make([]byte, TLS_PASSTHROUGH_MESSAGE_SIZE)
 
 
-	_, err := rand.Read(message[0:TLS_PASSTHROUGH_NONCE_SIZE])
+	_, err = rand.Read(message[0:TLS_PASSTHROUGH_NONCE_SIZE])
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -83,7 +86,11 @@ func VerifyTLSPassthroughMessage(
 		message = stub[:]
 		message = stub[:]
 	}
 	}
 
 
-	passthroughKey := derivePassthroughKey(useTimeFactor, obfuscatedKey)
+	passthroughKey, err := derivePassthroughKey(useTimeFactor, obfuscatedKey)
+	if err != nil {
+		// TODO: log error
+		return false
+	}
 
 
 	h := hmac.New(sha256.New, passthroughKey)
 	h := hmac.New(sha256.New, passthroughKey)
 	h.Write(message[0:TLS_PASSTHROUGH_NONCE_SIZE])
 	h.Write(message[0:TLS_PASSTHROUGH_NONCE_SIZE])
@@ -99,7 +106,7 @@ func VerifyTLSPassthroughMessage(
 var timePeriodSeconds = int64(TLS_PASSTHROUGH_TIME_PERIOD / time.Second)
 var timePeriodSeconds = int64(TLS_PASSTHROUGH_TIME_PERIOD / time.Second)
 
 
 func derivePassthroughKey(
 func derivePassthroughKey(
-	useTimeFactor bool, obfuscatedKey string) []byte {
+	useTimeFactor bool, obfuscatedKey string) ([]byte, error) {
 
 
 	secret := []byte(obfuscatedKey)
 	secret := []byte(obfuscatedKey)
 
 
@@ -132,7 +139,10 @@ func derivePassthroughKey(
 
 
 	key := make([]byte, TLS_PASSTHROUGH_KEY_SIZE)
 	key := make([]byte, TLS_PASSTHROUGH_KEY_SIZE)
 
 
-	_, _ = io.ReadFull(hkdf.New(sha256.New, secret, salt, nil), key)
+	_, err := io.ReadFull(hkdf.New(sha256.New, secret, salt, nil), key)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
 
 
-	return key
+	return key, nil
 }
 }

+ 5 - 1
psiphon/common/protocol/protocol.go

@@ -402,6 +402,7 @@ const (
 	QUIC_VERSION_V1            = "QUICv1"
 	QUIC_VERSION_V1            = "QUICv1"
 	QUIC_VERSION_RANDOMIZED_V1 = "RANDOMIZED-QUICv1"
 	QUIC_VERSION_RANDOMIZED_V1 = "RANDOMIZED-QUICv1"
 	QUIC_VERSION_OBFUSCATED_V1 = "OBFUSCATED-QUICv1"
 	QUIC_VERSION_OBFUSCATED_V1 = "OBFUSCATED-QUICv1"
+	QUIC_VERSION_DECOY_V1      = "DECOY-QUICv1"
 )
 )
 
 
 var SupportedQUICVersions = QUICVersions{
 var SupportedQUICVersions = QUICVersions{
@@ -412,6 +413,7 @@ var SupportedQUICVersions = QUICVersions{
 	QUIC_VERSION_V1,
 	QUIC_VERSION_V1,
 	QUIC_VERSION_RANDOMIZED_V1,
 	QUIC_VERSION_RANDOMIZED_V1,
 	QUIC_VERSION_OBFUSCATED_V1,
 	QUIC_VERSION_OBFUSCATED_V1,
+	QUIC_VERSION_DECOY_V1,
 }
 }
 
 
 var legacyQUICVersions = QUICVersions{
 var legacyQUICVersions = QUICVersions{
@@ -423,7 +425,9 @@ func QUICVersionHasRandomizedClientHello(version string) bool {
 }
 }
 
 
 func QUICVersionIsObfuscated(version string) bool {
 func QUICVersionIsObfuscated(version string) bool {
-	return version == QUIC_VERSION_OBFUSCATED || version == QUIC_VERSION_OBFUSCATED_V1
+	return version == QUIC_VERSION_OBFUSCATED ||
+		version == QUIC_VERSION_OBFUSCATED_V1 ||
+		version == QUIC_VERSION_DECOY_V1
 }
 }
 
 
 type QUICVersions []string
 type QUICVersions []string

+ 256 - 53
psiphon/common/quic/obfuscator.go

@@ -1,3 +1,4 @@
+//go:build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 
 
 /*
 /*
@@ -36,13 +37,51 @@ import (
 )
 )
 
 
 const (
 const (
-	MAX_QUIC_IPV4_PACKET_SIZE            = 1252
-	MAX_QUIC_IPV6_PACKET_SIZE            = 1232
-	MAX_OBFUSCATED_QUIC_IPV4_PACKET_SIZE = 1372
-	MAX_OBFUSCATED_QUIC_IPV6_PACKET_SIZE = 1352
-	MAX_PADDING                          = 64
-	NONCE_SIZE                           = 12
-	RANDOM_STREAM_LIMIT                  = 1<<38 - 64
+
+	// MAX_PACKET_SIZE is the largest packet size quic-go will produce,
+	// including post MTU discovery. This value is quic-go
+	// internal/protocol.MaxPacketBufferSize, which is the Ethernet MTU of
+	// 1500 less IPv6 and UDP header sizes.
+	//
+	// Legacy gQUIC quic-go will produce packets no larger than
+	// MAX_PRE_DISCOVERY_PACKET_SIZE_IPV4/IPV6.
+
+	MAX_PACKET_SIZE = 1452
+
+	// MAX_PRE_DISCOVERY_PACKET_SIZE_IPV4/IPV6 are the largest packet sizes
+	// quic-go will produce before MTU discovery, 1280 less IP and UDP header
+	// sizes. These values, which match quic-go
+	// internal/protocol.InitialPacketSizeIPv4/IPv6, are used to calculate
+	// maximum padding sizes.
+
+	MAX_PRE_DISCOVERY_PACKET_SIZE_IPV4 = 1252
+	MAX_PRE_DISCOVERY_PACKET_SIZE_IPV6 = 1232
+
+	// OBFUSCATED_MAX_PACKET_SIZE_ADJUSTMENT is the minimum amount of bytes
+	// required for obfuscation overhead, the nonce and the padding length.
+	// In IETF quic-go, this adjustment value is passed into quic-go and
+	// applied to packet construction so that quic-go produces max packet
+	// sizes reduced by this adjustment value.
+
+	OBFUSCATED_MAX_PACKET_SIZE_ADJUSTMENT = NONCE_SIZE + 1
+
+	// MIN_INITIAL_PACKET_SIZE is the minimum UDP packet payload size for
+	// Initial packets, an anti-amplification measure (see RFC 9000, section
+	// 14.1). To accomodate obfuscation prefix messages within the same
+	// Initial UDP packet, quic-go's enforcement of this size requirement is
+	// disabled and the enforcment is done by ObfuscatedPacketConn.
+
+	MIN_INITIAL_PACKET_SIZE = 1200
+
+	MAX_PADDING_SIZE       = 255
+	MAX_GQUIC_PADDING_SIZE = 64
+
+	MIN_DECOY_PACKETS = 0
+	MAX_DECOY_PACKETS = 10
+
+	NONCE_SIZE = 12
+
+	RANDOM_STREAM_LIMIT = 1<<38 - 64
 )
 )
 
 
 // ObfuscatedPacketConn wraps a QUIC net.PacketConn with an obfuscation layer
 // ObfuscatedPacketConn wraps a QUIC net.PacketConn with an obfuscation layer
@@ -61,15 +100,19 @@ const (
 // introducing some risk of fragmentation and/or dropped packets.
 // introducing some risk of fragmentation and/or dropped packets.
 type ObfuscatedPacketConn struct {
 type ObfuscatedPacketConn struct {
 	net.PacketConn
 	net.PacketConn
-	isServer       bool
-	isClosed       int32
-	runWaitGroup   *sync.WaitGroup
-	stopBroadcast  chan struct{}
-	obfuscationKey [32]byte
-	peerModesMutex sync.Mutex
-	peerModes      map[string]*peerMode
-	noncePRNG      *prng.PRNG
-	paddingPRNG    *prng.PRNG
+	isServer         bool
+	isIETFClient     bool
+	isDecoyClient    bool
+	isClosed         int32
+	runWaitGroup     *sync.WaitGroup
+	stopBroadcast    chan struct{}
+	obfuscationKey   [32]byte
+	peerModesMutex   sync.Mutex
+	peerModes        map[string]*peerMode
+	noncePRNG        *prng.PRNG
+	paddingPRNG      *prng.PRNG
+	decoyPacketCount int32
+	decoyBuffer      []byte
 }
 }
 
 
 type peerMode struct {
 type peerMode struct {
@@ -86,6 +129,8 @@ func (p *peerMode) isStale() bool {
 func NewObfuscatedPacketConn(
 func NewObfuscatedPacketConn(
 	conn net.PacketConn,
 	conn net.PacketConn,
 	isServer bool,
 	isServer bool,
+	isIETFClient bool,
+	isDecoyClient bool,
 	obfuscationKey string,
 	obfuscationKey string,
 	paddingSeed *prng.Seed) (*ObfuscatedPacketConn, error) {
 	paddingSeed *prng.Seed) (*ObfuscatedPacketConn, error) {
 
 
@@ -96,11 +141,13 @@ func NewObfuscatedPacketConn(
 	}
 	}
 
 
 	packetConn := &ObfuscatedPacketConn{
 	packetConn := &ObfuscatedPacketConn{
-		PacketConn:  conn,
-		isServer:    isServer,
-		peerModes:   make(map[string]*peerMode),
-		noncePRNG:   prng.NewPRNGWithSeed(nonceSeed),
-		paddingPRNG: prng.NewPRNGWithSeed(paddingSeed),
+		PacketConn:    conn,
+		isServer:      isServer,
+		isIETFClient:  isIETFClient,
+		isDecoyClient: isDecoyClient,
+		peerModes:     make(map[string]*peerMode),
+		noncePRNG:     prng.NewPRNGWithSeed(nonceSeed),
+		paddingPRNG:   prng.NewPRNGWithSeed(paddingSeed),
 	}
 	}
 
 
 	secret := []byte(obfuscationKey)
 	secret := []byte(obfuscationKey)
@@ -111,6 +158,12 @@ func NewObfuscatedPacketConn(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	if isDecoyClient {
+		packetConn.decoyPacketCount = int32(packetConn.paddingPRNG.Range(
+			MIN_DECOY_PACKETS, MAX_DECOY_PACKETS))
+		packetConn.decoyBuffer = make([]byte, MAX_PACKET_SIZE)
+	}
+
 	if isServer {
 	if isServer {
 
 
 		packetConn.runWaitGroup = new(sync.WaitGroup)
 		packetConn.runWaitGroup = new(sync.WaitGroup)
@@ -186,6 +239,55 @@ func (conn *ObfuscatedPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
 
 
 func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, bool, error) {
 func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, bool, error) {
 
 
+	for {
+		n, addr, isIETF, err := conn.readPacket(p)
+
+		// When enabled, and when a packet is received, sometimes immediately
+		// respond with a decoy packet, which is Sentirely random. Sending a
+		// small number of these packets early in the connection is intended
+		// to frustrate simple traffic fingerprinting which looks for a
+		// certain number of packets client->server, followed by a certain
+		// number of packets server->client, and so on.
+		//
+		// TODO: use a more sophisticated distribution; configure via tactics
+		// parameters; add server-side decoy packet injection.
+		//
+		// See also:
+		//
+		// Tor Project's Sharknado concept:
+		// https://gitlab.torproject.org/legacy/trac/-/issues/30716#note_2326086
+		//
+		// Lantern's OQUIC specification:
+		// https://github.com/getlantern/quicwrapper/blob/master/OQUIC.md
+
+		if conn.isIETFClient && conn.isDecoyClient {
+			count := atomic.LoadInt32(&conn.decoyPacketCount)
+			if count > 0 && conn.paddingPRNG.FlipCoin() {
+
+				if atomic.CompareAndSwapInt32(&conn.decoyPacketCount, count, count-1) {
+
+					packetSize := conn.paddingPRNG.Range(
+						1, getMaxPreDiscoveryPacketSize(addr))
+
+					// decoyBuffer is all zeros, so the QUIC Fixed Bit is zero.
+					// Ignore any errors when writing decoy packets.
+					_, _ = conn.WriteTo(conn.decoyBuffer[:packetSize], addr)
+				}
+			}
+		}
+
+		// Ignore/drop packets with an invalid QUIC Fixed Bit (see RFC 9000,
+		// Packet Formats).
+		if err == nil && (isIETF || conn.isIETFClient) && n > 0 && (p[0]&0x40) == 0 {
+			continue
+		}
+
+		return n, addr, isIETF, err
+	}
+}
+
+func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, error) {
+
 	n, addr, err := conn.PacketConn.ReadFrom(p)
 	n, addr, err := conn.PacketConn.ReadFrom(p)
 
 
 	// Data is processed even when err != nil, as ReadFrom may return both
 	// Data is processed even when err != nil, as ReadFrom may return both
@@ -199,7 +301,7 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 	// only when the function returns no error.
 	// only when the function returns no error.
 
 
 	isObfuscated := true
 	isObfuscated := true
-	var isIETF bool
+	isIETF := true
 	var address string
 	var address string
 	var firstFlowPacket bool
 	var firstFlowPacket bool
 	var lastPacketTime time.Time
 	var lastPacketTime time.Time
@@ -245,7 +347,7 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 
 
 			// Without addr, the mode cannot be determined.
 			// Without addr, the mode cannot be determined.
 			if addr == nil {
 			if addr == nil {
-				return n, addr, false, newTemporaryNetError(errors.Tracef("missing addr"))
+				return n, addr, true, newTemporaryNetError(errors.Tracef("missing addr"))
 			}
 			}
 
 
 			conn.peerModesMutex.Lock()
 			conn.peerModesMutex.Lock()
@@ -280,6 +382,10 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 
 
 			isIETF = mode.isIETF
 			isIETF = mode.isIETF
 			conn.peerModesMutex.Unlock()
 			conn.peerModesMutex.Unlock()
+
+		} else {
+
+			isIETF = conn.isIETFClient
 		}
 		}
 
 
 		if isObfuscated {
 		if isObfuscated {
@@ -288,19 +394,23 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 			// avoids allocting a buffer.
 			// avoids allocting a buffer.
 
 
 			if n < (NONCE_SIZE + 1) {
 			if n < (NONCE_SIZE + 1) {
-				return n, addr, false, newTemporaryNetError(errors.Tracef(
+				return n, addr, true, newTemporaryNetError(errors.Tracef(
 					"unexpected obfuscated QUIC packet length: %d", n))
 					"unexpected obfuscated QUIC packet length: %d", n))
 			}
 			}
 
 
 			cipher, err := chacha20.NewCipher(conn.obfuscationKey[:], p[0:NONCE_SIZE])
 			cipher, err := chacha20.NewCipher(conn.obfuscationKey[:], p[0:NONCE_SIZE])
 			if err != nil {
 			if err != nil {
-				return n, addr, false, errors.Trace(err)
+				return n, addr, true, errors.Trace(err)
 			}
 			}
 			cipher.XORKeyStream(p[NONCE_SIZE:], p[NONCE_SIZE:])
 			cipher.XORKeyStream(p[NONCE_SIZE:], p[NONCE_SIZE:])
 
 
+			// The padding length check allows legacy gQUIC padding to exceed
+			// its 64 byte maximum, as we don't yet know if this is gQUIC or
+			// IETF QUIC.
+
 			paddingLen := int(p[NONCE_SIZE])
 			paddingLen := int(p[NONCE_SIZE])
-			if paddingLen > MAX_PADDING || paddingLen > n-(NONCE_SIZE+1) {
-				return n, addr, false, newTemporaryNetError(errors.Tracef(
+			if paddingLen > MAX_PADDING_SIZE || paddingLen > n-(NONCE_SIZE+1) {
+				return n, addr, true, newTemporaryNetError(errors.Tracef(
 					"unexpected padding length: %d, %d", paddingLen, n))
 					"unexpected padding length: %d, %d", paddingLen, n))
 			}
 			}
 
 
@@ -309,6 +419,25 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 
 
 			if conn.isServer && firstFlowPacket {
 			if conn.isServer && firstFlowPacket {
 				isIETF = isIETFQUICClientHello(p[0:n])
 				isIETF = isIETFQUICClientHello(p[0:n])
+
+				// When an obfuscated packet looks like neither IETF nor
+				// gQUIC, force it through the IETF code path which will
+				// perform anti-probing check before sending any response
+				// packet. The gQUIC stack may respond with a version
+				// negotiation packet.
+				//
+				// Ensure that mode.isIETF is set to true before returning,
+				// so subsequent packets in the same flow are also forced
+				// through the same anti-probing code path.
+				//
+				// Limitation: the following race condition check is not
+				// consistent with this constraint. This will be resolved by
+				// disabling gQUIC or once gQUIC is ultimatel retired.
+
+				if !isIETF && !isGQUICClientHello(p[0:n]) {
+					isIETF = true
+				}
+
 				conn.peerModesMutex.Lock()
 				conn.peerModesMutex.Lock()
 				mode, ok := conn.peerModes[address]
 				mode, ok := conn.peerModes[address]
 
 
@@ -318,12 +447,27 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 				if !ok || mode.isObfuscated != true || mode.isIETF != false ||
 				if !ok || mode.isObfuscated != true || mode.isIETF != false ||
 					mode.lastPacketTime != lastPacketTime {
 					mode.lastPacketTime != lastPacketTime {
 					conn.peerModesMutex.Unlock()
 					conn.peerModesMutex.Unlock()
-					return n, addr, false, newTemporaryNetError(
+					return n, addr, true, newTemporaryNetError(
 						errors.Tracef("unexpected peer mode"))
 						errors.Tracef("unexpected peer mode"))
 				}
 				}
 
 
 				mode.isIETF = isIETF
 				mode.isIETF = isIETF
+
 				conn.peerModesMutex.Unlock()
 				conn.peerModesMutex.Unlock()
+
+				// Enforce the MIN_INITIAL_PACKET_SIZE size requirement for new flows.
+				//
+				// Limitations:
+				//
+				// - The Initial packet may be sent more than once, but we
+				//   only check the very first packet.
+				// - For session resumption, the first packet may be a
+				//   Handshake packet, not an Initial packet, and can be smaller.
+
+				if isIETF && n < MIN_INITIAL_PACKET_SIZE {
+					return n, addr, true, newTemporaryNetError(errors.Tracef(
+						"unexpected first QUIC packet length: %d", n))
+				}
 			}
 			}
 		}
 		}
 	}
 	}
@@ -333,7 +477,7 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 }
 }
 
 
 type obfuscatorBuffer struct {
 type obfuscatorBuffer struct {
-	buffer [MAX_OBFUSCATED_QUIC_IPV4_PACKET_SIZE]byte
+	buffer [MAX_PACKET_SIZE]byte
 }
 }
 
 
 var obfuscatorBufferPool = &sync.Pool{
 var obfuscatorBufferPool = &sync.Pool{
@@ -342,33 +486,32 @@ var obfuscatorBufferPool = &sync.Pool{
 	},
 	},
 }
 }
 
 
-func getMaxPacketSizes(addr net.Addr) (int, int) {
-	if udpAddr, ok := addr.(*net.UDPAddr); ok && udpAddr.IP.To4() == nil {
-		return MAX_QUIC_IPV6_PACKET_SIZE, MAX_OBFUSCATED_QUIC_IPV6_PACKET_SIZE
-	}
-	return MAX_QUIC_IPV4_PACKET_SIZE, MAX_OBFUSCATED_QUIC_IPV4_PACKET_SIZE
-}
-
 func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
 func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
 
 
 	n := len(p)
 	n := len(p)
 
 
 	isObfuscated := true
 	isObfuscated := true
+	isIETF := true
 
 
 	if conn.isServer {
 	if conn.isServer {
 
 
 		conn.peerModesMutex.Lock()
 		conn.peerModesMutex.Lock()
 		address := addr.String()
 		address := addr.String()
 		mode, ok := conn.peerModes[address]
 		mode, ok := conn.peerModes[address]
-		isObfuscated = ok && mode.isObfuscated
+		if ok {
+			isObfuscated = mode.isObfuscated
+			isIETF = mode.isIETF
+		}
 		conn.peerModesMutex.Unlock()
 		conn.peerModesMutex.Unlock()
+
+	} else {
+
+		isIETF = conn.isIETFClient
 	}
 	}
 
 
 	if isObfuscated {
 	if isObfuscated {
 
 
-		maxQUICPacketSize, maxObfuscatedPacketSize := getMaxPacketSizes(addr)
-
-		if n > maxQUICPacketSize {
+		if n > MAX_PACKET_SIZE {
 			return 0, newTemporaryNetError(errors.Tracef(
 			return 0, newTemporaryNetError(errors.Tracef(
 				"unexpected QUIC packet length: %d", n))
 				"unexpected QUIC packet length: %d", n))
 		}
 		}
@@ -391,18 +534,9 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 			nonce := buffer[0:NONCE_SIZE]
 			nonce := buffer[0:NONCE_SIZE]
 			conn.noncePRNG.Read(nonce)
 			conn.noncePRNG.Read(nonce)
 
 
-			// Obfuscated QUIC padding results in packets that exceed the
-			// QUIC max packet size of 1280.
-
-			maxPaddingLen := maxObfuscatedPacketSize - (n + (NONCE_SIZE + 1))
-			if maxPaddingLen < 0 {
-				maxPaddingLen = 0
-			}
-			if maxPaddingLen > MAX_PADDING {
-				maxPaddingLen = MAX_PADDING
-			}
+			maxPadding := getMaxPaddingSize(isIETF, addr, n)
 
 
-			paddingLen := conn.paddingPRNG.Intn(maxPaddingLen + 1)
+			paddingLen := conn.paddingPRNG.Intn(maxPadding + 1)
 			buffer[NONCE_SIZE] = uint8(paddingLen)
 			buffer[NONCE_SIZE] = uint8(paddingLen)
 
 
 			padding := buffer[(NONCE_SIZE + 1) : (NONCE_SIZE+1)+paddingLen]
 			padding := buffer[(NONCE_SIZE + 1) : (NONCE_SIZE+1)+paddingLen]
@@ -435,6 +569,75 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 	return n, err
 	return n, err
 }
 }
 
 
+func getMaxPreDiscoveryPacketSize(addr net.Addr) int {
+	maxPacketSize := MAX_PRE_DISCOVERY_PACKET_SIZE_IPV4
+	if udpAddr, ok := addr.(*net.UDPAddr); ok && udpAddr.IP.To4() == nil {
+		maxPacketSize = MAX_PRE_DISCOVERY_PACKET_SIZE_IPV6
+	}
+	return maxPacketSize
+}
+
+func getMaxPaddingSize(isIETF bool, addr net.Addr, packetSize int) int {
+
+	maxPacketSize := getMaxPreDiscoveryPacketSize(addr)
+
+	maxPadding := 0
+
+	if isIETF {
+
+		// quic-go starts with a maximum packet size of 1280, which is the
+		// IPv6 minimum MTU as well as very commonly supported for IPv4
+		// (quic-go may increase the maximum packet size via MTU discovery).
+		// Do not pad beyond that initial maximum size. As a result, padding
+		// is only added for smaller packets.
+		// OBFUSCATED_PACKET_SIZE_ADJUSTMENT is already factored in via
+		// Client/ServerInitalPacketPaddingAdjustment.
+
+		maxPadding = maxPacketSize - packetSize
+		if maxPadding < 0 {
+			maxPadding = 0
+		}
+		if maxPadding > MAX_PADDING_SIZE {
+			maxPadding = MAX_PADDING_SIZE
+		}
+
+	} else {
+
+		// Legacy gQUIC has a strict maximum packet size of 1280, and legacy
+		// obfuscation adds padding on top of that.
+
+		maxPadding = (maxPacketSize + NONCE_SIZE + 1 + MAX_GQUIC_PADDING_SIZE) - packetSize
+		if maxPadding < 0 {
+			maxPadding = 0
+		}
+		if maxPadding > MAX_GQUIC_PADDING_SIZE {
+			maxPadding = MAX_GQUIC_PADDING_SIZE
+		}
+	}
+
+	return maxPadding
+}
+
+func (conn *ObfuscatedPacketConn) serverMaxPacketSizeAdjustment(
+	addr net.Addr) int {
+
+	if !conn.isServer {
+		return 0
+	}
+
+	conn.peerModesMutex.Lock()
+	address := addr.String()
+	mode, ok := conn.peerModes[address]
+	isObfuscated := ok && mode.isObfuscated
+	conn.peerModesMutex.Unlock()
+
+	if isObfuscated {
+		return OBFUSCATED_MAX_PACKET_SIZE_ADJUSTMENT
+	}
+
+	return 0
+}
+
 func isQUICClientHello(buffer []byte) (bool, bool) {
 func isQUICClientHello(buffer []byte) (bool, bool) {
 
 
 	// As this function is called for every packet, it needs to be fast.
 	// As this function is called for every packet, it needs to be fast.
@@ -446,14 +649,14 @@ func isQUICClientHello(buffer []byte) (bool, bool) {
 
 
 	if isIETFQUICClientHello(buffer) {
 	if isIETFQUICClientHello(buffer) {
 		return true, true
 		return true, true
-	} else if isgQUICClientHello(buffer) {
+	} else if isGQUICClientHello(buffer) {
 		return true, false
 		return true, false
 	}
 	}
 
 
 	return false, false
 	return false, false
 }
 }
 
 
-func isgQUICClientHello(buffer []byte) bool {
+func isGQUICClientHello(buffer []byte) bool {
 
 
 	// In all currently supported versions, the first client packet contains
 	// In all currently supported versions, the first client packet contains
 	// the "CHLO" tag at one of the following offsets. The offset can vary for
 	// the "CHLO" tag at one of the following offsets. The offset can vary for

+ 138 - 7
psiphon/common/quic/quic.go

@@ -1,3 +1,4 @@
+//go:build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 
 
 /*
 /*
@@ -56,6 +57,7 @@ import (
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go"
@@ -87,17 +89,31 @@ var supportedVersionNumbers = map[string]uint32{
 	protocol.QUIC_VERSION_V1:            ietfQUIC1VersionNumber,
 	protocol.QUIC_VERSION_V1:            ietfQUIC1VersionNumber,
 	protocol.QUIC_VERSION_RANDOMIZED_V1: ietfQUIC1VersionNumber,
 	protocol.QUIC_VERSION_RANDOMIZED_V1: ietfQUIC1VersionNumber,
 	protocol.QUIC_VERSION_OBFUSCATED_V1: uint32(ietfQUIC1VersionNumber),
 	protocol.QUIC_VERSION_OBFUSCATED_V1: uint32(ietfQUIC1VersionNumber),
+	protocol.QUIC_VERSION_DECOY_V1:      uint32(ietfQUIC1VersionNumber),
 }
 }
 
 
 func isObfuscated(quicVersion string) bool {
 func isObfuscated(quicVersion string) bool {
 	return quicVersion == protocol.QUIC_VERSION_OBFUSCATED ||
 	return quicVersion == protocol.QUIC_VERSION_OBFUSCATED ||
-		quicVersion == protocol.QUIC_VERSION_OBFUSCATED_V1
+		quicVersion == protocol.QUIC_VERSION_OBFUSCATED_V1 ||
+		quicVersion == protocol.QUIC_VERSION_DECOY_V1
+}
+
+func isDecoy(quicVersion string) bool {
+	return quicVersion == protocol.QUIC_VERSION_DECOY_V1
 }
 }
 
 
 func isClientHelloRandomized(quicVersion string) bool {
 func isClientHelloRandomized(quicVersion string) bool {
 	return quicVersion == protocol.QUIC_VERSION_RANDOMIZED_V1
 	return quicVersion == protocol.QUIC_VERSION_RANDOMIZED_V1
 }
 }
 
 
+func isIETF(quicVersion string) bool {
+	versionNumber, ok := supportedVersionNumbers[quicVersion]
+	if !ok {
+		return false
+	}
+	return isIETFVersion(versionNumber)
+}
+
 func isIETFVersion(versionNumber uint32) bool {
 func isIETFVersion(versionNumber uint32) bool {
 	return versionNumber == ietfQUIC1VersionNumber
 	return versionNumber == ietfQUIC1VersionNumber
 }
 }
@@ -112,11 +128,13 @@ var serverIdleTimeout = SERVER_IDLE_TIMEOUT
 // Listener is a net.Listener.
 // Listener is a net.Listener.
 type Listener struct {
 type Listener struct {
 	*muxListener
 	*muxListener
+	clientRandomHistory *obfuscator.SeedHistory
 }
 }
 
 
 // Listen creates a new Listener.
 // Listen creates a new Listener.
 func Listen(
 func Listen(
 	logger common.Logger,
 	logger common.Logger,
+	irregularTunnelLogger func(string, error, common.LogFields),
 	address string,
 	address string,
 	obfuscationKey string) (net.Listener, error) {
 	obfuscationKey string) (net.Listener, error) {
 
 
@@ -148,22 +166,72 @@ func Listen(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
-	obfuscatedPacketConn, err := NewObfuscatedPacketConn(udpConn, true, obfuscationKey, seed)
+	obfuscatedPacketConn, err := NewObfuscatedPacketConn(
+		udpConn, true, false, false, obfuscationKey, seed)
 	if err != nil {
 	if err != nil {
 		udpConn.Close()
 		udpConn.Close()
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	// QUIC clients must prove knowledge of the obfuscated key via a message
+	// sent in the TLS ClientHello random field, or receive no UDP packets
+	// back from the server. This anti-probing mechanism is implemented using
+	// the existing Passthrough message and SeedHistory replay detection
+	// mechanisms. The replay history TTL is set to the validity period of
+	// the passthrough message.
+	//
+	// Irregular events are logged for invalid client activity.
+
+	clientRandomHistory := obfuscator.NewSeedHistory(
+		&obfuscator.SeedHistoryConfig{SeedTTL: obfuscator.TLS_PASSTHROUGH_TIME_PERIOD})
+
+	verifyClientHelloRandom := func(remoteAddr net.Addr, clientHelloRandom []byte) bool {
+
+		ok := obfuscator.VerifyTLSPassthroughMessage(
+			true, obfuscationKey, clientHelloRandom)
+		if !ok {
+			irregularTunnelLogger(
+				common.IPAddressFromAddr(remoteAddr),
+				errors.TraceNew("invalid client random message"),
+				nil)
+			return false
+		}
+
+		// Replay history is set to non-strict mode, allowing for a legitimate
+		// client to resend its Initial packet, as may happen. Since the
+		// source _port_ should be the same as the source IP in this case, we use
+		// the full IP:port value as the client address from which a replay is
+		// allowed.
+		//
+		// The non-strict case where ok is true and logFields is not nil is
+		// ignored, and nothing is logged in that scenario.
+
+		ok, logFields := clientRandomHistory.AddNew(
+			false, remoteAddr.String(), "client-hello-random", clientHelloRandom)
+		if !ok && logFields != nil {
+			irregularTunnelLogger(
+				common.IPAddressFromAddr(remoteAddr),
+				errors.TraceNew("duplicate client random message"),
+				*logFields)
+		}
+
+		return ok
+	}
+
 	// Note that, due to nature of muxListener, full accepts may happen before
 	// Note that, due to nature of muxListener, full accepts may happen before
 	// return and caller calls Accept.
 	// return and caller calls Accept.
 
 
-	listener, err := newMuxListener(logger, obfuscatedPacketConn, tlsCertificate)
+	muxListener, err := newMuxListener(
+		logger, verifyClientHelloRandom, obfuscatedPacketConn, tlsCertificate)
 	if err != nil {
 	if err != nil {
 		obfuscatedPacketConn.Close()
 		obfuscatedPacketConn.Close()
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
-	return &Listener{muxListener: listener}, nil
+	return &Listener{
+		muxListener:         muxListener,
+		clientRandomHistory: clientRandomHistory,
+	}, nil
 }
 }
 
 
 // Accept returns a net.Conn that wraps a single QUIC session and stream. The
 // Accept returns a net.Conn that wraps a single QUIC session and stream. The
@@ -210,7 +278,7 @@ func Dial(
 		return nil, errors.TraceNew("missing client hello randomization values")
 		return nil, errors.TraceNew("missing client hello randomization values")
 	}
 	}
 
 
-	if isObfuscated(quicVersion) && (obfuscationPaddingSeed == nil || obfuscationKey == "") {
+	if isObfuscated(quicVersion) && (obfuscationPaddingSeed == nil) || obfuscationKey == "" {
 		return nil, errors.TraceNew("missing obfuscation values")
 		return nil, errors.TraceNew("missing obfuscation values")
 	}
 	}
 
 
@@ -226,13 +294,48 @@ func Dial(
 		return nil, errors.Tracef("invalid destination port: %d", remoteAddr.Port)
 		return nil, errors.Tracef("invalid destination port: %d", remoteAddr.Port)
 	}
 	}
 
 
+	maxPacketSizeAdjustment := 0
+
 	if isObfuscated(quicVersion) {
 	if isObfuscated(quicVersion) {
 		obfuscatedPacketConn, err := NewObfuscatedPacketConn(
 		obfuscatedPacketConn, err := NewObfuscatedPacketConn(
-			packetConn, false, obfuscationKey, obfuscationPaddingSeed)
+			packetConn,
+			false,
+			isIETFVersion(versionNumber),
+			isDecoy(quicVersion),
+			obfuscationKey,
+			obfuscationPaddingSeed)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 		packetConn = obfuscatedPacketConn
 		packetConn = obfuscatedPacketConn
+
+		// Reserve space for packet obfuscation overhead so that quic-go will
+		// continue to produce packets of max size 1280.
+		maxPacketSizeAdjustment = OBFUSCATED_MAX_PACKET_SIZE_ADJUSTMENT
+	}
+
+	// As an anti-probing measure, QUIC clients must prove knowledge of the
+	// server obfuscation key in the first client packet sent to the server. In
+	// the case of QUIC, the first packet, the Initial packet, contains a TLS
+	// Client Hello, and we set the client random field to a value that both
+	// proves knowledge of the obfuscation key and is indistiguishable from
+	// random. This is the same "passthrough" technique used for TLS, although
+	// for QUIC the server simply doesn't respond to any packets instead of
+	// passing traffic through to a different server.
+	//
+	// Limitation: the legacy gQUIC implementation does not support this
+	// anti-probling measure; gQUIC must be disabled to ensure no response
+	// from the server.
+
+	var getClientHelloRandom func() ([]byte, error)
+	if obfuscationKey != "" {
+		getClientHelloRandom = func() ([]byte, error) {
+			random, err := obfuscator.MakeTLSPassthroughMessage(true, obfuscationKey)
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
+			return random, nil
+		}
 	}
 	}
 
 
 	session, err := dialQUIC(
 	session, err := dialQUIC(
@@ -242,6 +345,8 @@ func Dial(
 		quicSNIAddress,
 		quicSNIAddress,
 		versionNumber,
 		versionNumber,
 		clientHelloSeed,
 		clientHelloSeed,
+		getClientHelloRandom,
+		maxPacketSizeAdjustment,
 		false)
 		false)
 	if err != nil {
 	if err != nil {
 		packetConn.Close()
 		packetConn.Close()
@@ -574,6 +679,8 @@ func (t *QUICTransporter) dialQUIC() (retSession quicSession, retErr error) {
 		t.quicSNIAddress,
 		t.quicSNIAddress,
 		versionNumber,
 		versionNumber,
 		t.clientHelloSeed,
 		t.clientHelloSeed,
+		nil,
+		0,
 		true)
 		true)
 	if err != nil {
 	if err != nil {
 		packetConn.Close()
 		packetConn.Close()
@@ -735,6 +842,8 @@ func dialQUIC(
 	quicSNIAddress string,
 	quicSNIAddress string,
 	versionNumber uint32,
 	versionNumber uint32,
 	clientHelloSeed *prng.Seed,
 	clientHelloSeed *prng.Seed,
+	getClientHelloRandom func() ([]byte, error),
+	clientMaxPacketSizeAdjustment int,
 	dialEarly bool) (quicSession, error) {
 	dialEarly bool) (quicSession, error) {
 
 
 	if isIETFVersion(versionNumber) {
 	if isIETFVersion(versionNumber) {
@@ -744,7 +853,9 @@ func dialQUIC(
 			KeepAlive:            true,
 			KeepAlive:            true,
 			Versions: []ietf_quic.VersionNumber{
 			Versions: []ietf_quic.VersionNumber{
 				ietf_quic.VersionNumber(versionNumber)},
 				ietf_quic.VersionNumber(versionNumber)},
-			ClientHelloSeed: clientHelloSeed,
+			ClientHelloSeed:               clientHelloSeed,
+			GetClientHelloRandom:          getClientHelloRandom,
+			ClientMaxPacketSizeAdjustment: clientMaxPacketSizeAdjustment,
 		}
 		}
 
 
 		deadline, ok := ctx.Deadline()
 		deadline, ok := ctx.Deadline()
@@ -936,6 +1047,7 @@ type muxListener struct {
 
 
 func newMuxListener(
 func newMuxListener(
 	logger common.Logger,
 	logger common.Logger,
+	verifyClientHelloRandom func(net.Addr, []byte) bool,
 	conn *ObfuscatedPacketConn,
 	conn *ObfuscatedPacketConn,
 	tlsCertificate tls.Certificate) (*muxListener, error) {
 	tlsCertificate tls.Certificate) (*muxListener, error) {
 
 
@@ -966,6 +1078,25 @@ func newMuxListener(
 		MaxIncomingStreams:    1,
 		MaxIncomingStreams:    1,
 		MaxIncomingUniStreams: -1,
 		MaxIncomingUniStreams: -1,
 		KeepAlive:             true,
 		KeepAlive:             true,
+
+		// The quic-go server may respond with a version negotiation packet
+		// before reaching the Initial packet processing with its
+		// anti-probing defense. This may happen even for a malformed packet.
+		// To prevent all responses to probers, version negotiation is
+		// disabled, which disables sending these packets. The fact that the
+		// server does not issue version negotiation packets may be a
+		// fingerprint itself, but, regardless, probers cannot ellicit any
+		// reponse from the server without providing a well-formed Initial
+		// packet with a valid Client Hello random value.
+		//
+		// Limitation: once version negotiate is required, the order of
+		// quic-go operations may need to be changed in order to first check
+		// the Initial/Client Hello, and then issue any required version
+		// negotiation packet.
+		DisableVersionNegotiationPackets: true,
+
+		VerifyClientHelloRandom:       verifyClientHelloRandom,
+		ServerMaxPacketSizeAdjustment: conn.serverMaxPacketSizeAdjustment,
 	}
 	}
 
 
 	il, err := ietf_quic.Listen(listener.ietfQUICConn, tlsConfig, ietfQUICConfig)
 	il, err := ietf_quic.Listen(listener.ietfQUICConn, tlsConfig, ietfQUICConfig)

+ 80 - 5
psiphon/common/quic/quic_test.go

@@ -1,3 +1,4 @@
+//go:build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 // +build !PSIPHON_DISABLE_QUIC
 
 
 /*
 /*
@@ -23,6 +24,7 @@ package quic
 
 
 import (
 import (
 	"context"
 	"context"
+	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
 	"runtime"
 	"runtime"
@@ -31,6 +33,7 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/sync/errgroup"
@@ -38,13 +41,19 @@ import (
 
 
 func TestQUIC(t *testing.T) {
 func TestQUIC(t *testing.T) {
 	for quicVersion := range supportedVersionNumbers {
 	for quicVersion := range supportedVersionNumbers {
-		t.Run(quicVersion, func(t *testing.T) {
-			runQUIC(t, quicVersion)
+		t.Run(fmt.Sprintf("%s", quicVersion), func(t *testing.T) {
+			runQUIC(t, quicVersion, false)
 		})
 		})
+		if isIETF(quicVersion) {
+			t.Run(fmt.Sprintf("%s (invoke anti-probing)", quicVersion), func(t *testing.T) {
+				runQUIC(t, quicVersion, true)
+			})
+		}
 	}
 	}
 }
 }
 
 
-func runQUIC(t *testing.T, quicVersion string) {
+func runQUIC(
+	t *testing.T, quicVersion string, invokeAntiProbing bool) {
 
 
 	initGoroutines := getGoroutines()
 	initGoroutines := getGoroutines()
 
 
@@ -64,9 +73,16 @@ func runQUIC(t *testing.T, quicVersion string) {
 	// connection termination packets.
 	// connection termination packets.
 	serverIdleTimeout = 1 * time.Second
 	serverIdleTimeout = 1 * time.Second
 
 
+	irregularTunnelLogger := func(_ string, err error, _ common.LogFields) {
+		if !invokeAntiProbing {
+			t.Errorf("unexpected irregular tunnel event: %v", err)
+		}
+	}
+
 	obfuscationKey := prng.HexString(32)
 	obfuscationKey := prng.HexString(32)
 
 
-	listener, err := Listen(nil, "127.0.0.1:0", obfuscationKey)
+	listener, err := Listen(
+		nil, irregularTunnelLogger, "127.0.0.1:0", obfuscationKey)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("Listen failed: %s", err)
 		t.Fatalf("Listen failed: %s", err)
 	}
 	}
@@ -77,6 +93,12 @@ func runQUIC(t *testing.T, quicVersion string) {
 
 
 	testGroup.Go(func() error {
 	testGroup.Go(func() error {
 
 
+		if invokeAntiProbing {
+			// The quic-go server can still handshake new sessions even if
+			// Accept isn't called.
+			return nil
+		}
+
 		var serverGroup errgroup.Group
 		var serverGroup errgroup.Group
 
 
 		for i := 0; i < clients; i++ {
 		for i := 0; i < clients; i++ {
@@ -130,6 +152,12 @@ func runQUIC(t *testing.T, quicVersion string) {
 				return errors.Trace(err)
 				return errors.Trace(err)
 			}
 			}
 
 
+			clientObfuscationKey := obfuscationKey
+			if invokeAntiProbing {
+				clientObfuscationKey = prng.HexString(32)
+				packetConn = &countReadsConn{PacketConn: packetConn}
+			}
+
 			obfuscationPaddingSeed, err := prng.NewSeed()
 			obfuscationPaddingSeed, err := prng.NewSeed()
 			if err != nil {
 			if err != nil {
 				return errors.Trace(err)
 				return errors.Trace(err)
@@ -150,8 +178,27 @@ func runQUIC(t *testing.T, quicVersion string) {
 				serverAddress,
 				serverAddress,
 				quicVersion,
 				quicVersion,
 				clientHelloSeed,
 				clientHelloSeed,
-				obfuscationKey,
+				clientObfuscationKey,
 				obfuscationPaddingSeed)
 				obfuscationPaddingSeed)
+
+			if invokeAntiProbing {
+
+				if err == nil {
+					return errors.TraceNew(
+						"unexpected dial success with invalid client hello random")
+				}
+
+				readCount := packetConn.(*countReadsConn).getReadCount()
+
+				if readCount > 0 {
+					return errors.Tracef(
+						"unexpected %d read packets with invalid client hello random",
+						readCount)
+				}
+
+				return nil
+			}
+
 			if err != nil {
 			if err != nil {
 				return errors.Trace(err)
 				return errors.Trace(err)
 			}
 			}
@@ -206,6 +253,9 @@ func runQUIC(t *testing.T, quicVersion string) {
 
 
 	bytes := atomic.LoadInt64(&serverReceivedBytes)
 	bytes := atomic.LoadInt64(&serverReceivedBytes)
 	expectedBytes := int64(clients * bytesToSend)
 	expectedBytes := int64(clients * bytesToSend)
+	if invokeAntiProbing {
+		expectedBytes = 0
+	}
 	if bytes != expectedBytes {
 	if bytes != expectedBytes {
 		t.Errorf("unexpected serverReceivedBytes: %d vs. %d", bytes, expectedBytes)
 		t.Errorf("unexpected serverReceivedBytes: %d vs. %d", bytes, expectedBytes)
 	}
 	}
@@ -265,6 +315,23 @@ func runQUIC(t *testing.T, quicVersion string) {
 	}
 	}
 }
 }
 
 
+type countReadsConn struct {
+	net.PacketConn
+	readCount int32
+}
+
+func (conn *countReadsConn) ReadFrom(p []byte) (int, net.Addr, error) {
+	n, addr, err := conn.PacketConn.ReadFrom(p)
+	if n > 0 {
+		atomic.AddInt32(&conn.readCount, 1)
+	}
+	return n, addr, err
+}
+
+func (conn *countReadsConn) getReadCount() int {
+	return int(atomic.LoadInt32(&conn.readCount))
+}
+
 func getGoroutines() []runtime.StackRecord {
 func getGoroutines() []runtime.StackRecord {
 	n, _ := runtime.GoroutineProfile(nil)
 	n, _ := runtime.GoroutineProfile(nil)
 	r := make([]runtime.StackRecord, n)
 	r := make([]runtime.StackRecord, n)
@@ -318,6 +385,14 @@ func checkDanglingGoroutines(
 					break
 					break
 				}
 				}
 
 
+				// This goroutine, created by Listener.clientRandomHistory,
+				// terminates nondeterministically, based on garbage
+				// collection. Skip it.
+				if strings.Contains(funcNames[i], "go-cache-lru.(*janitor).Run") {
+					skip = true
+					break
+				}
+
 				for _, expected := range expectedDanglingGoroutines {
 				for _, expected := range expectedDanglingGoroutines {
 					if strings.Contains(funcNames[i], expected) {
 					if strings.Contains(funcNames[i], expected) {
 						isExpected = true
 						isExpected = true

+ 5 - 0
psiphon/server/tunnelServer.go

@@ -164,6 +164,11 @@ func (server *TunnelServer) Run() error {
 
 
 			listener, err = quic.Listen(
 			listener, err = quic.Listen(
 				CommonLogger(log),
 				CommonLogger(log),
+				func(clientAddress string, err error, logFields common.LogFields) {
+					logIrregularTunnel(
+						support, tunnelProtocol, listenPort, clientAddress,
+						errors.Trace(err), LogFields(logFields))
+				},
 				localAddress,
 				localAddress,
 				support.Config.ObfuscatedSSHKey)
 				support.Config.ObfuscatedSSHKey)
 
 

+ 7 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-15/common.go

@@ -748,6 +748,13 @@ type ExtraConfig struct {
 	// [Psiphon]
 	// [Psiphon]
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	ClientHelloPRNG *prng.PRNG
 	ClientHelloPRNG *prng.PRNG
+
+	// [Psiphon]
+	// GetClientHelloRandom is used to supply a specific value in the TLS
+	// Client Hello random field. This is used to send an anti-probing
+	// message, indistinguishable from random, that proves knowlegde of a
+	// shared secret key.
+	GetClientHelloRandom func() ([]byte, error)
 }
 }
 
 
 // Clone clones.
 // Clone clones.

+ 21 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-15/conn.go

@@ -1027,6 +1027,27 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (int, error) {
 	return c.writeRecordLocked(typ, data)
 	return c.writeRecordLocked(typ, data)
 }
 }
 
 
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	if len(data) < 1 {
+		return nil, errors.New("tls: missing message type")
+	}
+	if data[0] != typeClientHello {
+		return nil, errors.New("tls: unexpected message type")
+	}
+
+	// Unlike readHandshake, m is not retained and so making a copy of the
+	// input data is not necessary.
+
+	var m clientHelloMsg
+	if !m.unmarshal(data) {
+		return nil, errors.New("tls: unexpected message")
+	}
+
+	return m.random, nil
+}
+
 // readHandshake reads the next handshake message from
 // readHandshake reads the next handshake message from
 // the record layer.
 // the record layer.
 func (c *Conn) readHandshake() (interface{}, error) {
 func (c *Conn) readHandshake() (interface{}, error) {

+ 18 - 3
vendor/github.com/Psiphon-Labs/qtls-go1-15/handshake_client.go

@@ -94,6 +94,16 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 	// [Psiphon]
 	// [Psiphon]
 	if c.extraConfig != nil {
 	if c.extraConfig != nil {
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
+		if c.extraConfig.GetClientHelloRandom != nil {
+			helloRandom, err := c.extraConfig.GetClientHelloRandom()
+			if err == nil && len(helloRandom) != 32 {
+				err = errors.New("invalid length")
+			}
+			if err != nil {
+				return nil, nil, errors.New("tls: GetClientHelloRandom failed: " + err.Error())
+			}
+			copy(hello.random, helloRandom)
+		}
 	}
 	}
 
 
 	if c.handshakes > 0 {
 	if c.handshakes > 0 {
@@ -121,9 +131,14 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 		}
 		}
 	}
 	}
 
 
-	_, err := io.ReadFull(config.rand(), hello.random)
-	if err != nil {
-		return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+	// [Psiphon]
+	var err error
+	if c.extraConfig == nil || c.extraConfig.GetClientHelloRandom == nil {
+
+		_, err := io.ReadFull(config.rand(), hello.random)
+		if err != nil {
+			return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+		}
 	}
 	}
 
 
 	// A random session ID is used to detect when the server accepted a ticket
 	// A random session ID is used to detect when the server accepted a ticket

+ 7 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-16/common.go

@@ -762,6 +762,13 @@ type ExtraConfig struct {
 	// [Psiphon]
 	// [Psiphon]
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	ClientHelloPRNG *prng.PRNG
 	ClientHelloPRNG *prng.PRNG
+
+	// [Psiphon]
+	// GetClientHelloRandom is used to supply a specific value in the TLS
+	// Client Hello random field. This is used to send an anti-probing
+	// message, indistinguishable from random, that proves knowlegde of a
+	// shared secret key.
+	GetClientHelloRandom func() ([]byte, error)
 }
 }
 
 
 // Clone clones.
 // Clone clones.

+ 21 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-16/conn.go

@@ -1043,6 +1043,27 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (int, error) {
 	return c.writeRecordLocked(typ, data)
 	return c.writeRecordLocked(typ, data)
 }
 }
 
 
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	if len(data) < 1 {
+		return nil, errors.New("tls: missing message type")
+	}
+	if data[0] != typeClientHello {
+		return nil, errors.New("tls: unexpected message type")
+	}
+
+	// Unlike readHandshake, m is not retained and so making a copy of the
+	// input data is not necessary.
+
+	var m clientHelloMsg
+	if !m.unmarshal(data) {
+		return nil, errors.New("tls: unexpected message")
+	}
+
+	return m.random, nil
+}
+
 // readHandshake reads the next handshake message from
 // readHandshake reads the next handshake message from
 // the record layer.
 // the record layer.
 func (c *Conn) readHandshake() (interface{}, error) {
 func (c *Conn) readHandshake() (interface{}, error) {

+ 18 - 3
vendor/github.com/Psiphon-Labs/qtls-go1-16/handshake_client.go

@@ -95,6 +95,16 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 	// [Psiphon]
 	// [Psiphon]
 	if c.extraConfig != nil {
 	if c.extraConfig != nil {
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
+		if c.extraConfig.GetClientHelloRandom != nil {
+			helloRandom, err := c.extraConfig.GetClientHelloRandom()
+			if err == nil && len(helloRandom) != 32 {
+				err = errors.New("invalid length")
+			}
+			if err != nil {
+				return nil, nil, errors.New("tls: GetClientHelloRandom failed: " + err.Error())
+			}
+			copy(hello.random, helloRandom)
+		}
 	}
 	}
 
 
 	if c.handshakes > 0 {
 	if c.handshakes > 0 {
@@ -122,9 +132,14 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 		}
 		}
 	}
 	}
 
 
-	_, err := io.ReadFull(config.rand(), hello.random)
-	if err != nil {
-		return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+	// [Psiphon]
+	var err error
+	if c.extraConfig == nil || c.extraConfig.GetClientHelloRandom == nil {
+
+		_, err := io.ReadFull(config.rand(), hello.random)
+		if err != nil {
+			return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+		}
 	}
 	}
 
 
 	// A random session ID is used to detect when the server accepted a ticket
 	// A random session ID is used to detect when the server accepted a ticket

+ 7 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-17/common.go

@@ -788,6 +788,13 @@ type ExtraConfig struct {
 	// [Psiphon]
 	// [Psiphon]
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	// ClientHelloPRNG is used for Client Hello randomization and replay.
 	ClientHelloPRNG *prng.PRNG
 	ClientHelloPRNG *prng.PRNG
+
+	// [Psiphon]
+	// GetClientHelloRandom is used to supply a specific value in the TLS
+	// Client Hello random field. This is used to send an anti-probing
+	// message, indistinguishable from random, that proves knowlegde of a
+	// shared secret key.
+	GetClientHelloRandom func() ([]byte, error)
 }
 }
 
 
 // Clone clones.
 // Clone clones.

+ 21 - 0
vendor/github.com/Psiphon-Labs/qtls-go1-17/conn.go

@@ -1044,6 +1044,27 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (int, error) {
 	return c.writeRecordLocked(typ, data)
 	return c.writeRecordLocked(typ, data)
 }
 }
 
 
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	if len(data) < 1 {
+		return nil, errors.New("tls: missing message type")
+	}
+	if data[0] != typeClientHello {
+		return nil, errors.New("tls: unexpected message type")
+	}
+
+	// Unlike readHandshake, m is not retained and so making a copy of the
+	// input data is not necessary.
+
+	var m clientHelloMsg
+	if !m.unmarshal(data) {
+		return nil, errors.New("tls: unexpected message")
+	}
+
+	return m.random, nil
+}
+
 // readHandshake reads the next handshake message from
 // readHandshake reads the next handshake message from
 // the record layer.
 // the record layer.
 func (c *Conn) readHandshake() (interface{}, error) {
 func (c *Conn) readHandshake() (interface{}, error) {

+ 18 - 3
vendor/github.com/Psiphon-Labs/qtls-go1-17/handshake_client.go

@@ -97,6 +97,16 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 	// [Psiphon]
 	// [Psiphon]
 	if c.extraConfig != nil {
 	if c.extraConfig != nil {
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
 		hello.PRNG = c.extraConfig.ClientHelloPRNG
+		if c.extraConfig.GetClientHelloRandom != nil {
+			helloRandom, err := c.extraConfig.GetClientHelloRandom()
+			if err == nil && len(helloRandom) != 32 {
+				err = errors.New("invalid length")
+			}
+			if err != nil {
+				return nil, nil, errors.New("tls: GetClientHelloRandom failed: " + err.Error())
+			}
+			copy(hello.random, helloRandom)
+		}
 	}
 	}
 
 
 	if c.handshakes > 0 {
 	if c.handshakes > 0 {
@@ -123,9 +133,14 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 		hello.cipherSuites = append(hello.cipherSuites, suiteId)
 		hello.cipherSuites = append(hello.cipherSuites, suiteId)
 	}
 	}
 
 
-	_, err := io.ReadFull(config.rand(), hello.random)
-	if err != nil {
-		return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+	// [Psiphon]
+	var err error
+	if c.extraConfig == nil || c.extraConfig.GetClientHelloRandom == nil {
+
+		_, err = io.ReadFull(config.rand(), hello.random)
+		if err != nil {
+			return nil, nil, errors.New("tls: short read from Rand: " + err.Error())
+		}
 	}
 	}
 
 
 	// A random session ID is used to detect when the server accepted a ticket
 	// A random session ID is used to detect when the server accepted a ticket

+ 5 - 1
vendor/github.com/Psiphon-Labs/quic-go/config.go

@@ -121,6 +121,10 @@ func populateConfig(config *Config) *Config {
 		Tracer:                           config.Tracer,
 		Tracer:                           config.Tracer,
 
 
 		// [Psiphon]
 		// [Psiphon]
-		ClientHelloSeed: config.ClientHelloSeed,
+		ClientHelloSeed:               config.ClientHelloSeed,
+		GetClientHelloRandom:          config.GetClientHelloRandom,
+		VerifyClientHelloRandom:       config.VerifyClientHelloRandom,
+		ClientMaxPacketSizeAdjustment: config.ClientMaxPacketSizeAdjustment,
+		ServerMaxPacketSizeAdjustment: config.ServerMaxPacketSizeAdjustment,
 	}
 	}
 }
 }

+ 30 - 0
vendor/github.com/Psiphon-Labs/quic-go/interface.go

@@ -297,6 +297,36 @@ type Config struct {
 	// [Psiphon]
 	// [Psiphon]
 	// ClientHelloSeed is used for TLS Client Hello randomization and replay.
 	// ClientHelloSeed is used for TLS Client Hello randomization and replay.
 	ClientHelloSeed *prng.Seed
 	ClientHelloSeed *prng.Seed
+
+	// [Psiphon]
+	// GetClientHelloRandom is used by the QUIC client to supply a specific
+	// value in the TLS Client Hello random field. This is used to send an
+	// anti-probing message, indistinguishable from random, that proves
+	// knowlegde of a shared secret key.
+	GetClientHelloRandom func() ([]byte, error)
+
+	// [Psiphon]
+	// VerifyClientHelloRandom is used by the QUIC server to verify that the
+	// TLS Client Hello random field, supplied in the Initial packet for a
+	// new connection, was created using the shared secret key and is not
+	// replayed.
+	VerifyClientHelloRandom func(net.Addr, []byte) bool
+
+	// [Psiphon]
+	// ClientMaxPacketSizeAdjustment indicates that the max packet size should
+	// be reduced by the specified amount. This is used to reserve space for
+	// packet obfuscation overhead while remaining at or under the 1280
+	// initial target packet size as well as protocol.MaxPacketBufferSize,
+	// the maximum packet size under MTU discovery.
+	ClientMaxPacketSizeAdjustment int
+
+	// [Psiphon]
+	// ServerMaxPacketSizeAdjustment indicates that, for the flow associated
+	// with the given client address, the max packet size should be reduced
+	// by the specified amount. This is used to reserve space for packet
+	// obfuscation overhead while remaining at or under the 1280 target
+	// packet size. Must be set only for QUIC server configs.
+	ServerMaxPacketSizeAdjustment func(net.Addr) int
 }
 }
 
 
 // ConnectionState records basic details about a QUIC connection
 // ConnectionState records basic details about a QUIC connection

+ 2 - 0
vendor/github.com/Psiphon-Labs/quic-go/internal/handshake/crypto_setup.go

@@ -162,6 +162,7 @@ func NewCryptoSetupClient(
 	runner handshakeRunner,
 	runner handshakeRunner,
 	tlsConf *tls.Config,
 	tlsConf *tls.Config,
 	clientHelloSeed *prng.Seed,
 	clientHelloSeed *prng.Seed,
+	getClientHelloRandom func() ([]byte, error),
 	enable0RTT bool,
 	enable0RTT bool,
 	rttStats *utils.RTTStats,
 	rttStats *utils.RTTStats,
 	tracer logging.ConnectionTracer,
 	tracer logging.ConnectionTracer,
@@ -198,6 +199,7 @@ func NewCryptoSetupClient(
 
 
 	// [Psiphon]
 	// [Psiphon]
 	cs.extraConf.ClientHelloPRNG = clientHelloPRNG
 	cs.extraConf.ClientHelloPRNG = clientHelloPRNG
+	cs.extraConf.GetClientHelloRandom = getClientHelloRandom
 
 
 	cs.conn = qtls.Client(newConn(localAddr, remoteAddr, version), cs.tlsConf, cs.extraConf)
 	cs.conn = qtls.Client(newConn(localAddr, remoteAddr, version), cs.tlsConf, cs.extraConf)
 	return cs, clientHelloWritten
 	return cs, clientHelloWritten

+ 8 - 2
vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go115.go

@@ -1,5 +1,5 @@
-// +build go1.15
-// +build !go1.16
+//go:build go1.15 && !go1.16
+// +build go1.15,!go1.16
 
 
 package qtls
 package qtls
 
 
@@ -98,3 +98,9 @@ func CipherSuiteTLS13ByID(id uint16) *CipherSuiteTLS13 {
 		Hash:   cs.Hash,
 		Hash:   cs.Hash,
 	}
 	}
 }
 }
+
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	return qtls.ReadClientHelloRandom(data)
+}

+ 8 - 2
vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go116.go

@@ -1,5 +1,5 @@
-// +build go1.16
-// +build !go1.17
+//go:build go1.16 && !go1.17
+// +build go1.16,!go1.17
 
 
 package qtls
 package qtls
 
 
@@ -98,3 +98,9 @@ func CipherSuiteTLS13ByID(id uint16) *CipherSuiteTLS13 {
 		Hash:   cs.Hash,
 		Hash:   cs.Hash,
 	}
 	}
 }
 }
+
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	return qtls.ReadClientHelloRandom(data)
+}

+ 7 - 0
vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go117.go

@@ -1,3 +1,4 @@
+//go:build go1.17
 // +build go1.17
 // +build go1.17
 
 
 package qtls
 package qtls
@@ -97,3 +98,9 @@ func CipherSuiteTLS13ByID(id uint16) *CipherSuiteTLS13 {
 		Hash:   cs.Hash,
 		Hash:   cs.Hash,
 	}
 	}
 }
 }
+
+// [Psiphon]
+
+func ReadClientHelloRandom(data []byte) ([]byte, error) {
+	return qtls.ReadClientHelloRandom(data)
+}

+ 14 - 2
vendor/github.com/Psiphon-Labs/quic-go/packet_packer.go

@@ -111,7 +111,7 @@ func (p *packetContents) ToAckHandlerPacket(now time.Time, q *retransmissionQueu
 	}
 	}
 }
 }
 
 
-func getMaxPacketSize(addr net.Addr) protocol.ByteCount {
+func getMaxPacketSize(addr net.Addr, maxPacketSizeAdustment int) protocol.ByteCount {
 	maxSize := protocol.ByteCount(protocol.MinInitialPacketSize)
 	maxSize := protocol.ByteCount(protocol.MinInitialPacketSize)
 	// If this is not a UDP address, we don't know anything about the MTU.
 	// If this is not a UDP address, we don't know anything about the MTU.
 	// Use the minimum size of an Initial packet as the max packet size.
 	// Use the minimum size of an Initial packet as the max packet size.
@@ -121,6 +121,14 @@ func getMaxPacketSize(addr net.Addr) protocol.ByteCount {
 		} else {
 		} else {
 			maxSize = protocol.InitialPacketSizeIPv6
 			maxSize = protocol.InitialPacketSizeIPv6
 		}
 		}
+
+		// [Psiphon]
+		// Adjust the max packet size to allow for obfuscation overhead.
+		// TODO: internal/congestion.cubicSender continues to use initialMaxDatagramSize = protocol.InitialPacketSizeIPv4
+		if maxSize > protocol.ByteCount(maxPacketSizeAdustment) {
+			maxSize -= protocol.ByteCount(maxPacketSizeAdustment)
+		}
+
 	}
 	}
 	return maxSize
 	return maxSize
 }
 }
@@ -180,6 +188,10 @@ func newPacketPacker(
 	packetNumberManager packetNumberManager,
 	packetNumberManager packetNumberManager,
 	retransmissionQueue *retransmissionQueue,
 	retransmissionQueue *retransmissionQueue,
 	remoteAddr net.Addr, // only used for determining the max packet size
 	remoteAddr net.Addr, // only used for determining the max packet size
+
+	// [Psiphon]
+	maxPacketSizeAdustment int,
+
 	cryptoSetup sealingManager,
 	cryptoSetup sealingManager,
 	framer frameSource,
 	framer frameSource,
 	acks ackFrameSource,
 	acks ackFrameSource,
@@ -200,7 +212,7 @@ func newPacketPacker(
 		framer:              framer,
 		framer:              framer,
 		acks:                acks,
 		acks:                acks,
 		pnManager:           packetNumberManager,
 		pnManager:           packetNumberManager,
-		maxPacketSize:       getMaxPacketSize(remoteAddr),
+		maxPacketSize:       getMaxPacketSize(remoteAddr, maxPacketSizeAdustment),
 	}
 	}
 }
 }
 
 

+ 141 - 1
vendor/github.com/Psiphon-Labs/quic-go/server.go

@@ -15,6 +15,7 @@ import (
 	"github.com/Psiphon-Labs/quic-go/internal/handshake"
 	"github.com/Psiphon-Labs/quic-go/internal/handshake"
 	"github.com/Psiphon-Labs/quic-go/internal/protocol"
 	"github.com/Psiphon-Labs/quic-go/internal/protocol"
 	"github.com/Psiphon-Labs/quic-go/internal/qerr"
 	"github.com/Psiphon-Labs/quic-go/internal/qerr"
+	"github.com/Psiphon-Labs/quic-go/internal/qtls"
 	"github.com/Psiphon-Labs/quic-go/internal/utils"
 	"github.com/Psiphon-Labs/quic-go/internal/utils"
 	"github.com/Psiphon-Labs/quic-go/internal/wire"
 	"github.com/Psiphon-Labs/quic-go/internal/wire"
 	"github.com/Psiphon-Labs/quic-go/logging"
 	"github.com/Psiphon-Labs/quic-go/logging"
@@ -350,7 +351,17 @@ func (s *baseServer) handlePacketImpl(p *receivedPacket) bool /* is the buffer s
 	if !hdr.IsLongHeader {
 	if !hdr.IsLongHeader {
 		panic(fmt.Sprintf("misrouted packet: %#v", hdr))
 		panic(fmt.Sprintf("misrouted packet: %#v", hdr))
 	}
 	}
-	if hdr.Type == protocol.PacketTypeInitial && p.Size() < protocol.MinInitialPacketSize {
+
+	// [Psiphon]
+	// To accomodate additional messages, obfuscated QUIC packets may reserve
+	// significant space in the Initial packet and send less that 1200 QUIC
+	// bytes. In this configuration, the obfuscation layer enforces the
+	// anti-amplification 1200 byte rule, but it must be disabled here.
+	isObfuscated := s.config.ServerMaxPacketSizeAdjustment(p.remoteAddr) > 0
+
+	if !isObfuscated &&
+
+		hdr.Type == protocol.PacketTypeInitial && p.Size() < protocol.MinInitialPacketSize {
 		s.logger.Debugf("Dropping a packet that is too small to be a valid Initial (%d bytes)", p.Size())
 		s.logger.Debugf("Dropping a packet that is too small to be a valid Initial (%d bytes)", p.Size())
 		if s.config.Tracer != nil {
 		if s.config.Tracer != nil {
 			s.config.Tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeInitial, p.Size(), logging.PacketDropUnexpectedPacket)
 			s.config.Tracer.DroppedPacket(p.remoteAddr, logging.PacketTypeInitial, p.Size(), logging.PacketDropUnexpectedPacket)
@@ -392,6 +403,127 @@ func (s *baseServer) handlePacketImpl(p *receivedPacket) bool /* is the buffer s
 	return true
 	return true
 }
 }
 
 
+// [Psiphon]
+type stubCryptoSetup struct {
+	initialOpener handshake.LongHeaderOpener
+}
+
+var notSupported = errors.New("not supported")
+
+func (s *stubCryptoSetup) RunHandshake() {
+}
+
+func (s *stubCryptoSetup) Close() error {
+	return notSupported
+}
+
+func (s *stubCryptoSetup) ChangeConnectionID(protocol.ConnectionID) {
+}
+
+func (s *stubCryptoSetup) GetSessionTicket() ([]byte, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) HandleMessage([]byte, protocol.EncryptionLevel) bool {
+	return false
+}
+
+func (s *stubCryptoSetup) SetLargest1RTTAcked(protocol.PacketNumber) error {
+	return notSupported
+}
+
+func (s *stubCryptoSetup) SetHandshakeConfirmed() {
+}
+
+func (s *stubCryptoSetup) ConnectionState() handshake.ConnectionState {
+	return handshake.ConnectionState{}
+}
+
+func (s *stubCryptoSetup) GetInitialOpener() (handshake.LongHeaderOpener, error) {
+	return s.initialOpener, nil
+}
+
+func (s *stubCryptoSetup) GetHandshakeOpener() (handshake.LongHeaderOpener, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) Get0RTTOpener() (handshake.LongHeaderOpener, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) Get1RTTOpener() (handshake.ShortHeaderOpener, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) GetInitialSealer() (handshake.LongHeaderSealer, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) GetHandshakeSealer() (handshake.LongHeaderSealer, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) Get0RTTSealer() (handshake.LongHeaderSealer, error) {
+	return nil, notSupported
+}
+
+func (s *stubCryptoSetup) Get1RTTSealer() (handshake.ShortHeaderSealer, error) {
+	return nil, notSupported
+}
+
+// [Psiphon]
+// verifyClientHelloRandom unpacks an Initial packet, extracts the CRYPTO
+// frame, and calls Config.VerifyClientHelloRandom.
+func (s *baseServer) verifyClientHelloRandom(p *receivedPacket, hdr *wire.Header) error {
+
+	_, initialOpener := handshake.NewInitialAEAD(
+		hdr.DestConnectionID, protocol.PerspectiveServer, protocol.Version1)
+
+	cs := &stubCryptoSetup{
+		initialOpener: initialOpener,
+	}
+
+	// Make a copy of the packet data since this unpacking modifies it and the
+	// original packet data must be retained for subsequent processing.
+	data := append([]byte(nil), p.data...)
+
+	unpacker := newPacketUnpacker(cs, protocol.Version1)
+	unpacked, err := unpacker.Unpack(hdr, p.rcvTime, data)
+	if err != nil {
+		return fmt.Errorf("verifyClientHelloRandom: Unpack: %w", err)
+	}
+
+	parser := wire.NewFrameParser(s.config.EnableDatagrams, protocol.Version1)
+
+	r := bytes.NewReader(unpacked.data)
+	for {
+		frame, err := parser.ParseNext(r, protocol.EncryptionInitial)
+		if err != nil {
+			return fmt.Errorf("verifyClientHelloRandom: ParseNext: %w", err)
+		}
+		if frame == nil {
+			return errors.New("verifyClientHelloRandom: missing CRYPTO frame")
+		}
+		cryptoFrame, ok := frame.(*wire.CryptoFrame)
+		if !ok {
+			continue
+		}
+		if cryptoFrame.Offset != 0 {
+			return errors.New("verifyClientHelloRandom: unexpected CRYPTO frame offset")
+		}
+		random, err := qtls.ReadClientHelloRandom(cryptoFrame.Data)
+		if err != nil {
+			return fmt.Errorf("verifyClientHelloRandom: ReadClientHelloRandom: %w", err)
+		}
+		if !s.config.VerifyClientHelloRandom(p.remoteAddr, random) {
+			return fmt.Errorf("verifyClientHelloRandom: VerifyClientHelloRandom failed")
+		}
+		break
+	}
+
+	return nil
+}
+
 func (s *baseServer) handleInitialImpl(p *receivedPacket, hdr *wire.Header) error {
 func (s *baseServer) handleInitialImpl(p *receivedPacket, hdr *wire.Header) error {
 	if len(hdr.Token) == 0 && hdr.DestConnectionID.Len() < protocol.MinConnectionIDLenInitial {
 	if len(hdr.Token) == 0 && hdr.DestConnectionID.Len() < protocol.MinConnectionIDLenInitial {
 		p.buffer.Release()
 		p.buffer.Release()
@@ -401,6 +533,14 @@ func (s *baseServer) handleInitialImpl(p *receivedPacket, hdr *wire.Header) erro
 		return errors.New("too short connection ID")
 		return errors.New("too short connection ID")
 	}
 	}
 
 
+	// [Psiphon]
+	// Drop any Initial packet that fails verifyClientHelloRandom.
+	err := s.verifyClientHelloRandom(p, hdr)
+	if err != nil {
+		p.buffer.Release()
+		return err
+	}
+
 	var (
 	var (
 		token          *Token
 		token          *Token
 		retrySrcConnID *protocol.ConnectionID
 		retrySrcConnID *protocol.ConnectionID

+ 38 - 3
vendor/github.com/Psiphon-Labs/quic-go/session.go

@@ -283,9 +283,13 @@ var newSession = func(
 	)
 	)
 	s.preSetup()
 	s.preSetup()
 	s.ctx, s.ctxCancel = context.WithCancel(context.WithValue(context.Background(), SessionTracingKey, tracingID))
 	s.ctx, s.ctxCancel = context.WithCancel(context.WithValue(context.Background(), SessionTracingKey, tracingID))
+
+	// [Psiphon]
+	maxPacketSizeAdjustment := conf.ServerMaxPacketSizeAdjustment(s.RemoteAddr())
+
 	s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
 	s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
 		0,
 		0,
-		getMaxPacketSize(s.conn.RemoteAddr()),
+		getMaxPacketSize(s.conn.RemoteAddr(), maxPacketSizeAdjustment),
 		s.rttStats,
 		s.rttStats,
 		s.perspective,
 		s.perspective,
 		s.tracer,
 		s.tracer,
@@ -349,6 +353,10 @@ var newSession = func(
 		s.sentPacketHandler,
 		s.sentPacketHandler,
 		s.retransmissionQueue,
 		s.retransmissionQueue,
 		s.RemoteAddr(),
 		s.RemoteAddr(),
+
+		// [Psiphon]
+		maxPacketSizeAdjustment,
+
 		cs,
 		cs,
 		s.framer,
 		s.framer,
 		s.receivedPacketHandler,
 		s.receivedPacketHandler,
@@ -412,7 +420,10 @@ var newClientSession = func(
 	s.ctx, s.ctxCancel = context.WithCancel(context.WithValue(context.Background(), SessionTracingKey, tracingID))
 	s.ctx, s.ctxCancel = context.WithCancel(context.WithValue(context.Background(), SessionTracingKey, tracingID))
 	s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
 	s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
 		initialPacketNumber,
 		initialPacketNumber,
-		getMaxPacketSize(s.conn.RemoteAddr()),
+
+		// [Psiphon]
+		getMaxPacketSize(s.conn.RemoteAddr(), conf.ClientMaxPacketSizeAdjustment),
+
 		s.rttStats,
 		s.rttStats,
 		s.perspective,
 		s.perspective,
 		s.tracer,
 		s.tracer,
@@ -458,6 +469,7 @@ var newClientSession = func(
 
 
 		// [Psiphon]
 		// [Psiphon]
 		conf.ClientHelloSeed,
 		conf.ClientHelloSeed,
+		conf.GetClientHelloRandom,
 
 
 		enable0RTT,
 		enable0RTT,
 		s.rttStats,
 		s.rttStats,
@@ -477,6 +489,10 @@ var newClientSession = func(
 		s.sentPacketHandler,
 		s.sentPacketHandler,
 		s.retransmissionQueue,
 		s.retransmissionQueue,
 		s.RemoteAddr(),
 		s.RemoteAddr(),
+
+		// [Psiphon]
+		conf.ClientMaxPacketSizeAdjustment,
+
 		cs,
 		cs,
 		s.framer,
 		s.framer,
 		s.receivedPacketHandler,
 		s.receivedPacketHandler,
@@ -807,6 +823,16 @@ func (s *session) handleHandshakeComplete() {
 }
 }
 
 
 func (s *session) handleHandshakeConfirmed() {
 func (s *session) handleHandshakeConfirmed() {
+
+	// [Psiphon]
+	// Adjust the max packet size to allow for obfuscation overhead.
+	maxPacketSizeAdjustment := 0
+	if s.config.ServerMaxPacketSizeAdjustment != nil {
+		maxPacketSizeAdjustment = s.config.ServerMaxPacketSizeAdjustment(s.conn.RemoteAddr())
+	} else {
+		maxPacketSizeAdjustment = s.config.ClientMaxPacketSizeAdjustment
+	}
+
 	s.handshakeConfirmed = true
 	s.handshakeConfirmed = true
 	s.sentPacketHandler.SetHandshakeConfirmed()
 	s.sentPacketHandler.SetHandshakeConfirmed()
 	s.cryptoStreamHandler.SetHandshakeConfirmed()
 	s.cryptoStreamHandler.SetHandshakeConfirmed()
@@ -817,9 +843,18 @@ func (s *session) handleHandshakeConfirmed() {
 			maxPacketSize = protocol.MaxByteCount
 			maxPacketSize = protocol.MaxByteCount
 		}
 		}
 		maxPacketSize = utils.MinByteCount(maxPacketSize, protocol.MaxPacketBufferSize)
 		maxPacketSize = utils.MinByteCount(maxPacketSize, protocol.MaxPacketBufferSize)
+
+		// [Psiphon]
+		if maxPacketSize > protocol.ByteCount(maxPacketSizeAdjustment) {
+			maxPacketSize -= protocol.ByteCount(maxPacketSizeAdjustment)
+		}
+
 		s.mtuDiscoverer = newMTUDiscoverer(
 		s.mtuDiscoverer = newMTUDiscoverer(
 			s.rttStats,
 			s.rttStats,
-			getMaxPacketSize(s.conn.RemoteAddr()),
+
+			// [Psiphon]
+			getMaxPacketSize(s.conn.RemoteAddr(), maxPacketSizeAdjustment),
+
 			maxPacketSize,
 			maxPacketSize,
 			func(size protocol.ByteCount) {
 			func(size protocol.ByteCount) {
 				s.sentPacketHandler.SetMaxDatagramSize(size)
 				s.sentPacketHandler.SetMaxDatagramSize(size)