diff --git a/config/config.go b/config/config.go index 8b1ed89c..d9063415 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,7 @@ import ( R "github.com/Dreamacro/clash/rule" T "github.com/Dreamacro/clash/tunnel" + "github.com/samber/lo" "gopkg.in/yaml.v3" ) @@ -98,6 +99,7 @@ type Config struct { Users []auth.AuthUser Proxies map[string]C.Proxy Providers map[string]providerTypes.ProxyProvider + Tunnels []Tunnel } type RawDNS struct { @@ -122,6 +124,64 @@ type RawFallbackFilter struct { Domain []string `yaml:"domain"` } +type tunnel struct { + Network []string `yaml:"network"` + Address string `yaml:"address"` + Target string `yaml:"target"` + Proxy string `yaml:"proxy"` +} + +type Tunnel tunnel + +// UnmarshalYAML implements yaml.Unmarshaler +func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + if err := unmarshal(&tp); err != nil { + var inner tunnel + if err := unmarshal(&inner); err != nil { + return err + } + + *t = Tunnel(inner) + return nil + } + + // parse udp/tcp,address,target,proxy + parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string { + return strings.TrimSpace(s) + }) + if len(parts) != 4 { + return fmt.Errorf("invalid tunnel config %s", tp) + } + network := strings.Split(parts[0], "/") + + // validate network + for _, n := range network { + switch n { + case "tcp", "udp": + default: + return fmt.Errorf("invalid tunnel network %s", n) + } + } + + // validate address and target + address := parts[1] + target := parts[2] + for _, addr := range []string{address, target} { + if _, _, err := net.SplitHostPort(addr); err != nil { + return fmt.Errorf("invalid tunnel target or address %s", addr) + } + } + + *t = Tunnel(tunnel{ + Network: network, + Address: address, + Target: target, + Proxy: parts[3], + }) + return nil +} + type RawConfig struct { Port int `yaml:"port"` SocksPort int `yaml:"socks-port"` @@ -139,6 +199,7 @@ type RawConfig struct { Secret string `yaml:"secret"` Interface string `yaml:"interface-name"` RoutingMark int `yaml:"routing-mark"` + Tunnels []Tunnel `yaml:"tunnels"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` Hosts map[string]string `yaml:"hosts"` @@ -237,6 +298,14 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config.Users = parseAuthentication(rawCfg.Authentication) + config.Tunnels = rawCfg.Tunnels + // verify tunnels + for _, t := range config.Tunnels { + if _, ok := config.Proxies[t.Proxy]; !ok { + return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy) + } + } + return config, nil } diff --git a/constant/metadata.go b/constant/metadata.go index b829b7b0..cab6e37d 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -19,6 +19,7 @@ const ( SOCKS5 REDIR TPROXY + TUNNEL ) type NetWork int @@ -61,15 +62,16 @@ func (t Type) MarshalJSON() ([]byte, error) { // Metadata is used to store connection address type Metadata struct { - NetWork NetWork `json:"network"` - Type Type `json:"type"` - SrcIP net.IP `json:"sourceIP"` - DstIP net.IP `json:"destinationIP"` - SrcPort string `json:"sourcePort"` - DstPort string `json:"destinationPort"` - Host string `json:"host"` - DNSMode DNSMode `json:"dnsMode"` - ProcessPath string `json:"processPath"` + NetWork NetWork `json:"network"` + Type Type `json:"type"` + SrcIP net.IP `json:"sourceIP"` + DstIP net.IP `json:"destinationIP"` + SrcPort string `json:"sourcePort"` + DstPort string `json:"destinationPort"` + Host string `json:"host"` + DNSMode DNSMode `json:"dnsMode"` + ProcessPath string `json:"processPath"` + SpecialProxy string `json:"specialProxy"` } func (m *Metadata) RemoteAddress() string { diff --git a/go.mod b/go.mod index 25b2e1bd..66eef6b1 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mdlayher/netlink v1.6.2 github.com/miekg/dns v1.1.50 github.com/oschwald/geoip2-golang v1.8.0 + github.com/samber/lo v1.35.0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 go.etcd.io/bbolt v1.3.6 @@ -29,11 +30,11 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/josharian/native v1.0.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mdlayher/socket v0.2.3 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/text v0.4.0 // indirect golang.org/x/tools v0.1.12 // indirect diff --git a/go.sum b/go.sum index aac680fc..d92a0615 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,9 +33,7 @@ github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGu github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= @@ -50,6 +47,7 @@ github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= @@ -57,6 +55,8 @@ github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYx github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/samber/lo v1.35.0 h1:GlT8CV1GE+v97Y7MLF1wXvX6mjoxZ+hi61tj/ZcQwY0= +github.com/samber/lo v1.35.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -70,6 +70,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -84,6 +85,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.1-0.20221024173537-a3485e174077 h1:t5bjOfJPQfaG9NV1imLZM5E2uzaLGs5/NtyMtRNVjQ4= golang.org/x/crypto v0.1.1-0.20221024173537-a3485e174077/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -152,7 +155,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index dc1f8601..a131f4b1 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -18,7 +18,7 @@ import ( C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/dns" - P "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/listener" authStore "github.com/Dreamacro/clash/listener/auth" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" @@ -75,10 +75,11 @@ func ApplyConfig(cfg *config.Config, force bool) { updateGeneral(cfg.General, force) updateDNS(cfg.DNS) updateExperimental(cfg) + updateTunnels(cfg.Tunnels) } func GetGeneral() *config.General { - ports := P.GetPorts() + ports := listener.GetPorts() authenticator := []string{} if auth := authStore.Authenticator(); auth != nil { authenticator = auth.Users() @@ -92,8 +93,8 @@ func GetGeneral() *config.General { TProxyPort: ports.TProxyPort, MixedPort: ports.MixedPort, Authentication: authenticator, - AllowLan: P.AllowLan(), - BindAddress: P.BindAddress(), + AllowLan: listener.AllowLan(), + BindAddress: listener.BindAddress(), }, Mode: tunnel.Mode(), LogLevel: log.Level(), @@ -161,6 +162,10 @@ func updateRules(rules []C.Rule) { tunnel.UpdateRules(rules) } +func updateTunnels(tunnels []config.Tunnel) { + listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) +} + func updateGeneral(general *config.General, force bool) { log.SetLevel(general.LogLevel) tunnel.SetMode(general.Mode) @@ -176,19 +181,19 @@ func updateGeneral(general *config.General, force bool) { } allowLan := general.AllowLan - P.SetAllowLan(allowLan) + listener.SetAllowLan(allowLan) bindAddress := general.BindAddress - P.SetBindAddress(bindAddress) + listener.SetBindAddress(bindAddress) tcpIn := tunnel.TCPIn() udpIn := tunnel.UDPIn() - P.ReCreateHTTP(general.Port, tcpIn) - P.ReCreateSocks(general.SocksPort, tcpIn, udpIn) - P.ReCreateRedir(general.RedirPort, tcpIn, udpIn) - P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) - P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + listener.ReCreateHTTP(general.Port, tcpIn) + listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) + listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn) + listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) + listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { diff --git a/listener/listener.go b/listener/listener.go index b8e54b95..b5372327 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -1,34 +1,41 @@ -package proxy +package listener import ( "fmt" "net" "strconv" + "strings" "sync" "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/listener/http" "github.com/Dreamacro/clash/listener/mixed" "github.com/Dreamacro/clash/listener/redir" "github.com/Dreamacro/clash/listener/socks" "github.com/Dreamacro/clash/listener/tproxy" + "github.com/Dreamacro/clash/listener/tunnel" "github.com/Dreamacro/clash/log" + + "github.com/samber/lo" ) var ( allowLan = false bindAddress = "*" - 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 + 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 + tunnelTCPListeners = map[string]*tunnel.Listener{} + tunnelUDPListeners = map[string]*tunnel.PacketConn{} // lock for recreate function socksMux sync.Mutex @@ -36,6 +43,7 @@ var ( redirMux sync.Mutex tproxyMux sync.Mutex mixedMux sync.Mutex + tunnelMux sync.Mutex ) type Ports struct { @@ -301,6 +309,95 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) } +func PatchTunnel(tunnels []config.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { + tunnelMux.Lock() + defer tunnelMux.Unlock() + + type addrProxy struct { + network string + addr string + target string + proxy string + } + + tcpOld := lo.Map( + lo.Keys(tunnelTCPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "tcp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + udpOld := lo.Map( + lo.Keys(tunnelUDPListeners), + func(key string, _ int) addrProxy { + parts := strings.Split(key, "/") + return addrProxy{ + network: "udp", + addr: parts[0], + target: parts[1], + proxy: parts[2], + } + }, + ) + oldElm := lo.Union(tcpOld, udpOld) + + newElm := lo.FlatMap( + tunnels, + func(tunnel config.Tunnel, _ int) []addrProxy { + return lo.Map( + tunnel.Network, + func(network string, _ int) addrProxy { + return addrProxy{ + network: network, + addr: tunnel.Address, + target: tunnel.Target, + proxy: tunnel.Proxy, + } + }, + ) + }, + ) + + needClose, needCreate := lo.Difference(oldElm, newElm) + + for _, elm := range needClose { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + tunnelTCPListeners[key].Close() + delete(tunnelTCPListeners, key) + } else { + tunnelUDPListeners[key].Close() + delete(tunnelUDPListeners, key) + } + } + + for _, elm := range needCreate { + key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy) + if elm.network == "tcp" { + l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %w", elm.target, err) + continue + } + tunnelTCPListeners[key] = l + log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address()) + } else { + l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn) + if err != nil { + log.Errorln("Start tunnel %s error: %w", elm.target, err) + continue + } + tunnelUDPListeners[key] = l + log.Infoln("Tunnel(udp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelUDPListeners[key].Address()) + } + } +} + // GetPorts return the ports of proxy servers func GetPorts() *Ports { ports := &Ports{} diff --git a/listener/tunnel/packet.go b/listener/tunnel/packet.go new file mode 100644 index 00000000..0ade9726 --- /dev/null +++ b/listener/tunnel/packet.go @@ -0,0 +1,31 @@ +package tunnel + +import ( + "net" + + "github.com/Dreamacro/clash/common/pool" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack write UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + return c.pc.WriteTo(b, 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.payload) +} diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go new file mode 100644 index 00000000..4ae5865c --- /dev/null +++ b/listener/tunnel/tcp.go @@ -0,0 +1,75 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type Listener struct { + listener net.Listener + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +func (l *Listener) handleTCP(conn net.Conn, in chan<- C.ConnContext) { + conn.(*net.TCPConn).SetKeepAlive(true) + ctx := inbound.NewSocket(l.target, conn, C.TUNNEL) + ctx.Metadata().SpecialProxy = l.proxy + in <- ctx +} + +func New(addr, target, proxy string, in chan<- C.ConnContext) (*Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + rl := &Listener{ + listener: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + + go func() { + for { + c, err := l.Accept() + if err != nil { + if rl.closed { + break + } + continue + } + go rl.handleTCP(c, in) + } + }() + + return rl, nil +} diff --git a/listener/tunnel/udp.go b/listener/tunnel/udp.go new file mode 100644 index 00000000..ee0ecbaf --- /dev/null +++ b/listener/tunnel/udp.go @@ -0,0 +1,85 @@ +package tunnel + +import ( + "fmt" + "net" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/pool" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +type PacketConn struct { + conn net.PacketConn + addr string + target socks5.Addr + proxy string + closed bool +} + +// RawAddress implements C.Listener +func (l *PacketConn) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *PacketConn) Address() string { + return l.conn.LocalAddr().String() +} + +// Close implements C.Listener +func (l *PacketConn) Close() error { + l.closed = true + return l.conn.Close() +} + +func NewUDP(addr, target, proxy string, in chan<- *inbound.PacketAdapter) (*PacketConn, error) { + l, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + targetAddr := socks5.ParseAddr(target) + if targetAddr == nil { + return nil, fmt.Errorf("invalid target address %s", target) + } + + sl := &PacketConn{ + conn: l, + target: targetAddr, + proxy: proxy, + addr: addr, + } + go func() { + for { + buf := pool.Get(pool.UDPBufferSize) + n, remoteAddr, err := l.ReadFrom(buf) + if err != nil { + pool.Put(buf) + if sl.closed { + break + } + continue + } + sl.handleUDP(l, in, buf[:n], remoteAddr) + } + }() + + return sl, nil +} + +func (l *PacketConn) handleUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, addr net.Addr) { + packet := &packet{ + pc: pc, + rAddr: addr, + payload: buf, + } + + ctx := inbound.NewPacket(l.target, packet, C.TUNNEL) + ctx.Metadata().SpecialProxy = l.proxy + select { + case in <- ctx: + default: + } +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 0936056a..d659dd02 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -147,6 +147,15 @@ func preHandleMetadata(metadata *C.Metadata) error { } func resolveMetadata(ctx C.PlainContext, metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { + if metadata.SpecialProxy != "" { + var exist bool + proxy, exist = proxies[metadata.SpecialProxy] + if !exist { + err = fmt.Errorf("proxy %s not found", metadata.SpecialProxy) + return + } + } + switch mode { case Direct: proxy = proxies["DIRECT"] @@ -249,6 +258,8 @@ func handleUDPConn(packet *inbound.PacketAdapter) { pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule) switch true { + case metadata.SpecialProxy != "": + log.Infoln("[UDP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) case rule != nil: log.Infoln( "[UDP] %s --> %s match %s(%s) using %s", @@ -320,6 +331,8 @@ func handleTCPConn(connCtx C.ConnContext) { defer remoteConn.Close() switch true { + case metadata.SpecialProxy != "": + log.Infoln("[TCP] %s --> %s using %s", metadata.SourceAddress(), metadata.RemoteAddress(), metadata.SpecialProxy) case rule != nil: log.Infoln( "[TCP] %s --> %s match %s(%s) using %s",