瀏覽代碼

Merge pull request #732 from rod-hynes/mtu-discovery

Rework QUIC extended UDP socket support
Rod Hynes 9 月之前
父節點
當前提交
ee14db4b1d

+ 107 - 26
psiphon/common/quic/obfuscator.go

@@ -24,11 +24,11 @@ package quic
 
 
 import (
 import (
 	"crypto/sha256"
 	"crypto/sha256"
-	std_errors "errors"
 	"io"
 	"io"
 	"net"
 	"net"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
+	"syscall"
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
@@ -311,33 +311,11 @@ func (conn *ObfuscatedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 	return n, err
 	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
 // ReadBatch implements ietf_quic.batchConn. Providing this implementation
 // effectively disables the quic-go batch packet reading optimization, which
 // effectively disables the quic-go batch packet reading optimization, which
 // would otherwise bypass deobfuscation. Note that ipv4.Message is an alias
 // 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
 // for x/net/internal/socket.Message and quic-go uses this one type for both
 // IPv4 and IPv6 packets.
 // IPv4 and IPv6 packets.
-//
-// Read and Write 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.PacketConn
-	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.PacketConn
-	return n, oobn, err
-}
-
 func (conn *ObfuscatedPacketConn) ReadBatch(ms []ipv4.Message, _ int) (int, error) {
 func (conn *ObfuscatedPacketConn) ReadBatch(ms []ipv4.Message, _ int) (int, error) {
 
 
 	// Read a "batch" of 1 message, with any necessary deobfuscation performed
 	// Read a "batch" of 1 message, with any necessary deobfuscation performed
@@ -359,14 +337,47 @@ func (conn *ObfuscatedPacketConn) ReadBatch(ms []ipv4.Message, _ int) (int, erro
 	return 1, nil
 	return 1, nil
 }
 }
 
 
-var notSupported = std_errors.New("not supported")
+// SetReadBuffer and SetWriteBuffer are used by quic-go to optimize socket
+// buffer sizes.
+//
+// Unlike SyscallConn, quic-go will not fail on a SetReadBuffer/SetWriteBuffer
+// error, so it's safe to expose these functions even when the underlying
+// functionality is not always supported.
+//
+// When ObfuscatedPacketConn is wrapped in ObfuscatedPacketConnOOB,
+// SyscallConn is provided and quic-go's setReceiveBuffer will check that
+// buffer is set to expected size.
+func (conn *ObfuscatedPacketConn) SetReadBuffer(n int) error {
+	bufferConn, ok := conn.PacketConn.(interface{ SetReadBuffer(int) error })
+	if !ok {
+		return errors.Trace(errNotSupported)
+	}
+
+	// TODO: log errors in diagnostics. In quic-go, wrapConn logs errors from
+	// SetReadBuffer (via setReceiveBuffer) and proceeds; those quic-go logs
+	// are not captured.
 
 
+	return errors.Trace(bufferConn.SetReadBuffer(n))
+}
+
+func (conn *ObfuscatedPacketConn) SetWriteBuffer(n int) error {
+	bufferConn, ok := conn.PacketConn.(interface{ SetWriteBuffer(int) error })
+	if !ok {
+		return errors.Trace(errNotSupported)
+	}
+	return errors.Trace(bufferConn.SetWriteBuffer(n))
+}
+
+// Read and Write 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) Read(_ []byte) (int, error) {
 func (conn *ObfuscatedPacketConn) Read(_ []byte) (int, error) {
-	return 0, errors.Trace(notSupported)
+	return 0, errors.Trace(errNotSupported)
 }
 }
 
 
 func (conn *ObfuscatedPacketConn) Write(_ []byte) (int, error) {
 func (conn *ObfuscatedPacketConn) Write(_ []byte) (int, error) {
-	return 0, errors.Trace(notSupported)
+	return 0, errors.Trace(errNotSupported)
 }
 }
 
 
 func (conn *ObfuscatedPacketConn) RemoteAddr() net.Addr {
 func (conn *ObfuscatedPacketConn) RemoteAddr() net.Addr {
@@ -992,3 +1003,73 @@ func isIETFQUICClientHello(buffer []byte) bool {
 		buffer[3] == 0 &&
 		buffer[3] == 0 &&
 		buffer[4] == 0x1
 		buffer[4] == 0x1
 }
 }
+
+// ObfuscatedOOBCapablePacketConn implements quic-go's OOBCapablePacketConn
+// interface by exposing the necessary functionality in the underlying
+// ObfuscatedPacketConn.PacketConn. The packet conn should be a *net.UDPConn
+// and must implement SyscallConn/ReadMsgUDP/WriteMsgUDP.
+//
+// SyscallConn is used to set the DF bit for path MTU discovery, and is
+// required for MTU discovery to function. SyscallConn is also used to verify
+// the buffers set by SetReadBuffer and SetWriteBuffer.
+//
+// ReadMsgUDP and WriteMsgUDP are used to operate on the ECN bit.
+//
+// quic-go's wrapConn assumes SyscallConn works when provided, or else the
+// dial fails, so providing SyscallConn and then returning errNotSupported is
+// fatal.
+type ObfuscatedOOBCapablePacketConn struct {
+	*ObfuscatedPacketConn
+}
+
+func NewObfuscatedOOBCapablePacketConn(
+	conn *ObfuscatedPacketConn) *ObfuscatedOOBCapablePacketConn {
+
+	return &ObfuscatedOOBCapablePacketConn{ObfuscatedPacketConn: conn}
+}
+
+func (conn *ObfuscatedOOBCapablePacketConn) SyscallConn() (syscall.RawConn, error) {
+
+	// quic-go uses SyscallConn to set DF bit for path MTU discovery.
+
+	syscallConn, ok := conn.ObfuscatedPacketConn.PacketConn.(ietf_quic.OOBCapablePacketConn)
+	if !ok {
+		return nil, errors.Trace(errNotSupported)
+	}
+	rawConn, err := syscallConn.SyscallConn()
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+	return rawConn, nil
+}
+
+func (conn *ObfuscatedOOBCapablePacketConn) ReadMsgUDP(
+	p, oob []byte) (int, int, int, *net.UDPAddr, error) {
+
+	// quic-go uses ReadMsgUDP to read ECN bits.
+	//
+	// The underlying readPacketWithType performs its own type assertion and
+	// reads OOB bits when ObfuscatedPacketConn.PacketConn is capable.
+
+	n, oobn, flags, addr, _, err := conn.readPacketWithType(p, nil)
+	// Do not wrap any I/O err returned by conn.PacketConn
+	return n, oobn, flags, addr, err
+}
+
+func (conn *ObfuscatedOOBCapablePacketConn) WriteMsgUDP(
+	p, oob []byte, addr *net.UDPAddr) (int, int, error) {
+
+	// quic-go uses WriteMsgUDP to write ECN bits.
+	//
+	// The underlying readPacketWithType performs its own type assertion and
+	// writes OOB bits when ObfuscatedPacketConn.PacketConn is capable. If
+	// oob is not nil and the type assertion fails, writePacket fails.
+
+	n, oobn, err := conn.writePacket(p, oob, addr)
+	// Do not wrap any I/O err returned by conn.PacketConn
+	return n, oobn, err
+}
+
+var _ ietf_quic.OOBCapablePacketConn = &ObfuscatedOOBCapablePacketConn{}
+
+var _ ietf_quic.OOBCapablePacketConn = &common.WriteTimeoutUDPConn{}

+ 116 - 90
psiphon/common/quic/quic.go

@@ -53,7 +53,6 @@ import (
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
-	"syscall"
 	"time"
 	"time"
 
 
 	tls "github.com/Psiphon-Labs/psiphon-tls"
 	tls "github.com/Psiphon-Labs/psiphon-tls"
@@ -135,8 +134,8 @@ var serverIdleTimeout = SERVER_IDLE_TIMEOUT
 // Listener is a net.Listener.
 // Listener is a net.Listener.
 type Listener struct {
 type Listener struct {
 	quicListener
 	quicListener
-	obfuscatedPacketConn *ObfuscatedPacketConn
-	clientRandomHistory  *obfuscator.SeedHistory
+	packetConn          *ObfuscatedOOBCapablePacketConn
+	clientRandomHistory *obfuscator.SeedHistory
 }
 }
 
 
 // Listen creates a new Listener.
 // Listen creates a new Listener.
@@ -144,6 +143,7 @@ func Listen(
 	logger common.Logger,
 	logger common.Logger,
 	irregularTunnelLogger func(string, error, common.LogFields),
 	irregularTunnelLogger func(string, error, common.LogFields),
 	address string,
 	address string,
+	disablePathMTUDiscovery bool,
 	additionalMaxPacketSizeAdjustment int,
 	additionalMaxPacketSizeAdjustment int,
 	obfuscationKey string,
 	obfuscationKey string,
 	enableGQUIC bool) (net.Listener, error) {
 	enableGQUIC bool) (net.Listener, error) {
@@ -176,6 +176,27 @@ func Listen(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	// On the server side, the QUIC UDP socket is always wrapped with an
+	// ObfuscatedPacketConn, as the single socket will receive and need to
+	// handle both obfuscated and non-obfuscated QUIC protocol variants.
+	//
+	// The server UDP socket is further unconditionally wrapped with
+	// ObfuscatedOOBCapablePacketConn, which enables support for setting the
+	// OOB ECN bit, for congestion control, and the DF bit, required for path
+	// MTU discovery. Both of these IP packet bits will be set by quic-go.
+	//
+	// This unconditional wrapping is a trade-off, since this also causes
+	// quic-go to set the ECN and DF bits for obfuscated IETF QUIC.
+	// This is partially mitigated by the fact that the DF bit is very
+	// common, and that the ECN bit isn't set immediately.
+	//
+	// As a future enhancement, in the Psiphon-Labs/quic-go fork, add support
+	// for per-connection enabling of setting the ECN/DF bits.
+	//
+	// When gQUIC is enabled and the mux listener is used, the
+	// OOBCapablePacketConn features are masked and setting the ECN/DF bits
+	// and path MTU discovery are dissabled.
+
 	// Note that WriteTimeoutUDPConn is not used here in the server case, as
 	// Note that WriteTimeoutUDPConn is not used here in the server case, as
 	// the server UDP conn will have many concurrent writers, and each
 	// the server UDP conn will have many concurrent writers, and each
 	// SetWriteDeadline call by WriteTimeoutUDPConn would extend the deadline
 	// SetWriteDeadline call by WriteTimeoutUDPConn would extend the deadline
@@ -190,6 +211,9 @@ func Listen(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	obfuscatedOOBPacketConn := NewObfuscatedOOBCapablePacketConn(
+		obfuscatedPacketConn)
+
 	// QUIC clients must prove knowledge of the obfuscated key via a message
 	// QUIC clients must prove knowledge of the obfuscated key via a message
 	// sent in the TLS ClientHello random field, or receive no UDP packets
 	// sent in the TLS ClientHello random field, or receive no UDP packets
 	// back from the server. This anti-probing mechanism is implemented using
 	// back from the server. This anti-probing mechanism is implemented using
@@ -250,18 +274,19 @@ func Listen(
 		// pumping read packets though mux channels.
 		// pumping read packets though mux channels.
 
 
 		tlsConfig, ietfQUICConfig, err := makeServerIETFConfig(
 		tlsConfig, ietfQUICConfig, err := makeServerIETFConfig(
-			obfuscatedPacketConn,
+			obfuscatedOOBPacketConn,
+			disablePathMTUDiscovery,
 			additionalMaxPacketSizeAdjustment,
 			additionalMaxPacketSizeAdjustment,
 			verifyClientHelloRandom,
 			verifyClientHelloRandom,
 			tlsCertificate,
 			tlsCertificate,
 			obfuscationKey)
 			obfuscationKey)
 
 
 		if err != nil {
 		if err != nil {
-			obfuscatedPacketConn.Close()
+			obfuscatedOOBPacketConn.Close()
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 
 
-		tr := newIETFTransport(obfuscatedPacketConn)
+		tr := newIETFTransport(obfuscatedOOBPacketConn)
 
 
 		listener, err := tr.Listen(tlsConfig, ietfQUICConfig)
 		listener, err := tr.Listen(tlsConfig, ietfQUICConfig)
 		if err != nil {
 		if err != nil {
@@ -281,13 +306,13 @@ func Listen(
 
 
 		muxListener, err := newMuxListener(
 		muxListener, err := newMuxListener(
 			logger,
 			logger,
-			obfuscatedPacketConn,
+			obfuscatedOOBPacketConn,
 			additionalMaxPacketSizeAdjustment,
 			additionalMaxPacketSizeAdjustment,
 			verifyClientHelloRandom,
 			verifyClientHelloRandom,
 			tlsCertificate,
 			tlsCertificate,
 			obfuscationKey)
 			obfuscationKey)
 		if err != nil {
 		if err != nil {
-			obfuscatedPacketConn.Close()
+			obfuscatedOOBPacketConn.Close()
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 
 
@@ -295,14 +320,37 @@ func Listen(
 	}
 	}
 
 
 	return &Listener{
 	return &Listener{
-		quicListener:         quicListener,
-		obfuscatedPacketConn: obfuscatedPacketConn,
-		clientRandomHistory:  clientRandomHistory,
+		quicListener:        quicListener,
+		packetConn:          obfuscatedOOBPacketConn,
+		clientRandomHistory: clientRandomHistory,
+	}, nil
+}
+
+// Accept returns a net.Conn that wraps a single QUIC connection 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) {
+
+	connection, err := listener.quicListener.Accept()
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	return &Conn{
+		connection:           connection,
+		deferredAcceptStream: true,
 	}, nil
 	}, nil
 }
 }
 
 
+func (listener *Listener) Close() error {
+	_ = listener.packetConn.Close()
+	return listener.quicListener.Close()
+}
+
 func makeServerIETFConfig(
 func makeServerIETFConfig(
-	conn *ObfuscatedPacketConn,
+	conn *ObfuscatedOOBCapablePacketConn,
+	disablePathMTUDiscovery bool,
 	additionalMaxPacketSizeAdjustment int,
 	additionalMaxPacketSizeAdjustment int,
 	verifyClientHelloRandom func(net.Addr, []byte) bool,
 	verifyClientHelloRandom func(net.Addr, []byte) bool,
 	tlsCertificate tls.Certificate,
 	tlsCertificate tls.Certificate,
@@ -336,7 +384,9 @@ func makeServerIETFConfig(
 		})
 		})
 	}
 	}
 
 
-	serverMaxPacketSizeAdjustment := conn.serverMaxPacketSizeAdjustment
+	serverMaxPacketSizeAdjustment :=
+		conn.ObfuscatedPacketConn.serverMaxPacketSizeAdjustment
+
 	if additionalMaxPacketSizeAdjustment != 0 {
 	if additionalMaxPacketSizeAdjustment != 0 {
 		serverMaxPacketSizeAdjustment = func(addr net.Addr) int {
 		serverMaxPacketSizeAdjustment = func(addr net.Addr) int {
 			return conn.serverMaxPacketSizeAdjustment(addr) +
 			return conn.serverMaxPacketSizeAdjustment(addr) +
@@ -355,6 +405,7 @@ func makeServerIETFConfig(
 
 
 		VerifyClientHelloRandom:       verifyClientHelloRandom,
 		VerifyClientHelloRandom:       verifyClientHelloRandom,
 		ServerMaxPacketSizeAdjustment: serverMaxPacketSizeAdjustment,
 		ServerMaxPacketSizeAdjustment: serverMaxPacketSizeAdjustment,
+		DisablePathMTUDiscovery:       disablePathMTUDiscovery,
 	}
 	}
 
 
 	return tlsConfig, ietfQUICConfig, nil
 	return tlsConfig, ietfQUICConfig, nil
@@ -382,28 +433,6 @@ func newIETFTransport(conn net.PacketConn) *ietf_quic.Transport {
 	}
 	}
 }
 }
 
 
-// Accept returns a net.Conn that wraps a single QUIC connection 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) {
-
-	connection, err := listener.quicListener.Accept()
-	if err != nil {
-		return nil, errors.Trace(err)
-	}
-
-	return &Conn{
-		connection:           connection,
-		deferredAcceptStream: true,
-	}, nil
-}
-
-func (listener *Listener) Close() error {
-	_ = listener.obfuscatedPacketConn.Close()
-	return listener.quicListener.Close()
-}
-
 // Dial establishes a new QUIC connection and stream to the server specified
 // Dial establishes a new QUIC connection and stream to the server specified
 // by address.
 // by address.
 //
 //
@@ -455,40 +484,18 @@ func Dial(
 		return nil, errors.Tracef("invalid destination port: %d", remoteAddr.Port)
 		return nil, errors.Tracef("invalid destination port: %d", remoteAddr.Port)
 	}
 	}
 
 
-	udpConn, ok := packetConn.(*net.UDPConn)
-
-	if !ok || isObfuscated(quicVersion) {
+	udpConn, isUDPConn := packetConn.(*net.UDPConn)
 
 
-		// quic-go uses OOB operations to manipulate ECN bits in IP packet
-		// headers. These operations are available only when the packet conn
-		// is a *net.UDPConn. At this time, quic-go reads but does not write
-		// ECN OOB bits; see quic-go PR 2789.
-		//
-		// To guard against future writes to ECN bits, a potential fingerprint
-		// when using obfuscated QUIC, this non-OOB code path is taken for
-		// isObfuscated QUIC versions. This mitigates upstream fingerprints;
-		// see ObfuscatedPacketConn.writePacket for the server-side
-		// downstream limitation.
-		//
-		// Update: quic-go now writes ECN bits; see quic-go PR 3999.
+	// Ensure blocked packet writes eventually timeout.
+	if isUDPConn {
 
 
-		// Ensure blocked packet writes eventually timeout. Note that quic-go
-		// manages read deadlines; we set only the write deadline here.
-		packetConn = &common.WriteTimeoutPacketConn{
-			PacketConn: packetConn,
-		}
-
-		// Double check that OOB support won't be detected by quic-go.
-		_, ok := packetConn.(ietf_quic.OOBCapablePacketConn)
-		if ok {
-			return nil, errors.TraceNew("unexpected OOBCapablePacketConn")
+		packetConn = &common.WriteTimeoutUDPConn{
+			UDPConn: udpConn,
 		}
 		}
-
 	} else {
 	} else {
 
 
-		// Ensure blocked packet writes eventually timeout.
-		packetConn = &common.WriteTimeoutUDPConn{
-			UDPConn: udpConn,
+		packetConn = &common.WriteTimeoutPacketConn{
+			PacketConn: packetConn,
 		}
 		}
 	}
 	}
 
 
@@ -506,7 +513,25 @@ func Dial(
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
-		packetConn = obfuscatedPacketConn
+
+		// When available, expose the required UDP socket functionality to
+		// handle OOB bits, for handling the ECN bit, and the DF bit, for
+		// path MTU discovery.
+		if isUDPConn {
+			packetConn = NewObfuscatedOOBCapablePacketConn(obfuscatedPacketConn)
+		} else {
+			packetConn = obfuscatedPacketConn
+		}
+
+		// Disable path MTU in the client flow. This avoids setting the DF bit
+		// for client obfuscated QUIC packets. The downstream server flow
+		// will still perform MTU discovery.
+		//
+		// As a future enhancement, in the Psiphon-Labs/quic-go fork, consider
+		// enabling a delay for client flow MTU discovery so that early
+		// packets don't include the DF bit.
+
+		disablePathMTUDiscovery = true
 
 
 		// Reserve additional space for packet obfuscation overhead so that
 		// Reserve additional space for packet obfuscation overhead so that
 		// quic-go will continue to produce packets of max size 1280.
 		// quic-go will continue to produce packets of max size 1280.
@@ -1368,33 +1393,37 @@ func (conn *muxPacketConn) LocalAddr() net.Addr {
 }
 }
 
 
 func (conn *muxPacketConn) SetDeadline(t time.Time) error {
 func (conn *muxPacketConn) SetDeadline(t time.Time) error {
-	return errors.TraceNew("not supported")
+	return errors.Trace(errNotSupported)
 }
 }
 
 
 func (conn *muxPacketConn) SetReadDeadline(t time.Time) error {
 func (conn *muxPacketConn) SetReadDeadline(t time.Time) error {
-	return errors.TraceNew("not supported")
+	return errors.Trace(errNotSupported)
 }
 }
 
 
 func (conn *muxPacketConn) SetWriteDeadline(t time.Time) error {
 func (conn *muxPacketConn) SetWriteDeadline(t time.Time) error {
-	return errors.TraceNew("not supported")
+	return errors.Trace(errNotSupported)
 }
 }
 
 
-// SetReadBuffer, SetWriteBuffer, and SyscallConn provide passthroughs to the
-// underlying net.UDPConn implementations, used to optimize UDP buffer sizes.
-// See https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
-// and ietf_quic.setReceive/SendBuffer. Only the IETF stack will access these
+// SetReadBuffer and SetWriteBuffer provide passthroughs to the underlying
+// net.UDPConn implementations, used to optimize UDP buffer sizes. See
+// https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes and
+// ietf_quic.setReceive/SendBuffer. Only the IETF stack will access these
 // functions.
 // functions.
 //
 //
-// Limitation: due to the relayPackets/ReadFrom scheme, this simple
-// passthrough does not suffice to provide access to ReadMsgUDP for
-// https://godoc.org/github.com/lucas-clemente/quic-go#ECNCapablePacketConn.
-
+// Limitations:
+//   - SysconnCall is not passed through as it is not required in
+//     ietf_quic.setReceive/SendBuffer, and it may cause issues if used, by the
+//     IETF stack, to set the DF bit for path MTU discovery. As a result, MTU
+//     discovery is not enabled with the multiplexer.
+//   - Due to the relayPackets/ReadFrom scheme, this simple passthrough does not
+//     suffice to provide access to ReadMsgUDP for
+//     https://godoc.org/github.com/quic-go/quic-go#OOBCapablePacketConn.
 func (conn *muxPacketConn) SetReadBuffer(bytes int) error {
 func (conn *muxPacketConn) SetReadBuffer(bytes int) error {
 	c, ok := conn.listener.conn.PacketConn.(interface {
 	c, ok := conn.listener.conn.PacketConn.(interface {
 		SetReadBuffer(int) error
 		SetReadBuffer(int) error
 	})
 	})
 	if !ok {
 	if !ok {
-		return errors.TraceNew("not supported")
+		return errors.Trace(errNotSupported)
 	}
 	}
 	return c.SetReadBuffer(bytes)
 	return c.SetReadBuffer(bytes)
 }
 }
@@ -1404,21 +1433,11 @@ func (conn *muxPacketConn) SetWriteBuffer(bytes int) error {
 		SetWriteBuffer(int) error
 		SetWriteBuffer(int) error
 	})
 	})
 	if !ok {
 	if !ok {
-		return errors.TraceNew("not supported")
+		return errors.Trace(errNotSupported)
 	}
 	}
 	return c.SetWriteBuffer(bytes)
 	return c.SetWriteBuffer(bytes)
 }
 }
 
 
-func (conn *muxPacketConn) SyscallConn() (syscall.RawConn, error) {
-	c, ok := conn.listener.conn.PacketConn.(interface {
-		SyscallConn() (syscall.RawConn, error)
-	})
-	if !ok {
-		return nil, errors.TraceNew("not supported")
-	}
-	return c.SyscallConn()
-}
-
 // muxListener is a multiplexing packet conn listener which relays packets to
 // muxListener is a multiplexing packet conn listener which relays packets to
 // multiple quic-go listeners.
 // multiple quic-go listeners.
 type muxListener struct {
 type muxListener struct {
@@ -1426,7 +1445,7 @@ type muxListener struct {
 	isClosed            int32
 	isClosed            int32
 	runWaitGroup        *sync.WaitGroup
 	runWaitGroup        *sync.WaitGroup
 	stopBroadcast       chan struct{}
 	stopBroadcast       chan struct{}
-	conn                *ObfuscatedPacketConn
+	conn                *ObfuscatedOOBCapablePacketConn
 	packets             chan *packet
 	packets             chan *packet
 	acceptedConnections chan quicConnection
 	acceptedConnections chan quicConnection
 	ietfQUICConn        *muxPacketConn
 	ietfQUICConn        *muxPacketConn
@@ -1437,7 +1456,7 @@ type muxListener struct {
 
 
 func newMuxListener(
 func newMuxListener(
 	logger common.Logger,
 	logger common.Logger,
-	conn *ObfuscatedPacketConn,
+	conn *ObfuscatedOOBCapablePacketConn,
 	additionalMaxPacketSizeAdjustment int,
 	additionalMaxPacketSizeAdjustment int,
 	verifyClientHelloRandom func(net.Addr, []byte) bool,
 	verifyClientHelloRandom func(net.Addr, []byte) bool,
 	tlsCertificate tls.Certificate,
 	tlsCertificate tls.Certificate,
@@ -1459,8 +1478,13 @@ func newMuxListener(
 
 
 	listener.ietfQUICConn = newMuxPacketConn(conn.LocalAddr(), listener)
 	listener.ietfQUICConn = newMuxPacketConn(conn.LocalAddr(), listener)
 
 
+	// The muxListener does not expose the quic-go.OOBCapablePacketConn
+	// SyscallConn capability required for MTU discovery.
+	disablePathMTUDiscovery := true
+
 	tlsConfig, ietfQUICConfig, err := makeServerIETFConfig(
 	tlsConfig, ietfQUICConfig, err := makeServerIETFConfig(
 		conn,
 		conn,
+		disablePathMTUDiscovery,
 		additionalMaxPacketSizeAdjustment,
 		additionalMaxPacketSizeAdjustment,
 		verifyClientHelloRandom,
 		verifyClientHelloRandom,
 		tlsCertificate,
 		tlsCertificate,
@@ -1629,3 +1653,5 @@ func (listener *muxListener) Close() error {
 func (listener *muxListener) Addr() net.Addr {
 func (listener *muxListener) Addr() net.Addr {
 	return listener.conn.LocalAddr()
 	return listener.conn.LocalAddr()
 }
 }
+
+var errNotSupported = std_errors.New("not supported")

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

@@ -102,10 +102,14 @@ func runQUIC(
 
 
 	obfuscationKey := prng.HexString(32)
 	obfuscationKey := prng.HexString(32)
 
 
+	// Disabling MTU discovery is exercised in in-proxy test cases.
+	disablePathMTUDiscovery := false
+
 	listener, err := Listen(
 	listener, err := Listen(
 		nil,
 		nil,
 		irregularTunnelLogger,
 		irregularTunnelLogger,
 		"127.0.0.1:0",
 		"127.0.0.1:0",
+		disablePathMTUDiscovery,
 		0,
 		0,
 		obfuscationKey,
 		obfuscationKey,
 		enableGQUIC)
 		enableGQUIC)

+ 5 - 0
psiphon/meekConn.go

@@ -425,6 +425,11 @@ func DialMeek(
 
 
 		meek.connManager = newMeekUnderlyingConnManager(nil, nil, udpDialer)
 		meek.connManager = newMeekUnderlyingConnManager(nil, nil, udpDialer)
 
 
+		// Limitation: currently, the meekUnderlyingPacketConn wrapping done by
+		// dialPacketConn masks the quic-go.OOBCapablePacketConn capabilities
+		// of the underlying *net.UDPConn. With these capabilities unavailable,
+		// path MTU discovery and UDP socket buffer optimizations will be disabled.
+
 		var err error
 		var err error
 		transport, err = quic.NewQUICTransporter(
 		transport, err = quic.NewQUICTransporter(
 			ctx,
 			ctx,

+ 4 - 1
psiphon/server/server_test.go

@@ -1054,7 +1054,10 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 
 	serverConfig["RunPacketManipulator"] = runConfig.doPacketManipulation
 	serverConfig["RunPacketManipulator"] = runConfig.doPacketManipulation
 
 
-	if protocol.TunnelProtocolUsesQUIC(runConfig.tunnelProtocol) && quic.GQUICEnabled() {
+	if protocol.TunnelProtocolUsesQUIC(runConfig.tunnelProtocol) &&
+		!runConfig.limitQUICVersions &&
+		quic.GQUICEnabled() {
+
 		// Enable legacy QUIC version support.
 		// Enable legacy QUIC version support.
 		serverConfig["EnableGQUIC"] = true
 		serverConfig["EnableGQUIC"] = true
 	}
 	}

+ 11 - 7
psiphon/server/tunnelServer.go

@@ -173,27 +173,30 @@ func (server *TunnelServer) Run() error {
 			// in-proxy QUIC tunnel protocols don't support gQUIC.
 			// in-proxy QUIC tunnel protocols don't support gQUIC.
 			enableGQUIC := support.Config.EnableGQUIC && !usesInproxy
 			enableGQUIC := support.Config.EnableGQUIC && !usesInproxy
 
 
+			disablePathMTUDiscovery := false
 			maxPacketSizeAdjustment := 0
 			maxPacketSizeAdjustment := 0
 			if usesInproxy {
 			if usesInproxy {
 
 
 				// In the in-proxy WebRTC media stream mode, QUIC packets sent
 				// In the in-proxy WebRTC media stream mode, QUIC packets sent
 				// back to the client, via the proxy, are encapsulated in
 				// back to the client, via the proxy, are encapsulated in
 				// SRTP packet payloads, and the maximum QUIC packet size
 				// SRTP packet payloads, and the maximum QUIC packet size
-				// must be adjusted to fit.
+				// must be adjusted to fit. MTU discovery is disabled so the
+				// maximum packet size will not grow.
 				//
 				//
 				// Limitation: the WebRTC data channel mode does not have the
 				// Limitation: the WebRTC data channel mode does not have the
 				// same QUIC packet size constraint, since data channel
 				// same QUIC packet size constraint, since data channel
 				// messages can be far larger (up to 65536 bytes). However,
 				// messages can be far larger (up to 65536 bytes). However,
 				// the server, at this point, does not know whether
 				// the server, at this point, does not know whether
 				// individual connections are using WebRTC media streams or
 				// individual connections are using WebRTC media streams or
-				// data channels on the first hop, and will no know until API
-				// handshake information is delivered after the QUIC, OSSH,
-				// and SSH handshakes are completed. Currently the max packet
-				// size adjustment is set unconditionally. For data channels,
-				// this will result in suboptimal packet sizes (10s of bytes)
-				// and a corresponding different traffic shape on the 2nd hop.
+				// data channels on the first hop, and will not know until
+				// API handshake information is delivered after the QUIC,
+				// OSSH, and SSH handshakes are completed. Currently the max
+				// packet size adjustment is set unconditionally. For data
+				// channels, this will result in suboptimal packet sizes and
+				// a corresponding different traffic shape on the 2nd hop.
 
 
 				maxPacketSizeAdjustment = inproxy.GetQUICMaxPacketSizeAdjustment()
 				maxPacketSizeAdjustment = inproxy.GetQUICMaxPacketSizeAdjustment()
+				disablePathMTUDiscovery = true
 			}
 			}
 
 
 			logTunnelProtocol := tunnelProtocol
 			logTunnelProtocol := tunnelProtocol
@@ -205,6 +208,7 @@ func (server *TunnelServer) Run() error {
 						errors.Trace(err), LogFields(logFields))
 						errors.Trace(err), LogFields(logFields))
 				},
 				},
 				localAddress,
 				localAddress,
+				disablePathMTUDiscovery,
 				maxPacketSizeAdjustment,
 				maxPacketSizeAdjustment,
 				support.Config.ObfuscatedSSHKey,
 				support.Config.ObfuscatedSSHKey,
 				enableGQUIC)
 				enableGQUIC)