diff --git a/protocol/socks/handshake.go b/protocol/socks/handshake.go index 73a16b8..dc9c057 100644 --- a/protocol/socks/handshake.go +++ b/protocol/socks/handshake.go @@ -25,6 +25,10 @@ type HandlerEx interface { N.UDPConnectionHandlerEx } +type PacketListener interface { + ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) +} + func ClientHandshake4(conn io.ReadWriter, command byte, destination M.Socksaddr, username string) (socks4.Response, error) { err := socks4.WriteRequest(conn, socks4.Request{ Command: command, @@ -121,6 +125,8 @@ func HandleConnectionEx( ctx context.Context, conn net.Conn, reader *std_bufio.Reader, authenticator *auth.Authenticator, handler HandlerEx, + packetListener PacketListener, + // resolver TorResolver, source M.Socksaddr, onClose N.CloseHandlerFunc, ) error { @@ -148,6 +154,11 @@ func HandleConnectionEx( } handler.NewConnectionEx(auth.ContextWithUser(ctx, request.Username), NewLazyConn(conn, version), source, request.Destination, onClose) return nil + /*case CommandTorResolve, CommandTorResolvePTR: + if resolver == nil { + return E.New("socks4: torsocks: commands not implemented") + } + return handleTorSocks4(ctx, conn, request, resolver)*/ default: err = socks4.WriteResponse(conn, socks4.Response{ ReplyCode: socks4.ReplyCodeRejectedOrFailed, @@ -214,8 +225,15 @@ func HandleConnectionEx( handler.NewConnectionEx(ctx, NewLazyConn(conn, version), source, request.Destination, onClose) return nil case socks5.CommandUDPAssociate: - var udpConn *net.UDPConn - udpConn, err = net.ListenUDP(M.NetworkFromNetAddr("udp", M.AddrFromNet(conn.LocalAddr())), net.UDPAddrFromAddrPort(netip.AddrPortFrom(M.AddrFromNet(conn.LocalAddr()), 0))) + var ( + listenConfig net.ListenConfig + udpConn net.PacketConn + ) + if packetListener != nil { + udpConn, err = packetListener.ListenPacket(listenConfig, ctx, M.NetworkFromNetAddr("udp", M.AddrFromNet(conn.LocalAddr())), M.SocksaddrFrom(M.AddrFromNet(conn.LocalAddr()), 0).String()) + } else { + udpConn, err = listenConfig.ListenPacket(ctx, M.NetworkFromNetAddr("udp", M.AddrFromNet(conn.LocalAddr())), M.SocksaddrFrom(M.AddrFromNet(conn.LocalAddr()), 0).String()) + } if err != nil { return E.Cause(err, "socks5: listen udp") } @@ -236,6 +254,11 @@ func HandleConnectionEx( socksPacketConn = bufio.NewCachedPacketConn(socksPacketConn, firstPacket, destination) handler.NewPacketConnectionEx(ctx, socksPacketConn, source, destination, onClose) return nil + /*case CommandTorResolve, CommandTorResolvePTR: + if resolver == nil { + return E.New("socks4: torsocks: commands not implemented") + } + return handleTorSocks5(ctx, conn, request, resolver)*/ default: err = socks5.WriteResponse(conn, socks5.Response{ ReplyCode: socks5.ReplyCodeUnsupported, diff --git a/protocol/socks/handshake_tor.go b/protocol/socks/handshake_tor.go new file mode 100644 index 0000000..5d66322 --- /dev/null +++ b/protocol/socks/handshake_tor.go @@ -0,0 +1,146 @@ +package socks + +import ( + "context" + "net" + "net/netip" + "os" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/protocol/socks/socks4" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +const ( + CommandTorResolve byte = 0xF0 + CommandTorResolvePTR byte = 0xF1 +) + +type TorResolver interface { + LookupIP(ctx context.Context, host string) (netip.Addr, error) + LookupPTR(ctx context.Context, addr netip.Addr) (string, error) +} + +func handleTorSocks4(ctx context.Context, conn net.Conn, request socks4.Request, resolver TorResolver) error { + switch request.Command { + case CommandTorResolve: + if !request.Destination.IsFqdn() { + return E.New("socks4: torsocks: invalid destination") + } + ipAddr, err := resolver.LookupIP(ctx, request.Destination.Fqdn) + if err != nil { + err = socks4.WriteResponse(conn, socks4.Response{ + ReplyCode: socks4.ReplyCodeRejectedOrFailed, + }) + if err != nil { + return err + } + return E.Cause(err, "socks4: torsocks: lookup failed for domain: ", request.Destination.Fqdn) + } + err = socks4.WriteResponse(conn, socks4.Response{ + ReplyCode: socks4.ReplyCodeGranted, + Destination: M.SocksaddrFrom(ipAddr, 0), + }) + if err != nil { + return E.Cause(err, "socks4: torsocks: write response") + } + return nil + case CommandTorResolvePTR: + var ipAddr netip.Addr + if request.Destination.IsIP() { + ipAddr = request.Destination.Addr + } else if strings.HasSuffix(request.Destination.Fqdn, ".in-addr.arpa") { + ipAddr, _ = netip.ParseAddr(request.Destination.Fqdn[:len(request.Destination.Fqdn)-len(".in-addr.arpa")]) + } else if strings.HasSuffix(request.Destination.Fqdn, ".ip6.arpa") { + ipAddr, _ = netip.ParseAddr(strings.ReplaceAll(request.Destination.Fqdn[:len(request.Destination.Fqdn)-len(".ip6.arpa")], ".", ":")) + } + if !ipAddr.IsValid() { + return E.New("socks4: torsocks: invalid destination") + } + host, err := resolver.LookupPTR(ctx, ipAddr) + if err != nil { + err = socks4.WriteResponse(conn, socks4.Response{ + ReplyCode: socks4.ReplyCodeRejectedOrFailed, + }) + if err != nil { + return err + } + return E.Cause(err, "socks4: torsocks: lookup PTR failed for ip: ", ipAddr) + } + err = socks4.WriteResponse(conn, socks4.Response{ + ReplyCode: socks4.ReplyCodeGranted, + Destination: M.Socksaddr{ + Fqdn: host, + }, + }) + if err != nil { + return E.Cause(err, "socks4: torsocks: write response") + } + return nil + default: + return os.ErrInvalid + } +} + +func handleTorSocks5(ctx context.Context, conn net.Conn, request socks5.Request, resolver TorResolver) error { + switch request.Command { + case CommandTorResolve: + if !request.Destination.IsFqdn() { + return E.New("socks5: torsocks: invalid destination") + } + ipAddr, err := resolver.LookupIP(ctx, request.Destination.Fqdn) + if err != nil { + err = socks5.WriteResponse(conn, socks5.Response{ + ReplyCode: socks5.ReplyCodeFailure, + }) + if err != nil { + return err + } + return E.Cause(err, "socks5: torsocks: lookup failed for domain: ", request.Destination.Fqdn) + } + err = socks5.WriteResponse(conn, socks5.Response{ + ReplyCode: socks5.ReplyCodeSuccess, + Bind: M.SocksaddrFrom(ipAddr, 0), + }) + if err != nil { + return E.Cause(err, "socks5: torsocks: write response") + } + return nil + case CommandTorResolvePTR: + var ipAddr netip.Addr + if request.Destination.IsIP() { + ipAddr = request.Destination.Addr + } else if strings.HasSuffix(request.Destination.Fqdn, ".in-addr.arpa") { + ipAddr, _ = netip.ParseAddr(request.Destination.Fqdn[:len(request.Destination.Fqdn)-len(".in-addr.arpa")]) + } else if strings.HasSuffix(request.Destination.Fqdn, ".ip6.arpa") { + ipAddr, _ = netip.ParseAddr(strings.ReplaceAll(request.Destination.Fqdn[:len(request.Destination.Fqdn)-len(".ip6.arpa")], ".", ":")) + } + if !ipAddr.IsValid() { + return E.New("socks5: torsocks: invalid destination") + } + host, err := resolver.LookupPTR(ctx, ipAddr) + if err != nil { + err = socks5.WriteResponse(conn, socks5.Response{ + ReplyCode: socks5.ReplyCodeFailure, + }) + if err != nil { + return err + } + return E.Cause(err, "socks5: torsocks: lookup PTR failed for ip: ", ipAddr) + } + err = socks5.WriteResponse(conn, socks5.Response{ + ReplyCode: socks5.ReplyCodeSuccess, + Bind: M.Socksaddr{ + Fqdn: host, + }, + }) + if err != nil { + return E.Cause(err, "socks5: torsocks: write response") + } + return nil + default: + return os.ErrInvalid + } +}