mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-09 20:01:54 -04:00
net/socks5: correctly proxy half-closed TCP connections
Similar to #16462, when we are acting as a TCP proxy, we need to pass through half-closes correctly since clients and servers will sometimes close one direction of the connection and still rely on the other direction working. Fixes #20883. Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
1 parent
35848427ce
commit
027e249fcf
4 files changed
+151
-3
No files matched your search
+27
-2
@@ -13,6 +13,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/types/nettype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
|
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
|
||||||
@@ -127,11 +129,15 @@ func (ln *listener) Addr() net.Addr {
|
|||||||
type connWithOneByte struct {
|
type connWithOneByte struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
b byte
|
b byte
|
||||||
bRead bool
|
bRead bool
|
||||||
|
readClosed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
||||||
|
if c.readClosed {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
if c.bRead {
|
if c.bRead {
|
||||||
return c.Conn.Read(bs)
|
return c.Conn.Read(bs)
|
||||||
}
|
}
|
||||||
@@ -142,3 +148,22 @@ func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
|||||||
bs[0] = c.b
|
bs[0] = c.b
|
||||||
return 1, nil
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseRead implements [nettype.HalfCloser], allowing the underlying Conn
|
||||||
|
// to be half-closed if possible. Otherwise, this is a no-op.
|
||||||
|
func (c *connWithOneByte) CloseRead() error {
|
||||||
|
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
|
||||||
|
c.readClosed = true
|
||||||
|
return hc.CloseRead()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseWrite implements [nettype.HalfCloser], allowing the underlying Conn
|
||||||
|
// to be half-closed if possible. Otherwise, this is a no-op.
|
||||||
|
func (c *connWithOneByte) CloseWrite() error {
|
||||||
|
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
|
||||||
|
return hc.CloseWrite()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+29
-1
@@ -28,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"tailscale.com/syncs"
|
"tailscale.com/syncs"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/nettype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authentication METHODs described in RFC 1928, section 3.
|
// Authentication METHODs described in RFC 1928, section 3.
|
||||||
@@ -239,6 +240,16 @@ func (c *Conn) handleTCP() error {
|
|||||||
}
|
}
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
|
// As of 2026-09-16, `srv.dial` always returns either a TCPConn-type
|
||||||
|
// connection, or such a connection wrapped by a [tsdial.sysConn],
|
||||||
|
// which passes down calls to half-close the connection to its
|
||||||
|
// underlying Conn.
|
||||||
|
srvHalfCloser, srvIsHalfCloser := srv.(nettype.HalfCloser)
|
||||||
|
// As of 2026-09-16, `c.clientConn` always originates from a TCP listener,
|
||||||
|
// sometimes split up by [proxymux.SplitSOCKSAndHTTP], which passes down
|
||||||
|
// calls to half-close the connection to its underlying Conn.
|
||||||
|
clientHalfCloser, clientIsHalfCloser := c.clientConn.(nettype.HalfCloser)
|
||||||
|
|
||||||
localAddr := srv.LocalAddr().String()
|
localAddr := srv.LocalAddr().String()
|
||||||
serverAddr, serverPort, err := splitHostPort(localAddr)
|
serverAddr, serverPort, err := splitHostPort(localAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,6 +277,12 @@ func (c *Conn) handleTCP() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("from backend to client: %w", err)
|
err = fmt.Errorf("from backend to client: %w", err)
|
||||||
}
|
}
|
||||||
|
if clientIsHalfCloser {
|
||||||
|
err = errors.Join(err, clientHalfCloser.CloseWrite())
|
||||||
|
}
|
||||||
|
if srvIsHalfCloser {
|
||||||
|
err = errors.Join(srvHalfCloser.CloseRead())
|
||||||
|
}
|
||||||
errc <- err
|
errc <- err
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
@@ -273,9 +290,20 @@ func (c *Conn) handleTCP() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("from client to backend: %w", err)
|
err = fmt.Errorf("from client to backend: %w", err)
|
||||||
}
|
}
|
||||||
|
if clientIsHalfCloser {
|
||||||
|
err = errors.Join(err, clientHalfCloser.CloseRead())
|
||||||
|
}
|
||||||
|
if srvIsHalfCloser {
|
||||||
|
err = errors.Join(srvHalfCloser.CloseWrite())
|
||||||
|
}
|
||||||
errc <- err
|
errc <- err
|
||||||
}()
|
}()
|
||||||
return <-errc
|
// Wait for both sides of the connection to close.
|
||||||
|
var errs []error
|
||||||
|
for range 2 {
|
||||||
|
errs = append(errs, <-errc)
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) handleUDP() error {
|
func (c *Conn) handleUDP() error {
|
||||||
|
|||||||
@@ -484,3 +484,79 @@ func TestUDPLogNilLogf(t *testing.T) {
|
|||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTCPHalfClose(t *testing.T) {
|
||||||
|
const msg = "we are so winning"
|
||||||
|
|
||||||
|
// backend server which we'll use SOCKS5 to connect to
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backendServerPort := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
go func() {
|
||||||
|
c, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend accept conn: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
defer listener.Close()
|
||||||
|
tcpConn := c.(*net.TCPConn)
|
||||||
|
var buf [1500]byte
|
||||||
|
n, err := tcpConn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend read: %v", err)
|
||||||
|
}
|
||||||
|
res := string(buf[:n])
|
||||||
|
if res != msg {
|
||||||
|
t.Errorf("backend read: want %q, got %q", msg, res)
|
||||||
|
}
|
||||||
|
if err := tcpConn.CloseRead(); err != nil {
|
||||||
|
t.Errorf("backend closeread: %v", err)
|
||||||
|
}
|
||||||
|
_, err = tcpConn.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend write: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// SOCKS5 server
|
||||||
|
socks5, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
socks5Port := socks5.Addr().(*net.TCPAddr).Port
|
||||||
|
go socks5Server(socks5)
|
||||||
|
|
||||||
|
// Client
|
||||||
|
addr := fmt.Sprintf("localhost:%d", socks5Port)
|
||||||
|
socksDialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = fmt.Sprintf("localhost:%d", backendServerPort)
|
||||||
|
conn, err := socksDialer.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
tcpConn := conn.(*net.TCPConn)
|
||||||
|
_, err = tcpConn.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("client write: %v", err)
|
||||||
|
}
|
||||||
|
if err := tcpConn.CloseWrite(); err != nil {
|
||||||
|
t.Errorf("client closewrite: %v", err)
|
||||||
|
}
|
||||||
|
var buf [1500]byte
|
||||||
|
n, err := tcpConn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("client read: %v", err)
|
||||||
|
}
|
||||||
|
res := string(buf[:n])
|
||||||
|
if res != msg {
|
||||||
|
t.Errorf("client read: want %q, got %q", msg, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/syncs"
|
"tailscale.com/syncs"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/nettype"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
"tailscale.com/util/eventbus"
|
"tailscale.com/util/eventbus"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
@@ -125,6 +126,24 @@ func (c sysConn) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseRead implements [nettype.HalfCloser], allowing the underlying Conn
|
||||||
|
// to be half-closed if possible. Otherwise, this is a no-op.
|
||||||
|
func (c sysConn) CloseRead() error {
|
||||||
|
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
|
||||||
|
return hc.CloseRead()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseWrite implements [nettype.HalfCloser], allowing the underlying Conn
|
||||||
|
// to be half-closed if possible. Otherwise, this is a no-op.
|
||||||
|
func (c sysConn) CloseWrite() error {
|
||||||
|
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
|
||||||
|
return hc.CloseWrite()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
||||||
// etc). This is needed on some platforms to set sockopts to bind
|
// etc). This is needed on some platforms to set sockopts to bind
|
||||||
// to the same interface index.
|
// to the same interface index.
|
||||||
|
|||||||
Reference in new issue
Block a user