From f7f30d34062f31aff622516dac11b656c07e8194 Mon Sep 17 00:00:00 2001 From: duama <30264485+duament@users.noreply.github.com> Date: Sun, 8 Mar 2020 21:58:49 +0800 Subject: [PATCH] Feature: add UDP TPROXY support on Linux (#562) --- proxy/listener.go | 15 ++++++ proxy/redir/udp.go | 78 +++++++++++++++++++++++++++++++ proxy/redir/udp_linux.go | 69 +++++++++++++++++++++++++++ proxy/redir/udp_other.go | 16 +++++++ proxy/redir/utils.go | 41 ++++++++++++++++ proxy/redir/utils_linux.go | 96 ++++++++++++++++++++++++++++++++++++++ proxy/redir/utils_other.go | 12 +++++ 7 files changed, 327 insertions(+) create mode 100644 proxy/redir/udp.go create mode 100644 proxy/redir/udp_linux.go create mode 100644 proxy/redir/udp_other.go create mode 100644 proxy/redir/utils.go create mode 100644 proxy/redir/utils_linux.go create mode 100644 proxy/redir/utils_other.go diff --git a/proxy/listener.go b/proxy/listener.go index 79741581..1089e39d 100644 --- a/proxy/listener.go +++ b/proxy/listener.go @@ -5,6 +5,7 @@ import ( "net" "strconv" + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/proxy/http" "github.com/Dreamacro/clash/proxy/redir" "github.com/Dreamacro/clash/proxy/socks" @@ -18,6 +19,7 @@ var ( socksUDPListener *socks.SockUDPListener httpListener *http.HttpListener redirListener *redir.RedirListener + redirUDPListener *redir.RedirUDPListener ) type listener interface { @@ -131,6 +133,14 @@ func ReCreateRedir(port int) error { redirListener = nil } + if redirUDPListener != nil { + if redirUDPListener.Address() == addr { + return nil + } + redirUDPListener.Close() + redirUDPListener = nil + } + if portIsZero(addr) { return nil } @@ -141,6 +151,11 @@ func ReCreateRedir(port int) error { return err } + redirUDPListener, err = redir.NewRedirUDPProxy(addr) + if err != nil { + log.Warnln("Failed to start Redir UDP Listener: %s", err) + } + return nil } diff --git a/proxy/redir/udp.go b/proxy/redir/udp.go new file mode 100644 index 00000000..83506d86 --- /dev/null +++ b/proxy/redir/udp.go @@ -0,0 +1,78 @@ +package redir + +import ( + "net" + + adapters "github.com/Dreamacro/clash/adapters/inbound" + "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/component/socks5" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/tunnel" +) + +type RedirUDPListener struct { + net.PacketConn + address string + closed bool +} + +func NewRedirUDPProxy(addr string) (*RedirUDPListener, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + rl := &RedirUDPListener{l, addr, false} + + c := l.(*net.UDPConn) + + err = setsockopt(c, addr) + if err != nil { + return nil, err + } + + go func() { + oob := make([]byte, 1024) + for { + buf := pool.BufPool.Get().([]byte) + + n, oobn, _, remoteAddr, err := c.ReadMsgUDP(buf, oob) + if err != nil { + pool.BufPool.Put(buf[:cap(buf)]) + if rl.closed { + break + } + continue + } + + origDst, err := getOrigDst(oob, oobn) + if err != nil { + continue + } + handleRedirUDP(l, buf[:n], remoteAddr, origDst) + } + }() + + return rl, nil +} + +func (l *RedirUDPListener) Close() error { + l.closed = true + return l.PacketConn.Close() +} + +func (l *RedirUDPListener) Address() string { + return l.address +} + +func handleRedirUDP(pc net.PacketConn, buf []byte, addr *net.UDPAddr, origDst *net.UDPAddr) { + target := socks5.ParseAddrToSocksAddr(origDst) + + packet := &fakeConn{ + PacketConn: pc, + origDst: origDst, + rAddr: addr, + buf: buf, + } + tunnel.AddPacket(adapters.NewPacket(target, packet, C.REDIR)) +} diff --git a/proxy/redir/udp_linux.go b/proxy/redir/udp_linux.go new file mode 100644 index 00000000..83a82a72 --- /dev/null +++ b/proxy/redir/udp_linux.go @@ -0,0 +1,69 @@ +// +build linux + +package redir + +import ( + "encoding/binary" + "errors" + "net" + "syscall" +) + +const ( + IPV6_TRANSPARENT = 0x4b + IPV6_RECVORIGDSTADDR = 0x4a +) + +func setsockopt(c *net.UDPConn, addr string) error { + isIPv6 := true + host, _, err := net.SplitHostPort(addr) + if err != nil { + return err + } + ip := net.ParseIP(host) + if ip != nil && ip.To4() != nil { + isIPv6 = false + } + + rc, err := c.SyscallConn() + if err != nil { + return err + } + + rc.Control(func(fd uintptr) { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, IPV6_TRANSPARENT, 1) + } + + if err == nil { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) + } + if err == nil && isIPv6 { + err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + } + }) + + return err +} + +func getOrigDst(oob []byte, oobn int) (*net.UDPAddr, error) { + msgs, err := syscall.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return nil, err + } + + for _, msg := range msgs { + if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { + ip := net.IP(msg.Data[4:8]) + port := binary.BigEndian.Uint16(msg.Data[2:4]) + return &net.UDPAddr{IP: ip, Port: int(port)}, nil + } else if msg.Header.Level == syscall.SOL_IPV6 && msg.Header.Type == IPV6_RECVORIGDSTADDR { + ip := net.IP(msg.Data[8:24]) + port := binary.BigEndian.Uint16(msg.Data[2:4]) + return &net.UDPAddr{IP: ip, Port: int(port)}, nil + } + } + + return nil, errors.New("cannot find origDst") +} diff --git a/proxy/redir/udp_other.go b/proxy/redir/udp_other.go new file mode 100644 index 00000000..d9afdb68 --- /dev/null +++ b/proxy/redir/udp_other.go @@ -0,0 +1,16 @@ +// +build !linux + +package redir + +import ( + "errors" + "net" +) + +func setsockopt(c *net.UDPConn, addr string) error { + return errors.New("UDP redir not supported on current platform") +} + +func getOrigDst(oob []byte, oobn int) (*net.UDPAddr, error) { + return nil, errors.New("UDP redir not supported on current platform") +} diff --git a/proxy/redir/utils.go b/proxy/redir/utils.go new file mode 100644 index 00000000..71756d4d --- /dev/null +++ b/proxy/redir/utils.go @@ -0,0 +1,41 @@ +package redir + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type fakeConn struct { + net.PacketConn + origDst net.Addr + rAddr net.Addr + buf []byte +} + +func (c *fakeConn) Data() []byte { + return c.buf +} + +// WriteBack opens a new socket binding `origDst` to wirte UDP packet back +func (c *fakeConn) WriteBack(b []byte, addr net.Addr) (n int, err error) { + tc, err := dialUDP("udp", c.origDst.(*net.UDPAddr), c.rAddr.(*net.UDPAddr)) + if err != nil { + n = 0 + return + } + n, err = tc.Write(b) + tc.Close() + return +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *fakeConn) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *fakeConn) Close() error { + err := c.PacketConn.Close() + pool.BufPool.Put(c.buf[:cap(c.buf)]) + return err +} diff --git a/proxy/redir/utils_linux.go b/proxy/redir/utils_linux.go new file mode 100644 index 00000000..888601a4 --- /dev/null +++ b/proxy/redir/utils_linux.go @@ -0,0 +1,96 @@ +// +build linux + +package redir + +import ( + "fmt" + "net" + "os" + "strconv" + "syscall" +) + +// dialUDP acts like net.DialUDP for transparent proxy. +// It binds to a non-local address(`lAddr`). +func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) { + rSockAddr, err := udpAddrToSockAddr(rAddr) + if err != nil { + return nil, err + } + + lSockAddr, err := udpAddrToSockAddr(lAddr) + if err != nil { + return nil, err + } + + fd, err := syscall.Socket(udpAddrFamily(network, lAddr, rAddr), syscall.SOCK_DGRAM, 0) + if err != nil { + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + syscall.Close(fd) + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + syscall.Close(fd) + return nil, err + } + + if err = syscall.Bind(fd, lSockAddr); err != nil { + syscall.Close(fd) + return nil, err + } + + if err = syscall.Connect(fd, rSockAddr); err != nil { + syscall.Close(fd) + return nil, err + } + + fdFile := os.NewFile(uintptr(fd), fmt.Sprintf("net-udp-dial-%s", rAddr.String())) + defer fdFile.Close() + + c, err := net.FileConn(fdFile) + if err != nil { + syscall.Close(fd) + return nil, err + } + + return c.(*net.UDPConn), nil +} + +func udpAddrToSockAddr(addr *net.UDPAddr) (syscall.Sockaddr, error) { + switch { + case addr.IP.To4() != nil: + ip := [4]byte{} + copy(ip[:], addr.IP.To4()) + + return &syscall.SockaddrInet4{Addr: ip, Port: addr.Port}, nil + + default: + ip := [16]byte{} + copy(ip[:], addr.IP.To16()) + + zoneID, err := strconv.ParseUint(addr.Zone, 10, 32) + if err != nil { + zoneID = 0 + } + + return &syscall.SockaddrInet6{Addr: ip, Port: addr.Port, ZoneId: uint32(zoneID)}, nil + } +} + +func udpAddrFamily(net string, lAddr, rAddr *net.UDPAddr) int { + switch net[len(net)-1] { + case '4': + return syscall.AF_INET + case '6': + return syscall.AF_INET6 + } + + if (lAddr == nil || lAddr.IP.To4() != nil) && (rAddr == nil || lAddr.IP.To4() != nil) { + return syscall.AF_INET + } + return syscall.AF_INET6 +} diff --git a/proxy/redir/utils_other.go b/proxy/redir/utils_other.go new file mode 100644 index 00000000..faec71e2 --- /dev/null +++ b/proxy/redir/utils_other.go @@ -0,0 +1,12 @@ +// +build !linux + +package redir + +import ( + "errors" + "net" +) + +func dialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) { + return nil, errors.New("UDP redir not supported on current platform") +}