awg-shim/socks5.go
Rama main · réplica de lectura del repositorio original.
// Minimal SOCKS5 server (RFC 1928, no auth) dialing through netstack:
// CONNECT for TCP, UDP ASSOCIATE for UDP. Enough for sing-box's socks
// outbound; BIND is not implemented.
package awgshim
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
)
const (
cmdConnect = 0x01
cmdUDPAssociate = 0x03
atypIPv4 = 0x01
atypDomain = 0x03
atypIPv6 = 0x04
repOK = 0x00
repGeneralFailure = 0x01
repHostUnreachable = 0x04
repRefused = 0x05
repCmdNotSupported = 0x07
udpIdleTimeout = 2 * time.Minute
)
type socksServer struct {
tnet *netstack.Net
}
func (s *socksServer) serve(ln net.Listener) error {
for {
c, err := ln.Accept()
if err != nil {
return err
}
go s.handle(c)
}
}
func (s *socksServer) handle(c net.Conn) {
defer c.Close()
_ = c.SetDeadline(time.Now().Add(10 * time.Second))
// greeting: VER NMETHODS METHODS...
hdr := make([]byte, 2)
if _, err := io.ReadFull(c, hdr); err != nil || hdr[0] != 0x05 {
return
}
methods := make([]byte, hdr[1])
if _, err := io.ReadFull(c, methods); err != nil {
return
}
if _, err := c.Write([]byte{0x05, 0x00}); err != nil { // no auth
return
}
// request: VER CMD RSV ATYP DST.ADDR DST.PORT
req := make([]byte, 4)
if _, err := io.ReadFull(c, req); err != nil || req[0] != 0x05 {
return
}
dst, err := readAddr(c, req[3])
if err != nil {
_ = writeReply(c, repGeneralFailure)
return
}
_ = c.SetDeadline(time.Time{})
switch req[1] {
case cmdConnect:
s.handleConnect(c, dst)
case cmdUDPAssociate:
s.handleUDP(c)
default:
_ = writeReply(c, repCmdNotSupported)
}
}
func (s *socksServer) handleConnect(c net.Conn, dst string) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
remote, err := s.tnet.DialContext(ctx, "tcp", dst)
cancel()
if err != nil {
_ = writeReply(c, repHostUnreachable)
return
}
defer remote.Close()
if err := writeReply(c, repOK); err != nil {
return
}
done := make(chan struct{}, 2)
go func() { _, _ = io.Copy(remote, c); done <- struct{}{} }()
go func() { _, _ = io.Copy(c, remote); done <- struct{}{} }()
<-done // either direction closing tears the pair down
}
// ---------------------------------------------------------------------------
// UDP ASSOCIATE: host-side relay socket <-> per-destination netstack sockets
// ---------------------------------------------------------------------------
type udpAssoc struct {
mu sync.Mutex
conns map[netip.AddrPort]net.Conn // destination -> netstack socket
}
func (s *socksServer) handleUDP(c net.Conn) {
relay, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
_ = writeReply(c, repGeneralFailure)
return
}
defer relay.Close()
// reply with the relay address the client must send datagrams to
bnd := relay.LocalAddr().(*net.UDPAddr)
reply := []byte{0x05, repOK, 0x00, atypIPv4}
reply = append(reply, bnd.IP.To4()...)
reply = binary.BigEndian.AppendUint16(reply, uint16(bnd.Port))
if _, err := c.Write(reply); err != nil {
return
}
assoc := &udpAssoc{conns: map[netip.AddrPort]net.Conn{}}
go s.relayLoop(relay, assoc)
// the association lives as long as the control connection
_, _ = io.Copy(io.Discard, c)
relay.Close()
assoc.mu.Lock()
for _, rc := range assoc.conns {
rc.Close()
}
assoc.mu.Unlock()
}
func (s *socksServer) relayLoop(relay *net.UDPConn, assoc *udpAssoc) {
buf := make([]byte, 65535)
var client *net.UDPAddr // first sender wins; sing-box uses one socket
for {
n, from, err := relay.ReadFromUDP(buf)
if err != nil {
return
}
if client == nil {
client = from
} else if !from.IP.Equal(client.IP) || from.Port != client.Port {
continue
}
dst, payload, err := parseUDPHeader(buf[:n])
if err != nil {
continue
}
dstAP, err := s.resolveUDP(dst)
if err != nil {
continue
}
assoc.mu.Lock()
rc := assoc.conns[dstAP]
if rc == nil {
rc, err = s.tnet.DialUDP(nil, &net.UDPAddr{
IP: dstAP.Addr().AsSlice(),
Port: int(dstAP.Port()),
})
if err != nil {
assoc.mu.Unlock()
continue
}
assoc.conns[dstAP] = rc
go pumpReplies(relay, client, dstAP, rc, assoc)
}
assoc.mu.Unlock()
_ = rc.SetReadDeadline(time.Now().Add(udpIdleTimeout))
_, _ = rc.Write(payload)
}
}
// pumpReplies forwards datagrams coming back from one destination to the
// client, wrapped in the SOCKS5 UDP header. Exits on idle timeout or close.
func pumpReplies(relay *net.UDPConn, client *net.UDPAddr, dst netip.AddrPort, rc net.Conn, assoc *udpAssoc) {
defer func() {
assoc.mu.Lock()
delete(assoc.conns, dst)
assoc.mu.Unlock()
rc.Close()
}()
hdr := udpHeader(dst)
buf := make([]byte, 65535)
for {
_ = rc.SetReadDeadline(time.Now().Add(udpIdleTimeout))
n, err := rc.Read(buf)
if err != nil {
return
}
pkt := append(append([]byte{}, hdr...), buf[:n]...)
if _, err := relay.WriteToUDP(pkt, client); err != nil {
return
}
}
}
// resolveUDP turns a textual destination (possibly a domain) into ip:port,
// resolving through the tunnel's DNS.
func (s *socksServer) resolveUDP(dst string) (netip.AddrPort, error) {
host, portStr, err := net.SplitHostPort(dst)
if err != nil {
return netip.AddrPort{}, err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return netip.AddrPort{}, err
}
if addr, err := netip.ParseAddr(host); err == nil {
return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ips, err := s.tnet.LookupContextHost(ctx, host)
if err != nil || len(ips) == 0 {
return netip.AddrPort{}, fmt.Errorf("lookup %s: %v", host, err)
}
addr, err := netip.ParseAddr(ips[0])
if err != nil {
return netip.AddrPort{}, err
}
return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil
}
// ---------------------------------------------------------------------------
// wire helpers
// ---------------------------------------------------------------------------
// readAddr reads ATYP-specific DST.ADDR + DST.PORT and returns "host:port".
func readAddr(r io.Reader, atyp byte) (string, error) {
switch atyp {
case atypIPv4:
b := make([]byte, 6)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return joinIPPort(net.IP(b[:4]), b[4:]), nil
case atypIPv6:
b := make([]byte, 18)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return joinIPPort(net.IP(b[:16]), b[16:]), nil
case atypDomain:
l := make([]byte, 1)
if _, err := io.ReadFull(r, l); err != nil {
return "", err
}
b := make([]byte, int(l[0])+2)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
host := string(b[:l[0]])
return net.JoinHostPort(host, strconv.Itoa(int(binary.BigEndian.Uint16(b[l[0]:])))), nil
default:
return "", fmt.Errorf("bad atyp %d", atyp)
}
}
// parseUDPHeader parses a SOCKS5 UDP datagram: RSV(2) FRAG ATYP ADDR PORT DATA.
func parseUDPHeader(pkt []byte) (dst string, payload []byte, err error) {
if len(pkt) < 4 {
return "", nil, fmt.Errorf("short packet")
}
if pkt[2] != 0 {
return "", nil, fmt.Errorf("fragmentation unsupported")
}
atyp := pkt[3]
rest := pkt[4:]
var addrLen int
switch atyp {
case atypIPv4:
addrLen = 4
case atypIPv6:
addrLen = 16
case atypDomain:
if len(rest) < 1 {
return "", nil, fmt.Errorf("short domain")
}
addrLen = 1 + int(rest[0])
default:
return "", nil, fmt.Errorf("bad atyp %d", atyp)
}
if len(rest) < addrLen+2 {
return "", nil, fmt.Errorf("short addr")
}
port := strconv.Itoa(int(binary.BigEndian.Uint16(rest[addrLen : addrLen+2])))
var host string
if atyp == atypDomain {
host = string(rest[1:addrLen])
} else {
host = net.IP(rest[:addrLen]).String()
}
return net.JoinHostPort(host, port), rest[addrLen+2:], nil
}
// udpHeader builds the SOCKS5 UDP header naming dst as the datagram source.
func udpHeader(dst netip.AddrPort) []byte {
var h []byte
if dst.Addr().Is4() {
h = append([]byte{0, 0, 0, atypIPv4}, dst.Addr().AsSlice()...)
} else {
h = append([]byte{0, 0, 0, atypIPv6}, dst.Addr().AsSlice()...)
}
return binary.BigEndian.AppendUint16(h, dst.Port())
}
func joinIPPort(ip net.IP, portBytes []byte) string {
return net.JoinHostPort(ip.String(), strconv.Itoa(int(binary.BigEndian.Uint16(portBytes))))
}
func writeReply(c net.Conn, rep byte) error {
_, err := c.Write([]byte{0x05, rep, 0x00, atypIPv4, 0, 0, 0, 0, 0, 0})
return err
}