Refactor: tun config

This commit is contained in:
yaling888 2022-05-16 01:21:56 +08:00
parent e92bea8401
commit 8b3e42bf19
19 changed files with 375 additions and 163 deletions

View File

@ -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:

View File

@ -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

View File

@ -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() {

View File

@ -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
})
}

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -42,8 +42,6 @@ func configInterfaceRouting(interfaceName string, addr netip.Prefix) error {
}
}
go defaultInterfaceChangeMonitor()
return nil
}

View File

@ -205,8 +205,6 @@ startOver:
wintunInterfaceName = dev.Name()
go defaultInterfaceChangeMonitor()
return nil
}

View File

@ -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)

View File

@ -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{

View File

@ -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 {

View File

@ -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)

View File

@ -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())