From 2c2eb31e189d1d2b9b68547ccb95439a0e04adee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Jul 2022 08:42:02 +0800 Subject: [PATCH] Add redir tproxy and dns inbound --- adapter/handler.go | 4 + common/redir/redir_linux.go | 37 +++++++ common/redir/redir_other.go | 13 +++ common/redir/tproxy_linux.go | 132 +++++++++++++++++++++++++ common/redir/tproxy_other.go | 25 +++++ constant/proxy.go | 3 + docs/configuration/inbound/dns.md | 44 +++++++++ docs/configuration/inbound/index.md | 5 +- docs/configuration/inbound/redirect.md | 61 ++++++++++++ docs/configuration/inbound/tproxy.md | 71 +++++++++++++ inbound/builder.go | 6 ++ inbound/default.go | 95 +++++++++++++++--- inbound/direct.go | 12 ++- inbound/dns.go | 43 ++++++++ inbound/redirect.go | 43 ++++++++ inbound/tproxy.go | 108 ++++++++++++++++++++ mkdocs.yml | 5 +- option/inbound.go | 34 ++++++- test/clash_test.go | 2 +- 19 files changed, 723 insertions(+), 20 deletions(-) create mode 100644 common/redir/redir_linux.go create mode 100644 common/redir/redir_other.go create mode 100644 common/redir/tproxy_linux.go create mode 100644 common/redir/tproxy_other.go create mode 100644 docs/configuration/inbound/dns.md create mode 100644 docs/configuration/inbound/redirect.md create mode 100644 docs/configuration/inbound/tproxy.md create mode 100644 inbound/redirect.go create mode 100644 inbound/tproxy.go diff --git a/adapter/handler.go b/adapter/handler.go index 5ec23579..bc5bcfbb 100644 --- a/adapter/handler.go +++ b/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 } diff --git a/common/redir/redir_linux.go b/common/redir/redir_linux.go new file mode 100644 index 00000000..abb1b1a7 --- /dev/null +++ b/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 + } +} diff --git a/common/redir/redir_other.go b/common/redir/redir_other.go new file mode 100644 index 00000000..e33e2de9 --- /dev/null +++ b/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 +} diff --git a/common/redir/tproxy_linux.go b/common/redir/tproxy_linux.go new file mode 100644 index 00000000..795e9ffe --- /dev/null +++ b/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 +} diff --git a/common/redir/tproxy_other.go b/common/redir/tproxy_other.go new file mode 100644 index 00000000..9fa7f26f --- /dev/null +++ b/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 +} diff --git a/constant/proxy.go b/constant/proxy.go index ec7dddd1..b9649126 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -8,4 +8,7 @@ const ( TypeMixed = "mixed" TypeShadowsocks = "shadowsocks" TypeTun = "tun" + TypeRedirect = "redirect" + TypeTProxy = "tproxy" + TypeDNS = "dns" ) diff --git a/docs/configuration/inbound/dns.md b/docs/configuration/inbound/dns.md new file mode 100644 index 00000000..49b47661 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md index 8351797b..a7868396 100644 --- a/docs/configuration/inbound/index.md +++ b/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 diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md new file mode 100644 index 00000000..6f7d78a6 --- /dev/null +++ b/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). \ No newline at end of file diff --git a/docs/configuration/inbound/tproxy.md b/docs/configuration/inbound/tproxy.md new file mode 100644 index 00000000..11eba34a --- /dev/null +++ b/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. \ No newline at end of file diff --git a/inbound/builder.go b/inbound/builder.go index 571a4eb6..3074eb07 100644 --- a/inbound/builder.go +++ b/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) } diff --git a/inbound/default.go b/inbound/default.go index 597537fd..6ea59f43 100644 --- a/inbound/default.go +++ b/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 { diff --git a/inbound/direct.go b/inbound/direct.go index 9307a1d3..e2bab89d 100644 --- a/inbound/direct.go +++ b/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 } diff --git a/inbound/dns.go b/inbound/dns.go index facc8ae1..39f9fe27 100644 --- a/inbound/dns.go +++ b/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) diff --git a/inbound/redirect.go b/inbound/redirect.go new file mode 100644 index 00000000..59a1084b --- /dev/null +++ b/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) +} diff --git a/inbound/tproxy.go b/inbound/tproxy.go new file mode 100644 index 00000000..016247ce --- /dev/null +++ b/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())) +} diff --git a/mkdocs.yml b/mkdocs.yml index 0433455c..9c9e6712 100644 --- a/mkdocs.yml +++ b/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 diff --git a/option/inbound.go b/option/inbound.go index f1c3f713..4001c10f 100644 --- a/option/inbound.go +++ b/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"` +} diff --git a/test/clash_test.go b/test/clash_test.go index f5d31d44..e2a81aff 100644 --- a/test/clash_test.go +++ b/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