Просмотр исходного кода

Support negotiating specific QUIC versions, configurable via tactics

Rod Hynes 7 лет назад
Родитель
Сommit
bbd763f626

+ 43 - 0
psiphon/common/parameters/clientParameters.go

@@ -92,6 +92,8 @@ const (
 	LimitTunnelProtocols                       = "LimitTunnelProtocols"
 	LimitTLSProfilesProbability                = "LimitTLSProfilesProbability"
 	LimitTLSProfiles                           = "LimitTLSProfiles"
+	LimitQUICVersionsProbability               = "LimitQUICVersionsProbability"
+	LimitQUICVersions                          = "LimitQUICVersions"
 	FragmentorProbability                      = "FragmentorProbability"
 	FragmentorLimitProtocols                   = "FragmentorLimitProtocols"
 	FragmentorMinTotalBytes                    = "FragmentorMinTotalBytes"
@@ -228,6 +230,9 @@ var defaultClientParameters = map[string]struct {
 	LimitTLSProfilesProbability: {value: 1.0, minimum: 0.0},
 	LimitTLSProfiles:            {value: protocol.TLSProfiles{}},
 
+	LimitQUICVersionsProbability: {value: 1.0, minimum: 0.0},
+	LimitQUICVersions:            {value: []string{protocol.QUIC_VERSION_GQUIC43}},
+
 	FragmentorProbability:    {value: 0.5, minimum: 0.0},
 	FragmentorLimitProtocols: {value: protocol.TunnelProtocols{}},
 	FragmentorMinTotalBytes:  {value: 0, minimum: 0},
@@ -505,6 +510,15 @@ func (p *ClientParameters) Set(
 						return nil, common.ContextError(err)
 					}
 				}
+			case protocol.QUICVersions:
+				if skipOnError {
+					newValue = v.PruneInvalid()
+				} else {
+					err := v.Validate()
+					if err != nil {
+						return nil, common.ContextError(err)
+					}
+				}
 			}
 
 			// Enforce any minimums. Assumes defaultClientParameters[name]
@@ -740,6 +754,35 @@ func (p *ClientParametersSnapshot) TLSProfiles(name string) protocol.TLSProfiles
 	return value
 }
 
+// QUICVersions returns a protocol.QUICVersions parameter value.
+// If there is a corresponding Probability value, a weighted coin flip
+// will be performed and, depending on the result, the value or the
+// parameter default will be returned.
+func (p *ClientParametersSnapshot) QUICVersions(name string) protocol.QUICVersions {
+
+	probabilityName := name + "Probability"
+	_, ok := p.parameters[probabilityName]
+	if ok {
+		probabilityValue := float64(1.0)
+		p.getValue(probabilityName, &probabilityValue)
+		if !common.FlipWeightedCoin(probabilityValue) {
+			defaultParameter, ok := defaultClientParameters[name]
+			if ok {
+				defaultValue, ok := defaultParameter.value.(protocol.QUICVersions)
+				if ok {
+					value := make(protocol.QUICVersions, len(defaultValue))
+					copy(value, defaultValue)
+					return value
+				}
+			}
+		}
+	}
+
+	value := protocol.QUICVersions{}
+	p.getValue(name, &value)
+	return value
+}
+
 // DownloadURLs returns a DownloadURLs parameter value.
 func (p *ClientParametersSnapshot) DownloadURLs(name string) DownloadURLs {
 	value := DownloadURLs{}

+ 33 - 0
psiphon/common/protocol/protocol.go

@@ -223,6 +223,39 @@ func (profiles TLSProfiles) PruneInvalid() TLSProfiles {
 	return q
 }
 
+const (
+	QUIC_VERSION_GQUIC39 = "gQUICv39"
+	QUIC_VERSION_GQUIC43 = "gQUICv43"
+	QUIC_VERSION_GQUIC44 = "gQUICv44"
+)
+
+var SupportedQUICVersions = QUICVersions{
+	QUIC_VERSION_GQUIC39,
+	QUIC_VERSION_GQUIC43,
+	QUIC_VERSION_GQUIC44,
+}
+
+type QUICVersions []string
+
+func (versions QUICVersions) Validate() error {
+	for _, v := range versions {
+		if !common.Contains(SupportedQUICVersions, v) {
+			return common.ContextError(fmt.Errorf("invalid QUIC version: %s", v))
+		}
+	}
+	return nil
+}
+
+func (versions QUICVersions) PruneInvalid() QUICVersions {
+	u := make(QUICVersions, 0)
+	for _, v := range versions {
+		if common.Contains(SupportedQUICVersions, v) {
+			u = append(u, v)
+		}
+	}
+	return u
+}
+
 type HandshakeResponse struct {
 	SSHSessionID           string              `json:"ssh_session_id"`
 	Homepages              []string            `json:"homepages"`

+ 21 - 1
psiphon/common/quic/quic.go

@@ -43,6 +43,7 @@ package quic
 import (
 	"context"
 	"crypto/tls"
+	"fmt"
 	"io"
 	"net"
 	"sync"
@@ -50,6 +51,7 @@ import (
 	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	quic_go "github.com/lucas-clemente/quic-go"
 	"github.com/lucas-clemente/quic-go/qerr"
 )
@@ -120,6 +122,12 @@ func (listener *Listener) Accept() (net.Conn, error) {
 	}, nil
 }
 
+var supportedVersionNumbers = map[string]quic_go.VersionNumber{
+	protocol.QUIC_VERSION_GQUIC39: quic_go.VersionGQUIC39,
+	protocol.QUIC_VERSION_GQUIC43: quic_go.VersionGQUIC43,
+	protocol.QUIC_VERSION_GQUIC44: quic_go.VersionGQUIC44,
+}
+
 // Dial establishes a new QUIC session and stream to the server specified by
 // address.
 //
@@ -133,12 +141,24 @@ func Dial(
 	ctx context.Context,
 	packetConn net.PacketConn,
 	remoteAddr *net.UDPAddr,
-	quicSNIAddress string) (net.Conn, error) {
+	quicSNIAddress string,
+	negotiateQUICVersion string) (net.Conn, error) {
+
+	var versions []quic_go.VersionNumber
+
+	if negotiateQUICVersion != "" {
+		versionNumber, ok := supportedVersionNumbers[negotiateQUICVersion]
+		if !ok {
+			return nil, common.ContextError(fmt.Errorf("unsupported version: %s", negotiateQUICVersion))
+		}
+		versions = []quic_go.VersionNumber{versionNumber}
+	}
 
 	quicConfig := &quic_go.Config{
 		HandshakeTimeout: time.Duration(1<<63 - 1),
 		IdleTimeout:      CLIENT_IDLE_TIMEOUT,
 		KeepAlive:        true,
+		Versions:         versions,
 	}
 
 	deadline, ok := ctx.Deadline()

+ 28 - 1
psiphon/tunnel.go

@@ -904,7 +904,8 @@ func dialSsh(
 			ctx,
 			packetConn,
 			remoteAddr,
-			quicDialSNIAddress)
+			quicDialSNIAddress,
+			selectQUICVersion(config.clientParameters))
 		if err != nil {
 			return nil, common.ContextError(err)
 		}
@@ -1119,6 +1120,32 @@ func dialSsh(
 		nil
 }
 
+func selectQUICVersion(
+	clientParameters *parameters.ClientParameters) string {
+
+	limitQUICVersions := clientParameters.Get().QUICVersions(parameters.LimitQUICVersions)
+
+	quicVersions := make([]string, 0)
+
+	for _, quicVersion := range protocol.SupportedQUICVersions {
+
+		if len(limitQUICVersions) > 0 &&
+			!common.Contains(limitQUICVersions, quicVersion) {
+			continue
+		}
+
+		quicVersions = append(quicVersions, quicVersion)
+	}
+
+	if len(quicVersions) == 0 {
+		return ""
+	}
+
+	choice, _ := common.MakeSecureRandomInt(len(quicVersions))
+
+	return quicVersions[choice]
+}
+
 func makeRandomPeriod(min, max time.Duration) time.Duration {
 	period, err := common.MakeSecureRandomPeriod(min, max)
 	if err != nil {