mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 08:31:30 +00:00
Add redir tproxy and dns inbound
This commit is contained in:
parent
e13b72afca
commit
2c2eb31e18
|
@ -17,6 +17,10 @@ type PacketHandler interface {
|
||||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
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 {
|
type PacketConnectionHandler interface {
|
||||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
}
|
}
|
||||||
|
|
37
common/redir/redir_linux.go
Normal file
37
common/redir/redir_linux.go
Normal file
|
@ -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
common/redir/redir_other.go
Normal file
13
common/redir/redir_other.go
Normal file
|
@ -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
common/redir/tproxy_linux.go
Normal file
132
common/redir/tproxy_linux.go
Normal file
|
@ -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
common/redir/tproxy_other.go
Normal file
25
common/redir/tproxy_other.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -8,4 +8,7 @@ const (
|
||||||
TypeMixed = "mixed"
|
TypeMixed = "mixed"
|
||||||
TypeShadowsocks = "shadowsocks"
|
TypeShadowsocks = "shadowsocks"
|
||||||
TypeTun = "tun"
|
TypeTun = "tun"
|
||||||
|
TypeRedirect = "redirect"
|
||||||
|
TypeTProxy = "tproxy"
|
||||||
|
TypeDNS = "dns"
|
||||||
)
|
)
|
||||||
|
|
44
docs/configuration/inbound/dns.md
Normal file
44
docs/configuration/inbound/dns.md
Normal file
|
@ -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.
|
|
@ -15,12 +15,15 @@
|
||||||
|
|
||||||
| Type | Format |
|
| Type | Format |
|
||||||
|---------------|------------------------------|
|
|---------------|------------------------------|
|
||||||
| `tun` | [Tun](./tun) |
|
|
||||||
| `direct` | [Direct](./direct) |
|
| `direct` | [Direct](./direct) |
|
||||||
| `mixed` | [Mixed](./mixed) |
|
| `mixed` | [Mixed](./mixed) |
|
||||||
| `socks` | [Socks](./socks) |
|
| `socks` | [Socks](./socks) |
|
||||||
| `http` | [HTTP](./http) |
|
| `http` | [HTTP](./http) |
|
||||||
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
|
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
|
||||||
|
| `tun` | [Tun](./tun) |
|
||||||
|
| `redirect` | [Redirect](./redirect) |
|
||||||
|
| `tproxy` | [TProxy](./tproxy) |
|
||||||
|
| `dns` | [DNS](./dns) |
|
||||||
|
|
||||||
#### tag
|
#### tag
|
||||||
|
|
||||||
|
|
61
docs/configuration/inbound/redirect.md
Normal file
61
docs/configuration/inbound/redirect.md
Normal file
|
@ -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
docs/configuration/inbound/tproxy.md
Normal file
71
docs/configuration/inbound/tproxy.md
Normal file
|
@ -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.
|
|
@ -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)
|
return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
|
||||||
case C.TypeTun:
|
case C.TypeTun:
|
||||||
return NewTun(ctx, router, logger, options.Tag, options.TunOptions)
|
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:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ type myInboundAdapter struct {
|
||||||
listenOptions option.ListenOptions
|
listenOptions option.ListenOptions
|
||||||
connHandler adapter.ConnectionHandler
|
connHandler adapter.ConnectionHandler
|
||||||
packetHandler adapter.PacketHandler
|
packetHandler adapter.PacketHandler
|
||||||
|
oobPacketHandler adapter.OOBPacketHandler
|
||||||
packetUpstream any
|
packetUpstream any
|
||||||
|
|
||||||
// http mixed
|
// http mixed
|
||||||
|
@ -85,12 +86,20 @@ func (a *myInboundAdapter) Start() error {
|
||||||
a.packetForce6 = M.SocksaddrFromNet(udpConn.LocalAddr()).Addr.Is6()
|
a.packetForce6 = M.SocksaddrFromNet(udpConn.LocalAddr()).Addr.Is6()
|
||||||
a.packetOutboundClosed = make(chan struct{})
|
a.packetOutboundClosed = make(chan struct{})
|
||||||
a.packetOutbound = make(chan *myInboundPacket)
|
a.packetOutbound = make(chan *myInboundPacket)
|
||||||
|
if a.oobPacketHandler != nil {
|
||||||
|
if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
|
||||||
|
go a.loopUDPOOBIn()
|
||||||
|
} else {
|
||||||
|
go a.loopUDPOOBInThreadSafe()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
|
if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler {
|
||||||
go a.loopUDPIn()
|
go a.loopUDPIn()
|
||||||
} else {
|
} else {
|
||||||
go a.loopUDPInThreadSafe()
|
go a.loopUDPInThreadSafe()
|
||||||
}
|
}
|
||||||
go a.loopUDPOut()
|
go a.loopUDPOut()
|
||||||
|
}
|
||||||
a.logger.Info("udp server started at ", udpConn.LocalAddr())
|
a.logger.Info("udp server started at ", udpConn.LocalAddr())
|
||||||
}
|
}
|
||||||
if a.setSystemProxy {
|
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() {
|
func (a *myInboundAdapter) loopUDPInThreadSafe() {
|
||||||
defer close(a.packetOutboundClosed)
|
defer close(a.packetOutboundClosed)
|
||||||
packetService := (*myInboundPacketAdapter)(a)
|
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() {
|
func (a *myInboundAdapter) loopUDPOut() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -46,7 +46,13 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog
|
||||||
inbound.overrideOption = 3
|
inbound.overrideOption = 3
|
||||||
inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort}
|
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.connHandler = inbound
|
||||||
inbound.packetHandler = inbound
|
inbound.packetHandler = inbound
|
||||||
inbound.packetUpstream = inbound.udpNat
|
inbound.packetUpstream = inbound.udpNat
|
||||||
|
@ -79,6 +85,8 @@ func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.B
|
||||||
case 3:
|
case 3:
|
||||||
metadata.Destination.Port = d.overrideDestination.Port
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,59 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/udpnat"
|
||||||
|
|
||||||
"golang.org/x/net/dns/dnsmessage"
|
"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 {
|
func NewDNSConnection(ctx context.Context, router adapter.Router, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
ctx = adapter.WithContext(ctx, &metadata)
|
ctx = adapter.WithContext(ctx, &metadata)
|
||||||
_buffer := buf.StackNewSize(1024)
|
_buffer := buf.StackNewSize(1024)
|
||||||
|
|
43
inbound/redirect.go
Normal file
43
inbound/redirect.go
Normal file
|
@ -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
inbound/tproxy.go
Normal file
108
inbound/tproxy.go
Normal file
|
@ -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()))
|
||||||
|
}
|
|
@ -40,12 +40,15 @@ nav:
|
||||||
- DNS Rule: configuration/dns/rule.md
|
- DNS Rule: configuration/dns/rule.md
|
||||||
- Inbound:
|
- Inbound:
|
||||||
- configuration/inbound/index.md
|
- configuration/inbound/index.md
|
||||||
- Tun: configuration/inbound/tun.md
|
|
||||||
- Direct: configuration/inbound/direct.md
|
- Direct: configuration/inbound/direct.md
|
||||||
- Mixed: configuration/inbound/mixed.md
|
- Mixed: configuration/inbound/mixed.md
|
||||||
- Socks: configuration/inbound/socks.md
|
- Socks: configuration/inbound/socks.md
|
||||||
- HTTP: configuration/inbound/http.md
|
- HTTP: configuration/inbound/http.md
|
||||||
- Shadowsocks: configuration/inbound/shadowsocks.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:
|
- Outbound:
|
||||||
- configuration/outbound/index.md
|
- configuration/outbound/index.md
|
||||||
- Direct: configuration/outbound/direct.md
|
- Direct: configuration/outbound/direct.md
|
||||||
|
|
|
@ -18,6 +18,9 @@ type _Inbound struct {
|
||||||
MixedOptions HTTPMixedInboundOptions `json:"-"`
|
MixedOptions HTTPMixedInboundOptions `json:"-"`
|
||||||
ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
|
ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
|
||||||
TunOptions TunInboundOptions `json:"-"`
|
TunOptions TunInboundOptions `json:"-"`
|
||||||
|
RedirectOptions RedirectInboundOptions `json:"-"`
|
||||||
|
TProxyOptions TProxyInboundOptions `json:"-"`
|
||||||
|
DNSOptions DNSInboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Inbound _Inbound
|
type Inbound _Inbound
|
||||||
|
@ -30,7 +33,10 @@ func (h Inbound) Equals(other Inbound) bool {
|
||||||
h.HTTPOptions.Equals(other.HTTPOptions) &&
|
h.HTTPOptions.Equals(other.HTTPOptions) &&
|
||||||
h.MixedOptions.Equals(other.MixedOptions) &&
|
h.MixedOptions.Equals(other.MixedOptions) &&
|
||||||
h.ShadowsocksOptions.Equals(other.ShadowsocksOptions) &&
|
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) {
|
func (h Inbound) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -48,6 +54,12 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
|
||||||
v = h.ShadowsocksOptions
|
v = h.ShadowsocksOptions
|
||||||
case C.TypeTun:
|
case C.TypeTun:
|
||||||
v = h.TunOptions
|
v = h.TunOptions
|
||||||
|
case C.TypeRedirect:
|
||||||
|
v = h.RedirectOptions
|
||||||
|
case C.TypeTProxy:
|
||||||
|
v = h.TProxyOptions
|
||||||
|
case C.TypeDNS:
|
||||||
|
v = h.DNSOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", h.Type)
|
return nil, E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
@ -73,6 +85,12 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
||||||
v = &h.ShadowsocksOptions
|
v = &h.ShadowsocksOptions
|
||||||
case C.TypeTun:
|
case C.TypeTun:
|
||||||
v = &h.TunOptions
|
v = &h.TunOptions
|
||||||
|
case C.TypeRedirect:
|
||||||
|
v = &h.RedirectOptions
|
||||||
|
case C.TypeTProxy:
|
||||||
|
v = &h.TProxyOptions
|
||||||
|
case C.TypeDNS:
|
||||||
|
v = &h.DNSOptions
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -164,3 +182,17 @@ type TunInboundOptions struct {
|
||||||
HijackDNS bool `json:"hijack_dns,omitempty"`
|
HijackDNS bool `json:"hijack_dns,omitempty"`
|
||||||
InboundOptions
|
InboundOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedirectInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type TProxyInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -13,13 +13,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// kanged from clash
|
// kanged from clash
|
||||||
|
|
Loading…
Reference in a new issue