Browse Source

Add redir tproxy and dns inbound

世界 3 năm trước cách đây
mục cha
commit
2c2eb31e18

+ 4 - 0
adapter/handler.go

@@ -17,6 +17,10 @@ type PacketHandler interface {
 	NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
 }
 
+type OOBPacketHandler interface {
+	NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
+}
+
 type PacketConnectionHandler interface {
 	NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 }

+ 37 - 0
common/redir/redir_linux.go

@@ -0,0 +1,37 @@
+package redir
+
+import (
+	"net"
+	"net/netip"
+	"syscall"
+
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) {
+	rawConn, err := conn.(syscall.Conn).SyscallConn()
+	if err != nil {
+		return
+	}
+	var rawFd uintptr
+	err = rawConn.Control(func(fd uintptr) {
+		rawFd = fd
+	})
+	if err != nil {
+		return
+	}
+	const SO_ORIGINAL_DST = 80
+	if conn.RemoteAddr().(*net.TCPAddr).IP.To4() != nil {
+		raw, err := syscall.GetsockoptIPv6Mreq(int(rawFd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
+		if err != nil {
+			return netip.AddrPort{}, err
+		}
+		return netip.AddrPortFrom(M.AddrFromIP(raw.Multiaddr[4:8]), uint16(raw.Multiaddr[2])<<8+uint16(raw.Multiaddr[3])), nil
+	} else {
+		raw, err := syscall.GetsockoptIPv6MTUInfo(int(rawFd), syscall.IPPROTO_IPV6, SO_ORIGINAL_DST)
+		if err != nil {
+			return netip.AddrPort{}, err
+		}
+		return netip.AddrPortFrom(M.AddrFromIP(raw.Addr.Addr[:]), raw.Addr.Port), nil
+	}
+}

+ 13 - 0
common/redir/redir_other.go

@@ -0,0 +1,13 @@
+//go:build !linux
+
+package redir
+
+import (
+	"net"
+	"net/netip"
+	"os"
+)
+
+func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) {
+	return netip.AddrPort{}, os.ErrInvalid
+}

+ 132 - 0
common/redir/tproxy_linux.go

@@ -0,0 +1,132 @@
+package redir
+
+import (
+	"encoding/binary"
+	"net"
+	"net/netip"
+	"os"
+	"strconv"
+	"syscall"
+
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
+
+	"golang.org/x/sys/unix"
+)
+
+func TProxy(fd uintptr, isIPv6 bool) error {
+	err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
+	if err != nil {
+		return err
+	}
+	if isIPv6 {
+		err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
+	}
+	return err
+}
+
+func TProxyUDP(fd uintptr, isIPv6 bool) error {
+	err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1)
+	if err != nil {
+		return err
+	}
+	if isIPv6 {
+		err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
+	controlMessages, err := unix.ParseSocketControlMessage(oob)
+	if err != nil {
+		return netip.AddrPort{}, err
+	}
+	for _, message := range controlMessages {
+		if message.Header.Level == unix.SOL_IP && message.Header.Type == unix.IP_RECVORIGDSTADDR {
+			return netip.AddrPortFrom(M.AddrFromIP(message.Data[4:8]), binary.BigEndian.Uint16(message.Data[2:4])), nil
+		} else if message.Header.Level == unix.SOL_IPV6 && message.Header.Type == unix.IPV6_RECVORIGDSTADDR {
+			return netip.AddrPortFrom(M.AddrFromIP(message.Data[8:24]), binary.BigEndian.Uint16(message.Data[2:4])), nil
+		}
+	}
+	return netip.AddrPort{}, E.New("not found")
+}
+
+func DialUDP(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(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), F.ToString("net-udp-dial-", rAddr))
+	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(lAddr, rAddr *net.UDPAddr) int {
+	if (lAddr == nil || lAddr.IP.To4() != nil) && (rAddr == nil || lAddr.IP.To4() != nil) {
+		return syscall.AF_INET
+	}
+	return syscall.AF_INET6
+}

+ 25 - 0
common/redir/tproxy_other.go

@@ -0,0 +1,25 @@
+//go:build !linux
+
+package redir
+
+import (
+	"net"
+	"net/netip"
+	"os"
+)
+
+func TProxy(fd uintptr, isIPv6 bool) error {
+	return os.ErrInvalid
+}
+
+func TProxyUDP(fd uintptr, isIPv6 bool) error {
+	return os.ErrInvalid
+}
+
+func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
+	return netip.AddrPort{}, os.ErrInvalid
+}
+
+func DialUDP(network string, lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) {
+	return nil, os.ErrInvalid
+}

+ 3 - 0
constant/proxy.go

@@ -8,4 +8,7 @@ const (
 	TypeMixed       = "mixed"
 	TypeShadowsocks = "shadowsocks"
 	TypeTun         = "tun"
+	TypeRedirect    = "redirect"
+	TypeTProxy      = "tproxy"
+	TypeDNS         = "dns"
 )

+ 44 - 0
docs/configuration/inbound/dns.md

@@ -0,0 +1,44 @@
+`dns` inbound is a DNS server.
+
+### Structure
+
+```json
+{
+  "inbounds": [
+    {
+      "type": "dns",
+      "tag": "dns-in",
+      
+      "listen": "::",
+      "listen_port": 5353,
+      "network": "udp"
+    }
+  ]
+}
+```
+
+!!! note ""
+    
+    There are no outbound connections by the DNS inbound, all requests are handled internally.
+
+### Listen Fields
+
+#### listen
+
+==Required==
+
+Listen address.
+
+#### listen_port
+
+==Required==
+
+Listen port.
+
+### DNS Fields
+
+#### network
+
+Listen network, one of `tcp` `udp`.
+
+Both if empty.

+ 4 - 1
docs/configuration/inbound/index.md

@@ -15,12 +15,15 @@
 
 | Type          | Format                       |
 |---------------|------------------------------|
-| `tun`         | [Tun](./tun)                 |
 | `direct`      | [Direct](./direct)           |
 | `mixed`       | [Mixed](./mixed)             |
 | `socks`       | [Socks](./socks)             |
 | `http`        | [HTTP](./http)               |
 | `shadowsocks` | [Shadowsocks](./shadowsocks) |
+| `tun`         | [Tun](./tun)                 |
+| `redirect`    | [Redirect](./redirect)       |
+| `tproxy`      | [TProxy](./tproxy)           |
+| `dns`         | [DNS](./dns)                 |
 
 #### tag
 

+ 61 - 0
docs/configuration/inbound/redirect.md

@@ -0,0 +1,61 @@
+`redirect` inbound is a linux Redirect server.
+
+### Structure
+
+```json
+{
+  "inbounds": [
+    {
+      "type": "redirect",
+      "tag": "redirect-in",
+      
+      "listen": "::",
+      "listen_port": 5353,
+      "sniff": false,
+      "sniff_override_destination": false,
+      "domain_strategy": "prefer_ipv6",
+      "udp_timeout": 300
+    }
+  ]
+}
+```
+
+### Listen Fields
+
+#### listen
+
+==Required==
+
+Listen address.
+
+#### listen_port
+
+==Required==
+
+Listen port.
+
+#### sniff
+
+Enable sniffing.
+
+Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
+
+This does not break zero copy, like splice.
+
+#### sniff_override_destination
+
+Override the connection destination address with the sniffed domain.
+
+If the domain name is invalid (like tor), this will not work.
+
+#### domain_strategy
+
+One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
+
+If set, the requested domain name will be resolved to IP before routing.
+
+If `sniff_override_destination` is in effect, its value will be taken as a fallback.
+
+#### udp_timeout
+
+UDP NAT expiration time in seconds, default is 300 (5 minutes).

+ 71 - 0
docs/configuration/inbound/tproxy.md

@@ -0,0 +1,71 @@
+`tproxy` inbound is a linux TProxy server.
+
+### Structure
+
+```json
+{
+  "inbounds": [
+    {
+      "type": "tproxy",
+      "tag": "tproxy-in",
+      
+      "listen": "::",
+      "listen_port": 5353,
+      "sniff": false,
+      "sniff_override_destination": false,
+      "domain_strategy": "prefer_ipv6",
+      "udp_timeout": 300,
+      
+      "network": "udp"
+    }
+  ]
+}
+```
+
+### Listen Fields
+
+#### listen
+
+==Required==
+
+Listen address.
+
+#### listen_port
+
+==Required==
+
+Listen port.
+
+#### sniff
+
+Enable sniffing.
+
+Reads domain names for routing, supports HTTP TLS for TCP, QUIC for UDP.
+
+This does not break zero copy, like splice.
+
+#### sniff_override_destination
+
+Override the connection destination address with the sniffed domain.
+
+If the domain name is invalid (like tor), this will not work.
+
+#### domain_strategy
+
+One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
+
+If set, the requested domain name will be resolved to IP before routing.
+
+If `sniff_override_destination` is in effect, its value will be taken as a fallback.
+
+#### udp_timeout
+
+UDP NAT expiration time in seconds, default is 300 (5 minutes).
+
+### TProxy Fields
+
+#### network
+
+Listen network, one of `tcp` `udp`.
+
+Both if empty.

+ 6 - 0
inbound/builder.go

@@ -28,6 +28,12 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
 	case C.TypeTun:
 		return NewTun(ctx, router, logger, options.Tag, options.TunOptions)
+	case C.TypeRedirect:
+		return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil
+	case C.TypeTProxy:
+		return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil
+	case C.TypeDNS:
+		return NewDNS(ctx, router, logger, options.Tag, options.DNSOptions), nil
 	default:
 		return nil, E.New("unknown inbound type: ", options.Type)
 	}

+ 81 - 14
inbound/default.go

@@ -26,16 +26,17 @@ import (
 var _ adapter.Inbound = (*myInboundAdapter)(nil)
 
 type myInboundAdapter struct {
-	protocol       string
-	network        []string
-	ctx            context.Context
-	router         adapter.Router
-	logger         log.ContextLogger
-	tag            string
-	listenOptions  option.ListenOptions
-	connHandler    adapter.ConnectionHandler
-	packetHandler  adapter.PacketHandler
-	packetUpstream any
+	protocol         string
+	network          []string
+	ctx              context.Context
+	router           adapter.Router
+	logger           log.ContextLogger
+	tag              string
+	listenOptions    option.ListenOptions
+	connHandler      adapter.ConnectionHandler
+	packetHandler    adapter.PacketHandler
+	oobPacketHandler adapter.OOBPacketHandler
+	packetUpstream   any
 
 	// http mixed
 
@@ -85,12 +86,20 @@ func (a *myInboundAdapter) Start() error {
 		a.packetForce6 = M.SocksaddrFromNet(udpConn.LocalAddr()).Addr.Is6()
 		a.packetOutboundClosed = make(chan struct{})
 		a.packetOutbound = make(chan *myInboundPacket)
-		if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
-			go a.loopUDPIn()
+		if a.oobPacketHandler != nil {
+			if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
+				go a.loopUDPOOBIn()
+			} else {
+				go a.loopUDPOOBInThreadSafe()
+			}
 		} else {
-			go a.loopUDPInThreadSafe()
+			if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
+				go a.loopUDPIn()
+			} else {
+				go a.loopUDPInThreadSafe()
+			}
+			go a.loopUDPOut()
 		}
-		go a.loopUDPOut()
 		a.logger.Info("udp server started at ", udpConn.LocalAddr())
 	}
 	if a.setSystemProxy {
@@ -194,6 +203,37 @@ func (a *myInboundAdapter) loopUDPIn() {
 	}
 }
 
+func (a *myInboundAdapter) loopUDPOOBIn() {
+	defer close(a.packetOutboundClosed)
+	_buffer := buf.StackNewPacket()
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	buffer.IncRef()
+	defer buffer.DecRef()
+	packetService := (*myInboundPacketAdapter)(a)
+	oob := make([]byte, 1024)
+	for {
+		buffer.Reset()
+		n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
+		if err != nil {
+			return
+		}
+		buffer.Truncate(n)
+		var metadata adapter.InboundContext
+		metadata.Inbound = a.tag
+		metadata.SniffEnabled = a.listenOptions.SniffEnabled
+		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
+		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
+		metadata.Network = C.NetworkUDP
+		metadata.Source = M.SocksaddrFromNetIP(addr)
+		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
+		if err != nil {
+			a.newError(E.Cause(err, "process packet from ", metadata.Source))
+		}
+	}
+}
+
 func (a *myInboundAdapter) loopUDPInThreadSafe() {
 	defer close(a.packetOutboundClosed)
 	packetService := (*myInboundPacketAdapter)(a)
@@ -220,6 +260,33 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
 	}
 }
 
+func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
+	defer close(a.packetOutboundClosed)
+	packetService := (*myInboundPacketAdapter)(a)
+	oob := make([]byte, 1024)
+	for {
+		buffer := buf.NewPacket()
+		n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
+		if err != nil {
+			buffer.Release()
+			return
+		}
+		buffer.Truncate(n)
+		var metadata adapter.InboundContext
+		metadata.Inbound = a.tag
+		metadata.SniffEnabled = a.listenOptions.SniffEnabled
+		metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
+		metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
+		metadata.Network = C.NetworkUDP
+		metadata.Source = M.SocksaddrFromNetIP(addr)
+		err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata)
+		if err != nil {
+			buffer.Release()
+			a.newError(E.Cause(err, "process packet from ", metadata.Source))
+		}
+	}
+}
+
 func (a *myInboundAdapter) loopUDPOut() {
 	for {
 		select {

+ 10 - 2
inbound/direct.go

@@ -46,7 +46,13 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog
 		inbound.overrideOption = 3
 		inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort}
 	}
-	inbound.udpNat = udpnat.New[netip.AddrPort](options.UDPTimeout, inbound.upstreamContextHandler())
+	var udpTimeout int64
+	if options.UDPTimeout != 0 {
+		udpTimeout = options.UDPTimeout
+	} else {
+		udpTimeout = 300
+	}
+	inbound.udpNat = udpnat.New[netip.AddrPort](udpTimeout, inbound.upstreamContextHandler())
 	inbound.connHandler = inbound
 	inbound.packetHandler = inbound
 	inbound.packetUpstream = inbound.udpNat
@@ -79,6 +85,8 @@ func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.B
 	case 3:
 		metadata.Destination.Port = d.overrideDestination.Port
 	}
-	d.udpNat.NewPacketDirect(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), metadata.Source.AddrPort(), conn, buffer, adapter.UpstreamMetadata(metadata))
+	d.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) {
+		return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), natConn
+	})
 	return nil
 }

+ 43 - 0
inbound/dns.go

@@ -5,16 +5,59 @@ import (
 	"encoding/binary"
 	"io"
 	"net"
+	"net/netip"
 
 	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing/common"
 	"github.com/sagernet/sing/common/buf"
 	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/udpnat"
 
 	"golang.org/x/net/dns/dnsmessage"
 )
 
+type DNS struct {
+	myInboundAdapter
+	udpNat *udpnat.Service[netip.AddrPort]
+}
+
+func NewDNS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DNSInboundOptions) *DNS {
+	dns := &DNS{
+		myInboundAdapter: myInboundAdapter{
+			protocol:      C.TypeTProxy,
+			network:       options.Network.Build(),
+			ctx:           ctx,
+			router:        router,
+			logger:        logger,
+			tag:           tag,
+			listenOptions: options.ListenOptions,
+		},
+	}
+	dns.connHandler = dns
+	dns.packetHandler = dns
+	dns.udpNat = udpnat.New[netip.AddrPort](10, adapter.NewUpstreamContextHandler(nil, dns.newPacketConnection, dns))
+	dns.packetUpstream = dns.udpNat
+	return dns
+}
+
+func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return NewDNSConnection(ctx, d.router, d.logger, conn, metadata)
+}
+
+func (d *DNS) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error {
+	d.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) {
+		return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), natConn
+	})
+	return nil
+}
+
+func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return NewDNSPacketConnection(ctx, d.router, d.logger, conn, metadata)
+}
+
 func NewDNSConnection(ctx context.Context, router adapter.Router, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
 	ctx = adapter.WithContext(ctx, &metadata)
 	_buffer := buf.StackNewSize(1024)

+ 43 - 0
inbound/redirect.go

@@ -0,0 +1,43 @@
+package inbound
+
+import (
+	"context"
+	"net"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/redir"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+type Redirect struct {
+	myInboundAdapter
+}
+
+func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect {
+	redirect := &Redirect{
+		myInboundAdapter{
+			protocol:      C.TypeRedirect,
+			network:       []string{C.NetworkTCP},
+			ctx:           ctx,
+			router:        router,
+			logger:        logger,
+			tag:           tag,
+			listenOptions: options.ListenOptions,
+		},
+	}
+	redirect.connHandler = redirect
+	return redirect
+}
+
+func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	destination, err := redir.GetOriginalDestination(conn)
+	if err != nil {
+		return E.Cause(err, "get redirect destination")
+	}
+	metadata.Destination = M.SocksaddrFromNetIP(destination)
+	return r.newConnection(ctx, conn, metadata)
+}

+ 108 - 0
inbound/tproxy.go

@@ -0,0 +1,108 @@
+package inbound
+
+import (
+	"context"
+	"net"
+	"net/netip"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/redir"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/udpnat"
+)
+
+type TProxy struct {
+	myInboundAdapter
+	udpNat *udpnat.Service[netip.AddrPort]
+}
+
+func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) *TProxy {
+	tproxy := &TProxy{
+		myInboundAdapter: myInboundAdapter{
+			protocol:      C.TypeTProxy,
+			network:       options.Network.Build(),
+			ctx:           ctx,
+			router:        router,
+			logger:        logger,
+			tag:           tag,
+			listenOptions: options.ListenOptions,
+		},
+	}
+	var udpTimeout int64
+	if options.UDPTimeout != 0 {
+		udpTimeout = options.UDPTimeout
+	} else {
+		udpTimeout = 300
+	}
+	tproxy.connHandler = tproxy
+	tproxy.oobPacketHandler = tproxy
+	tproxy.udpNat = udpnat.New[netip.AddrPort](udpTimeout, tproxy.upstreamContextHandler())
+	tproxy.packetUpstream = tproxy.udpNat
+	return tproxy
+}
+
+func (t *TProxy) Start() error {
+	err := t.myInboundAdapter.Start()
+	if err != nil {
+		return err
+	}
+	if t.tcpListener != nil {
+		tcpFd, err := common.GetFileDescriptor(t.tcpListener)
+		if err != nil {
+			return err
+		}
+		err = redir.TProxy(tcpFd, M.SocksaddrFromNet(t.tcpListener.Addr()).Addr.Is6())
+		if err != nil {
+			return E.Cause(err, "configure tproxy TCP listener")
+		}
+	}
+	if t.udpConn != nil {
+		udpFd, err := common.GetFileDescriptor(t.udpConn)
+		if err != nil {
+			return err
+		}
+		err = redir.TProxyUDP(udpFd, M.SocksaddrFromNet(t.udpConn.LocalAddr()).Addr.Is6())
+		if err != nil {
+			return E.Cause(err, "configure tproxy UDP listener")
+		}
+	}
+	return nil
+}
+
+func (t *TProxy) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr())
+	return t.newConnection(ctx, conn, metadata)
+}
+
+func (t *TProxy) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata adapter.InboundContext) error {
+	destination, err := redir.GetOriginalDestinationFromOOB(oob)
+	if err != nil {
+		return E.Cause(err, "get tproxy destination")
+	}
+	metadata.Destination = M.SocksaddrFromNetIP(destination)
+	t.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) {
+		return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), &tproxyPacketWriter{natConn}
+	})
+	return nil
+}
+
+type tproxyPacketWriter struct {
+	source N.PacketConn
+}
+
+func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	defer buffer.Release()
+	udpConn, err := redir.DialUDP(destination.UDPAddr(), M.SocksaddrFromNet(w.source.LocalAddr()).UDPAddr())
+	if err != nil {
+		return E.Cause(err, "tproxy udp write back")
+	}
+	defer udpConn.Close()
+	return common.Error(udpConn.Write(buffer.Bytes()))
+}

+ 4 - 1
mkdocs.yml

@@ -40,12 +40,15 @@ nav:
           - DNS Rule: configuration/dns/rule.md
       - Inbound:
           - configuration/inbound/index.md
-          - Tun: configuration/inbound/tun.md
           - Direct: configuration/inbound/direct.md
           - Mixed: configuration/inbound/mixed.md
           - Socks: configuration/inbound/socks.md
           - HTTP: configuration/inbound/http.md
           - Shadowsocks: configuration/inbound/shadowsocks.md
+          - Tun: configuration/inbound/tun.md
+          - Redirect: configuration/inbound/redirect.md
+          - TProxy: configuration/inbound/tproxy.md
+          - DNS: configuration/inbound/dns.md
       - Outbound:
           - configuration/outbound/index.md
           - Direct: configuration/outbound/direct.md

+ 33 - 1
option/inbound.go

@@ -18,6 +18,9 @@ type _Inbound struct {
 	MixedOptions       HTTPMixedInboundOptions   `json:"-"`
 	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
 	TunOptions         TunInboundOptions         `json:"-"`
+	RedirectOptions    RedirectInboundOptions    `json:"-"`
+	TProxyOptions      TProxyInboundOptions      `json:"-"`
+	DNSOptions         DNSInboundOptions         `json:"-"`
 }
 
 type Inbound _Inbound
@@ -30,7 +33,10 @@ func (h Inbound) Equals(other Inbound) bool {
 		h.HTTPOptions.Equals(other.HTTPOptions) &&
 		h.MixedOptions.Equals(other.MixedOptions) &&
 		h.ShadowsocksOptions.Equals(other.ShadowsocksOptions) &&
-		h.TunOptions == other.TunOptions
+		h.TunOptions == other.TunOptions &&
+		h.RedirectOptions == other.RedirectOptions &&
+		h.TProxyOptions == other.TProxyOptions &&
+		h.DNSOptions == other.DNSOptions
 }
 
 func (h Inbound) MarshalJSON() ([]byte, error) {
@@ -48,6 +54,12 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 		v = h.ShadowsocksOptions
 	case C.TypeTun:
 		v = h.TunOptions
+	case C.TypeRedirect:
+		v = h.RedirectOptions
+	case C.TypeTProxy:
+		v = h.TProxyOptions
+	case C.TypeDNS:
+		v = h.DNSOptions
 	default:
 		return nil, E.New("unknown inbound type: ", h.Type)
 	}
@@ -73,6 +85,12 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.ShadowsocksOptions
 	case C.TypeTun:
 		v = &h.TunOptions
+	case C.TypeRedirect:
+		v = &h.RedirectOptions
+	case C.TypeTProxy:
+		v = &h.TProxyOptions
+	case C.TypeDNS:
+		v = &h.DNSOptions
 	default:
 		return nil
 	}
@@ -164,3 +182,17 @@ type TunInboundOptions struct {
 	HijackDNS     bool          `json:"hijack_dns,omitempty"`
 	InboundOptions
 }
+
+type RedirectInboundOptions struct {
+	ListenOptions
+}
+
+type TProxyInboundOptions struct {
+	ListenOptions
+	Network NetworkList `json:"network,omitempty"`
+}
+
+type DNSInboundOptions struct {
+	ListenOptions
+	Network NetworkList `json:"network,omitempty"`
+}

+ 1 - 1
test/clash_test.go

@@ -13,13 +13,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/sagernet/sing-box/log"
 	F "github.com/sagernet/sing/common/format"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/sagernet/sing-box/log"
 )
 
 // kanged from clash