|
|
@@ -1,5 +1,5 @@
|
|
|
/*
|
|
|
- * Copyright (c) 2014, Psiphon Inc.
|
|
|
+ * Copyright (c) 2015, Psiphon Inc.
|
|
|
* All rights reserved.
|
|
|
*
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
@@ -25,9 +25,10 @@ import (
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "io"
|
|
|
"net"
|
|
|
"strings"
|
|
|
- "sync/atomic"
|
|
|
+ "sync"
|
|
|
"time"
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
@@ -39,7 +40,14 @@ import (
|
|
|
// implements Tunneler.
|
|
|
type Tunneler interface {
|
|
|
Dial(remoteAddr string) (conn net.Conn, err error)
|
|
|
- SignalFailure()
|
|
|
+ SignalComponentFailure()
|
|
|
+}
|
|
|
+
|
|
|
+// TunnerOwner specifies the interface required by Tunnel to notify its
|
|
|
+// owner when it has failed. The owner may, as in the case of the Controller,
|
|
|
+// remove the tunnel from its list of active tunnels.
|
|
|
+type TunnelOwner interface {
|
|
|
+ SignalTunnelFailure(tunnel *Tunnel)
|
|
|
}
|
|
|
|
|
|
const (
|
|
|
@@ -61,16 +69,18 @@ var SupportedTunnelProtocols = []string{
|
|
|
// tunnel includes a network connection to the specified server
|
|
|
// and an SSH session built on top of that transport.
|
|
|
type Tunnel struct {
|
|
|
- serverEntry *ServerEntry
|
|
|
- sessionId string
|
|
|
- sessionStarted int32
|
|
|
- protocol string
|
|
|
- conn Conn
|
|
|
- sshClient *ssh.Client
|
|
|
- sshKeepAliveQuit chan struct{}
|
|
|
- portForwardFailures chan int
|
|
|
- portForwardFailureTotal int
|
|
|
- regexps *Regexps
|
|
|
+ mutex *sync.Mutex
|
|
|
+ isClosed bool
|
|
|
+ serverEntry *ServerEntry
|
|
|
+ session *Session
|
|
|
+ protocol string
|
|
|
+ conn Conn
|
|
|
+ closedSignal chan struct{}
|
|
|
+ sshClient *ssh.Client
|
|
|
+ operateWaitGroup *sync.WaitGroup
|
|
|
+ shutdownOperateBroadcast chan struct{}
|
|
|
+ portForwardFailures chan int
|
|
|
+ portForwardFailureTotal int
|
|
|
}
|
|
|
|
|
|
// EstablishTunnel first makes a network transport connection to the
|
|
|
@@ -84,16 +94,163 @@ type Tunnel struct {
|
|
|
// the first protocol in SupportedTunnelProtocols that's also in the
|
|
|
// server capabilities is used.
|
|
|
func EstablishTunnel(
|
|
|
- config *Config, pendingConns *Conns, serverEntry *ServerEntry) (tunnel *Tunnel, err error) {
|
|
|
+ config *Config,
|
|
|
+ pendingConns *Conns,
|
|
|
+ serverEntry *ServerEntry,
|
|
|
+ tunnelOwner TunnelOwner) (tunnel *Tunnel, err error) {
|
|
|
+
|
|
|
+ selectedProtocol, err := selectProtocol(config, serverEntry)
|
|
|
+ if err != nil {
|
|
|
+ return nil, ContextError(err)
|
|
|
+ }
|
|
|
+ Notice(NOTICE_INFO, "connecting to %s in region %s using %s",
|
|
|
+ serverEntry.IpAddress, serverEntry.Region, selectedProtocol)
|
|
|
|
|
|
- // Select the protocol
|
|
|
- var selectedProtocol string
|
|
|
+ // Generate a session Id for the Psiphon server API. This is generated now so
|
|
|
+ // that it can be sent with the SSH password payload, which helps the server
|
|
|
+ // associate client geo location, used in server API stats, with the session ID.
|
|
|
+ sessionId, err := MakeSessionId()
|
|
|
+ if err != nil {
|
|
|
+ return nil, ContextError(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build transport layers and establish SSH connection
|
|
|
+ conn, closedSignal, sshClient, err := dialSsh(
|
|
|
+ config, pendingConns, serverEntry, selectedProtocol, sessionId)
|
|
|
+ if err != nil {
|
|
|
+ return nil, ContextError(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cleanup on error
|
|
|
+ defer func() {
|
|
|
+ if err != nil {
|
|
|
+ conn.Close()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ // The tunnel is now connected
|
|
|
+ tunnel = &Tunnel{
|
|
|
+ mutex: new(sync.Mutex),
|
|
|
+ isClosed: false,
|
|
|
+ serverEntry: serverEntry,
|
|
|
+ protocol: selectedProtocol,
|
|
|
+ conn: conn,
|
|
|
+ closedSignal: closedSignal,
|
|
|
+ sshClient: sshClient,
|
|
|
+ operateWaitGroup: new(sync.WaitGroup),
|
|
|
+ shutdownOperateBroadcast: make(chan struct{}),
|
|
|
+ // portForwardFailures buffer size is large enough to receive the thresold number
|
|
|
+ // of failure reports without blocking. Senders can drop failures without blocking.
|
|
|
+ portForwardFailures: make(chan int, config.PortForwardFailureThreshold)}
|
|
|
+
|
|
|
+ // Create a new Psiphon API session for this tunnel
|
|
|
+ Notice(NOTICE_INFO, "starting session for %s", tunnel.serverEntry.IpAddress)
|
|
|
+ tunnel.session, err = NewSession(config, tunnel, sessionId)
|
|
|
+ if err != nil {
|
|
|
+ return nil, ContextError(fmt.Errorf("error starting session for %s: %s", tunnel.serverEntry.IpAddress, err))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Now that network operations are complete, cancel interruptibility
|
|
|
+ pendingConns.Remove(conn)
|
|
|
+
|
|
|
+ // Promote this successful tunnel to first rank so it's one
|
|
|
+ // of the first candidates next time establish runs.
|
|
|
+ PromoteServerEntry(tunnel.serverEntry.IpAddress)
|
|
|
+
|
|
|
+ // Spawn the operateTunnel goroutine, which monitors the tunnel and handles periodic stats updates.
|
|
|
+ tunnel.operateWaitGroup.Add(1)
|
|
|
+ go tunnel.operateTunnel(config, tunnelOwner)
|
|
|
+
|
|
|
+ return tunnel, nil
|
|
|
+}
|
|
|
+
|
|
|
+// Close stops operating the tunnel and closes the underlying connection.
|
|
|
+// Supports multiple and/or concurrent calls to Close().
|
|
|
+func (tunnel *Tunnel) Close() {
|
|
|
+ tunnel.mutex.Lock()
|
|
|
+ if !tunnel.isClosed {
|
|
|
+ close(tunnel.shutdownOperateBroadcast)
|
|
|
+ tunnel.operateWaitGroup.Wait()
|
|
|
+ tunnel.conn.Close()
|
|
|
+ }
|
|
|
+ tunnel.isClosed = true
|
|
|
+ tunnel.mutex.Unlock()
|
|
|
+}
|
|
|
+
|
|
|
+// Dial establishes a port forward connection through the tunnel
|
|
|
+func (tunnel *Tunnel) Dial(remoteAddr string) (conn net.Conn, err error) {
|
|
|
+ tunnel.mutex.Lock()
|
|
|
+ isClosed := tunnel.isClosed
|
|
|
+ tunnel.mutex.Unlock()
|
|
|
+ if isClosed {
|
|
|
+ return nil, errors.New("tunnel is closed")
|
|
|
+ }
|
|
|
+
|
|
|
+ sshPortForwardConn, err := tunnel.sshClient.Dial("tcp", remoteAddr)
|
|
|
+ if err != nil {
|
|
|
+ // TODO: conditional on type of error or error message?
|
|
|
+ select {
|
|
|
+ case tunnel.portForwardFailures <- 1:
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ return nil, ContextError(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return &TunneledConn{
|
|
|
+ Conn: sshPortForwardConn,
|
|
|
+ tunnel: tunnel},
|
|
|
+ nil
|
|
|
+}
|
|
|
+
|
|
|
+// TunneledConn implements net.Conn and wraps a port foward connection.
|
|
|
+// It is used to hook into Read and Write to observe I/O errors and
|
|
|
+// report these errors back to the tunnel monitor as port forward failures.
|
|
|
+type TunneledConn struct {
|
|
|
+ net.Conn
|
|
|
+ tunnel *Tunnel
|
|
|
+}
|
|
|
+
|
|
|
+func (conn *TunneledConn) Read(buffer []byte) (n int, err error) {
|
|
|
+ n, err = conn.Conn.Read(buffer)
|
|
|
+ if err != nil && err != io.EOF {
|
|
|
+ // Report 1 new failure. Won't block; assumes the receiver
|
|
|
+ // has a sufficient buffer for the threshold number of reports.
|
|
|
+ // TODO: conditional on type of error or error message?
|
|
|
+ select {
|
|
|
+ case conn.tunnel.portForwardFailures <- 1:
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+func (conn *TunneledConn) Write(buffer []byte) (n int, err error) {
|
|
|
+ n, err = conn.Conn.Write(buffer)
|
|
|
+ if err != nil && err != io.EOF {
|
|
|
+ // Same as TunneledConn.Read()
|
|
|
+ select {
|
|
|
+ case conn.tunnel.portForwardFailures <- 1:
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// SignalComponentFailure notifies the tunnel that an associated component has failed.
|
|
|
+// This will terminate the tunnel.
|
|
|
+func (tunnel *Tunnel) SignalComponentFailure() {
|
|
|
+ Notice(NOTICE_ALERT, "tunnel received component failure signal")
|
|
|
+ tunnel.Close()
|
|
|
+}
|
|
|
+
|
|
|
+// selectProtocol is a helper that picks the tunnel protocol
|
|
|
+func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol string, err error) {
|
|
|
// 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) {
|
|
|
- return nil, ContextError(fmt.Errorf("server does not have required capability"))
|
|
|
+ return "", ContextError(fmt.Errorf("server does not have required capability"))
|
|
|
}
|
|
|
selectedProtocol = config.TunnelProtocol
|
|
|
} else {
|
|
|
@@ -106,11 +263,19 @@ func EstablishTunnel(
|
|
|
}
|
|
|
}
|
|
|
if selectedProtocol == "" {
|
|
|
- return nil, ContextError(fmt.Errorf("server does not have any supported capabilities"))
|
|
|
+ return "", ContextError(fmt.Errorf("server does not have any supported capabilities"))
|
|
|
}
|
|
|
}
|
|
|
- Notice(NOTICE_INFO, "connecting to %s in region %s using %s",
|
|
|
- serverEntry.IpAddress, serverEntry.Region, selectedProtocol)
|
|
|
+ return selectedProtocol, nil
|
|
|
+}
|
|
|
+
|
|
|
+// dialSsh is a helper that builds the transport layers and establishes the SSH connection
|
|
|
+func dialSsh(
|
|
|
+ config *Config,
|
|
|
+ pendingConns *Conns,
|
|
|
+ serverEntry *ServerEntry,
|
|
|
+ selectedProtocol,
|
|
|
+ sessionId string) (conn Conn, closedSignal chan struct{}, sshClient *ssh.Client, 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.
|
|
|
@@ -134,14 +299,6 @@ func EstablishTunnel(
|
|
|
port = serverEntry.SshPort
|
|
|
}
|
|
|
|
|
|
- // Generate a session Id for the Psiphon server API. This is generated now so
|
|
|
- // that it can be sent with the SSH password payload, which helps the server
|
|
|
- // associate client geo location, used in server API stats, with the session ID.
|
|
|
- sessionId, err := MakeSessionId()
|
|
|
- if err != nil {
|
|
|
- return nil, ContextError(err)
|
|
|
- }
|
|
|
-
|
|
|
// Create the base transport: meek or direct connection
|
|
|
dialConfig := &DialConfig{
|
|
|
ConnectTimeout: TUNNEL_CONNECT_TIMEOUT,
|
|
|
@@ -151,42 +308,54 @@ func EstablishTunnel(
|
|
|
BindToDeviceProvider: config.BindToDeviceProvider,
|
|
|
BindToDeviceDnsServer: config.BindToDeviceDnsServer,
|
|
|
}
|
|
|
- var conn Conn
|
|
|
if useMeek {
|
|
|
conn, err = DialMeek(serverEntry, sessionId, useFronting, dialConfig)
|
|
|
if err != nil {
|
|
|
- return nil, ContextError(err)
|
|
|
+ return nil, nil, nil, ContextError(err)
|
|
|
}
|
|
|
- // TODO: MeekConn doesn't go into pendingConns since there's no direct connection to
|
|
|
- // interrupt; underlying HTTP connections may be candidates for interruption, but only
|
|
|
- // after relay starts polling...
|
|
|
} else {
|
|
|
conn, err = DialTCP(fmt.Sprintf("%s:%d", serverEntry.IpAddress, port), dialConfig)
|
|
|
if err != nil {
|
|
|
- return nil, ContextError(err)
|
|
|
+ return nil, nil, nil, ContextError(err)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ cleanupConn := conn
|
|
|
defer func() {
|
|
|
// Cleanup on error
|
|
|
if err != nil {
|
|
|
- conn.Close()
|
|
|
+ cleanupConn.Close()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
+ // Create signal which is triggered when the underlying network connection is closed,
|
|
|
+ // this is used in operateTunnel to detect an unexpected disconnect. SetClosedSignal
|
|
|
+ // is called here, well before operateTunnel, so that we don't need to handle the
|
|
|
+ // "already closed" with a tunnelOwner.SignalTunnelFailure() in operateTunnel (this
|
|
|
+ // was previously the order of events, which caused the establish process to sometimes
|
|
|
+ // run briefly when not needed).
|
|
|
+ closedSignal = make(chan struct{}, 1)
|
|
|
+ if !conn.SetClosedSignal(closedSignal) {
|
|
|
+ // Conn is already closed. This is not unexpected -- for example,
|
|
|
+ // when establish is interrupted.
|
|
|
+ // TODO: make this not log an error when called from establishTunnelWorker?
|
|
|
+ return nil, nil, nil, ContextError(errors.New("conn already closed"))
|
|
|
+ }
|
|
|
+
|
|
|
// Add obfuscated SSH layer
|
|
|
var sshConn net.Conn
|
|
|
sshConn = conn
|
|
|
if useObfuscatedSsh {
|
|
|
sshConn, err = NewObfuscatedSshConn(conn, serverEntry.SshObfuscatedKey)
|
|
|
if err != nil {
|
|
|
- return 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, ContextError(err)
|
|
|
+ return nil, nil, nil, ContextError(err)
|
|
|
}
|
|
|
sshCertChecker := &ssh.CertChecker{
|
|
|
HostKeyFallback: func(addr string, remote net.Addr, publicKey ssh.PublicKey) error {
|
|
|
@@ -202,7 +371,7 @@ func EstablishTunnel(
|
|
|
SshPassword string `json:"SshPassword"`
|
|
|
}{sessionId, serverEntry.SshPassword})
|
|
|
if err != nil {
|
|
|
- return nil, ContextError(err)
|
|
|
+ return nil, nil, nil, ContextError(err)
|
|
|
}
|
|
|
sshClientConfig := &ssh.ClientConfig{
|
|
|
User: serverEntry.SshUsername,
|
|
|
@@ -216,88 +385,93 @@ func EstablishTunnel(
|
|
|
sshAddress := ""
|
|
|
sshClientConn, sshChans, sshReqs, err := ssh.NewClientConn(sshConn, sshAddress, sshClientConfig)
|
|
|
if err != nil {
|
|
|
- return nil, ContextError(err)
|
|
|
+ return nil, nil, nil, ContextError(err)
|
|
|
}
|
|
|
- sshClient := ssh.NewClient(sshClientConn, sshChans, sshReqs)
|
|
|
-
|
|
|
- // Run a goroutine to periodically execute SSH keepalive
|
|
|
- sshKeepAliveQuit := make(chan struct{})
|
|
|
- go func() {
|
|
|
- sshKeepAliveTicker := time.NewTicker(TUNNEL_SSH_KEEP_ALIVE_PERIOD)
|
|
|
- for {
|
|
|
- select {
|
|
|
- case <-sshKeepAliveTicker.C:
|
|
|
- _, _, err := sshClient.SendRequest("keepalive@openssh.com", true, nil)
|
|
|
- if err != nil {
|
|
|
- Notice(NOTICE_ALERT, "ssh keep alive failed: %s", err)
|
|
|
- // TODO: call Tunnel.Close()?
|
|
|
- sshKeepAliveTicker.Stop()
|
|
|
- conn.Close()
|
|
|
- }
|
|
|
- case <-sshKeepAliveQuit:
|
|
|
- sshKeepAliveTicker.Stop()
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- }()
|
|
|
+ sshClient = ssh.NewClient(sshClientConn, sshChans, sshReqs)
|
|
|
|
|
|
- return &Tunnel{
|
|
|
- serverEntry: serverEntry,
|
|
|
- sessionId: sessionId,
|
|
|
- protocol: selectedProtocol,
|
|
|
- conn: conn,
|
|
|
- sshClient: sshClient,
|
|
|
- sshKeepAliveQuit: sshKeepAliveQuit,
|
|
|
- // portForwardFailures buffer size is large enough to receive the thresold number
|
|
|
- // of failure reports without blocking. Senders can drop failures without blocking.
|
|
|
- portForwardFailures: make(chan int, config.PortForwardFailureThreshold)},
|
|
|
- nil
|
|
|
+ return conn, closedSignal, sshClient, nil
|
|
|
}
|
|
|
|
|
|
-// Close terminates the tunnel.
|
|
|
-func (tunnel *Tunnel) Close() {
|
|
|
- if tunnel.sshKeepAliveQuit != nil {
|
|
|
- close(tunnel.sshKeepAliveQuit)
|
|
|
- tunnel.sshKeepAliveQuit = nil
|
|
|
- }
|
|
|
- if tunnel.conn != nil {
|
|
|
- tunnel.conn.Close()
|
|
|
- }
|
|
|
-}
|
|
|
+// operateTunnel periodically sends stats updates to the Psiphon API and
|
|
|
+// monitors the tunnel for failures:
|
|
|
+//
|
|
|
+// 1. Overall tunnel failure: the tunnel sends a signal to the ClosedSignal
|
|
|
+// channel on keep-alive failure and other transport I/O errors. In case
|
|
|
+// of such a failure, the tunnel is marked as failed.
|
|
|
+//
|
|
|
+// 2. Tunnel port forward failures: the tunnel connection may stay up but
|
|
|
+// the client may still fail to establish port forwards due to server load
|
|
|
+// and other conditions. After a threshold number of such failures, the
|
|
|
+// overall tunnel is marked as failed.
|
|
|
+//
|
|
|
+// TODO: currently, any connect (dial), read, or write error associated with
|
|
|
+// a port forward is counted as a failure. It may be important to differentiate
|
|
|
+// between failures due to Psiphon server conditions and failures due to the
|
|
|
+// origin/target server (in the latter case, the tunnel is healthy). Here are
|
|
|
+// some typical error messages to consider matching against (or ignoring):
|
|
|
+//
|
|
|
+// - "ssh: rejected: administratively prohibited (open failed)"
|
|
|
+// - "ssh: rejected: connect failed (Connection timed out)"
|
|
|
+// - "write tcp ... broken pipe"
|
|
|
+// - "read tcp ... connection reset by peer"
|
|
|
+// - "ssh: unexpected packet in response to channel open: <nil>"
|
|
|
+//
|
|
|
+func (tunnel *Tunnel) operateTunnel(config *Config, tunnelOwner TunnelOwner) {
|
|
|
+ defer tunnel.operateWaitGroup.Done()
|
|
|
|
|
|
-func (tunnel *Tunnel) IsSessionStarted() bool {
|
|
|
- return atomic.LoadInt32(&tunnel.sessionStarted) == 1
|
|
|
-}
|
|
|
+ // Note: not using a Ticker since NextSendPeriod() is not a fixed time period
|
|
|
+ statsTimer := time.NewTimer(NextSendPeriod())
|
|
|
+ defer statsTimer.Stop()
|
|
|
|
|
|
-func (tunnel *Tunnel) SetSessionStarted() {
|
|
|
- atomic.StoreInt32(&tunnel.sessionStarted, 1)
|
|
|
-}
|
|
|
+ sshKeepAliveTicker := time.NewTicker(TUNNEL_SSH_KEEP_ALIVE_PERIOD)
|
|
|
+ defer sshKeepAliveTicker.Stop()
|
|
|
|
|
|
-// Dial establishes a port forward connection through the tunnel
|
|
|
-func (tunnel *Tunnel) Dial(remoteAddr string) (conn net.Conn, err error) {
|
|
|
- // TODO: should this track port forward failures as in Controller.DialWithTunnel?
|
|
|
- return tunnel.sshClient.Dial("tcp", remoteAddr)
|
|
|
-}
|
|
|
+ var err error
|
|
|
+ for err == nil {
|
|
|
+ select {
|
|
|
+ case <-statsTimer.C:
|
|
|
+ sendStats(tunnel, false)
|
|
|
+ statsTimer.Reset(NextSendPeriod())
|
|
|
|
|
|
-// SignalFailure notifies the tunnel that an associated component has failed.
|
|
|
-// This will terminate the tunnel.
|
|
|
-func (tunnel *Tunnel) SignalFailure() {
|
|
|
- Notice(NOTICE_ALERT, "tunnel received failure signal")
|
|
|
- tunnel.Close()
|
|
|
-}
|
|
|
+ case <-sshKeepAliveTicker.C:
|
|
|
+ _, _, err := tunnel.sshClient.SendRequest("keepalive@openssh.com", true, nil)
|
|
|
+ err = fmt.Errorf("ssh keep alive failed: %s", err)
|
|
|
|
|
|
-// ServerID provides a unique identifier for the server the tunnel connects to.
|
|
|
-// This ID is consistent between multiple tunnels connected to that server.
|
|
|
-func (tunnel *Tunnel) ServerID() string {
|
|
|
- return tunnel.serverEntry.IpAddress
|
|
|
-}
|
|
|
+ case failures := <-tunnel.portForwardFailures:
|
|
|
+ // Note: no mutex on portForwardFailureTotal; only referenced here
|
|
|
+ tunnel.portForwardFailureTotal += failures
|
|
|
+ Notice(
|
|
|
+ NOTICE_INFO, "port forward failures for %s: %d",
|
|
|
+ tunnel.serverEntry.IpAddress, tunnel.portForwardFailureTotal)
|
|
|
+ if tunnel.portForwardFailureTotal > config.PortForwardFailureThreshold {
|
|
|
+ err = errors.New("tunnel exceeded port forward failure threshold")
|
|
|
+ }
|
|
|
+
|
|
|
+ case <-tunnel.closedSignal:
|
|
|
+ err = errors.New("tunnel closed unexpectedly")
|
|
|
+
|
|
|
+ case <-tunnel.shutdownOperateBroadcast:
|
|
|
+ // Send final stats
|
|
|
+ sendStats(tunnel, true)
|
|
|
+ Notice(NOTICE_INFO, "shutdown operate tunnel")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-// StatsRegexps gets the Regexps used for the statistics for this tunnel.
|
|
|
-func (tunnel *Tunnel) StatsRegexps() *Regexps {
|
|
|
- return tunnel.regexps
|
|
|
+ if err != nil {
|
|
|
+ Notice(NOTICE_ALERT, "operate tunnel error for %s: %s", tunnel.serverEntry.IpAddress, err)
|
|
|
+ tunnelOwner.SignalTunnelFailure(tunnel)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// SetStatsRegexps sets the Regexps used for the statistics for this tunnel.
|
|
|
-func (tunnel *Tunnel) SetStatsRegexps(regexps *Regexps) {
|
|
|
- tunnel.regexps = regexps
|
|
|
+// sendStats is a helper for sending session stats to the server.
|
|
|
+func sendStats(tunnel *Tunnel, final bool) {
|
|
|
+ payload := GetForServer(tunnel.serverEntry.IpAddress)
|
|
|
+ if payload != nil {
|
|
|
+ err := tunnel.session.DoStatusRequest(payload, final)
|
|
|
+ if err != nil {
|
|
|
+ Notice(NOTICE_ALERT, "DoStatusRequest failed for %s: %s", tunnel.serverEntry.IpAddress, err)
|
|
|
+ PutBack(tunnel.serverEntry.IpAddress, payload)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|