diff --git a/config/config.go b/config/config.go index 572031d3..f04f13de 100644 --- a/config/config.go +++ b/config/config.go @@ -60,15 +60,19 @@ type General struct { // Inbound config type Inbound struct { - Port int `json:"port"` - SocksPort int `json:"socks-port"` - RedirPort int `json:"redir-port"` - TProxyPort int `json:"tproxy-port"` - MixedPort int `json:"mixed-port"` - Authentication []string `json:"authentication"` - AllowLan bool `json:"allow-lan"` - BindAddress string `json:"bind-address"` - InboundTfo bool `json:"inbound-tfo"` + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + ShadowSocksConfig string `json:"ss-config"` + VmessConfig string `json:"vmess-config"` + TcpTunConfig string `json:"tcptun-config"` + UdpTunConfig string `json:"udptun-config"` + Authentication []string `json:"authentication"` + AllowLan bool `json:"allow-lan"` + BindAddress string `json:"bind-address"` + InboundTfo bool `json:"inbound-tfo"` } // Controller config @@ -284,6 +288,10 @@ type RawConfig struct { RedirPort int `yaml:"redir-port"` TProxyPort int `yaml:"tproxy-port"` MixedPort int `yaml:"mixed-port"` + ShadowSocksConfig string `yaml:"ss-config"` + VmessConfig string `yaml:"vmess-config"` + TcpTunConfig string `yaml:"tcptun-config"` + UdpTunConfig string `yaml:"udptun-config"` InboundTfo bool `yaml:"inbound-tfo"` Authentication []string `yaml:"authentication"` AllowLan bool `yaml:"allow-lan"` @@ -526,14 +534,18 @@ func parseGeneral(cfg *RawConfig) (*General, error) { cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun return &General{ Inbound: Inbound{ - Port: cfg.Port, - SocksPort: cfg.SocksPort, - RedirPort: cfg.RedirPort, - TProxyPort: cfg.TProxyPort, - MixedPort: cfg.MixedPort, - AllowLan: cfg.AllowLan, - BindAddress: cfg.BindAddress, - InboundTfo: cfg.InboundTfo, + Port: cfg.Port, + SocksPort: cfg.SocksPort, + RedirPort: cfg.RedirPort, + TProxyPort: cfg.TProxyPort, + MixedPort: cfg.MixedPort, + ShadowSocksConfig: cfg.ShadowSocksConfig, + VmessConfig: cfg.VmessConfig, + TcpTunConfig: cfg.TcpTunConfig, + UdpTunConfig: cfg.UdpTunConfig, + AllowLan: cfg.AllowLan, + BindAddress: cfg.BindAddress, + InboundTfo: cfg.InboundTfo, }, Controller: Controller{ ExternalController: cfg.ExternalController, diff --git a/constant/metadata.go b/constant/metadata.go index 2d52f2bf..61a98be3 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -20,8 +20,12 @@ const ( HTTPS SOCKS4 SOCKS5 + SHADOWSOCKS + VMESS REDIR TPROXY + TCPTUN + UDPTUN TUN INNER ) @@ -53,10 +57,18 @@ func (t Type) String() string { return "Socks4" case SOCKS5: return "Socks5" + case SHADOWSOCKS: + return "ShadowSocks" + case VMESS: + return "Vmess" case REDIR: return "Redir" case TPROXY: return "TProxy" + case TCPTUN: + return "TcpTun" + case UDPTUN: + return "UdpTun" case TUN: return "Tun" case INNER: diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 760cb52f..fad07136 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -105,14 +105,18 @@ func GetGeneral() *config.General { general := &config.General{ Inbound: config.Inbound{ - Port: ports.Port, - SocksPort: ports.SocksPort, - RedirPort: ports.RedirPort, - TProxyPort: ports.TProxyPort, - MixedPort: ports.MixedPort, - Authentication: authenticator, - AllowLan: P.AllowLan(), - BindAddress: P.BindAddress(), + Port: ports.Port, + SocksPort: ports.SocksPort, + RedirPort: ports.RedirPort, + TProxyPort: ports.TProxyPort, + MixedPort: ports.MixedPort, + ShadowSocksConfig: ports.ShadowSocksConfig, + VmessConfig: ports.VmessConfig, + TcpTunConfig: ports.TcpTunConfig, + UdpTunConfig: ports.UdpTunConfig, + Authentication: authenticator, + AllowLan: P.AllowLan(), + BindAddress: P.BindAddress(), }, Mode: tunnel.Mode(), LogLevel: log.Level(), @@ -342,6 +346,10 @@ func updateGeneral(general *config.General, force bool) { P.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + P.ReCreateShadowSocks(general.ShadowSocksConfig, tcpIn, udpIn) + P.ReCreateVmess(general.VmessConfig, tcpIn, udpIn) + P.ReCreateTcpTun(general.TcpTunConfig, tcpIn, udpIn) + P.ReCreateUdpTun(general.UdpTunConfig, tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { diff --git a/hub/route/configs.go b/hub/route/configs.go index 78c631ea..1e08dbda 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -35,20 +35,24 @@ func configRouter() http.Handler { } type configSchema struct { - Port *int `json:"port"` - SocksPort *int `json:"socks-port"` - RedirPort *int `json:"redir-port"` - TProxyPort *int `json:"tproxy-port"` - MixedPort *int `json:"mixed-port"` - Tun *tunSchema `json:"tun"` - AllowLan *bool `json:"allow-lan"` - BindAddress *string `json:"bind-address"` - Mode *tunnel.TunnelMode `json:"mode"` - LogLevel *log.LogLevel `json:"log-level"` - IPv6 *bool `json:"ipv6"` - Sniffing *bool `json:"sniffing"` - TcpConcurrent *bool `json:"tcp-concurrent"` - InterfaceName *string `json:"interface-name"` + Port *int `json:"port"` + SocksPort *int `json:"socks-port"` + RedirPort *int `json:"redir-port"` + TProxyPort *int `json:"tproxy-port"` + MixedPort *int `json:"mixed-port"` + Tun *tunSchema `json:"tun"` + ShadowSocksConfig *string `json:"ss-config"` + VmessConfig *string `json:"vmess-config"` + TcptunConfig *string `json:"tcptun-config"` + UdptunConfig *string `json:"udptun-config"` + AllowLan *bool `json:"allow-lan"` + BindAddress *string `json:"bind-address"` + Mode *tunnel.TunnelMode `json:"mode"` + LogLevel *log.LogLevel `json:"log-level"` + IPv6 *bool `json:"ipv6"` + Sniffing *bool `json:"sniffing"` + TcpConcurrent *bool `json:"tcp-concurrent"` + InterfaceName *string `json:"interface-name"` } type tunSchema struct { @@ -90,6 +94,14 @@ func pointerOrDefault(p *int, def int) int { return def } +func pointerOrDefaultString(p *string, def string) string { + if p != nil { + return *p + } + + return def +} + func pointerOrDefaultTun(p *tunSchema, def config.Tun) config.Tun { if p != nil { def.Enable = p.Enable @@ -187,6 +199,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn) P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) P.ReCreateTun(pointerOrDefaultTun(general.Tun, P.LastTunConf), tcpIn, udpIn) + P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn) + P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn) + P.ReCreateTcpTun(pointerOrDefaultString(general.TcptunConfig, ports.TcpTunConfig), tcpIn, udpIn) + P.ReCreateUdpTun(pointerOrDefaultString(general.UdptunConfig, ports.UdpTunConfig), tcpIn, udpIn) if general.Mode != nil { tunnel.SetMode(*general.Mode) diff --git a/listener/listener.go b/listener/listener.go index e608cb1e..c9e95226 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -2,7 +2,6 @@ package proxy import ( "fmt" - "github.com/Dreamacro/clash/listener/sing_tun" "golang.org/x/exp/slices" "net" "sort" @@ -18,8 +17,12 @@ import ( "github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/listener/mixed" "github.com/Dreamacro/clash/listener/redir" + "github.com/Dreamacro/clash/listener/sing_shadowsocks" + "github.com/Dreamacro/clash/listener/sing_tun" + "github.com/Dreamacro/clash/listener/sing_vmess" "github.com/Dreamacro/clash/listener/socks" "github.com/Dreamacro/clash/listener/tproxy" + "github.com/Dreamacro/clash/listener/tunnel" "github.com/Dreamacro/clash/log" ) @@ -28,19 +31,23 @@ var ( bindAddress = "*" inboundTfo = false - socksListener *socks.Listener - socksUDPListener *socks.UDPListener - httpListener *http.Listener - redirListener *redir.Listener - redirUDPListener *tproxy.UDPListener - tproxyListener *tproxy.Listener - tproxyUDPListener *tproxy.UDPListener - mixedListener *mixed.Listener - mixedUDPLister *socks.UDPListener - tunLister *sing_tun.Listener - autoRedirListener *autoredir.Listener - autoRedirProgram *ebpf.TcEBpfProgram - tcProgram *ebpf.TcEBpfProgram + socksListener *socks.Listener + socksUDPListener *socks.UDPListener + httpListener *http.Listener + redirListener *redir.Listener + redirUDPListener *tproxy.UDPListener + tproxyListener *tproxy.Listener + tproxyUDPListener *tproxy.UDPListener + mixedListener *mixed.Listener + mixedUDPLister *socks.UDPListener + tunLister *sing_tun.Listener + shadowSocksListener *sing_shadowsocks.Listener + vmessListener *sing_vmess.Listener + tcpTunListener *tunnel.Listener + udpTunListener *tunnel.UdpListener + autoRedirListener *autoredir.Listener + autoRedirProgram *ebpf.TcEBpfProgram + tcProgram *ebpf.TcEBpfProgram // lock for recreate function socksMux sync.Mutex @@ -49,6 +56,10 @@ var ( tproxyMux sync.Mutex mixedMux sync.Mutex tunMux sync.Mutex + ssMux sync.Mutex + vmessMux sync.Mutex + tcpTunMux sync.Mutex + udpTunMux sync.Mutex autoRedirMux sync.Mutex tcMux sync.Mutex @@ -56,11 +67,15 @@ var ( ) type Ports struct { - Port int `json:"port"` - SocksPort int `json:"socks-port"` - RedirPort int `json:"redir-port"` - TProxyPort int `json:"tproxy-port"` - MixedPort int `json:"mixed-port"` + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + TProxyPort int `json:"tproxy-port"` + MixedPort int `json:"mixed-port"` + ShadowSocksConfig string `json:"ss-config"` + VmessConfig string `json:"vmess-config"` + TcpTunConfig string `json:"tcptun-config"` + UdpTunConfig string `json:"udptun-config"` } func GetTunConf() config.Tun { @@ -235,6 +250,156 @@ func ReCreateRedir(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("Redirect proxy listening at: %s", redirListener.Address()) } +func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + ssMux.Lock() + defer ssMux.Unlock() + + var err error + defer func() { + if err != nil { + log.Errorln("Start ShadowSocks server error: %s", err.Error()) + } + }() + + shouldIgnore := false + + if shadowSocksListener != nil { + if shadowSocksListener.Config() != shadowSocksConfig { + shadowSocksListener.Close() + shadowSocksListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + if len(shadowSocksConfig) == 0 { + return + } + + listener, err := sing_shadowsocks.New(shadowSocksConfig, tcpIn, udpIn) + if err != nil { + return + } + + shadowSocksListener = listener + + return +} + +func ReCreateVmess(vmessConfig string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + vmessMux.Lock() + defer vmessMux.Unlock() + + var err error + defer func() { + if err != nil { + log.Errorln("Start Vmess server error: %s", err.Error()) + } + }() + + shouldIgnore := false + + if vmessListener != nil { + if vmessListener.Config() != vmessConfig { + vmessListener.Close() + vmessListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + if len(vmessConfig) == 0 { + return + } + + listener, err := sing_vmess.New(vmessConfig, tcpIn, udpIn) + if err != nil { + return + } + + vmessListener = listener + + return +} + +func ReCreateTcpTun(config string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + tcpTunMux.Lock() + defer tcpTunMux.Unlock() + shouldIgnore := false + + var err error + defer func() { + if err != nil { + log.Errorln("Start TcpTun server error: %s", err.Error()) + } + }() + + if tcpTunListener != nil { + if tcpTunListener.Config() != config { + tcpTunListener.Close() + tcpTunListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + tcpListener, err := tunnel.New(config, tcpIn) + if err != nil { + return + } + + tcpTunListener = tcpListener + + return +} + +func ReCreateUdpTun(config string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + udpTunMux.Lock() + defer udpTunMux.Unlock() + shouldIgnore := false + + var err error + defer func() { + if err != nil { + log.Errorln("Start UdpTun server error: %s", err.Error()) + } + }() + + if udpTunListener != nil { + if udpTunListener.Config() != config { + udpTunListener.Close() + udpTunListener = nil + } else { + shouldIgnore = true + } + } + + if shouldIgnore { + return + } + + udpListener, err := tunnel.NewUdp(config, udpIn) + if err != nil { + return + } + + udpTunListener = udpListener + + return +} + func ReCreateTProxy(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { tproxyMux.Lock() defer tproxyMux.Unlock() @@ -490,6 +655,22 @@ func GetPorts() *Ports { ports.MixedPort = port } + if shadowSocksListener != nil { + ports.ShadowSocksConfig = shadowSocksListener.Config() + } + + if vmessListener != nil { + ports.VmessConfig = vmessListener.Config() + } + + if tcpTunListener != nil { + ports.TcpTunConfig = tcpTunListener.Config() + } + + if udpTunListener != nil { + ports.UdpTunConfig = udpTunListener.Config() + } + return ports } diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go new file mode 100644 index 00000000..cc2c840e --- /dev/null +++ b/listener/sing_shadowsocks/server.go @@ -0,0 +1,173 @@ +package sing_shadowsocks + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/sing" + "github.com/Dreamacro/clash/log" + + shadowsocks "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/metadata" +) + +type Listener struct { + closed bool + config string + listeners []net.Listener + udpListeners []net.PacketConn + service shadowsocks.Service +} + +var _listener *Listener + +func New(config string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (*Listener, error) { + addr, cipher, password, err := parseSSURL(config) + if err != nil { + return nil, err + } + udpTimeout := int64(sing.UDPTimeout.Seconds()) + + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.SHADOWSOCKS, + } + + sl := &Listener{false, config, nil, nil, nil} + + switch { + case cipher == shadowsocks.MethodNone: + sl.service = shadowsocks.NewNoneService(udpTimeout, h) + case common.Contains(shadowaead.List, cipher): + sl.service, err = shadowaead.NewService(cipher, nil, password, udpTimeout, h) + case common.Contains(shadowaead_2022.List, cipher): + sl.service, err = shadowaead_2022.NewServiceWithPassword(cipher, password, udpTimeout, h) + default: + err = fmt.Errorf("shadowsocks: unsupported method: %s", cipher) + } + if err != nil { + return nil, err + } + + _listener = sl + + for _, addr := range strings.Split(addr, ",") { + addr := addr + + //UDP + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl.udpListeners = append(sl.udpListeners, ul) + + go func() { + conn := bufio.NewPacketConn(ul) + for { + buff := buf.NewPacket() + remoteAddr, err := conn.ReadPacket(buff) + if err != nil { + buff.Release() + if sl.closed { + break + } + continue + } + _ = sl.service.NewPacket(context.TODO(), conn, buff, metadata.Metadata{ + Protocol: "shadowsocks", + Source: remoteAddr, + }) + } + }() + + //TCP + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + sl.listeners = append(sl.listeners, l) + + go func() { + log.Infoln("ShadowSocks proxy listening at: %s", l.Addr().String()) + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + + go sl.HandleConn(c) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() { + l.closed = true + for _, lis := range l.listeners { + _ = lis.Close() + } + for _, lis := range l.udpListeners { + _ = lis.Close() + } +} + +func (l *Listener) Config() string { + return l.config +} + +func (l *Listener) HandleConn(conn net.Conn) { + err := l.service.NewConnection(context.TODO(), conn, metadata.Metadata{ + Protocol: "shadowsocks", + Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + }) + if err != nil { + _ = conn.Close() + return + } +} + +func HandleShadowSocks(conn net.Conn) bool { + if _listener != nil && _listener.service != nil { + go _listener.HandleConn(conn) + return true + } + return false +} + +func parseSSURL(s string) (addr, cipher, password string, err error) { + u, err := url.Parse(s) + if err != nil { + return + } + + addr = u.Host + if u.User != nil { + cipher = u.User.Username() + password, _ = u.User.Password() + } + return +} diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go new file mode 100644 index 00000000..c8d0a2a1 --- /dev/null +++ b/listener/sing_vmess/server.go @@ -0,0 +1,126 @@ +package sing_vmess + +import ( + "context" + "net" + "net/url" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/sing" + "github.com/Dreamacro/clash/log" + + vmess "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common/metadata" +) + +type Listener struct { + closed bool + config string + listeners []net.Listener + service *vmess.Service[string] +} + +var _listener *Listener + +func New(config string, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (*Listener, error) { + addr, username, password, err := parseVmessURL(config) + if err != nil { + return nil, err + } + + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.VMESS, + } + + service := vmess.NewService[string](h) + err = service.UpdateUsers([]string{username}, []string{password}, []int{1}) + if err != nil { + return nil, err + } + + err = service.Start() + if err != nil { + return nil, err + } + + sl := &Listener{false, config, nil, service} + _listener = sl + + for _, addr := range strings.Split(addr, ",") { + addr := addr + + //TCP + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + sl.listeners = append(sl.listeners, l) + + go func() { + log.Infoln("Vmess proxy listening at: %s", l.Addr().String()) + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + + go sl.HandleConn(c) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() { + l.closed = true + for _, lis := range l.listeners { + _ = lis.Close() + } + _ = l.service.Close() +} + +func (l *Listener) Config() string { + return l.config +} + +func (l *Listener) HandleConn(conn net.Conn) { + err := l.service.NewConnection(context.TODO(), conn, metadata.Metadata{ + Protocol: "vmess", + Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + }) + if err != nil { + _ = conn.Close() + return + } +} + +func HandleVmess(conn net.Conn) bool { + if _listener != nil && _listener.service != nil { + go _listener.HandleConn(conn) + return true + } + return false +} + +func parseVmessURL(s string) (addr, username, password string, err error) { + u, err := url.Parse(s) + if err != nil { + return + } + + addr = u.Host + if u.User != nil { + username = u.User.Username() + password, _ = u.User.Password() + } + return +} diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go new file mode 100644 index 00000000..d51ec47a --- /dev/null +++ b/listener/tunnel/tcp.go @@ -0,0 +1,68 @@ +package tunnel + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + closed bool + config string + listeners []net.Listener +} + +func New(config string, in chan<- C.ConnContext) (*Listener, error) { + tl := &Listener{false, config, nil} + pl := PairList{} + err := pl.Set(config) + if err != nil { + return nil, err + } + + for _, p := range pl { + addr := p[0] + target := p[1] + go func() { + tgt := socks5.ParseAddr(target) + if tgt == nil { + log.Errorln("invalid target address %q", target) + return + } + l, err := net.Listen("tcp", addr) + if err != nil { + return + } + tl.listeners = append(tl.listeners, l) + log.Infoln("TCP tunnel %s <-> %s", l.Addr().String(), target) + for { + c, err := l.Accept() + if err != nil { + if tl.closed { + break + } + continue + } + _ = c.(*net.TCPConn).SetKeepAlive(true) + + in <- inbound.NewSocket(tgt, c, C.TCPTUN) + } + }() + } + + return tl, nil +} + +func (l *Listener) Close() { + l.closed = true + for _, lis := range l.listeners { + _ = lis.Close() + } +} + +func (l *Listener) Config() string { + return l.config +} diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go new file mode 100644 index 00000000..1f43122b --- /dev/null +++ b/listener/tunnel/udp.go @@ -0,0 +1,79 @@ +package tunnel + +import ( + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/socks5" +) + +type UdpListener struct { + closed bool + config string + listeners []net.PacketConn +} + +func NewUdp(config string, in chan<- *inbound.PacketAdapter) (*UdpListener, error) { + ul := &UdpListener{false, config, nil} + pl := PairList{} + err := pl.Set(config) + if err != nil { + return nil, err + } + + for _, p := range pl { + addr := p[0] + target := p[1] + go func() { + tgt := socks5.ParseAddr(target) + if tgt == nil { + log.Errorln("invalid target address %q", target) + return + } + l, err := net.ListenPacket("udp", addr) + if err != nil { + return + } + ul.listeners = append(ul.listeners, l) + log.Infoln("Udp tunnel %s <-> %s", l.LocalAddr().String(), target) + for { + buf := pool.Get(pool.RelayBufferSize) + n, remoteAddr, err := l.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if ul.closed { + break + } + continue + } + packet := &packet{ + pc: l, + rAddr: remoteAddr, + payload: buf[:n], + bufRef: buf, + } + select { + case in <- inbound.NewPacket(tgt, packet, C.UDPTUN): + default: + } + + } + }() + } + + return ul, nil +} + +func (l *UdpListener) Close() { + l.closed = true + for _, lis := range l.listeners { + _ = lis.Close() + } +} + +func (l *UdpListener) Config() string { + return l.config +} diff --git a/listener/tunnel/utils.go b/listener/tunnel/utils.go new file mode 100644 index 00000000..7cce7406 --- /dev/null +++ b/listener/tunnel/utils.go @@ -0,0 +1,63 @@ +package tunnel + +import ( + "errors" + "net" + "strings" + + "github.com/Dreamacro/clash/common/pool" +) + +type PairList [][2]string // key1=val1,key2=val2,... + +func (l PairList) String() string { + s := make([]string, len(l)) + for i, pair := range l { + s[i] = pair[0] + "=" + pair[1] + } + return strings.Join(s, ",") +} +func (l *PairList) Set(s string) error { + for _, item := range strings.Split(s, ",") { + pair := strings.Split(item, "=") + if len(pair) != 2 { + return nil + } + *l = append(*l, [2]string{pair[0], pair[1]}) + } + return nil +} + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte + bufRef []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack wirtes UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + if addr == nil { + err = errors.New("address is invalid") + return + } + packet := b + return c.pc.WriteTo(packet, c.rAddr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + pool.Put(c.bufRef) +} + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +}