From 8d6f171888af231aa0235b604e9f5ce602a96a6d Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Tue, 26 Jan 2021 17:35:59 -0500 Subject: [PATCH 1/4] Use implicit name resolution for TCP destinations When connecting to a TCP destination by name, go's implicit resolution behavior tries all available addresses until it finds one that works (fallback), with a preference for IPv6 if possible (happy eyeballs). This is better than our current behavior (pick one IPv4 address). The Outline client doesn't rely on named destinations, but other Shadowsocks clients do. This is an alternative to #100. This change has one key difference from the previous behavior: IP validation is enforced after the connection is established, not before. A hostile user cannot use this to send data to a private service, but they might be able to detect the existence of that service based on how long the server waits before closing the connection. --- service/tcp.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/service/tcp.go b/service/tcp.go index c954d3d6..90e302bd 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -149,18 +149,15 @@ func (s *tcpService) SetTargetIPValidator(targetIPValidator onet.TargetIPValidat } func dialTarget(tgtAddr socks.Addr, proxyMetrics *metrics.ProxyMetrics, targetIPValidator onet.TargetIPValidator) (onet.DuplexConn, *onet.ConnectionError) { - tgtTCPAddr, err := net.ResolveTCPAddr("tcp", tgtAddr.String()) + tgtConn, err := net.Dial("tcp", tgtAddr.String()) if err != nil { - return nil, onet.NewConnectionError("ERR_RESOLVE_ADDRESS", fmt.Sprintf("Failed to resolve target address %v", tgtAddr.String()), err) + return nil, onet.NewConnectionError("ERR_CONNECT", "Failed to connect to target", err) } - if err := targetIPValidator(tgtTCPAddr.IP); err != nil { + tgtTCPConn := tgtConn.(*net.TCPConn) + if err := targetIPValidator(tgtTCPConn.RemoteAddr().(*net.TCPAddr).IP); err != nil { + tgtTCPConn.Close() return nil, err } - - tgtTCPConn, err := net.DialTCP("tcp", nil, tgtTCPAddr) - if err != nil { - return nil, onet.NewConnectionError("ERR_CONNECT", "Failed to connect to target", err) - } tgtTCPConn.SetKeepAlive(true) return metrics.MeasureConn(tgtTCPConn, &proxyMetrics.ProxyTarget, &proxyMetrics.TargetProxy), nil } From 046dbd43cc5e06699297fd5920edc0f448d76b49 Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Thu, 28 Jan 2021 11:22:52 -0500 Subject: [PATCH 2/4] Use Dialer.Control to check IP before connection --- integration_test/integration_test.go | 57 ++++++++++++++++++++++++++++ service/tcp.go | 20 +++++++--- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/integration_test/integration_test.go b/integration_test/integration_test.go index f6f23df5..8c34965e 100644 --- a/integration_test/integration_test.go +++ b/integration_test/integration_test.go @@ -29,6 +29,8 @@ import ( "github.com/Jigsaw-Code/outline-ss-server/service/metrics" ss "github.com/Jigsaw-Code/outline-ss-server/shadowsocks" logging "github.com/op/go-logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const maxUDPPacketSize = 64 * 1024 @@ -162,6 +164,61 @@ func TestTCPEcho(t *testing.T) { echoRunning.Wait() } +type statusMetrics struct { + metrics.NoOpMetrics + statuses []string +} + +func (m *statusMetrics) AddClosedTCPConnection(clientLocation, accessKey, status string, data metrics.ProxyMetrics, timeToCipher, duration time.Duration) { + m.statuses = append(m.statuses, status) +} + +func TestRestrictedAddresses(t *testing.T) { + proxyListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) + require.NoError(t, err, "ListenTCP failed: %v", err) + secrets := ss.MakeTestSecrets(1) + cipherList, err := service.MakeTestCiphers(secrets) + require.NoError(t, err) + const testTimeout = 200 * time.Millisecond + testMetrics := &statusMetrics{} + proxy := service.NewTCPService(cipherList, nil, testMetrics, testTimeout) + go proxy.Serve(proxyListener) + + proxyHost, proxyPort, err := net.SplitHostPort(proxyListener.Addr().String()) + require.NoError(t, err) + portNum, err := strconv.Atoi(proxyPort) + require.NoError(t, err) + client, err := client.NewClient(proxyHost, portNum, secrets[0], ss.TestCipher) + require.NoError(t, err, "Failed to create ShadowsocksClient") + + buf := make([]byte, 10) + + addresses := []string{ + "localhost:9999", + "[::1]:80", + "10.0.0.1:1234", + "[fc00::1]:54321", + } + + for _, address := range addresses { + conn, err := client.DialTCP(nil, address) + require.NoError(t, err, "Failed to dial %v", address) + n, err := conn.Read(buf) + assert.Equal(t, 0, n, "Server should close without replying on rejected address") + assert.Equal(t, io.EOF, err) + conn.Close() + } + + proxy.GracefulStop() + + assert.ElementsMatch(t, testMetrics.statuses, []string{ + "ERR_ADDRESS_INVALID", + "ERR_ADDRESS_INVALID", + "ERR_ADDRESS_PRIVATE", + "ERR_ADDRESS_PRIVATE", + }) +} + // Metrics about one UDP packet. type udpRecord struct { location, accessKey, status string diff --git a/service/tcp.go b/service/tcp.go index 90e302bd..69d71a05 100644 --- a/service/tcp.go +++ b/service/tcp.go @@ -23,6 +23,7 @@ import ( "io/ioutil" "net" "sync" + "syscall" "time" onet "github.com/Jigsaw-Code/outline-ss-server/net" @@ -149,15 +150,22 @@ func (s *tcpService) SetTargetIPValidator(targetIPValidator onet.TargetIPValidat } func dialTarget(tgtAddr socks.Addr, proxyMetrics *metrics.ProxyMetrics, targetIPValidator onet.TargetIPValidator) (onet.DuplexConn, *onet.ConnectionError) { - tgtConn, err := net.Dial("tcp", tgtAddr.String()) - if err != nil { + var ipError *onet.ConnectionError + dialer := net.Dialer{Control: func(network, address string, c syscall.RawConn) error { + ip, _, _ := net.SplitHostPort(address) + ipError = targetIPValidator(net.ParseIP(ip)) + if ipError != nil { + return errors.New(ipError.Message) + } + return nil + }} + tgtConn, err := dialer.Dial("tcp", tgtAddr.String()) + if ipError != nil { + return nil, ipError + } else if err != nil { return nil, onet.NewConnectionError("ERR_CONNECT", "Failed to connect to target", err) } tgtTCPConn := tgtConn.(*net.TCPConn) - if err := targetIPValidator(tgtTCPConn.RemoteAddr().(*net.TCPAddr).IP); err != nil { - tgtTCPConn.Close() - return nil, err - } tgtTCPConn.SetKeepAlive(true) return metrics.MeasureConn(tgtTCPConn, &proxyMetrics.ProxyTarget, &proxyMetrics.TargetProxy), nil } From c6a4dcb67f081ee81483e987a8fb88d337e7e9fb Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Thu, 28 Jan 2021 11:37:49 -0500 Subject: [PATCH 3/4] Make race detector happy --- integration_test/integration_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration_test/integration_test.go b/integration_test/integration_test.go index 8c34965e..906d854c 100644 --- a/integration_test/integration_test.go +++ b/integration_test/integration_test.go @@ -166,11 +166,14 @@ func TestTCPEcho(t *testing.T) { type statusMetrics struct { metrics.NoOpMetrics + sync.Mutex statuses []string } func (m *statusMetrics) AddClosedTCPConnection(clientLocation, accessKey, status string, data metrics.ProxyMetrics, timeToCipher, duration time.Duration) { + m.Lock() m.statuses = append(m.statuses, status) + m.Unlock() } func TestRestrictedAddresses(t *testing.T) { From 7bb30b38b9b716154bd73a26d5d7b6ab07758346 Mon Sep 17 00:00:00 2001 From: Ben Schwartz Date: Thu, 28 Jan 2021 13:54:12 -0500 Subject: [PATCH 4/4] Move expected status definition up --- integration_test/integration_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/integration_test/integration_test.go b/integration_test/integration_test.go index 906d854c..de674e7e 100644 --- a/integration_test/integration_test.go +++ b/integration_test/integration_test.go @@ -203,6 +203,13 @@ func TestRestrictedAddresses(t *testing.T) { "[fc00::1]:54321", } + expectedStatus := []string{ + "ERR_ADDRESS_INVALID", + "ERR_ADDRESS_INVALID", + "ERR_ADDRESS_PRIVATE", + "ERR_ADDRESS_PRIVATE", + } + for _, address := range addresses { conn, err := client.DialTCP(nil, address) require.NoError(t, err, "Failed to dial %v", address) @@ -213,13 +220,7 @@ func TestRestrictedAddresses(t *testing.T) { } proxy.GracefulStop() - - assert.ElementsMatch(t, testMetrics.statuses, []string{ - "ERR_ADDRESS_INVALID", - "ERR_ADDRESS_INVALID", - "ERR_ADDRESS_PRIVATE", - "ERR_ADDRESS_PRIVATE", - }) + assert.ElementsMatch(t, testMetrics.statuses, expectedStatus) } // Metrics about one UDP packet.