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

+ 2 - 1
ConsoleClient/main.go

@@ -275,7 +275,8 @@ func main() {
 		}()
 		}()
 
 
 		limitTunnelProtocols := config.GetClientParameters().TunnelProtocols(parameters.LimitTunnelProtocols)
 		limitTunnelProtocols := config.GetClientParameters().TunnelProtocols(parameters.LimitTunnelProtocols)
-		if psiphon.CountServerEntries(config.EgressRegion, limitTunnelProtocols) == 0 {
+		if psiphon.CountServerEntries(
+			config.UseUpstreamProxy(), config.EgressRegion, limitTunnelProtocols) == 0 {
 			embeddedServerListWaitGroup.Wait()
 			embeddedServerListWaitGroup.Wait()
 		} else {
 		} else {
 			defer embeddedServerListWaitGroup.Wait()
 			defer embeddedServerListWaitGroup.Wait()

+ 10 - 4
psiphon/common/protocol/protocol.go

@@ -35,6 +35,7 @@ const (
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET = "UNFRONTED-MEEK-SESSION-TICKET-OSSH"
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET = "UNFRONTED-MEEK-SESSION-TICKET-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
+	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH           = "QUIC-OSSH"
 
 
 	SERVER_ENTRY_SOURCE_EMBEDDED   = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_EMBEDDED   = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_REMOTE     = "REMOTE"
 	SERVER_ENTRY_SOURCE_REMOTE     = "REMOTE"
@@ -77,13 +78,14 @@ func (t TunnelProtocols) Validate() error {
 }
 }
 
 
 var SupportedTunnelProtocols = TunnelProtocols{
 var SupportedTunnelProtocols = TunnelProtocols{
-	TUNNEL_PROTOCOL_FRONTED_MEEK,
-	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
+	TUNNEL_PROTOCOL_SSH,
+	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
-	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
-	TUNNEL_PROTOCOL_SSH,
+	TUNNEL_PROTOCOL_FRONTED_MEEK,
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
+	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
 }
 }
 
 
 var SupportedServerEntrySources = TunnelProtocols{
 var SupportedServerEntrySources = TunnelProtocols{
@@ -122,6 +124,10 @@ func TunnelProtocolUsesObfuscatedSessionTickets(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET
 	return protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET
 }
 }
 
 
+func TunnelProtocolUsesQUIC(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH
+}
+
 func UseClientTunnelProtocol(
 func UseClientTunnelProtocol(
 	clientProtocol string,
 	clientProtocol string,
 	serverProtocols TunnelProtocols) bool {
 	serverProtocols TunnelProtocols) bool {

+ 6 - 0
psiphon/common/protocol/serverEntry.go

@@ -47,6 +47,7 @@ type ServerEntry struct {
 	SshPassword                   string   `json:"sshPassword"`
 	SshPassword                   string   `json:"sshPassword"`
 	SshHostKey                    string   `json:"sshHostKey"`
 	SshHostKey                    string   `json:"sshHostKey"`
 	SshObfuscatedPort             int      `json:"sshObfuscatedPort"`
 	SshObfuscatedPort             int      `json:"sshObfuscatedPort"`
+	SshObfuscatedQUICPort         int      `json:"sshObfuscatedQUICPort"`
 	SshObfuscatedKey              string   `json:"sshObfuscatedKey"`
 	SshObfuscatedKey              string   `json:"sshObfuscatedKey"`
 	Capabilities                  []string `json:"capabilities"`
 	Capabilities                  []string `json:"capabilities"`
 	Region                        string   `json:"region"`
 	Region                        string   `json:"region"`
@@ -92,6 +93,7 @@ func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
 // GetSupportedProtocols returns a list of tunnel protocols supported
 // GetSupportedProtocols returns a list of tunnel protocols supported
 // by the ServerEntry's capabilities.
 // by the ServerEntry's capabilities.
 func (serverEntry *ServerEntry) GetSupportedProtocols(
 func (serverEntry *ServerEntry) GetSupportedProtocols(
+	useUpstreamProxy bool,
 	limitTunnelProtocols []string,
 	limitTunnelProtocols []string,
 	impairedTunnelProtocols []string,
 	impairedTunnelProtocols []string,
 	excludeMeek bool) []string {
 	excludeMeek bool) []string {
@@ -100,6 +102,10 @@ func (serverEntry *ServerEntry) GetSupportedProtocols(
 
 
 	for _, protocol := range SupportedTunnelProtocols {
 	for _, protocol := range SupportedTunnelProtocols {
 
 
+		if useUpstreamProxy && TunnelProtocolUsesQUIC(protocol) {
+			continue
+		}
+
 		if len(limitTunnelProtocols) > 0 &&
 		if len(limitTunnelProtocols) > 0 &&
 			!common.Contains(limitTunnelProtocols, protocol) {
 			!common.Contains(limitTunnelProtocols, protocol) {
 			continue
 			continue

+ 20 - 12
psiphon/common/quic/quic.go

@@ -60,11 +60,14 @@ type Listener struct {
 	quic_go.Listener
 	quic_go.Listener
 }
 }
 
 
-// NewListener creates a new Listener. The inputs certificate/privateKey
-// specify the TLS key pair to be used by QUIC.
-func NewListener(
-	addr string,
-	certificate, privateKey string) (*Listener, error) {
+// Listen creates a new Listener.
+func Listen(addr string) (*Listener, error) {
+
+	certificate, privateKey, err := common.GenerateWebServerCertificate(
+		common.GenerateHostName())
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
 
 
 	tlsCertificate, err := tls.X509KeyPair(
 	tlsCertificate, err := tls.X509KeyPair(
 		[]byte(certificate), []byte(privateKey))
 		[]byte(certificate), []byte(privateKey))
@@ -113,14 +116,13 @@ func (listener *Listener) Accept() (net.Conn, error) {
 }
 }
 
 
 // Dial establishes a new QUIC session and stream to the server specified by
 // Dial establishes a new QUIC session and stream to the server specified by
-// remoteAddr. packetConn is used as the underlying packet connection for
-// QUIC. hostname specifies the SNI value to use in TLS. The dial may be
-// cancelled by ctx; packetConn will be closed if the dial is cancelled.
+// address. packetConn is used as the underlying packet connection for QUIC.
+// The dial may be cancelled by ctx; packetConn will be closed if the dial is
+// cancelled.
 func Dial(
 func Dial(
 	ctx context.Context,
 	ctx context.Context,
 	packetConn net.PacketConn,
 	packetConn net.PacketConn,
-	remoteAddr net.Addr,
-	hostname string) (net.Conn, error) {
+	address string) (net.Conn, error) {
 
 
 	type dialResult struct {
 	type dialResult struct {
 		conn *Conn
 		conn *Conn
@@ -140,10 +142,16 @@ func Dial(
 			quicConfig.HandshakeTimeout = deadline.Sub(time.Now())
 			quicConfig.HandshakeTimeout = deadline.Sub(time.Now())
 		}
 		}
 
 
+		udpAddr, err := net.ResolveUDPAddr("udp", address)
+		if err != nil {
+			resultChannel <- dialResult{err: err}
+			return
+		}
+
 		session, err := quic_go.Dial(
 		session, err := quic_go.Dial(
 			packetConn,
 			packetConn,
-			remoteAddr,
-			hostname,
+			udpAddr,
+			address,
 			&tls.Config{InsecureSkipVerify: true},
 			&tls.Config{InsecureSkipVerify: true},
 			quicConfig)
 			quicConfig)
 		if err != nil {
 		if err != nil {

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

@@ -21,7 +21,6 @@ package quic
 
 
 import (
 import (
 	"context"
 	"context"
-	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
 	"sync/atomic"
 	"sync/atomic"
@@ -40,20 +39,12 @@ func TestQUIC(t *testing.T) {
 	serverReceivedBytes := int64(0)
 	serverReceivedBytes := int64(0)
 	clientReceivedBytes := int64(0)
 	clientReceivedBytes := int64(0)
 
 
-	hostname := "www.example.com"
-	certificate, privateKey, err := common.GenerateWebServerCertificate(hostname)
-
-	listener, err := NewListener("127.0.0.1:0", certificate, privateKey)
-	if err != nil {
-		t.Errorf("NewListener failed: %s", err)
-	}
-
-	serverAddr, err := net.ResolveUDPAddr("udp", listener.Addr().String())
+	listener, err := Listen("127.0.0.1:0")
 	if err != nil {
 	if err != nil {
-		t.Errorf("ResolveUDPAddr failed: %s", err)
+		t.Errorf("Listen failed: %s", err)
 	}
 	}
 
 
-	serverHost := fmt.Sprintf("%s:%d", hostname, serverAddr.Port)
+	serverAddress := listener.Addr().String()
 
 
 	testGroup, testCtx := errgroup.WithContext(context.Background())
 	testGroup, testCtx := errgroup.WithContext(context.Background())
 
 
@@ -107,7 +98,7 @@ func TestQUIC(t *testing.T) {
 				return common.ContextError(err)
 				return common.ContextError(err)
 			}
 			}
 
 
-			conn, err := Dial(ctx, packetConn, serverAddr, serverHost)
+			conn, err := Dial(ctx, packetConn, serverAddress)
 			if err != nil {
 			if err != nil {
 				return common.ContextError(err)
 				return common.ContextError(err)
 			}
 			}

+ 4 - 0
psiphon/config.go

@@ -781,6 +781,10 @@ func (config *Config) GetAuthorizations() []string {
 	return config.authorizations
 	return config.authorizations
 }
 }
 
 
+func (config *Config) UseUpstreamProxy() bool {
+	return config.UpstreamProxyURL != ""
+}
+
 func (config *Config) makeConfigParameters() map[string]interface{} {
 func (config *Config) makeConfigParameters() map[string]interface{} {
 
 
 	// Build set of config values to apply to parameters.
 	// Build set of config values to apply to parameters.

+ 8 - 5
psiphon/dataStore.go

@@ -593,7 +593,8 @@ func newTargetServerEntryIterator(config *Config, isTactics bool) (bool, *Server
 		if len(limitTunnelProtocols) > 0 {
 		if len(limitTunnelProtocols) > 0 {
 			// At the ServerEntryIterator level, only limitTunnelProtocols is applied;
 			// At the ServerEntryIterator level, only limitTunnelProtocols is applied;
 			// impairedTunnelProtocols and excludeMeek are handled higher up.
 			// impairedTunnelProtocols and excludeMeek are handled higher up.
-			if len(serverEntry.GetSupportedProtocols(limitTunnelProtocols, nil, false)) == 0 {
+			if len(serverEntry.GetSupportedProtocols(
+				config.UseUpstreamProxy(), limitTunnelProtocols, nil, false)) == 0 {
 				return false, nil, common.ContextError(errors.New("TargetServerEntry does not support LimitTunnelProtocols"))
 				return false, nil, common.ContextError(errors.New("TargetServerEntry does not support LimitTunnelProtocols"))
 			}
 			}
 		}
 		}
@@ -633,7 +634,8 @@ func (iterator *ServerEntryIterator) Reset() error {
 		limitTunnelProtocols := iterator.config.clientParameters.Get().TunnelProtocols(
 		limitTunnelProtocols := iterator.config.clientParameters.Get().TunnelProtocols(
 			parameters.LimitTunnelProtocols)
 			parameters.LimitTunnelProtocols)
 
 
-		count := CountServerEntries(iterator.config.EgressRegion, limitTunnelProtocols)
+		count := CountServerEntries(
+			iterator.config.UseUpstreamProxy(), iterator.config.EgressRegion, limitTunnelProtocols)
 		NoticeCandidateServers(iterator.config.EgressRegion, limitTunnelProtocols, count)
 		NoticeCandidateServers(iterator.config.EgressRegion, limitTunnelProtocols, count)
 
 
 		// LimitTunnelProtocols may have changed since the last ReportAvailableRegions,
 		// LimitTunnelProtocols may have changed since the last ReportAvailableRegions,
@@ -835,7 +837,7 @@ func scanServerEntries(scanner func(*protocol.ServerEntry)) error {
 
 
 // CountServerEntries returns a count of stored servers for the
 // CountServerEntries returns a count of stored servers for the
 // specified region and tunnel protocols.
 // specified region and tunnel protocols.
-func CountServerEntries(region string, tunnelProtocols []string) int {
+func CountServerEntries(useUpstreamProxy bool, region string, tunnelProtocols []string) int {
 	checkInitDataStore()
 	checkInitDataStore()
 
 
 	count := 0
 	count := 0
@@ -844,7 +846,7 @@ func CountServerEntries(region string, tunnelProtocols []string) int {
 			(len(tunnelProtocols) == 0 ||
 			(len(tunnelProtocols) == 0 ||
 				// When CountServerEntries is called only limitTunnelProtocols is known;
 				// When CountServerEntries is called only limitTunnelProtocols is known;
 				// impairedTunnelProtocols and excludeMeek may not apply.
 				// impairedTunnelProtocols and excludeMeek may not apply.
-				len(serverEntry.GetSupportedProtocols(tunnelProtocols, nil, false)) > 0) {
+				len(serverEntry.GetSupportedProtocols(useUpstreamProxy, tunnelProtocols, nil, false)) > 0) {
 			count += 1
 			count += 1
 		}
 		}
 	})
 	})
@@ -905,7 +907,8 @@ func ReportAvailableRegions(config *Config) {
 		if len(limitTunnelProtocols) == 0 ||
 		if len(limitTunnelProtocols) == 0 ||
 			// When ReportAvailableRegions is called only limitTunnelProtocols is known;
 			// When ReportAvailableRegions is called only limitTunnelProtocols is known;
 			// impairedTunnelProtocols and excludeMeek may not apply.
 			// impairedTunnelProtocols and excludeMeek may not apply.
-			len(serverEntry.GetSupportedProtocols(limitTunnelProtocols, nil, false)) > 0 {
+			len(serverEntry.GetSupportedProtocols(
+				config.UseUpstreamProxy(), limitTunnelProtocols, nil, false)) > 0 {
 
 
 			regions[serverEntry.Region] = true
 			regions[serverEntry.Region] = true
 		}
 		}

+ 2 - 0
psiphon/server/config.go

@@ -734,6 +734,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 
 
 	sshPort := params.TunnelProtocolPorts["SSH"]
 	sshPort := params.TunnelProtocolPorts["SSH"]
 	obfuscatedSSHPort := params.TunnelProtocolPorts["OSSH"]
 	obfuscatedSSHPort := params.TunnelProtocolPorts["OSSH"]
+	obfuscatedSSHQUICPort := params.TunnelProtocolPorts["QUIC-OSSH"]
 
 
 	// Meek port limitations
 	// Meek port limitations
 	// - fronted meek protocols are hard-wired in the client to be port 443 or 80.
 	// - fronted meek protocols are hard-wired in the client to be port 443 or 80.
@@ -771,6 +772,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, []byt
 		SshPassword:                   sshPassword,
 		SshPassword:                   sshPassword,
 		SshHostKey:                    base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
 		SshHostKey:                    base64.RawStdEncoding.EncodeToString(sshPublicKey.Marshal()),
 		SshObfuscatedPort:             obfuscatedSSHPort,
 		SshObfuscatedPort:             obfuscatedSSHPort,
+		SshObfuscatedQUICPort:         obfuscatedSSHQUICPort,
 		SshObfuscatedKey:              obfuscatedSSHKey,
 		SshObfuscatedKey:              obfuscatedSSHKey,
 		Capabilities:                  capabilities,
 		Capabilities:                  capabilities,
 		Region:                        "US",
 		Region:                        "US",

+ 15 - 0
psiphon/server/server_test.go

@@ -193,6 +193,21 @@ func TestUnfrontedMeekSessionTicket(t *testing.T) {
 		})
 		})
 }
 }
 
 
+func TestQUICOSSH(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "QUIC-OSSH",
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
 func TestWebTransportAPIRequests(t *testing.T) {
 func TestWebTransportAPIRequests(t *testing.T) {
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{

+ 10 - 1
psiphon/server/tunnelServer.go

@@ -41,6 +41,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"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"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun"
 	"github.com/marusama/semaphore"
 	"github.com/marusama/semaphore"
@@ -136,7 +137,15 @@ func (server *TunnelServer) Run() error {
 		localAddress := fmt.Sprintf(
 		localAddress := fmt.Sprintf(
 			"%s:%d", support.Config.ServerIPAddress, listenPort)
 			"%s:%d", support.Config.ServerIPAddress, listenPort)
 
 
-		listener, err := net.Listen("tcp", localAddress)
+		var listener net.Listener
+		var err error
+		if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) {
+			listener, err = quic.Listen(localAddress)
+
+		} else {
+			listener, err = net.Listen("tcp", localAddress)
+		}
+
 		if err != nil {
 		if err != nil {
 			for _, existingListener := range listeners {
 			for _, existingListener := range listeners {
 				existingListener.Listener.Close()
 				existingListener.Listener.Close()

+ 31 - 5
psiphon/tunnel.go

@@ -39,6 +39,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"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"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	regen "github.com/zach-klippenstein/goregen"
 	regen "github.com/zach-klippenstein/goregen"
@@ -536,6 +537,7 @@ func selectProtocol(
 	usePriorityProtocol bool) (selectedProtocol string, err error) {
 	usePriorityProtocol bool) (selectedProtocol string, err error) {
 
 
 	candidateProtocols := serverEntry.GetSupportedProtocols(
 	candidateProtocols := serverEntry.GetSupportedProtocols(
+		config.UseUpstreamProxy(),
 		config.clientParameters.Get().TunnelProtocols(parameters.LimitTunnelProtocols),
 		config.clientParameters.Get().TunnelProtocols(parameters.LimitTunnelProtocols),
 		impairedProtocols,
 		impairedProtocols,
 		excludeMeek)
 		excludeMeek)
@@ -745,7 +747,7 @@ func initDialConfig(
 
 
 	var upstreamProxyType string
 	var upstreamProxyType string
 
 
-	if config.UpstreamProxyURL != "" {
+	if config.UseUpstreamProxy() {
 		// Note: UpstreamProxyURL will be validated in the dial
 		// Note: UpstreamProxyURL will be validated in the dial
 		proxyURL, err := url.Parse(config.UpstreamProxyURL)
 		proxyURL, err := url.Parse(config.UpstreamProxyURL)
 		if err == nil {
 		if err == nil {
@@ -877,19 +879,23 @@ func dialSsh(
 	var selectedSSHClientVersion bool
 	var selectedSSHClientVersion bool
 	SSHClientVersion := ""
 	SSHClientVersion := ""
 	useObfuscatedSsh := false
 	useObfuscatedSsh := false
-	var directTCPDialAddress string
+	var directDialAddress string
 	var meekConfig *MeekConfig
 	var meekConfig *MeekConfig
 	var err error
 	var err error
 
 
 	switch selectedProtocol {
 	switch selectedProtocol {
 	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH:
 	case protocol.TUNNEL_PROTOCOL_OBFUSCATED_SSH:
 		useObfuscatedSsh = true
 		useObfuscatedSsh = true
-		directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
+		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
+
+	case protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH:
+		useObfuscatedSsh = true
+		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedQUICPort)
 
 
 	case protocol.TUNNEL_PROTOCOL_SSH:
 	case protocol.TUNNEL_PROTOCOL_SSH:
 		selectedSSHClientVersion = true
 		selectedSSHClientVersion = true
 		SSHClientVersion = pickSSHClientVersion()
 		SSHClientVersion = pickSSHClientVersion()
-		directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshPort)
+		directDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshPort)
 
 
 	default:
 	default:
 		useObfuscatedSsh = true
 		useObfuscatedSsh = true
@@ -921,6 +927,7 @@ func dialSsh(
 
 
 	var dialConn net.Conn
 	var dialConn net.Conn
 	if meekConfig != nil {
 	if meekConfig != nil {
+
 		dialConn, err = DialMeek(
 		dialConn, err = DialMeek(
 			ctx,
 			ctx,
 			meekConfig,
 			meekConfig,
@@ -928,10 +935,29 @@ func dialSsh(
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
+
+	} else if protocol.TunnelProtocolUsesQUIC(selectedProtocol) {
+
+		// TODO:
+		// - use dialConfig?
+		// - SO_BINDTODEVICE etc.
+		packetConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: nil, Port: 0})
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+		dialConn, err = quic.Dial(
+			ctx,
+			packetConn,
+			directDialAddress)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
 	} else {
 	} else {
+
 		dialConn, err = DialTCPFragmentor(
 		dialConn, err = DialTCPFragmentor(
 			ctx,
 			ctx,
-			directTCPDialAddress,
+			directDialAddress,
 			dialConfig,
 			dialConfig,
 			selectedProtocol,
 			selectedProtocol,
 			config.clientParameters,
 			config.clientParameters,