From 8b3e42bf199777074cde58a9e5d30a32549ac6d2 Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Mon, 16 May 2022 01:21:56 +0800 Subject: [PATCH] Refactor: tun config --- README.md | 69 +++++++++-- config/config.go | 108 +++++++--------- constant/metadata.go | 4 +- constant/tun.go | 116 +++++++++++++++++- go.mod | 1 + go.sum | 2 + hub/executor/executor.go | 27 ++-- hub/route/configs.go | 35 ++++-- listener/listener.go | 42 ++++--- listener/tun/ipstack/commons/dns.go | 7 +- listener/tun/ipstack/commons/router.go | 62 +++++++--- listener/tun/ipstack/commons/router_darwin.go | 14 ++- listener/tun/ipstack/commons/router_linux.go | 2 - .../tun/ipstack/commons/router_windows.go | 2 - listener/tun/ipstack/gvisor/handler.go | 6 +- listener/tun/ipstack/gvisor/stack.go | 5 +- listener/tun/ipstack/system/stack.go | 8 +- listener/tun/tun_adapter.go | 15 ++- tunnel/tunnel.go | 13 +- 19 files changed, 375 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 797040a6..4d05af57 100644 --- a/README.md +++ b/README.md @@ -102,19 +102,60 @@ Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fak ``` ### TUN configuration -Supports macOS, Linux and Windows. +Simply add the following to the main configuration: + +#### NOTE: +> auto-route and auto-detect-interface only available on macOS, Windows and Linux, receive IPv4 traffic -On Windows, you should download the [Wintun](https://www.wintun.net) driver and copy `wintun.dll` into the system32 directory. ```yaml -# Enable the TUN listener tun: enable: true - stack: gvisor # System or gVisor + stack: system # or gvisor # device: tun://utun8 # or fd://xxx, it's optional - dns-hijack: - - 0.0.0.0:53 # hijack all public + # dns-hijack: + # - 8.8.8.8:53 + # - tcp://8.8.8.8:53 + # - any:53 + # - tcp://any:53 + auto-route: true # auto set global route + auto-detect-interface: true # conflict with interface-name +``` +or +```yaml +interface-name: en0 + +tun: + enable: true + stack: system # or gvisor + # dns-hijack: + # - 8.8.8.8:53 + # - tcp://8.8.8.8:53 auto-route: true # auto set global route ``` +It's recommended to use fake-ip mode for the DNS server. + +Clash needs elevated permission to create TUN device: +```sh +$ sudo ./clash +``` +Then manually create the default route and DNS server. If your device already has some TUN device, Clash TUN might not work. In this case, fake-ip-filter may helpful. + +Enjoy! :) + +#### For Windows: +go to [https://www.wintun.net](https://www.wintun.net) and download the latest release, copy the right `wintun.dll` into the system32 directory. +```yaml +tun: + enable: true + stack: gvisor # or system + dns-hijack: + - 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53 + auto-route: true # auto set global route for Windows + # It is recommended to use `interface-name` + auto-detect-interface: true # auto detect interface, conflict with `interface-name` +``` +Finally, open the Clash + ### Rules configuration - Support rule `GEOSITE`. - Support rule `USER-AGENT`. @@ -206,6 +247,12 @@ proxies: # skip-cert-verify: true ``` +### Sniffing configuration +Sniff TLS SNI. +```yaml +sniffing: true +``` + ### IPTABLES configuration Work on Linux OS who's supported `iptables` @@ -244,14 +291,14 @@ $ systemctl start clash ``` ### Display Process name -To display process name online by click [https://yaling888.github.io/yacd/](https://yaling888.github.io/yacd/). +To display process name online by click [https://yacd.clash-plus.cf](https://yacd.clash-plus.cf). You can download the [Dashboard](https://github.com/yaling888/yacd/archive/gh-pages.zip) into Clash home directory: ```sh -cd ~/.config/clash -curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o yacd-gh-pages.zip -unzip yacd-gh-pages.zip -mv yacd-gh-pages dashboard +$ cd ~/.config/clash +$ curl -LJ https://github.com/yaling888/yacd/archive/gh-pages.zip -o yacd-gh-pages.zip +$ unzip yacd-gh-pages.zip +$ mv yacd-gh-pages dashboard ``` Add to config file: diff --git a/config/config.go b/config/config.go index c1e8505e..d876ff18 100644 --- a/config/config.go +++ b/config/config.go @@ -97,11 +97,12 @@ type Profile struct { // Tun config type Tun struct { - Enable bool `yaml:"enable" json:"enable"` - Device string `yaml:"device" json:"device"` - Stack C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute bool `yaml:"auto-route" json:"auto-route"` + Enable bool `yaml:"enable" json:"enable"` + Device string `yaml:"device" json:"device"` + Stack C.TUNStack `yaml:"stack" json:"stack"` + DNSHijack []C.DNSUrl `yaml:"dns-hijack" json:"dns-hijack"` + AutoRoute bool `yaml:"auto-route" json:"auto-route"` + AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"` } // IPTables config @@ -122,7 +123,6 @@ type Experimental struct{} // Config is clash config manager type Config struct { General *General - Tun *Tun IPTables *IPTables Mitm *Mitm DNS *DNS @@ -159,14 +159,6 @@ type RawFallbackFilter struct { GeoSite []string `yaml:"geosite"` } -type RawTun struct { - Enable bool `yaml:"enable" json:"enable"` - Device string `yaml:"device" json:"device"` - Stack C.TUNStack `yaml:"stack" json:"stack"` - DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` - AutoRoute bool `yaml:"auto-route" json:"auto-route"` -} - type RawMitm struct { Hosts []string `yaml:"hosts" json:"hosts"` Rules []string `yaml:"rules" json:"rules"` @@ -194,7 +186,7 @@ type RawConfig struct { ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` Hosts map[string]string `yaml:"hosts"` DNS RawDNS `yaml:"dns"` - Tun RawTun `yaml:"tun"` + Tun Tun `yaml:"tun"` IPTables IPTables `yaml:"iptables"` MITM RawMitm `yaml:"mitm"` Experimental Experimental `yaml:"experimental"` @@ -226,12 +218,26 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Rule: []string{}, Proxy: []map[string]any{}, ProxyGroup: []map[string]any{}, - Tun: RawTun{ - Enable: false, - Device: "", - Stack: C.TunGvisor, - DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query - AutoRoute: true, + Tun: Tun{ + Enable: false, + Device: "", + Stack: C.TunGvisor, + DNSHijack: []C.DNSUrl{ // default hijack all dns lookup + { + Network: "udp", + AddrPort: C.DNSAddrPort{ + AddrPort: netip.MustParseAddrPort("0.0.0.0:53"), + }, + }, + { + Network: "tcp", + AddrPort: C.DNSAddrPort{ + AddrPort: netip.MustParseAddrPort("0.0.0.0:53"), + }, + }, + }, + AutoRoute: false, + AutoDetectInterface: false, }, IPTables: IPTables{ Enable: false, @@ -253,7 +259,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { "223.5.5.5", }, NameServer: []string{ // default if user not set - "https://doh.pub/dns-query", + "https://120.53.53.53/dns-query", "tls://223.5.5.5:853", }, }, @@ -286,14 +292,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.General = general - tunCfg, err := parseTun(rawCfg.Tun, config.General) - if err != nil { - return nil, err - } - config.Tun = tunCfg - - dialer.DefaultInterface.Store(config.General.Interface) - proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err @@ -342,6 +340,19 @@ func parseGeneral(cfg *RawConfig) (*General, error) { } } + if cfg.Tun.Enable && cfg.Tun.AutoDetectInterface { + outboundInterface, err := commons.GetAutoDetectInterface() + if err != nil && cfg.Interface == "" { + return nil, fmt.Errorf("get auto detect interface fail: %w", err) + } + + if outboundInterface != "" { + cfg.Interface = outboundInterface + } + } + + dialer.DefaultInterface.Store(cfg.Interface) + return &General{ Inbound: Inbound{ Port: cfg.Port, @@ -363,6 +374,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { IPv6: cfg.IPv6, Interface: cfg.Interface, RoutingMark: cfg.RoutingMark, + Tun: cfg.Tun, }, nil } @@ -794,7 +806,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R } func parseAuthentication(rawRecords []string) []auth.AuthUser { - users := []auth.AuthUser{} + var users []auth.AuthUser for _, line := range rawRecords { if user, pass, found := strings.Cut(line, ":"); found { users = append(users, auth.AuthUser{User: user, Pass: pass}) @@ -803,40 +815,6 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser { return users } -func parseTun(rawTun RawTun, general *General) (*Tun, error) { - if (rawTun.Enable || general.TProxyPort != 0) && general.Interface == "" { - autoDetectInterfaceName, err := commons.GetAutoDetectInterface() - if err != nil || autoDetectInterfaceName == "" { - return nil, fmt.Errorf("can not find auto detect interface: %w. you must be detect `interface-name` if tun set to enable or `tproxy-port` isn't zore", err) - } - - general.Interface = autoDetectInterfaceName - } - - var dnsHijack []netip.AddrPort - - for _, d := range rawTun.DNSHijack { - if _, after, ok := strings.Cut(d, "://"); ok { - d = after - } - - addrPort, err := netip.ParseAddrPort(d) - if err != nil { - return nil, fmt.Errorf("parse dns-hijack url error: %w", err) - } - - dnsHijack = append(dnsHijack, addrPort) - } - - return &Tun{ - Enable: rawTun.Enable, - Device: rawTun.Device, - Stack: rawTun.Stack, - DNSHijack: dnsHijack, - AutoRoute: rawTun.AutoRoute, - }, nil -} - func parseMitm(rawMitm RawMitm) (*Mitm, error) { var ( req []C.Rewrite diff --git a/constant/metadata.go b/constant/metadata.go index 2e5332bb..bdca0320 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -101,8 +101,8 @@ func (m *Metadata) Resolved() bool { // Pure is used to solve unexpected behavior // when dialing proxy connection in DNSMapping mode. -func (m *Metadata) Pure() *Metadata { - if m.DNSMode == DNSMapping && m.DstIP.IsValid() { +func (m *Metadata) Pure(isMitmOutbound bool) *Metadata { + if !isMitmOutbound && m.DNSMode == DNSMapping && m.DstIP.IsValid() { copyM := *m copyM.Host = "" if copyM.DstIP.Is4() { diff --git a/constant/tun.go b/constant/tun.go index 10e24861..ddd65d71 100644 --- a/constant/tun.go +++ b/constant/tun.go @@ -3,7 +3,11 @@ package constant import ( "encoding/json" "errors" + "net/netip" + "strconv" "strings" + + "golang.org/x/exp/slices" ) var StackTypeMapping = map[string]TUNStack{ @@ -40,7 +44,7 @@ func (e TUNStack) MarshalYAML() (any, error) { // UnmarshalJSON unserialize TUNStack with json func (e *TUNStack) UnmarshalJSON(data []byte) error { var tp string - json.Unmarshal(data, &tp) + _ = json.Unmarshal(data, &tp) mode, exist := StackTypeMapping[strings.ToUpper(tp)] if !exist { return errors.New("invalid tun stack") @@ -64,3 +68,113 @@ func (e TUNStack) String() string { return "unknown" } } + +type DNSAddrPort struct { + netip.AddrPort +} + +func (p *DNSAddrPort) UnmarshalText(text []byte) error { + if len(text) == 0 { + *p = DNSAddrPort{} + return nil + } + + addrPort := string(text) + if strings.HasPrefix(addrPort, "any") { + _, port, _ := strings.Cut(addrPort, "any") + addrPort = "0.0.0.0" + port + } + + ap, err := netip.ParseAddrPort(addrPort) + *p = DNSAddrPort{AddrPort: ap} + return err +} + +func (p DNSAddrPort) String() string { + addrPort := p.AddrPort.String() + if p.AddrPort.Addr().IsUnspecified() { + addrPort = "any:" + strconv.Itoa(int(p.AddrPort.Port())) + } + return addrPort +} + +type DNSUrl struct { + Network string + AddrPort DNSAddrPort +} + +func (d *DNSUrl) UnmarshalYAML(unmarshal func(any) error) error { + var text string + if err := unmarshal(&text); err != nil { + return err + } + + text = strings.ToLower(text) + network := "udp" + if before, after, found := strings.Cut(text, "://"); found { + network = before + text = after + } + + if network != "udp" && network != "tcp" { + return errors.New("invalid dns url schema") + } + + ap := &DNSAddrPort{} + if err := ap.UnmarshalText([]byte(text)); err != nil { + return err + } + + *d = DNSUrl{Network: network, AddrPort: *ap} + + return nil +} + +func (d DNSUrl) MarshalYAML() (any, error) { + return d.String(), nil +} + +func (d *DNSUrl) UnmarshalJSON(data []byte) error { + var text string + if err := json.Unmarshal(data, &text); err != nil { + return err + } + + text = strings.ToLower(text) + network := "udp" + if before, after, found := strings.Cut(text, "://"); found { + network = before + text = after + } + + if network != "udp" && network != "tcp" { + return errors.New("invalid dns url schema") + } + + ap := &DNSAddrPort{} + if err := ap.UnmarshalText([]byte(text)); err != nil { + return err + } + + *d = DNSUrl{Network: network, AddrPort: *ap} + + return nil +} + +func (d DNSUrl) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +func (d DNSUrl) String() string { + return d.Network + "://" + d.AddrPort.String() +} + +func RemoveDuplicateDNSUrl(slice []DNSUrl) []DNSUrl { + slices.SortFunc[DNSUrl](slice, func(a, b DNSUrl) bool { + return a.Network < b.Network || (a.Network == b.Network && a.AddrPort.Addr().Less(b.AddrPort.Addr())) + }) + + return slices.CompactFunc[[]DNSUrl, DNSUrl](slice, func(a, b DNSUrl) bool { + return a.Network == b.Network && a.AddrPort == b.AddrPort + }) +} diff --git a/go.mod b/go.mod index 5016499f..39e23ab0 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( go.uber.org/atomic v1.9.0 go.uber.org/automaxprocs v1.5.1 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 + golang.org/x/exp v0.0.0-20220428152302-39d4317da171 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 diff --git a/go.sum b/go.sum index 1b8a0eb8..76a2fddc 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,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.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4= +golang.org/x/exp v0.0.0-20220428152302-39d4317da171/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.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 54e651f6..14857155 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -82,10 +82,9 @@ func ApplyConfig(cfg *config.Config, force bool) { updateHosts(cfg.Hosts) updateMitm(cfg.Mitm) updateProfile(cfg) - updateDNS(cfg.DNS, cfg.Tun) + updateDNS(cfg.DNS, cfg.General.Tun) updateGeneral(cfg.General, force) updateIPTables(cfg) - updateTun(cfg.Tun, cfg.DNS) updateExperimental(cfg) log.SetLevel(cfg.General.LogLevel) @@ -94,8 +93,8 @@ func ApplyConfig(cfg *config.Config, force bool) { func GetGeneral() *config.General { ports := P.GetPorts() authenticator := []string{} - if auth := authStore.Authenticator(); auth != nil { - authenticator = auth.Users() + if authM := authStore.Authenticator(); authM != nil { + authenticator = authM.Users() } general := &config.General{ @@ -119,9 +118,9 @@ func GetGeneral() *config.General { return general } -func updateExperimental(c *config.Config) {} +func updateExperimental(_ *config.Config) {} -func updateDNS(c *config.DNS, t *config.Tun) { +func updateDNS(c *config.DNS, t config.Tun) { cfg := dns.Config{ Main: c.NameServer, Fallback: c.Fallback, @@ -172,6 +171,10 @@ func updateDNS(c *config.DNS, t *config.Tun) { } dns.ReCreateServer("", nil, nil) } + + if cfg.Pool != nil { + P.SetTunAddressPrefix(cfg.Pool.IPNet()) + } } func updateHosts(tree *trie.DomainTrie[netip.Addr]) { @@ -186,15 +189,6 @@ func updateRules(rules []C.Rule) { tunnel.UpdateRules(rules) } -func updateTun(tun *config.Tun, dns *config.DNS) { - var tunAddressPrefix *netip.Prefix - if dns.FakeIPRange != nil { - tunAddressPrefix = dns.FakeIPRange.IPNet() - } - - P.ReCreateTun(tun, tunAddressPrefix, tunnel.TCPIn(), tunnel.UDPIn()) -} - func updateGeneral(general *config.General, force bool) { tunnel.SetMode(general.Mode) resolver.DisableIPv6 = !general.IPv6 @@ -232,6 +226,7 @@ func updateGeneral(general *config.General, force bool) { P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) P.ReCreateMitm(general.MitmPort, tcpIn) + P.ReCreateTun(&general.Tun, tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { @@ -273,7 +268,7 @@ func patchSelectGroup(proxies map[string]C.Proxy) { continue } - selector.Set(selected) + _ = selector.Set(selected) } } diff --git a/hub/route/configs.go b/hub/route/configs.go index c9a4e833..2e749fe6 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -1,15 +1,17 @@ package route import ( + "encoding/json" "net/http" - "net/netip" "path/filepath" + "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/hub/executor" P "github.com/Dreamacro/clash/listener" + "github.com/Dreamacro/clash/listener/tun/ipstack/commons" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" @@ -41,11 +43,12 @@ type configSchema struct { } type tunConfigSchema struct { - Enable *bool `json:"enable"` - Device *string `json:"device"` - Stack *constant.TUNStack `json:"stack"` - DNSHijack *[]netip.AddrPort `json:"dns-hijack"` - AutoRoute *bool `json:"auto-route"` + Enable *bool `json:"enable,omitempty"` + Device *string `json:"device,omitempty"` + Stack *constant.TUNStack `json:"stack,omitempty"` + DNSHijack *[]constant.DNSUrl `json:"dns-hijack,omitempty"` + AutoRoute *bool `json:"auto-route,omitempty"` + AutoDetectInterface *bool `json:"auto-detect-interface,omitempty"` } func getConfigs(w http.ResponseWriter, r *http.Request) { @@ -120,10 +123,26 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { if tunSchema.AutoRoute != nil { tunConf.AutoRoute = *tunSchema.AutoRoute } + if tunSchema.AutoDetectInterface != nil { + tunConf.AutoDetectInterface = *tunSchema.AutoDetectInterface + } - P.ReCreateTun(&tunConf, nil, tcpIn, udpIn) + if dialer.DefaultInterface.Load() == "" && tunConf.Enable { + outboundInterface, err := commons.GetAutoDetectInterface() + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("Get auto detect interface fail: "+err.Error())) + return + } + dialer.DefaultInterface.Store(outboundInterface) + } + + P.ReCreateTun(&tunConf, tcpIn, udpIn) } + msg, _ := json.Marshal(general) + log.Warnln("[REST-API] patch config by: %s", string(msg)) + render.NoContent(w, r) } @@ -151,6 +170,7 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, newError(err.Error())) return } + log.Warnln("[REST-API] update config by payload") } else { if req.Path == "" { req.Path = constant.Path.Config() @@ -167,6 +187,7 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, newError(err.Error())) return } + log.Warnln("[REST-API] reload config from path: %s", req.Path) } executor.ApplyConfig(cfg, force) diff --git a/listener/listener.go b/listener/listener.go index e2a182ba..c9913a22 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -8,7 +8,6 @@ import ( "net" "net/netip" "os" - "sort" "strconv" "sync" "time" @@ -36,6 +35,7 @@ var ( bindAddress = "*" lastTunConf *config.Tun lastTunAddressPrefix *netip.Prefix + tunAddressPrefix *netip.Prefix socksListener *socks.Listener socksUDPListener *socks.UDPListener @@ -70,11 +70,24 @@ type Ports struct { func GetTunConf() config.Tun { if lastTunConf == nil { + addrPort := C.DNSAddrPort{ + AddrPort: netip.MustParseAddrPort("0.0.0.0:53"), + } return config.Tun{ - Enable: false, - Stack: C.TunGvisor, - DNSHijack: []netip.AddrPort{netip.MustParseAddrPort("0.0.0.0:53")}, - AutoRoute: true, + Enable: false, + Stack: C.TunGvisor, + DNSHijack: []C.DNSUrl{ // default hijack all dns query + { + Network: "udp", + AddrPort: addrPort, + }, + { + Network: "tcp", + AddrPort: addrPort, + }, + }, + AutoRoute: true, + AutoDetectInterface: false, } } return *lastTunConf @@ -96,6 +109,10 @@ func SetBindAddress(host string) { bindAddress = host } +func SetTunAddressPrefix(tunAddress *netip.Prefix) { + tunAddressPrefix = tunAddress +} + func ReCreateHTTP(port int, tcpIn chan<- C.ConnContext) { httpMux.Lock() defer httpMux.Unlock() @@ -335,7 +352,7 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address()) } -func ReCreateTun(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { +func ReCreateTun(tunConf *config.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) { tunMux.Lock() defer tunMux.Unlock() @@ -350,6 +367,8 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan tunAddressPrefix = lastTunAddressPrefix } + tunConf.DNSHijack = C.RemoveDuplicateDNSUrl(tunConf.DNSHijack) + if tunStackListener != nil { if !hasTunConfigChange(tunConf, tunAddressPrefix) { return @@ -519,14 +538,6 @@ func hasTunConfigChange(tunConf *config.Tun, tunAddressPrefix *netip.Prefix) boo return true } - sort.Slice(lastTunConf.DNSHijack, func(i, j int) bool { - return lastTunConf.DNSHijack[i].Addr().Less(lastTunConf.DNSHijack[j].Addr()) - }) - - sort.Slice(tunConf.DNSHijack, func(i, j int) bool { - return tunConf.DNSHijack[i].Addr().Less(tunConf.DNSHijack[j].Addr()) - }) - for i, dns := range tunConf.DNSHijack { if dns != lastTunConf.DNSHijack[i] { return true @@ -536,7 +547,8 @@ func hasTunConfigChange(tunConf *config.Tun, tunAddressPrefix *netip.Prefix) boo if lastTunConf.Enable != tunConf.Enable || lastTunConf.Device != tunConf.Device || lastTunConf.Stack != tunConf.Stack || - lastTunConf.AutoRoute != tunConf.AutoRoute { + lastTunConf.AutoRoute != tunConf.AutoRoute || + lastTunConf.AutoDetectInterface != tunConf.AutoDetectInterface { return true } diff --git a/listener/tun/ipstack/commons/dns.go b/listener/tun/ipstack/commons/dns.go index 667c2642..bef1fb4e 100644 --- a/listener/tun/ipstack/commons/dns.go +++ b/listener/tun/ipstack/commons/dns.go @@ -5,15 +5,16 @@ import ( "time" "github.com/Dreamacro/clash/component/resolver" + C "github.com/Dreamacro/clash/constant" D "github.com/miekg/dns" ) const DefaultDnsReadTimeout = time.Second * 10 -func ShouldHijackDns(dnsAdds []netip.AddrPort, targetAddr netip.AddrPort) bool { - for _, addrPort := range dnsAdds { - if addrPort == targetAddr || (addrPort.Addr().IsUnspecified() && targetAddr.Port() == 53) { +func ShouldHijackDns(dnsHijack []C.DNSUrl, targetAddr netip.AddrPort, network string) bool { + for _, dns := range dnsHijack { + if dns.Network == network && (dns.AddrPort.AddrPort == targetAddr || (dns.AddrPort.Addr().IsUnspecified() && dns.AddrPort.Port() == targetAddr.Port())) { return true } } diff --git a/listener/tun/ipstack/commons/router.go b/listener/tun/ipstack/commons/router.go index 1cebfa98..d378cef7 100644 --- a/listener/tun/ipstack/commons/router.go +++ b/listener/tun/ipstack/commons/router.go @@ -3,6 +3,7 @@ package commons import ( "fmt" "net" + "sync" "time" "github.com/Dreamacro/clash/component/dialer" @@ -12,7 +13,10 @@ import ( var ( defaultRoutes = []string{"1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1"} - defaultInterfaceMonitorDuration = 20 * time.Second + monitorDuration = 10 * time.Second + monitorStarted = false + monitorStop = make(chan struct{}, 2) + monitorMux sync.Mutex ) func ipv4MaskString(bits int) string { @@ -24,26 +28,52 @@ func ipv4MaskString(bits int) string { return fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) } -func defaultInterfaceChangeMonitor() { - t := time.NewTicker(defaultInterfaceMonitorDuration) +func StartDefaultInterfaceChangeMonitor() { + monitorMux.Lock() + if monitorStarted { + monitorMux.Unlock() + return + } + monitorStarted = true + monitorMux.Unlock() + + select { + case <-monitorStop: + default: + } + + t := time.NewTicker(monitorDuration) defer t.Stop() for { - <-t.C + select { + case <-t.C: + interfaceName, err := GetAutoDetectInterface() + if err != nil { + log.Warnln("[TUN] default interface monitor err: %v", err) + continue + } - interfaceName, err := GetAutoDetectInterface() - if err != nil { - log.Warnln("[TUN] default interface monitor exited, cause: %v", err) + old := dialer.DefaultInterface.Load() + if interfaceName == old { + continue + } + + dialer.DefaultInterface.Store(interfaceName) + + log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, interfaceName) + case <-monitorStop: break } - - old := dialer.DefaultInterface.Load() - if interfaceName == old { - continue - } - - dialer.DefaultInterface.Store(interfaceName) - - log.Warnln("[TUN] default interface changed by monitor, %s => %s", old, interfaceName) + } +} + +func StopDefaultInterfaceChangeMonitor() { + monitorMux.Lock() + defer monitorMux.Unlock() + + if monitorStarted { + monitorStop <- struct{}{} + monitorStarted = false } } diff --git a/listener/tun/ipstack/commons/router_darwin.go b/listener/tun/ipstack/commons/router_darwin.go index 15beaf2d..5cbab1fa 100644 --- a/listener/tun/ipstack/commons/router_darwin.go +++ b/listener/tun/ipstack/commons/router_darwin.go @@ -3,13 +3,23 @@ package commons import ( "fmt" "net/netip" + "strings" "github.com/Dreamacro/clash/common/cmd" "github.com/Dreamacro/clash/listener/tun/device" ) func GetAutoDetectInterface() (string, error) { - return cmd.ExecCmd("/bin/bash -c /sbin/route -n get default | grep 'interface:' | awk -F ' ' 'NR==1{print $2}' | xargs echo -n") + rs, err := cmd.ExecCmd("/bin/bash -c /sbin/route -n get default | grep 'interface:' | awk -F ' ' 'NR==1{print $2}' | xargs echo -n") + if err != nil { + return "", err + } + + if rs == "" || strings.HasSuffix(rs, "\n") { + return "", fmt.Errorf("invalid interface name: %s", rs) + } + + return rs, nil } func ConfigInterfaceAddress(dev device.Device, addr netip.Prefix, forceMTU int, autoRoute bool) error { @@ -54,8 +64,6 @@ func configInterfaceRouting(interfaceName string, addr netip.Prefix) error { } } - go defaultInterfaceChangeMonitor() - return execRouterCmd("add", "-inet6", "2000::/3", interfaceName) } diff --git a/listener/tun/ipstack/commons/router_linux.go b/listener/tun/ipstack/commons/router_linux.go index 63479bae..06df068c 100644 --- a/listener/tun/ipstack/commons/router_linux.go +++ b/listener/tun/ipstack/commons/router_linux.go @@ -42,8 +42,6 @@ func configInterfaceRouting(interfaceName string, addr netip.Prefix) error { } } - go defaultInterfaceChangeMonitor() - return nil } diff --git a/listener/tun/ipstack/commons/router_windows.go b/listener/tun/ipstack/commons/router_windows.go index 2e5a0dea..b29b20ab 100644 --- a/listener/tun/ipstack/commons/router_windows.go +++ b/listener/tun/ipstack/commons/router_windows.go @@ -205,8 +205,6 @@ startOver: wintunInterfaceName = dev.Name() - go defaultInterfaceChangeMonitor() - return nil } diff --git a/listener/tun/ipstack/gvisor/handler.go b/listener/tun/ipstack/gvisor/handler.go index 9560937c..476374bd 100644 --- a/listener/tun/ipstack/gvisor/handler.go +++ b/listener/tun/ipstack/gvisor/handler.go @@ -20,7 +20,7 @@ var _ adapter.Handler = (*gvHandler)(nil) type gvHandler struct { gateway netip.Addr - dnsHijack []netip.AddrPort + dnsHijack []C.DNSUrl tcpIn chan<- C.ConnContext udpIn chan<- *inbound.PacketAdapter @@ -37,7 +37,7 @@ func (gh *gvHandler) HandleTCP(tunConn adapter.TCPConn) { rAddrPort := netip.AddrPortFrom(nnip.IpToAddr(rAddr.IP), id.LocalPort) - if D.ShouldHijackDns(gh.dnsHijack, rAddrPort) { + if D.ShouldHijackDns(gh.dnsHijack, rAddrPort, "tcp") { go func() { log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String()) @@ -111,7 +111,7 @@ func (gh *gvHandler) HandleUDP(tunConn adapter.UDPConn) { payload := buf[:n] - if D.ShouldHijackDns(gh.dnsHijack, rAddrPort) { + if D.ShouldHijackDns(gh.dnsHijack, rAddrPort, "udp") { go func() { defer func() { _ = pool.Put(buf) diff --git a/listener/tun/ipstack/gvisor/stack.go b/listener/tun/ipstack/gvisor/stack.go index 1307bc71..f6e432ca 100644 --- a/listener/tun/ipstack/gvisor/stack.go +++ b/listener/tun/ipstack/gvisor/stack.go @@ -8,6 +8,7 @@ import ( C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/listener/tun/device" "github.com/Dreamacro/clash/listener/tun/ipstack" + "github.com/Dreamacro/clash/listener/tun/ipstack/commons" "github.com/Dreamacro/clash/listener/tun/ipstack/gvisor/option" "gvisor.dev/gvisor/pkg/tcpip" @@ -25,6 +26,8 @@ type gvStack struct { } func (s *gvStack) Close() error { + commons.StopDefaultInterfaceChangeMonitor() + var err error if s.device != nil { err = s.device.Close() @@ -37,7 +40,7 @@ func (s *gvStack) Close() error { } // New allocates a new *gvStack with given options. -func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter, opts ...option.Option) (ipstack.Stack, error) { +func New(device device.Device, dnsHijack []C.DNSUrl, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter, opts ...option.Option) (ipstack.Stack, error) { s := &gvStack{ Stack: stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ diff --git a/listener/tun/ipstack/system/stack.go b/listener/tun/ipstack/system/stack.go index 803e5db0..fd6b7cde 100644 --- a/listener/tun/ipstack/system/stack.go +++ b/listener/tun/ipstack/system/stack.go @@ -34,6 +34,8 @@ type sysStack struct { } func (s *sysStack) Close() error { + D.StopDefaultInterfaceChangeMonitor() + defer func() { if s.device != nil { _ = s.device.Close() @@ -49,7 +51,7 @@ func (s *sysStack) Close() error { return err } -func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.Stack, error) { +func New(device device.Device, dnsHijack []C.DNSUrl, tunAddress netip.Prefix, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.PacketAdapter) (ipstack.Stack, error) { var ( gateway = tunAddress.Masked().Addr().Next() portal = gateway.Next() @@ -91,7 +93,7 @@ func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Pref continue } - if D.ShouldHijackDns(dnsAddr, rAddrPort) { + if D.ShouldHijackDns(dnsAddr, rAddrPort, "tcp") { go func() { log.Debugln("[TUN] hijack dns tcp: %s", rAddrPort.String()) @@ -175,7 +177,7 @@ func New(device device.Device, dnsHijack []netip.AddrPort, tunAddress netip.Pref continue } - if D.ShouldHijackDns(dnsAddr, rAddrPort) { + if D.ShouldHijackDns(dnsAddr, rAddrPort, "udp") { go func() { msg, err := D.RelayDnsPacket(raw) if err != nil { diff --git a/listener/tun/tun_adapter.go b/listener/tun/tun_adapter.go index 8d861c9e..d9178f3e 100644 --- a/listener/tun/tun_adapter.go +++ b/listener/tun/tun_adapter.go @@ -38,6 +38,12 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con err error ) + defer func() { + if err != nil && tunDevice != nil { + _ = tunDevice.Close() + } + }() + if devName == "" { devName = generateDeviceName() } @@ -63,26 +69,22 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con case C.TunGvisor: err = tunDevice.UseEndpoint() if err != nil { - _ = tunDevice.Close() return nil, fmt.Errorf("can't attach endpoint to tun: %w", err) } tunStack, err = gvisor.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn, option.WithDefault()) if err != nil { - _ = tunDevice.Close() return nil, fmt.Errorf("can't New gvisor stack: %w", err) } case C.TunSystem: err = tunDevice.UseIOBased() if err != nil { - _ = tunDevice.Close() return nil, fmt.Errorf("can't New system stack: %w", err) } tunStack, err = system.New(tunDevice, tunConf.DNSHijack, tunAddress, tcpIn, udpIn) if err != nil { - _ = tunDevice.Close() return nil, fmt.Errorf("can't New system stack: %w", err) } default: @@ -92,10 +94,13 @@ func New(tunConf *config.Tun, tunAddressPrefix *netip.Prefix, tcpIn chan<- C.Con // setting address and routing err = commons.ConfigInterfaceAddress(tunDevice, tunAddress, mtu, autoRoute) if err != nil { - _ = tunDevice.Close() return nil, fmt.Errorf("setting interface address and routing failed: %w", err) } + if tunConf.AutoDetectInterface { + go commons.StartDefaultInterfaceChangeMonitor() + } + setAtLatest(stackType, devName) log.Infoln("TUN stack listening at: %s(%s), mtu: %d, auto route: %v, ip stack: %s", tunDevice.Name(), tunAddress.Masked().Addr().Next().String(), mtu, autoRoute, stackType) diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 2c329ae2..85f39969 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -269,7 +269,7 @@ func handleUDPConn(packet *inbound.PacketAdapter) { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) defer cancel() - rawPc, err := proxy.ListenPacketContext(ctx, metadata.Pure()) + rawPc, err := proxy.ListenPacketContext(ctx, metadata.Pure(false)) if err != nil { if rule == nil { log.Warnln("[UDP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error()) @@ -321,14 +321,11 @@ func handleTCPConn(connCtx C.ConnContext) { return } - mtd := metadata - if proxy != mitmProxy { - mtd = metadata.Pure() - } + isMitmOutbound := proxy == mitmProxy ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() - remoteConn, err := proxy.DialContext(ctx, mtd) + remoteConn, err := proxy.DialContext(ctx, metadata.Pure(isMitmOutbound)) if err != nil { if rule == nil { log.Warnln("[TCP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error()) @@ -338,7 +335,7 @@ func handleTCPConn(connCtx C.ConnContext) { return } - if remoteConn.Chains().Last() != "REJECT" && proxy != mitmProxy { + if remoteConn.Chains().Last() != "REJECT" && !isMitmOutbound { remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule) } @@ -347,7 +344,7 @@ func handleTCPConn(connCtx C.ConnContext) { }(remoteConn) switch true { - case proxy == mitmProxy: + case isMitmOutbound: break case rule != nil: log.Infoln("[TCP] %s(%s) --> %s match %s(%s) using %s", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress(), rule.RuleType().String(), rule.Payload(), remoteConn.Chains().String())