Browse Source

Report new fronted meek stats

Rod Hynes 10 years ago
parent
commit
b3b2df95d6
4 changed files with 76 additions and 13 deletions
  1. 3 0
      psiphon/TCPConn.go
  2. 7 0
      psiphon/net.go
  3. 25 0
      psiphon/serverApi.go
  4. 41 13
      psiphon/tunnel.go

+ 3 - 0
psiphon/TCPConn.go

@@ -61,6 +61,9 @@ func makeTCPDialer(config *DialConfig) func(network, addr string) (net.Conn, err
 		if err != nil {
 			return nil, ContextError(err)
 		}
+		if config.ResolvedIPCallback != nil {
+			config.ResolvedIPCallback(conn.RemoteAddr().String())
+		}
 		return conn, nil
 	}
 }

+ 7 - 0
psiphon/net.go

@@ -88,6 +88,13 @@ type DialConfig struct {
 	// When set, this value may be used, pre-connection, to select performance
 	// or circumvention optimization strategies for the given region.
 	DeviceRegion string
+
+	// ResolvedIPCallback, when set, is called with the IP address that was
+	// dialed. This is either the specified IP address in the dial address,
+	// or the resolved IP address in the case where the dial address is a
+	// domain name.
+	// The callback may be invoked by a concurrent goroutine.
+	ResolvedIPCallback func(string)
 }
 
 // DeviceBinder defines the interface to the external BindToDevice provider

+ 25 - 0
psiphon/serverApi.go

@@ -51,6 +51,16 @@ type ServerContext struct {
 	serverHandshakeTimestamp string
 }
 
+// FrontedMeekStats holds extra stats that are only gathered for
+// TUNNEL_PROTOCOL_FRONTED_MEEK. Specifically, which fronting address was
+// selected, what IP address a fronting domain resolved to, and whether SNI
+// was enabled.
+type FrontedMeekStats struct {
+	frontingAddress   string
+	resolvedIPAddress string
+	enabledSNI        bool
+}
+
 // nextTunnelNumber is a monotonically increasing number assigned to each
 // successive tunnel connection. The sessionId and tunnelNumber together
 // form a globally unique identifier for tunnels, which is used for
@@ -599,6 +609,21 @@ func makeBaseRequestUrl(tunnel *Tunnel, port, sessionId string) string {
 	requestUrl.WriteString(tunnel.config.ClientPlatform)
 	requestUrl.WriteString("&tunnel_whole_device=")
 	requestUrl.WriteString(strconv.Itoa(tunnel.config.TunnelWholeDevice))
+	requestUrl.WriteString("&device_region=")
+	requestUrl.WriteString(tunnel.config.DeviceRegion)
+	if tunnel.frontedMeekStats != nil {
+		requestUrl.WriteString("&fronting_address=")
+		requestUrl.WriteString(tunnel.frontedMeekStats.frontingAddress)
+		requestUrl.WriteString("&fronting_resolved_ip_address=")
+		requestUrl.WriteString(tunnel.frontedMeekStats.resolvedIPAddress)
+		requestUrl.WriteString("&fronting_enabled_sni=")
+		if tunnel.frontedMeekStats.enabledSNI {
+			requestUrl.WriteString("1")
+		} else {
+			requestUrl.WriteString("0")
+		}
+	}
+
 	return requestUrl.String()
 }
 

+ 41 - 13
psiphon/tunnel.go

@@ -28,6 +28,7 @@ import (
 	"io"
 	"net"
 	"sync"
+	"sync/atomic"
 	"time"
 
 	regen "github.com/Psiphon-Inc/goregen"
@@ -77,6 +78,7 @@ type Tunnel struct {
 	signalPortForwardFailure chan struct{}
 	totalPortForwardFailures int
 	startTime                time.Time
+	frontedMeekStats         *FrontedMeekStats
 }
 
 // EstablishTunnel first makes a network transport connection to the
@@ -103,7 +105,7 @@ func EstablishTunnel(
 	}
 
 	// Build transport layers and establish SSH connection
-	conn, sshClient, err := dialSsh(
+	conn, sshClient, frontedMeekStats, err := dialSsh(
 		config, pendingConns, serverEntry, selectedProtocol, sessionId)
 	if err != nil {
 		return nil, ContextError(err)
@@ -132,6 +134,7 @@ func EstablishTunnel(
 		// A buffer allows at least one signal to be sent even when the receiver is
 		// not listening. Senders should not block.
 		signalPortForwardFailure: make(chan struct{}, 1),
+		frontedMeekStats:         frontedMeekStats,
 	}
 
 	// Create a new Psiphon API server context for this tunnel. This includes
@@ -331,13 +334,16 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 	return selectedProtocol, nil
 }
 
-// dialSsh is a helper that builds the transport layers and establishes the SSH connection
+// dialSsh is a helper that builds the transport layers and establishes the SSH connection.
+// When TUNNEL_PROTOCOL_FRONTED_MEEK is selected, additional FrontedMeekStats are recorded
+// and returned.
 func dialSsh(
 	config *Config,
 	pendingConns *Conns,
 	serverEntry *ServerEntry,
 	selectedProtocol,
-	sessionId string) (conn net.Conn, sshClient *ssh.Client, err error) {
+	sessionId string) (
+	conn net.Conn, sshClient *ssh.Client, frontedMeekStats *FrontedMeekStats, err error) {
 
 	// The meek protocols tunnel obfuscated SSH. Obfuscated SSH is layered on top of SSH.
 	// So depending on which protocol is used, multiple layers are initialized.
@@ -376,7 +382,7 @@ func dialSsh(
 
 			frontingAddress, err = regen.Generate(serverEntry.MeekFrontingAddressesRegex)
 			if err != nil {
-				return nil, nil, ContextError(err)
+				return nil, nil, nil, ContextError(err)
 			}
 		} else {
 
@@ -384,21 +390,34 @@ func dialSsh(
 			// fronting-capable servers.
 
 			if len(serverEntry.MeekFrontingAddresses) == 0 {
-				return nil, nil, ContextError(errors.New("MeekFrontingAddresses is empty"))
+				return nil, nil, nil, ContextError(errors.New("MeekFrontingAddresses is empty"))
 			}
 			index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingAddresses))
 			if err != nil {
-				return nil, nil, ContextError(err)
+				return nil, nil, nil, ContextError(err)
 			}
 			frontingAddress = serverEntry.MeekFrontingAddresses[index]
 		}
 	}
+
 	NoticeConnectingServer(
 		serverEntry.IpAddress,
 		serverEntry.Region,
 		selectedProtocol,
 		frontingAddress)
 
+	// Use an asynchronous callback to record the resolved IP address when
+	// dialing a domain name. Note that DialMeek doesn't immediately
+	// establish any HTTPS connections, so the resolved IP address won't be
+	// reported until during/after ssh session establishment (the ssh traffic
+	// is meek payload). So don't Load() the IP address value until after that
+	// has completed to ensure a result.
+	var resolvedIPAddress atomic.Value
+	resolvedIPAddress.Store("")
+	setResolvedIPAddress := func(IPAddress string) {
+		resolvedIPAddress.Store(IPAddress)
+	}
+
 	// Create the base transport: meek or direct connection
 	dialConfig := &DialConfig{
 		UpstreamProxyUrl:              config.UpstreamProxyUrl,
@@ -409,16 +428,17 @@ func dialSsh(
 		UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
 		TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
 		DeviceRegion:                  config.DeviceRegion,
+		ResolvedIPCallback:            setResolvedIPAddress,
 	}
 	if useMeek {
 		conn, err = DialMeek(serverEntry, sessionId, useMeekHttps, frontingAddress, dialConfig)
 		if err != nil {
-			return nil, nil, ContextError(err)
+			return nil, nil, nil, ContextError(err)
 		}
 	} else {
 		conn, err = DialTCP(fmt.Sprintf("%s:%d", serverEntry.IpAddress, port), dialConfig)
 		if err != nil {
-			return nil, nil, ContextError(err)
+			return nil, nil, nil, ContextError(err)
 		}
 	}
 
@@ -436,14 +456,14 @@ func dialSsh(
 	if useObfuscatedSsh {
 		sshConn, err = NewObfuscatedSshConn(conn, serverEntry.SshObfuscatedKey)
 		if err != nil {
-			return nil, nil, ContextError(err)
+			return nil, nil, nil, ContextError(err)
 		}
 	}
 
 	// Now establish the SSH session over the sshConn transport
 	expectedPublicKey, err := base64.StdEncoding.DecodeString(serverEntry.SshHostKey)
 	if err != nil {
-		return nil, nil, ContextError(err)
+		return nil, nil, nil, ContextError(err)
 	}
 	sshCertChecker := &ssh.CertChecker{
 		HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error {
@@ -459,7 +479,7 @@ func dialSsh(
 			SshPassword string `json:"SshPassword"`
 		}{sessionId, serverEntry.SshPassword})
 	if err != nil {
-		return nil, nil, ContextError(err)
+		return nil, nil, nil, ContextError(err)
 	}
 	sshClientConfig := &ssh.ClientConfig{
 		User: serverEntry.SshUsername,
@@ -503,10 +523,18 @@ func dialSsh(
 
 	result := <-resultChannel
 	if result.err != nil {
-		return nil, nil, ContextError(result.err)
+		return nil, nil, nil, ContextError(result.err)
+	}
+
+	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK {
+		frontedMeekStats = &FrontedMeekStats{
+			frontingAddress:   frontingAddress,
+			resolvedIPAddress: resolvedIPAddress.Load().(string),
+			enabledSNI:        false,
+		}
 	}
 
-	return conn, result.sshClient, nil
+	return conn, result.sshClient, frontedMeekStats, nil
 }
 
 // operateTunnel monitors the health of the tunnel and performs