Rod Hynes пре 7 година
родитељ
комит
92e8623c81

+ 12 - 3
psiphon/common/protocol/protocol.go

@@ -38,6 +38,7 @@ const (
 	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH           = "QUIC-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH   = "FRONTED-QUIC-OSSH"
 	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH     = "MARIONETTE-OSSH"
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH       = "TAPDANCE-OSSH"
 
@@ -101,6 +102,7 @@ var SupportedTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_FRONTED_MEEK,
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 }
@@ -128,12 +130,14 @@ func TunnelProtocolUsesObfuscatedSSH(protocol string) bool {
 
 func TunnelProtocolUsesMeek(protocol string) bool {
 	return TunnelProtocolUsesMeekHTTP(protocol) ||
-		TunnelProtocolUsesMeekHTTPS(protocol)
+		TunnelProtocolUsesMeekHTTPS(protocol) ||
+		TunnelProtocolUsesFrontedQUIC(protocol)
 }
 
 func TunnelProtocolUsesFrontedMeek(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
-		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP
+		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP ||
+		protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
 }
 
 func TunnelProtocolUsesMeekHTTP(protocol string) bool {
@@ -152,7 +156,12 @@ func TunnelProtocolUsesObfuscatedSessionTickets(protocol string) bool {
 }
 
 func TunnelProtocolUsesQUIC(protocol string) bool {
-	return protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH
+	return protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH ||
+		protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
+}
+
+func TunnelProtocolUsesFrontedQUIC(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
 }
 
 func TunnelProtocolUsesMarionette(protocol string) bool {

+ 70 - 0
psiphon/common/quic/quic.go

@@ -54,6 +54,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	quic_go "github.com/lucas-clemente/quic-go"
+	"github.com/lucas-clemente/quic-go/h2quic"
 	"github.com/lucas-clemente/quic-go/qerr"
 )
 
@@ -495,3 +496,72 @@ func (conn *loggingPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
 		}
 	}
 }
+
+// QUICTransporter implements the psiphon.transporter interface, used in
+// psiphon.MeekConn for HTTP requests, which requires a RoundTripper and
+// CloseIdleConnections.
+type QUICTransporter struct {
+	*h2quic.RoundTripper
+}
+
+// CloseIdleConnections wraps h2quic.RoundTripper.Close, which provides the
+// necessary functionality for psiphon.transporter as used by
+// psiphon.MeekConn. Note that, unlike http.Transport.CloseIdleConnections,
+// the connections are closed regardless of idle status.
+func (t *QUICTransporter) CloseIdleConnections() {
+	t.RoundTripper.Close()
+}
+
+// NewQUICTransporter creates a new QUICTransporter.
+func NewQUICTransporter(
+	ctx context.Context,
+	udpDialer func() (net.PacketConn, *net.UDPAddr, error),
+	quicSNIAddress string,
+	negotiateQUICVersion string) *QUICTransporter {
+
+	dialFunc := func(_, _ string, _ *tls.Config, _ *quic_go.Config) (quic_go.Session, 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,
+		}
+
+		packetConn, remoteAddr, err := udpDialer()
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
+		session, err := quic_go.DialContext(
+			ctx,
+			packetConn,
+			remoteAddr,
+			quicSNIAddress,
+			&tls.Config{InsecureSkipVerify: true},
+			quicConfig)
+		if err != nil {
+			packetConn.Close()
+			return nil, common.ContextError(err)
+		}
+
+		return session, nil
+
+	}
+
+	return &QUICTransporter{
+		RoundTripper: &h2quic.RoundTripper{
+			Dial: dialFunc,
+		},
+	}
+}

+ 20 - 0
psiphon/controller_test.go

@@ -436,6 +436,26 @@ func TestQUIC(t *testing.T) {
 		})
 }
 
+func TestFrontedQUIC(t *testing.T) {
+
+	t.Skipf("temporarily disabled")
+
+	controllerRun(t,
+		&controllerRunConfig{
+			expectNoServerEntries:    false,
+			protocol:                 protocol.TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH,
+			clientIsLatestVersion:    false,
+			disableUntunneledUpgrade: true,
+			disableEstablishing:      false,
+			disableApi:               false,
+			tunnelPoolSize:           1,
+			useUpstreamProxy:         false,
+			disruptNetwork:           false,
+			transformHostNames:       false,
+			useFragmentor:            false,
+		})
+}
+
 type controllerRunConfig struct {
 	expectNoServerEntries    bool
 	protocol                 string

+ 21 - 7
psiphon/dialParameters.go

@@ -334,11 +334,8 @@ func MakeDialParameters(
 
 	if !isReplay || !replayHostname {
 
-		if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
-
-			dialParams.QUICDialSNIAddress = fmt.Sprintf("%s:%d", common.GenerateHostName(), serverEntry.SshObfuscatedQUICPort)
-
-		} else if protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) {
+		if protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) ||
+			protocol.TunnelProtocolUsesFrontedQUIC(dialParams.TunnelProtocol) {
 
 			dialParams.MeekSNIServerName = ""
 			if p.WeightedCoinFlip(parameters.TransformHostNameProbability) {
@@ -359,13 +356,18 @@ func MakeDialParameters(
 			} else {
 				dialParams.MeekHostHeader = fmt.Sprintf("%s:%d", hostname, serverEntry.MeekServerPort)
 			}
+		} else if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
+
+			dialParams.QUICDialSNIAddress = fmt.Sprintf(
+				"%s:%d", common.GenerateHostName(), serverEntry.SshObfuscatedQUICPort)
 		}
 	}
 
 	if (!isReplay || !replayQUICVersion) &&
 		protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 
-		dialParams.QUICVersion = selectQUICVersion(p)
+		allowObfuscatedQUIC := !protocol.TunnelProtocolUsesFrontedQUIC(dialParams.TunnelProtocol)
+		dialParams.QUICVersion = selectQUICVersion(allowObfuscatedQUIC, p)
 	}
 
 	if (!isReplay || !replayObfuscatedQUIC) &&
@@ -410,6 +412,12 @@ func MakeDialParameters(
 	case protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH:
 		dialParams.DirectDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedQUICPort)
 
+	case protocol.TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH:
+		dialParams.MeekDialAddress = fmt.Sprintf("%s:443", dialParams.MeekFrontingDialAddress)
+		dialParams.MeekHostHeader = dialParams.MeekFrontingHost
+		if !dialParams.MeekTransformedHostName {
+			dialParams.MeekSNIServerName = dialParams.MeekFrontingDialAddress
+		}
 	case protocol.TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH:
 		// Note: port comes from marionnete "format"
 		dialParams.DirectDialAddress = serverEntry.IpAddress
@@ -539,6 +547,7 @@ func MakeDialParameters(
 		dialParams.meekConfig = &MeekConfig{
 			ClientParameters:              config.clientParameters,
 			DialAddress:                   dialParams.MeekDialAddress,
+			QUICVersion:                   dialParams.QUICVersion,
 			UseHTTPS:                      protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol),
 			TLSProfile:                    dialParams.TLSProfile,
 			RandomizedTLSProfileSeed:      dialParams.RandomizedTLSProfileSeed,
@@ -688,7 +697,7 @@ func selectFrontingParameters(serverEntry *protocol.ServerEntry) (string, string
 	return frontingDialHost, frontingHost, nil
 }
 
-func selectQUICVersion(p *parameters.ClientParametersSnapshot) string {
+func selectQUICVersion(allowObfuscatedQUIC bool, p *parameters.ClientParametersSnapshot) string {
 
 	limitQUICVersions := p.QUICVersions(parameters.LimitQUICVersions)
 
@@ -701,6 +710,11 @@ func selectQUICVersion(p *parameters.ClientParametersSnapshot) string {
 			continue
 		}
 
+		if !allowObfuscatedQUIC &&
+			protocol.QUICVersionIsObfuscated(quicVersion) {
+			continue
+		}
+
 		quicVersions = append(quicVersions, quicVersion)
 	}
 

+ 15 - 4
psiphon/dialParameters_test.go

@@ -179,10 +179,21 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		t.Fatalf("missing meek HTTPS fields")
 	}
 
-	if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) &&
-		(dialParams.QUICVersion == "" ||
-			dialParams.QUICDialSNIAddress == "") {
-		t.Fatalf("missing meek HTTPS fields")
+	if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) {
+		if dialParams.QUICVersion == "" {
+			t.Fatalf("missing QUIC version field")
+		}
+		if protocol.TunnelProtocolUsesFrontedQUIC(tunnelProtocol) {
+			if dialParams.MeekFrontingDialAddress == "" ||
+				dialParams.MeekFrontingHost == "" ||
+				dialParams.MeekSNIServerName == "" {
+				t.Fatalf("missing fronted QUIC fields")
+			}
+		} else {
+			if dialParams.QUICDialSNIAddress == "" {
+				t.Fatalf("missing QUIC SNI field")
+			}
+		}
 	}
 
 	if dialParams.LivenessTestSeed == nil {

+ 34 - 4
psiphon/meekConn.go

@@ -46,6 +46,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"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/quic"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
 )
 
@@ -74,7 +75,12 @@ type MeekConfig struct {
 	// where host may be a domain name or IP address.
 	DialAddress string
 
+	// QUICVersion indicates whether to use QUIC and which QUIC version
+	// to use. QUIC is not used when "".
+	QUICVersion string
+
 	// UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
+	// Ignored when QUICVersion is configured.
 	UseHTTPS bool
 
 	// TLSProfile specifies the value for CustomTLSConfig.TLSProfile for all
@@ -90,8 +96,8 @@ type MeekConfig struct {
 	// session tickets. Assumes UseHTTPS is true.
 	UseObfuscatedSessionTickets bool
 
-	// SNIServerName is the value to place in the TLS SNI server_name
-	// field when HTTPS is used.
+	// SNIServerName is the value to place in the TLS/QUIC SNI server_name
+	// field when HTTPS or QUIC is used.
 	SNIServerName string
 
 	// HostHeader is the value to place in the HTTP request Host header.
@@ -203,14 +209,38 @@ func DialMeek(
 		}
 	}()
 
-	// Configure transport: HTTP or HTTPS
+	// Configure transport: QUIC or HTTPS or HTTP
 
 	var scheme string
 	var transport transporter
 	var additionalHeaders http.Header
 	var proxyUrl func(*http.Request) (*url.URL, error)
 
-	if meekConfig.UseHTTPS {
+	if meekConfig.QUICVersion != "" {
+
+		scheme = "https"
+
+		udpDialer := func() (net.PacketConn, *net.UDPAddr, error) {
+			packetConn, remoteAddr, err := NewUDPConn(
+				runCtx,
+				meekConfig.DialAddress,
+				dialConfig)
+			if err != nil {
+				return nil, nil, common.ContextError(err)
+			}
+			return packetConn, remoteAddr, nil
+		}
+
+		_, port, _ := net.SplitHostPort(meekConfig.DialAddress)
+		quicDialSNIAddress := fmt.Sprintf("%s:%s", meekConfig.SNIServerName, port)
+
+		transport = quic.NewQUICTransporter(
+			runCtx,
+			udpDialer,
+			quicDialSNIAddress,
+			meekConfig.QUICVersion)
+
+	} else if meekConfig.UseHTTPS {
 
 		// Custom TLS dialer:
 		//