Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+107
-25
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package socks5 is a SOCKS5 server implementation.
|
||||
@@ -15,16 +15,20 @@ package socks5
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
// Authentication METHODs described in RFC 1928, section 3.
|
||||
@@ -129,7 +133,7 @@ func (s *Server) Serve(ln net.Listener) error {
|
||||
}
|
||||
go func() {
|
||||
defer c.Close()
|
||||
conn := &Conn{logf: s.Logf, clientConn: c, srv: s}
|
||||
conn := &Conn{clientConn: c, srv: s}
|
||||
err := conn.Run()
|
||||
if err != nil {
|
||||
s.logf("client connection failed: %v", err)
|
||||
@@ -144,15 +148,24 @@ type Conn struct {
|
||||
// The struct is filled by each of the internal
|
||||
// methods in turn as the transaction progresses.
|
||||
|
||||
logf logger.Logf
|
||||
srv *Server
|
||||
clientConn net.Conn
|
||||
request *request
|
||||
|
||||
udpClientAddr net.Addr
|
||||
// udpClientAddr is the address the client sends its UDP datagrams from.
|
||||
// The goroutine reading from the client writes it, and a goroutine per
|
||||
// target reads it to address the responses, so it needs a lock.
|
||||
udpClientAddr syncs.MutexValue[net.Addr]
|
||||
|
||||
udpTargetConns map[socksAddr]net.Conn
|
||||
}
|
||||
|
||||
// logf logs to the server's logger, which falls back to the standard logger
|
||||
// when Server.Logf is nil.
|
||||
func (c *Conn) logf(format string, args ...any) {
|
||||
c.srv.logf(format, args...)
|
||||
}
|
||||
|
||||
// Run starts the new connection.
|
||||
func (c *Conn) Run() error {
|
||||
needAuth := c.srv.Username != "" || c.srv.Password != ""
|
||||
@@ -172,7 +185,14 @@ func (c *Conn) Run() error {
|
||||
}
|
||||
|
||||
user, pwd, err := parseClientAuth(c.clientConn)
|
||||
if err != nil || user != c.srv.Username || pwd != c.srv.Password {
|
||||
// Compare both credentials in constant time. The listener is reachable by
|
||||
// any local process, so a data-dependent comparison would let one recover
|
||||
// the username or password a byte at a time by timing the reject. Evaluate
|
||||
// both halves unconditionally so the username result doesn't gate whether
|
||||
// the password is examined.
|
||||
userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(c.srv.Username))
|
||||
pwdMatch := subtle.ConstantTimeCompare([]byte(pwd), []byte(c.srv.Password))
|
||||
if err != nil || userMatch != 1 || pwdMatch != 1 {
|
||||
c.clientConn.Write([]byte{1, 1}) // auth error
|
||||
return err
|
||||
}
|
||||
@@ -242,21 +262,85 @@ func (c *Conn) handleTCP() error {
|
||||
c.clientConn.Write(buf)
|
||||
|
||||
errc := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(c.clientConn, srv)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from backend to client: %w", err)
|
||||
go func() { errc <- pump(c.clientConn, srv, "from backend to client") }()
|
||||
go func() { errc <- pump(srv, c.clientConn, "from client to backend") }()
|
||||
err = <-errc
|
||||
if err == nil {
|
||||
return <-errc
|
||||
}
|
||||
// One direction failed, which means one of the two sockets is dead.
|
||||
//
|
||||
// If it failed writing to (or half-closing) its destination, the other
|
||||
// direction is reading from that dead socket. Its reads return whatever
|
||||
// arrived before the failure and then an error, so leave it alone to
|
||||
// deliver that data rather than truncating it.
|
||||
//
|
||||
// If it failed reading from its source, the other direction is writing
|
||||
// into that dead socket and may be blocked reading from the live peer,
|
||||
// which has no reason to send anything. Close both connections to
|
||||
// unblock it. Nothing deliverable is lost: the dead socket has already
|
||||
// returned everything it received, and anything from the live peer
|
||||
// could not be delivered to it anyway.
|
||||
if _, isDstErr := errors.AsType[dstError](err); !isDstErr {
|
||||
srv.Close()
|
||||
c.clientConn.Close()
|
||||
}
|
||||
<-errc
|
||||
return err
|
||||
}
|
||||
|
||||
// pump copies from src to dst until src returns EOF or either side fails.
|
||||
//
|
||||
// On EOF it half-closes dst, if dst supports it, so dst's peer sees EOF while
|
||||
// the other direction of the proxied connection keeps flowing. It does not
|
||||
// half-close src's read side: that has no effect on the wire, and after src
|
||||
// has returned EOF, both macOS and Linux may reject the shutdown with
|
||||
// ENOTCONN.
|
||||
//
|
||||
// Errors from writing to or half-closing dst are returned wrapped in a
|
||||
// [dstError]. Errors from reading src are not.
|
||||
//
|
||||
// As of 2026-09-16, both ends of a proxied connection support half-closing:
|
||||
// [Server.dial] returns a [*net.TCPConn], possibly wrapped by a
|
||||
// tsdial.sysConn, and the client connection comes from a TCP listener,
|
||||
// possibly wrapped by proxymux.SplitSOCKSAndHTTP. Both wrappers pass
|
||||
// half-closes through to the underlying connection.
|
||||
func pump(dst, src net.Conn, dir string) error {
|
||||
if _, err := io.Copy(dstWriter{dst}, src); err != nil {
|
||||
return fmt.Errorf("%s: %w", dir, err)
|
||||
}
|
||||
if hc, ok := dst.(nettype.HalfCloser); ok {
|
||||
if err := hc.CloseWrite(); err != nil {
|
||||
return fmt.Errorf("%s: close write: %w", dir, dstError{err})
|
||||
}
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(srv, c.clientConn)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from client to backend: %w", err)
|
||||
}
|
||||
errc <- err
|
||||
}()
|
||||
return <-errc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dstError wraps an error from a [pump]'s destination, as opposed to one
|
||||
// from reading its source.
|
||||
type dstError struct{ error }
|
||||
|
||||
func (e dstError) Unwrap() error { return e.error }
|
||||
|
||||
// dstWriter wraps a [pump]'s destination so that its write errors are
|
||||
// reported as a [dstError].
|
||||
//
|
||||
// Wrapping the destination hides its ReadFrom method from [io.Copy] and hides
|
||||
// its type from the source's WriteTo, so when both ends are bare TCP
|
||||
// connections the kernel splice fast path is not used. That costs about a
|
||||
// third of the relay throughput on Linux loopback, but knowing which side
|
||||
// failed is what lets handleTCP avoid truncating a reply after the backend
|
||||
// resets, and in the common userspace-networking and proxymux configurations
|
||||
// the connections are wrapped types with no fast path anyway.
|
||||
type dstWriter struct{ io.Writer }
|
||||
|
||||
func (w dstWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.Writer.Write(p)
|
||||
if err != nil {
|
||||
err = dstError{err}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *Conn) handleUDP() error {
|
||||
@@ -402,7 +486,7 @@ func (c *Conn) handleUDPRequest(
|
||||
if err != nil {
|
||||
return fmt.Errorf("read from client: %w", err)
|
||||
}
|
||||
c.udpClientAddr = addr
|
||||
c.udpClientAddr.Store(addr)
|
||||
req, data, err := parseUDPRequest(buf[:n])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse udp request: %w", err)
|
||||
@@ -442,7 +526,7 @@ func (c *Conn) handleUDPResponse(
|
||||
}
|
||||
data := append(pkt, buf[:n]...)
|
||||
// use addr from client to send back
|
||||
nn, err := clientConn.WriteTo(data, c.udpClientAddr)
|
||||
nn, err := clientConn.WriteTo(data, c.udpClientAddr.Load())
|
||||
if err != nil {
|
||||
return fmt.Errorf("write to client: %w", err)
|
||||
}
|
||||
@@ -488,10 +572,8 @@ func parseClientGreeting(r io.Reader, authMethod byte) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read methods")
|
||||
}
|
||||
for _, m := range methods {
|
||||
if m == authMethod {
|
||||
return nil
|
||||
}
|
||||
if slices.Contains(methods, authMethod) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no acceptable auth methods")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user