Explorar o código

Enable ECN support and disable gQUIC by default

Rod Hynes %!s(int64=4) %!d(string=hai) anos
pai
achega
06d73fed46

+ 2 - 2
psiphon/common/activity.go

@@ -138,7 +138,7 @@ func (conn *ActivityMonitoredConn) Read(buffer []byte) (int, error) {
 			conn.lruEntry.Touch()
 		}
 	}
-	// Note: no context error to preserve error type
+	// Note: no trace error to preserve error type
 	return n, err
 }
 
@@ -162,7 +162,7 @@ func (conn *ActivityMonitoredConn) Write(buffer []byte) (int, error) {
 		}
 
 	}
-	// Note: no context error to preserve error type
+	// Note: no trace error to preserve error type
 	return n, err
 }
 

+ 123 - 32
psiphon/common/quic/obfuscator.go

@@ -24,6 +24,7 @@ package quic
 
 import (
 	"crypto/sha256"
+	std_errors "errors"
 	"io"
 	"net"
 	"sync"
@@ -33,7 +34,9 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/Yawning/chacha20"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+	ietf_quic "github.com/Psiphon-Labs/quic-go"
 	"golang.org/x/crypto/hkdf"
+	"golang.org/x/net/ipv4"
 )
 
 const (
@@ -99,7 +102,7 @@ const (
 // payload will increase UDP packets beyond the QUIC max of 1280 bytes,
 // introducing some risk of fragmentation and/or dropped packets.
 type ObfuscatedPacketConn struct {
-	net.PacketConn
+	ietf_quic.OOBCapablePacketConn
 	isServer         bool
 	isIETFClient     bool
 	isDecoyClient    bool
@@ -134,6 +137,11 @@ func NewObfuscatedPacketConn(
 	obfuscationKey string,
 	paddingSeed *prng.Seed) (*ObfuscatedPacketConn, error) {
 
+	oobPacketConn, ok := conn.(ietf_quic.OOBCapablePacketConn)
+	if !ok {
+		return nil, errors.TraceNew("conn must support OOBCapablePacketConn")
+	}
+
 	// There is no replay of obfuscation "encryption", just padding.
 	nonceSeed, err := prng.NewSeed()
 	if err != nil {
@@ -141,13 +149,13 @@ func NewObfuscatedPacketConn(
 	}
 
 	packetConn := &ObfuscatedPacketConn{
-		PacketConn:    conn,
-		isServer:      isServer,
-		isIETFClient:  isIETFClient,
-		isDecoyClient: isDecoyClient,
-		peerModes:     make(map[string]*peerMode),
-		noncePRNG:     prng.NewPRNGWithSeed(nonceSeed),
-		paddingPRNG:   prng.NewPRNGWithSeed(paddingSeed),
+		OOBCapablePacketConn: oobPacketConn,
+		isServer:             isServer,
+		isIETFClient:         isIETFClient,
+		isDecoyClient:        isDecoyClient,
+		peerModes:            make(map[string]*peerMode),
+		noncePRNG:            prng.NewPRNGWithSeed(nonceSeed),
+		paddingPRNG:          prng.NewPRNGWithSeed(paddingSeed),
 	}
 
 	secret := []byte(obfuscationKey)
@@ -209,7 +217,7 @@ func (conn *ObfuscatedPacketConn) Close() error {
 		conn.runWaitGroup.Wait()
 	}
 
-	return conn.PacketConn.Close()
+	return conn.OOBCapablePacketConn.Close()
 }
 
 type temporaryNetError struct {
@@ -233,14 +241,88 @@ func (e *temporaryNetError) Error() string {
 }
 
 func (conn *ObfuscatedPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
-	n, addr, _, err := conn.readFromWithType(p)
+	n, _, _, addr, _, err := conn.readPacketWithType(p, nil)
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
 	return n, addr, err
 }
 
-func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, bool, error) {
+func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
+	udpAddr, ok := addr.(*net.UDPAddr)
+	if !ok {
+		return 0, errors.TraceNew("unexpected addr type")
+	}
+	n, _, err := conn.writePacket(p, nil, udpAddr)
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+	return n, err
+}
+
+// ReadMsgUDP, and WriteMsgUDP satisfy the ietf_quic.OOBCapablePacketConn
+// interface. In non-muxListener mode, quic-go will access the
+// ObfuscatedPacketConn directly and use these functions to set ECN bits.
+//
+// ReadBatch implements ietf_quic.batchConn. Providing this implementation
+// effectively disables the quic-go batch packet reading optimization, which
+// would otherwise bypass deobfuscation. Note that ipv4.Message is an alias
+// for x/net/internal/socket.Message and quic-go uses this one type for both
+// IPv4 and IPv6 packets.
+//
+// Read, Write, and RemoteAddr are present to satisfy the net.Conn interface,
+// to which ObfuscatedPacketConn is converted internally, via quic-go, in
+// x/net/ipv[4|6] for OOB manipulation. These functions do not need to be
+// implemented.
+
+func (conn *ObfuscatedPacketConn) ReadMsgUDP(p, oob []byte) (int, int, int, *net.UDPAddr, error) {
+	n, oobn, flags, addr, _, err := conn.readPacketWithType(p, nil)
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+	return n, oobn, flags, addr, err
+}
+
+func (conn *ObfuscatedPacketConn) WriteMsgUDP(p, oob []byte, addr *net.UDPAddr) (int, int, error) {
+	n, oobn, err := conn.writePacket(p, oob, addr)
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+	return n, oobn, err
+}
+
+func (conn *ObfuscatedPacketConn) ReadBatch(ms []ipv4.Message, _ int) (int, error) {
+
+	// Read a "batch" of 1 message, with any necessary deobfuscation performed
+	// by readPacketWithType.
+	//
+	// TODO: implement proper batch packet reading here, along with batch
+	// deobfuscation.
+
+	if len(ms) < 1 || len(ms[0].Buffers[0]) < 1 {
+		return 0, errors.TraceNew("unexpected message buffer size")
+	}
+	var err error
+	ms[0].N, ms[0].NN, ms[0].Flags, ms[0].Addr, _, err =
+		conn.readPacketWithType(ms[0].Buffers[0], ms[0].OOB)
+	if err != nil {
+		// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+		return 0, err
+	}
+	return 1, nil
+}
+
+var notSupported = std_errors.New("not supported")
+
+func (conn *ObfuscatedPacketConn) Read(_ []byte) (int, error) {
+	return 0, errors.Trace(notSupported)
+}
+
+func (conn *ObfuscatedPacketConn) Write(_ []byte) (int, error) {
+	return 0, errors.Trace(notSupported)
+}
+
+func (conn *ObfuscatedPacketConn) RemoteAddr() net.Addr {
+	return nil
+}
+
+func (conn *ObfuscatedPacketConn) readPacketWithType(
+	p, oob []byte) (int, int, int, *net.UDPAddr, bool, error) {
 
 	for {
-		n, addr, isIETF, err := conn.readPacket(p)
+		n, oobn, flags, addr, isIETF, err := conn.readPacket(p, oob)
 
 		// When enabled, and when a packet is received, sometimes immediately
 		// respond with a decoy packet, which is Sentirely random. Sending a
@@ -282,13 +364,14 @@ func (conn *ObfuscatedPacketConn) readFromWithType(p []byte) (int, net.Addr, boo
 			continue
 		}
 
-		return n, addr, isIETF, err
+		return n, oobn, flags, addr, isIETF, err
 	}
 }
 
-func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, error) {
+func (conn *ObfuscatedPacketConn) readPacket(
+	p, oob []byte) (int, int, int, *net.UDPAddr, bool, error) {
 
-	n, addr, err := conn.PacketConn.ReadFrom(p)
+	n, oobn, flags, addr, err := conn.OOBCapablePacketConn.ReadMsgUDP(p, oob)
 
 	// Data is processed even when err != nil, as ReadFrom may return both
 	// a packet and an error, such as io.EOF.
@@ -341,13 +424,14 @@ func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, err
 			isObfuscated = !isQUIC
 
 			if isObfuscated && isIETF {
-				return n, addr, false, newTemporaryNetError(
+				return n, oobn, flags, addr, false, newTemporaryNetError(
 					errors.Tracef("unexpected isQUIC result"))
 			}
 
 			// Without addr, the mode cannot be determined.
 			if addr == nil {
-				return n, addr, true, newTemporaryNetError(errors.Tracef("missing addr"))
+				return n, oobn, flags, addr, true, newTemporaryNetError(
+					errors.Tracef("missing addr"))
 			}
 
 			conn.peerModesMutex.Lock()
@@ -394,13 +478,13 @@ func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, err
 			// avoids allocting a buffer.
 
 			if n < (NONCE_SIZE + 1) {
-				return n, addr, true, newTemporaryNetError(errors.Tracef(
-					"unexpected obfuscated QUIC packet length: %d", n))
+				return n, oobn, flags, addr, true, newTemporaryNetError(
+					errors.Tracef("unexpected obfuscated QUIC packet length: %d", n))
 			}
 
 			cipher, err := chacha20.NewCipher(conn.obfuscationKey[:], p[0:NONCE_SIZE])
 			if err != nil {
-				return n, addr, true, errors.Trace(err)
+				return n, oobn, flags, addr, true, errors.Trace(err)
 			}
 			cipher.XORKeyStream(p[NONCE_SIZE:], p[NONCE_SIZE:])
 
@@ -410,8 +494,8 @@ func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, err
 
 			paddingLen := int(p[NONCE_SIZE])
 			if paddingLen > MAX_PADDING_SIZE || paddingLen > n-(NONCE_SIZE+1) {
-				return n, addr, true, newTemporaryNetError(errors.Tracef(
-					"unexpected padding length: %d, %d", paddingLen, n))
+				return n, oobn, flags, addr, true, newTemporaryNetError(
+					errors.Tracef("unexpected padding length: %d, %d", paddingLen, n))
 			}
 
 			n -= (NONCE_SIZE + 1) + paddingLen
@@ -447,7 +531,7 @@ func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, err
 				if !ok || mode.isObfuscated != true || mode.isIETF != false ||
 					mode.lastPacketTime != lastPacketTime {
 					conn.peerModesMutex.Unlock()
-					return n, addr, true, newTemporaryNetError(
+					return n, oobn, flags, addr, true, newTemporaryNetError(
 						errors.Tracef("unexpected peer mode"))
 				}
 
@@ -465,15 +549,15 @@ func (conn *ObfuscatedPacketConn) readPacket(p []byte) (int, net.Addr, bool, err
 				//   Handshake packet, not an Initial packet, and can be smaller.
 
 				if isIETF && n < MIN_INITIAL_PACKET_SIZE {
-					return n, addr, true, newTemporaryNetError(errors.Tracef(
+					return n, oobn, flags, addr, true, newTemporaryNetError(errors.Tracef(
 						"unexpected first QUIC packet length: %d", n))
 				}
 			}
 		}
 	}
 
-	// Do not wrap any err returned by conn.PacketConn.ReadFrom.
-	return n, addr, isIETF, err
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+	return n, oobn, flags, addr, isIETF, err
 }
 
 type obfuscatorBuffer struct {
@@ -486,7 +570,8 @@ var obfuscatorBufferPool = &sync.Pool{
 	},
 }
 
-func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
+func (conn *ObfuscatedPacketConn) writePacket(
+	p, oob []byte, addr *net.UDPAddr) (int, int, error) {
 
 	n := len(p)
 
@@ -512,7 +597,7 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 	if isObfuscated {
 
 		if n > MAX_PACKET_SIZE {
-			return 0, newTemporaryNetError(errors.Tracef(
+			return 0, 0, newTemporaryNetError(errors.Tracef(
 				"unexpected QUIC packet length: %d", n))
 		}
 
@@ -547,7 +632,7 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 
 			cipher, err := chacha20.NewCipher(conn.obfuscationKey[:], nonce)
 			if err != nil {
-				return 0, errors.Trace(err)
+				return 0, 0, errors.Trace(err)
 			}
 			packet := buffer[NONCE_SIZE:dataLen]
 			cipher.XORKeyStream(packet, packet)
@@ -563,10 +648,16 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 		}
 	}
 
-	_, err := conn.PacketConn.WriteTo(p, addr)
+	_, oobn, err := conn.OOBCapablePacketConn.WriteMsgUDP(p, oob, addr)
 
-	// Do not wrap any err returned by conn.PacketConn.WriteTo.
-	return n, err
+	// quic-go uses OOB to manipulate ECN bits in the IP header; these are not
+	// obfuscated.
+	//
+	// Return n = len(input p) bytes written even when p is an obsfuscated
+	// buffer and longer than the input p.
+
+	// Do not wrap any I/O err returned by conn.OOBCapablePacketConn
+	return n, oobn, err
 }
 
 func getMaxPreDiscoveryPacketSize(addr net.Addr) int {

+ 101 - 47
psiphon/common/quic/quic.go

@@ -127,8 +127,9 @@ var serverIdleTimeout = SERVER_IDLE_TIMEOUT
 
 // Listener is a net.Listener.
 type Listener struct {
-	*muxListener
-	clientRandomHistory *obfuscator.SeedHistory
+	quicListener
+	obfuscatedPacketConn *ObfuscatedPacketConn
+	clientRandomHistory  *obfuscator.SeedHistory
 }
 
 // Listen creates a new Listener.
@@ -136,7 +137,8 @@ func Listen(
 	logger common.Logger,
 	irregularTunnelLogger func(string, error, common.LogFields),
 	address string,
-	obfuscationKey string) (net.Listener, error) {
+	obfuscationKey string,
+	enableGQUIC bool) (net.Listener, error) {
 
 	certificate, privateKey, err := common.GenerateWebServerCertificate(
 		values.GetHostName())
@@ -218,29 +220,99 @@ func Listen(
 		return ok
 	}
 
-	// Note that, due to nature of muxListener, full accepts may happen before
-	// return and caller calls Accept.
+	var quicListener quicListener
 
-	muxListener, err := newMuxListener(
-		logger, verifyClientHelloRandom, obfuscatedPacketConn, tlsCertificate)
-	if err != nil {
-		obfuscatedPacketConn.Close()
-		return nil, errors.Trace(err)
+	if !enableGQUIC {
+
+		// When gQUIC is disabled, skip the muxListener entirely. This allows
+		// quic-go to enable ECN operations as the packet conn is a
+		// quic-goOOBCapablePacketConn; this provides some performance
+		// optimizations and also generate packets that may be harder to
+		// fingerprint, due to lack of ECN bits in IP packets otherwise.
+		// Skipping muxListener also avoids the additional overhead of
+		// pumping read packets though mux channels.
+
+		tlsConfig, ietfQUICConfig := makeIETFConfig(
+			obfuscatedPacketConn, verifyClientHelloRandom, tlsCertificate)
+
+		listener, err := ietf_quic.Listen(
+			obfuscatedPacketConn, tlsConfig, ietfQUICConfig)
+		if err != nil {
+			obfuscatedPacketConn.Close()
+			return nil, errors.Trace(err)
+		}
+
+		quicListener = &ietfQUICListener{Listener: listener}
+
+	} else {
+
+		// Note that, due to nature of muxListener, full accepts may happen before
+		// return and caller calls Accept.
+
+		muxListener, err := newMuxListener(
+			logger, verifyClientHelloRandom, obfuscatedPacketConn, tlsCertificate)
+		if err != nil {
+			obfuscatedPacketConn.Close()
+			return nil, errors.Trace(err)
+		}
+
+		quicListener = muxListener
 	}
 
 	return &Listener{
-		muxListener:         muxListener,
-		clientRandomHistory: clientRandomHistory,
+		quicListener:         quicListener,
+		obfuscatedPacketConn: obfuscatedPacketConn,
+		clientRandomHistory:  clientRandomHistory,
 	}, nil
 }
 
+func makeIETFConfig(
+	conn *ObfuscatedPacketConn,
+	verifyClientHelloRandom func(net.Addr, []byte) bool,
+	tlsCertificate tls.Certificate) (*tls.Config, *ietf_quic.Config) {
+
+	tlsConfig := &tls.Config{
+		Certificates: []tls.Certificate{tlsCertificate},
+		NextProtos:   []string{getALPN(ietfQUIC1VersionNumber)},
+	}
+
+	ietfQUICConfig := &ietf_quic.Config{
+		HandshakeIdleTimeout:  SERVER_HANDSHAKE_TIMEOUT,
+		MaxIdleTimeout:        serverIdleTimeout,
+		MaxIncomingStreams:    1,
+		MaxIncomingUniStreams: -1,
+		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,
+	}
+
+	return tlsConfig, ietfQUICConfig
+}
+
 // Accept returns a net.Conn that wraps a single QUIC session and stream. The
 // stream establishment is deferred until the first Read or Write, allowing
 // Accept to be called in a fast loop while goroutines spawned to handle each
 // net.Conn will perform the blocking AcceptStream.
 func (listener *Listener) Accept() (net.Conn, error) {
 
-	session, err := listener.muxListener.Accept()
+	session, err := listener.quicListener.Accept()
 	if err != nil {
 		return nil, errors.Trace(err)
 	}
@@ -251,6 +323,16 @@ func (listener *Listener) Accept() (net.Conn, error) {
 	}, nil
 }
 
+func (listener *Listener) Close() error {
+
+	// First close the underlying packet conn to ensure all quic-go goroutines
+	// as well as any blocking Accept call goroutine is interrupted. Note
+	// that muxListener does this as well, so this is for the IETF-only case.
+	_ = listener.obfuscatedPacketConn.Close()
+
+	return listener.quicListener.Close()
+}
+
 // Dial establishes a new QUIC session and stream to the server specified by
 // address.
 //
@@ -719,6 +801,7 @@ func (t *QUICTransporter) dialQUIC() (retSession quicSession, retErr error) {
 
 type quicListener interface {
 	Close() error
+	Addr() net.Addr
 	Accept() (quicSession, error)
 }
 
@@ -1010,7 +1093,7 @@ func (conn *muxPacketConn) SetWriteDeadline(t time.Time) error {
 // https://godoc.org/github.com/lucas-clemente/quic-go#ECNCapablePacketConn.
 
 func (conn *muxPacketConn) SetReadBuffer(bytes int) error {
-	c, ok := conn.listener.conn.PacketConn.(interface {
+	c, ok := conn.listener.conn.OOBCapablePacketConn.(interface {
 		SetReadBuffer(int) error
 	})
 	if !ok {
@@ -1020,7 +1103,7 @@ func (conn *muxPacketConn) SetReadBuffer(bytes int) error {
 }
 
 func (conn *muxPacketConn) SyscallConn() (syscall.RawConn, error) {
-	c, ok := conn.listener.conn.PacketConn.(interface {
+	c, ok := conn.listener.conn.OOBCapablePacketConn.(interface {
 		SyscallConn() (syscall.RawConn, error)
 	})
 	if !ok {
@@ -1067,37 +1150,8 @@ func newMuxListener(
 
 	listener.ietfQUICConn = newMuxPacketConn(conn.LocalAddr(), listener)
 
-	tlsConfig := &tls.Config{
-		Certificates: []tls.Certificate{tlsCertificate},
-		NextProtos:   []string{getALPN(ietfQUIC1VersionNumber)},
-	}
-
-	ietfQUICConfig := &ietf_quic.Config{
-		HandshakeIdleTimeout:  SERVER_HANDSHAKE_TIMEOUT,
-		MaxIdleTimeout:        serverIdleTimeout,
-		MaxIncomingStreams:    1,
-		MaxIncomingUniStreams: -1,
-		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,
-	}
+	tlsConfig, ietfQUICConfig := makeIETFConfig(
+		conn, verifyClientHelloRandom, tlsCertificate)
 
 	il, err := ietf_quic.Listen(listener.ietfQUICConn, tlsConfig, ietfQUICConfig)
 	if err != nil {
@@ -1157,7 +1211,7 @@ func (listener *muxListener) relayPackets() {
 
 		var isIETF bool
 		var err error
-		p.size, p.addr, isIETF, err = listener.conn.readFromWithType(p.data)
+		p.size, _, _, p.addr, isIETF, err = listener.conn.readPacketWithType(p.data, nil)
 		if err != nil {
 			if listener.logger != nil {
 				message := "readFromWithType failed"

+ 16 - 4
psiphon/common/quic/quic_test.go

@@ -42,18 +42,26 @@ import (
 func TestQUIC(t *testing.T) {
 	for quicVersion := range supportedVersionNumbers {
 		t.Run(fmt.Sprintf("%s", quicVersion), func(t *testing.T) {
-			runQUIC(t, quicVersion, false)
+			runQUIC(t, quicVersion, true, false)
 		})
 		if isIETF(quicVersion) {
 			t.Run(fmt.Sprintf("%s (invoke anti-probing)", quicVersion), func(t *testing.T) {
-				runQUIC(t, quicVersion, true)
+				runQUIC(t, quicVersion, true, true)
+			})
+		}
+		if isIETF(quicVersion) {
+			t.Run(fmt.Sprintf("%s (disable gQUIC)", quicVersion), func(t *testing.T) {
+				runQUIC(t, quicVersion, false, false)
 			})
 		}
 	}
 }
 
 func runQUIC(
-	t *testing.T, quicVersion string, invokeAntiProbing bool) {
+	t *testing.T,
+	quicVersion string,
+	enableGQUIC bool,
+	invokeAntiProbing bool) {
 
 	initGoroutines := getGoroutines()
 
@@ -82,7 +90,11 @@ func runQUIC(
 	obfuscationKey := prng.HexString(32)
 
 	listener, err := Listen(
-		nil, irregularTunnelLogger, "127.0.0.1:0", obfuscationKey)
+		nil,
+		irregularTunnelLogger,
+		"127.0.0.1:0",
+		obfuscationKey,
+		enableGQUIC)
 	if err != nil {
 		t.Fatalf("Listen failed: %s", err)
 	}

+ 6 - 0
psiphon/server/config.go

@@ -175,6 +175,12 @@ type Config struct {
 	// passthrough servers only.
 	LegacyPassthrough bool
 
+	// EnableGQUIC indicates whether to enable legacy gQUIC QUIC-OSSH
+	// versions, for backwards compatibility with all versions used by older
+	// clients. Enabling gQUIC degrades the anti-probing stance of QUIC-OSSH,
+	// as the legacy gQUIC stack will respond to probing packets.
+	EnableGQUIC bool
+
 	// SSHPrivateKey is the SSH host key. The same key is used for
 	// all protocols, run by this server instance, which use SSH.
 	SSHPrivateKey string

+ 2 - 1
psiphon/server/tunnelServer.go

@@ -170,7 +170,8 @@ func (server *TunnelServer) Run() error {
 						errors.Trace(err), LogFields(logFields))
 				},
 				localAddress,
-				support.Config.ObfuscatedSSHKey)
+				support.Config.ObfuscatedSSHKey,
+				support.Config.EnableGQUIC)
 
 		} else if protocol.TunnelProtocolUsesMarionette(tunnelProtocol) {