mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-22 03:25:16 -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 @@
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
|
||||
@@ -127,11 +129,15 @@ func (ln *listener) Addr() net.Addr {
|
||||
type connWithOneByte struct {
|
||||
net.Conn
|
||||
|
||||
b byte
|
||||
bRead bool
|
||||
b byte
|
||||
bRead bool
|
||||
readClosed bool
|
||||
}
|
||||
|
||||
func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
||||
if c.readClosed {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
if c.bRead {
|
||||
return c.Conn.Read(bs)
|
||||
}
|
||||
@@ -142,3 +148,22 @@ func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
||||
bs[0] = c.b
|
||||
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 @@
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
// Authentication METHODs described in RFC 1928, section 3.
|
||||
@@ -239,6 +240,16 @@ func (c *Conn) handleTCP() error {
|
||||
}
|
||||
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()
|
||||
serverAddr, serverPort, err := splitHostPort(localAddr)
|
||||
if err != nil {
|
||||
@@ -266,6 +277,12 @@ func (c *Conn) handleTCP() error {
|
||||
if err != nil {
|
||||
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
|
||||
}()
|
||||
go func() {
|
||||
@@ -273,9 +290,20 @@ func (c *Conn) handleTCP() error {
|
||||
if err != nil {
|
||||
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
|
||||
}()
|
||||
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 {
|
||||
|
||||
@@ -484,3 +484,79 @@ func TestUDPLogNilLogf(t *testing.T) {
|
||||
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 @@
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -125,6 +126,24 @@ func (c sysConn) Close() error {
|
||||
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",
|
||||
// etc). This is needed on some platforms to set sockopts to bind
|
||||
// to the same interface index.
|
||||
|
||||
Reference in new issue
Block a user