Browse Source

Add TCPPortForwardRedirects
* config option to redirect specified port forward
destinations to alternate destination addresses
* intended for routing tunneled web API requests

Rod Hynes 9 years ago
parent
commit
d43b27405f
2 changed files with 61 additions and 11 deletions
  1. 46 8
      psiphon/server/config.go
  2. 15 3
      psiphon/server/tunnelServer.go

+ 46 - 8
psiphon/server/config.go

@@ -173,6 +173,11 @@ type Config struct {
 	// directly by this server, which parses the SSH channel using the
 	// udpgw protocol. Handling includes udpgw transparent DNS: tunneled
 	// UDP DNS packets are rerouted to the host's DNS server.
+	//
+	// The intercept is applied before the port forward destination is
+	// validated against SSH_DISALLOWED_PORT_FORWARD_HOSTS and
+	// AllowTCPPorts/DenyTCPPorts. So the intercept address may be any
+	// otherwise prohibited destination.
 	UDPInterceptUdpgwServerAddress string
 
 	// DNSResolverIPAddress specifies the IP address of a DNS server
@@ -181,6 +186,21 @@ type Config struct {
 	// "nameserver" entry.
 	DNSResolverIPAddress string
 
+	// TCPPortForwardRedirects is a mapping from client port forward
+	// destination to an alternate destination address. When the client's
+	// port forward HostToConnect and PortToConnect matches a redirect,
+	// the redirect is substituted and dialed instead of the original
+	// destination.
+	//
+	// The redirect is applied after the original destination is
+	// validated against SSH_DISALLOWED_PORT_FORWARD_HOSTS and
+	// AllowTCPPorts/DenyTCPPorts. So the redirect may map to any
+	// otherwise prohibited destination.
+	//
+	// The redirect is applied after UDPInterceptUdpgwServerAddress is
+	// checked. So the redirect address will not be intercepted.
+	TCPPortForwardRedirects map[string]string
+
 	// LoadMonitorPeriodSeconds indicates how frequently to log server
 	// load information (number of connected clients per tunnel protocol,
 	// number of running goroutines, amount of memory allocated, etc.)
@@ -257,19 +277,26 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		}
 	}
 
-	validateNetworkAddress := func(address string) error {
-		host, port, err := net.SplitHostPort(address)
-		if err == nil && net.ParseIP(host) == nil {
-			err = errors.New("Host must be an IP address")
+	validateNetworkAddress := func(address string, requireIPaddress bool) error {
+		host, portStr, err := net.SplitHostPort(address)
+		if err != nil {
+			return err
+		}
+		if requireIPaddress && net.ParseIP(host) == nil {
+			return errors.New("host must be an IP address")
+		}
+		port, err := strconv.Atoi(portStr)
+		if err != nil {
+			return err
 		}
-		if err == nil {
-			_, err = strconv.Atoi(port)
+		if port < 0 || port > 65535 {
+			return errors.New("invalid port")
 		}
-		return err
+		return nil
 	}
 
 	if config.UDPInterceptUdpgwServerAddress != "" {
-		if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress); err != nil {
+		if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress, true); err != nil {
 			return nil, fmt.Errorf("UDPInterceptUdpgwServerAddress is invalid: %s", err)
 		}
 	}
@@ -280,6 +307,17 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		}
 	}
 
+	if config.TCPPortForwardRedirects != nil {
+		for destination, redirect := range config.TCPPortForwardRedirects {
+			if err := validateNetworkAddress(destination, false); err != nil {
+				return nil, fmt.Errorf("TCPPortForwardRedirects destination %s is invalid: %s", destination, err)
+			}
+			if err := validateNetworkAddress(redirect, false); err != nil {
+				return nil, fmt.Errorf("TCPPortForwardRedirects redirect %s is invalid: %s", redirect, err)
+			}
+		}
+	}
+
 	return &config, nil
 }
 

+ 15 - 3
psiphon/server/tunnelServer.go

@@ -26,6 +26,7 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -801,9 +802,7 @@ func (sshClient *sshClient) handleNewPortForwardChannel(newChannel ssh.NewChanne
 	// TODO: also support UDP explicitly, e.g. with a custom "direct-udp" channel type?
 	isUDPChannel := sshClient.sshServer.support.Config.UDPInterceptUdpgwServerAddress != "" &&
 		sshClient.sshServer.support.Config.UDPInterceptUdpgwServerAddress ==
-			fmt.Sprintf("%s:%d",
-				directTcpipExtraData.HostToConnect,
-				directTcpipExtraData.PortToConnect)
+			net.JoinHostPort(directTcpipExtraData.HostToConnect, strconv.Itoa(int(directTcpipExtraData.PortToConnect)))
 
 	if isUDPChannel {
 		sshClient.handleUDPChannel(newChannel)
@@ -891,6 +890,19 @@ func (sshClient *sshClient) handleTCPChannel(
 		return
 	}
 
+	// Note: redirects are applied *after* isPortForwardPermitted allows the original destination
+	if sshClient.sshServer.support.Config.TCPPortForwardRedirects != nil {
+		destination := net.JoinHostPort(hostToConnect, strconv.Itoa(portToConnect))
+		if redirect, ok := sshClient.sshServer.support.Config.TCPPortForwardRedirects[destination]; ok {
+			// Note: redirect format is validated when config is loaded
+			host, portStr, _ := net.SplitHostPort(redirect)
+			port, _ := strconv.Atoi(portStr)
+			hostToConnect = host
+			portToConnect = port
+			log.WithContextFields(LogFields{"destination": destination, "redirect": redirect}).Debug("port forward redirect")
+		}
+	}
+
 	var bytesUp, bytesDown int64
 	sshClient.openedPortForward(sshClient.tcpTrafficState)
 	defer func() {