Feature: add UDP TPROXY support on Linux (#562)

This commit is contained in:
duama 2020-03-08 21:58:49 +08:00 committed by GitHub
parent b2c9cbb43e
commit f7f30d3406
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 327 additions and 0 deletions

View file

@ -5,6 +5,7 @@ import (
"net" "net"
"strconv" "strconv"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/proxy/http" "github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/redir" "github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks" "github.com/Dreamacro/clash/proxy/socks"
@ -18,6 +19,7 @@ var (
socksUDPListener *socks.SockUDPListener socksUDPListener *socks.SockUDPListener
httpListener *http.HttpListener httpListener *http.HttpListener
redirListener *redir.RedirListener redirListener *redir.RedirListener
redirUDPListener *redir.RedirUDPListener
) )
type listener interface { type listener interface {
@ -131,6 +133,14 @@ func ReCreateRedir(port int) error {
redirListener = nil redirListener = nil
} }
if redirUDPListener != nil {
if redirUDPListener.Address() == addr {
return nil
}
redirUDPListener.Close()
redirUDPListener = nil
}
if portIsZero(addr) { if portIsZero(addr) {
return nil return nil
} }
@ -141,6 +151,11 @@ func ReCreateRedir(port int) error {
return err return err
} }
redirUDPListener, err = redir.NewRedirUDPProxy(addr)
if err != nil {
log.Warnln("Failed to start Redir UDP Listener: %s", err)
}
return nil return nil
} }

78
proxy/redir/udp.go Normal file
View file

@ -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))
}

69
proxy/redir/udp_linux.go Normal file
View file

@ -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")
}

16
proxy/redir/udp_other.go Normal file
View file

@ -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")
}

41
proxy/redir/utils.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}