Feature: add UDP TPROXY support on Linux (#562)
This commit is contained in:
parent
b2c9cbb43e
commit
f7f30d3406
7 changed files with 327 additions and 0 deletions
|
@ -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
78
proxy/redir/udp.go
Normal 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
69
proxy/redir/udp_linux.go
Normal 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
16
proxy/redir/udp_other.go
Normal 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
41
proxy/redir/utils.go
Normal 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
|
||||||
|
}
|
96
proxy/redir/utils_linux.go
Normal file
96
proxy/redir/utils_linux.go
Normal 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
|
||||||
|
}
|
12
proxy/redir/utils_other.go
Normal file
12
proxy/redir/utils_other.go
Normal 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")
|
||||||
|
}
|
Loading…
Reference in a new issue