Procházet zdrojové kódy

Implement impaired protocol mechanism

Rod Hynes před 10 roky
rodič
revize
903822d945
4 změnil soubory, kde provedl 155 přidání a 64 odebrání
  1. 2 0
      psiphon/config.go
  2. 91 26
      psiphon/controller.go
  3. 55 0
      psiphon/serverEntry.go
  4. 7 38
      psiphon/tunnel.go

+ 2 - 0
psiphon/config.go

@@ -59,6 +59,8 @@ const (
 	FETCH_ROUTES_TIMEOUT                         = 1 * time.Minute
 	DOWNLOAD_UPGRADE_TIMEOUT                     = 15 * time.Minute
 	DOWNLOAD_UPGRADE_RETRY_PAUSE_PERIOD          = 5 * time.Second
+	IMPAIRED_PROTOCOL_CLASSIFICATION_DURATION    = 2 * time.Minute
+	IMPAIRED_PROTOCOL_CLASSIFICATION_THRESHOLD   = 3
 )
 
 // To distinguish omitted timeout params from explicit 0 value timeout

+ 91 - 26
psiphon/controller.go

@@ -34,28 +34,29 @@ import (
 // connect to; establishes and monitors tunnels; and runs local proxies which
 // route traffic through the tunnels.
 type Controller struct {
-	config                      *Config
-	sessionId                   string
-	componentFailureSignal      chan struct{}
-	shutdownBroadcast           chan struct{}
-	runWaitGroup                *sync.WaitGroup
-	establishedTunnels          chan *Tunnel
-	failedTunnels               chan *Tunnel
-	tunnelMutex                 sync.Mutex
-	establishedOnce             bool
-	tunnels                     []*Tunnel
-	nextTunnel                  int
-	startedConnectedReporter    bool
-	startedUpgradeDownloader    bool
-	isEstablishing              bool
-	establishWaitGroup          *sync.WaitGroup
-	stopEstablishingBroadcast   chan struct{}
-	candidateServerEntries      chan *ServerEntry
-	establishPendingConns       *Conns
-	untunneledPendingConns      *Conns
-	untunneledDialConfig        *DialConfig
-	splitTunnelClassifier       *SplitTunnelClassifier
-	signalFetchRemoteServerList chan struct{}
+	config                         *Config
+	sessionId                      string
+	componentFailureSignal         chan struct{}
+	shutdownBroadcast              chan struct{}
+	runWaitGroup                   *sync.WaitGroup
+	establishedTunnels             chan *Tunnel
+	failedTunnels                  chan *Tunnel
+	tunnelMutex                    sync.Mutex
+	establishedOnce                bool
+	tunnels                        []*Tunnel
+	nextTunnel                     int
+	startedConnectedReporter       bool
+	startedUpgradeDownloader       bool
+	isEstablishing                 bool
+	establishWaitGroup             *sync.WaitGroup
+	stopEstablishingBroadcast      chan struct{}
+	candidateServerEntries         chan *ServerEntry
+	establishPendingConns          *Conns
+	untunneledPendingConns         *Conns
+	untunneledDialConfig           *DialConfig
+	splitTunnelClassifier          *SplitTunnelClassifier
+	signalFetchRemoteServerList    chan struct{}
+	impairedProtocolClassification map[string]int
 }
 
 // NewController initializes a new controller.
@@ -102,7 +103,8 @@ func NewController(config *Config) (controller *Controller, err error) {
 		untunneledDialConfig:     untunneledDialConfig,
 		// A buffer allows at least one signal to be sent even when the receiver is
 		// not listening. Senders should not block.
-		signalFetchRemoteServerList: make(chan struct{}, 1),
+		signalFetchRemoteServerList:    make(chan struct{}, 1),
+		impairedProtocolClassification: make(map[string]int),
 	}
 
 	controller.splitTunnelClassifier = NewSplitTunnelClassifier(config, controller)
@@ -419,6 +421,8 @@ loop:
 			default:
 			}
 
+			controller.classifyImpairedProtocol(failedTunnel)
+
 			// Concurrency note: only this goroutine may call startEstablishing/stopEstablishing
 			// and access isEstablishing.
 			if !controller.isEstablishing {
@@ -462,6 +466,48 @@ loop:
 	NoticeInfo("exiting run tunnels")
 }
 
+// classifyImpairedProtocol tracks "impaired" protocol classifications for failed
+// tunnels. A protocol is classified as impaired if a tunnel using that protocol
+// fails, repeatedly, shortly after the start of the session. During tunnel
+// establishment, impaired protocols are briefly skipped.
+//
+// One purpose of this measure is to defend against an attack where the adversary,
+// for example, tags an OSSH TCP connection as an "unidentified" protocol; allows
+// it to connect; but then kills the underlying TCP connection after a short time.
+// Since OSSH has less latency than other protocols that may bypass an "unidentified"
+// filter, these other protocols might never be selected for use.
+//
+// Concurrency note: only the runTunnels() goroutine may call classifyImpairedProtocol
+func (controller *Controller) classifyImpairedProtocol(failedTunnel *Tunnel) {
+	if failedTunnel.sessionStartTime.Add(IMPAIRED_PROTOCOL_CLASSIFICATION_DURATION).After(time.Now()) {
+		controller.impairedProtocolClassification[failedTunnel.protocol] += 1
+	} else {
+		controller.impairedProtocolClassification[failedTunnel.protocol] = 0
+	}
+	if len(controller.getImpairedProtocols()) == len(SupportedTunnelProtocols) {
+		// Reset classification if all protocols are classified as impaired as
+		// the network situation (or attack) may not be protocol-specific.
+		controller.impairedProtocolClassification = make(map[string]int)
+	}
+}
+
+// getImpairedProtocols returns a list of protocols that have sufficient
+// classifications to be considered impaired protocols.
+//
+// Concurrency note: only the runTunnels() goroutine may call getImpairedProtocols
+func (controller *Controller) getImpairedProtocols() []string {
+	if len(controller.impairedProtocolClassification) > 0 {
+		NoticeInfo("impaired protocols: %+v", controller.impairedProtocolClassification)
+	}
+	impairedProtocols := make([]string, 0)
+	for protocol, count := range controller.impairedProtocolClassification {
+		if count >= IMPAIRED_PROTOCOL_CLASSIFICATION_THRESHOLD {
+			impairedProtocols = append(impairedProtocols, protocol)
+		}
+	}
+	return impairedProtocols
+}
+
 // SignalTunnelFailure implements the TunnelOwner interface. This function
 // is called by Tunnel.operateTunnel when the tunnel has detected that it
 // has failed. The Controller will signal runTunnels to create a new
@@ -676,7 +722,8 @@ func (controller *Controller) startEstablishing() {
 	}
 
 	controller.establishWaitGroup.Add(1)
-	go controller.establishCandidateGenerator()
+	go controller.establishCandidateGenerator(
+		controller.getImpairedProtocols())
 }
 
 // stopEstablishing signals the establish goroutines to stop and waits
@@ -704,7 +751,7 @@ func (controller *Controller) stopEstablishing() {
 // establishCandidateGenerator populates the candidate queue with server entries
 // from the data store. Server entries are iterated in rank order, so that promoted
 // servers with higher rank are priority candidates.
-func (controller *Controller) establishCandidateGenerator() {
+func (controller *Controller) establishCandidateGenerator(impairedProtocols []string) {
 	defer controller.establishWaitGroup.Done()
 	defer close(controller.candidateServerEntries)
 
@@ -729,7 +776,7 @@ loop:
 
 		// Send each iterator server entry to the establish workers
 		startTime := time.Now()
-		for {
+		for i := 0; ; i++ {
 			serverEntry, err := iterator.Next()
 			if err != nil {
 				NoticeAlert("failed to get next candidate: %s", err)
@@ -741,6 +788,24 @@ loop:
 				break
 			}
 
+			// Disable impaired protocols. This is only done for the
+			// first iteration of the ESTABLISH_TUNNEL_WORK_TIME_SECONDS
+			// loop since (a) one iteration should be sufficient to
+			// evade the attack; (b) there's a good chance of false
+			// positives (such as short session durations due to network
+			// hopping on a mobile device).
+			// Impaired protocols logic is not applied when
+			// config.TunnelProtocol is specified.
+			if i == 0 && controller.config.TunnelProtocol == "" {
+				serverEntry.DisableImpairedProtocols(impairedProtocols)
+				if len(serverEntry.GetSupportedProtocols()) == 0 {
+					// Skip this server entry, as it has no supported
+					// protocols after disabling the impaired ones
+					// TODO: modify ServerEntryIterator to skip these?
+					continue
+				}
+			}
+
 			// TODO: here we could generate multiple candidates from the
 			// server entry when there are many MeekFrontingAddresses.
 

+ 55 - 0
psiphon/serverEntry.go

@@ -29,6 +29,20 @@ import (
 	"strings"
 )
 
+const (
+	TUNNEL_PROTOCOL_SSH            = "SSH"
+	TUNNEL_PROTOCOL_OBFUSCATED_SSH = "OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK = "UNFRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK   = "FRONTED-MEEK-OSSH"
+)
+
+var SupportedTunnelProtocols = []string{
+	TUNNEL_PROTOCOL_FRONTED_MEEK,
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
+	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_SSH,
+}
+
 // ServerEntry represents a Psiphon server. It contains information
 // about how to estalish a tunnel connection to the server through
 // several protocols. ServerEntry are JSON records downloaded from
@@ -54,6 +68,47 @@ type ServerEntry struct {
 	MeekFrontingAddresses         []string `json:"meekFrontingAddresses"`
 }
 
+// SupportsProtocol returns true if and only if the ServerEntry has
+// the necessary capability to support the specified tunnel protocol.
+func (serverEntry *ServerEntry) SupportsProtocol(protocol string) bool {
+	requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
+	return Contains(serverEntry.Capabilities, requiredCapability)
+}
+
+// GetSupportedProtocols returns a list of tunnel protocols supported
+// by the ServerEntry's capabilities.
+func (serverEntry *ServerEntry) GetSupportedProtocols() []string {
+	supportedProtocols := make([]string, 0)
+	for _, protocol := range SupportedTunnelProtocols {
+		requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
+		if Contains(serverEntry.Capabilities, requiredCapability) {
+			supportedProtocols = append(supportedProtocols, protocol)
+		}
+	}
+	return supportedProtocols
+}
+
+// DisableImpairedProtocols modifies the ServerEntry to disable
+// the specified protocols.
+// Note: this assumes that protocol capabilities are 1-to-1.
+func (serverEntry *ServerEntry) DisableImpairedProtocols(impairedProtocols []string) {
+	capabilities := make([]string, 0)
+	for _, capability := range serverEntry.Capabilities {
+		omit := false
+		for _, protocol := range impairedProtocols {
+			requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
+			if capability == requiredCapability {
+				omit = true
+				break
+			}
+		}
+		if !omit {
+			capabilities = append(capabilities, capability)
+		}
+	}
+	serverEntry.Capabilities = capabilities
+}
+
 // DecodeServerEntry extracts server entries from the encoding
 // used by remote server lists and Psiphon server handshake requests.
 func DecodeServerEntry(encodedServerEntry string) (serverEntry *ServerEntry, err error) {

+ 7 - 38
psiphon/tunnel.go

@@ -27,7 +27,6 @@ import (
 	"fmt"
 	"io"
 	"net"
-	"strings"
 	"sync"
 	"time"
 
@@ -58,21 +57,6 @@ type TunnelOwner interface {
 	SignalTunnelFailure(tunnel *Tunnel)
 }
 
-const (
-	TUNNEL_PROTOCOL_SSH            = "SSH"
-	TUNNEL_PROTOCOL_OBFUSCATED_SSH = "OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK = "UNFRONTED-MEEK-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_MEEK   = "FRONTED-MEEK-OSSH"
-)
-
-// This is a list of supported tunnel protocols, in default preference order
-var SupportedTunnelProtocols = []string{
-	TUNNEL_PROTOCOL_FRONTED_MEEK,
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
-	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
-	TUNNEL_PROTOCOL_SSH,
-}
-
 // Tunnel is a connection to a Psiphon server. An established
 // tunnel includes a network connection to the specified server
 // and an SSH session built on top of that transport.
@@ -88,6 +72,7 @@ type Tunnel struct {
 	shutdownOperateBroadcast chan struct{}
 	portForwardFailures      chan int
 	portForwardFailureTotal  int
+	sessionStartTime         time.Time
 }
 
 // EstablishTunnel first makes a network transport connection to the
@@ -98,8 +83,7 @@ type Tunnel struct {
 // plain SSH over TCP, obfuscated SSH over TCP, or obfuscated SSH over
 // HTTP (meek protocol).
 // When requiredProtocol is not blank, that protocol is used. Otherwise,
-// the first protocol in SupportedTunnelProtocols that's also in the
-// server capabilities is used.
+// the a random supported protocol is used.
 func EstablishTunnel(
 	config *Config,
 	sessionId string,
@@ -155,6 +139,8 @@ func EstablishTunnel(
 		}
 	}
 
+	tunnel.sessionStartTime = time.Now()
+
 	// Now that network operations are complete, cancel interruptibility
 	pendingConns.Remove(conn)
 
@@ -306,8 +292,7 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 	// TODO: properly handle protocols (e.g. FRONTED-MEEK-OSSH) vs. capabilities (e.g., {FRONTED-MEEK, OSSH})
 	// for now, the code is simply assuming that MEEK capabilities imply OSSH capability.
 	if config.TunnelProtocol != "" {
-		requiredCapability := strings.TrimSuffix(config.TunnelProtocol, "-OSSH")
-		if !Contains(serverEntry.Capabilities, requiredCapability) {
+		if !serverEntry.SupportsProtocol(config.TunnelProtocol) {
 			return "", ContextError(fmt.Errorf("server does not have required capability"))
 		}
 		selectedProtocol = config.TunnelProtocol
@@ -315,26 +300,10 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 		// Pick at random from the supported protocols. This ensures that we'll eventually
 		// try all possible protocols. Depending on network configuration, it may be the
 		// case that some protocol is only available through multi-capability servers,
-		// and a simplr ranked preference of protocols could lead to that protocol never
+		// and a simpler ranked preference of protocols could lead to that protocol never
 		// being selected.
 
-		// TODO: this is a good spot to apply protocol selection weightings. This would be
-		// to defend against an attack where the adversary, for example, classifies OSSH as
-		// an "unidentified" protocol; allows it to connect; but then kills the underlying
-		// TCP connection after a short time. Since OSSH has less latency than other protocols
-		// that may bypass an "unidentified" filter, other protocols which would be otherwise
-		// classified and not killed might never be selected for use.
-		// So one proposed defense is to add negative selection weights to the protocol
-		// associated with failed tunnels (controller.failedTunnels) with short session
-		// durations.
-
-		candidateProtocols := make([]string, 0)
-		for _, protocol := range SupportedTunnelProtocols {
-			requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
-			if Contains(serverEntry.Capabilities, requiredCapability) {
-				candidateProtocols = append(candidateProtocols, protocol)
-			}
-		}
+		candidateProtocols := serverEntry.GetSupportedProtocols()
 		if len(candidateProtocols) == 0 {
 			return "", ContextError(fmt.Errorf("server does not have any supported capabilities"))
 		}