diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go index ed560818d8..894910aac1 100644 --- a/adapter/inbound/addition.go +++ b/adapter/inbound/addition.go @@ -69,3 +69,5 @@ func WithDSCP(dscp uint8) Addition { metadata.DSCP = dscp } } + +func Placeholder(metadata *C.Metadata) {} diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 4cb0db004f..738ed15479 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -205,7 +205,6 @@ func strategyStickySessions(url string) strategyFn { proxy := proxies[nowIdx] if proxy.AliveForTestUrl(url) { if nowIdx != idx { - lruCache.Delete(key) lruCache.Set(key, nowIdx) } @@ -215,7 +214,6 @@ func strategyStickySessions(url string) strategyFn { } } - lruCache.Delete(key) lruCache.Set(key, 0) return proxies[0] } diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 67dc104dfb..efc38aabee 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -69,7 +69,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide } if groupOption.IncludeAllProviders { - groupOption.Use = append(groupOption.Use, AllProviders...) + groupOption.Use = AllProviders } if groupOption.IncludeAllProxies { if groupOption.Filter != "" { diff --git a/common/lru/lrucache.go b/common/lru/lrucache.go index 6f32ed18b1..35f605b10c 100644 --- a/common/lru/lrucache.go +++ b/common/lru/lrucache.go @@ -223,6 +223,10 @@ func (c *LruCache[K, V]) Delete(key K) { c.mu.Lock() defer c.mu.Unlock() + c.delete(key) +} + +func (c *LruCache[K, V]) delete(key K) { if le, ok := c.cache[key]; ok { c.deleteElement(le) } @@ -255,6 +259,34 @@ func (c *LruCache[K, V]) Clear() error { return nil } +// Compute either sets the computed new value for the key or deletes +// the value for the key. When the delete result of the valueFn function +// is set to true, the value will be deleted, if it exists. When delete +// is set to false, the value is updated to the newValue. +// The ok result indicates whether value was computed and stored, thus, is +// present in the map. The actual result contains the new value in cases where +// the value was computed and stored. +func (c *LruCache[K, V]) Compute( + key K, + valueFn func(oldValue V, loaded bool) (newValue V, delete bool), +) (actual V, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if el := c.get(key); el != nil { + actual, ok = el.value, true + } + if newValue, del := valueFn(actual, ok); del { + if ok { // data not in cache, so needn't delete + c.delete(key) + } + return lo.Empty[V](), false + } else { + c.set(key, newValue) + return newValue, true + } +} + type entry[K comparable, V any] struct { key K value V diff --git a/common/queue/queue.go b/common/queue/queue.go index cb58e2f5a2..d1b6beebe5 100644 --- a/common/queue/queue.go +++ b/common/queue/queue.go @@ -59,8 +59,8 @@ func (q *Queue[T]) Copy() []T { // Len returns the number of items in this queue. func (q *Queue[T]) Len() int64 { - q.lock.Lock() - defer q.lock.Unlock() + q.lock.RLock() + defer q.lock.RUnlock() return int64(len(q.items)) } diff --git a/common/utils/callback.go b/common/utils/callback.go index df950d3a81..ad734c0fd6 100644 --- a/common/utils/callback.go +++ b/common/utils/callback.go @@ -17,8 +17,8 @@ func NewCallback[T any]() *Callback[T] { } func (c *Callback[T]) Register(item func(T)) io.Closer { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mutex.Lock() + defer c.mutex.Unlock() element := c.list.PushBack(item) return &callbackCloser[T]{ element: element, diff --git a/component/cidr/ipcidr_set.go b/component/cidr/ipcidr_set.go index 521fabab13..4907146039 100644 --- a/component/cidr/ipcidr_set.go +++ b/component/cidr/ipcidr_set.go @@ -57,6 +57,16 @@ func (set *IpCidrSet) Merge() error { return nil } +func (set *IpCidrSet) Foreach(f func(prefix netip.Prefix) bool) { + for _, r := range set.rr { + for _, prefix := range r.Prefixes() { + if !f(prefix) { + return + } + } + } +} + // ToIPSet not safe convert to *netipx.IPSet // be careful, must be used after Merge func (set *IpCidrSet) ToIPSet() *netipx.IPSet { diff --git a/component/cidr/ipcidr_set_bin.go b/component/cidr/ipcidr_set_bin.go new file mode 100644 index 0000000000..f6a0348856 --- /dev/null +++ b/component/cidr/ipcidr_set_bin.go @@ -0,0 +1,77 @@ +package cidr + +import ( + "encoding/binary" + "errors" + "io" + "net/netip" + + "go4.org/netipx" +) + +func (ss *IpCidrSet) WriteBin(w io.Writer) (err error) { + // version + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + + // rr + err = binary.Write(w, binary.BigEndian, int64(len(ss.rr))) + if err != nil { + return err + } + for _, r := range ss.rr { + err = binary.Write(w, binary.BigEndian, r.From().As16()) + if err != nil { + return err + } + err = binary.Write(w, binary.BigEndian, r.To().As16()) + if err != nil { + return err + } + } + + return nil +} + +func ReadIpCidrSet(r io.Reader) (ss *IpCidrSet, err error) { + // version + version := make([]byte, 1) + _, err = io.ReadFull(r, version) + if err != nil { + return nil, err + } + if version[0] != 1 { + return nil, errors.New("version is invalid") + } + + ss = NewIpCidrSet() + var length int64 + + // rr + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ss.rr = make([]netipx.IPRange, length) + for i := int64(0); i < length; i++ { + var a16 [16]byte + err = binary.Read(r, binary.BigEndian, &a16) + if err != nil { + return nil, err + } + from := netip.AddrFrom16(a16).Unmap() + err = binary.Read(r, binary.BigEndian, &a16) + if err != nil { + return nil, err + } + to := netip.AddrFrom16(a16).Unmap() + ss.rr[i] = netipx.IPRangeFrom(from, to) + } + + return ss, nil +} diff --git a/component/process/process.go b/component/process/process.go index 76ec2c45e2..84020c4d38 100644 --- a/component/process/process.go +++ b/component/process/process.go @@ -3,6 +3,8 @@ package process import ( "errors" "net/netip" + + C "github.com/metacubex/mihomo/constant" ) var ( @@ -19,3 +21,18 @@ const ( func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) { return findProcessName(network, srcIP, srcPort) } + +// PackageNameResolver +// never change type traits because it's used in CFMA +type PackageNameResolver func(metadata *C.Metadata) (string, error) + +// DefaultPackageNameResolver +// never change type traits because it's used in CFMA +var DefaultPackageNameResolver PackageNameResolver + +func FindPackageName(metadata *C.Metadata) (string, error) { + if resolver := DefaultPackageNameResolver; resolver != nil { + return resolver(metadata) + } + return "", ErrPlatformNotSupport +} diff --git a/component/process/process_android.go b/component/process/process_android.go deleted file mode 100644 index fd5d3b6cba..0000000000 --- a/component/process/process_android.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build android && cmfa - -package process - -import "github.com/metacubex/mihomo/constant" - -type PackageNameResolver func(metadata *constant.Metadata) (string, error) - -var DefaultPackageNameResolver PackageNameResolver - -func FindPackageName(metadata *constant.Metadata) (string, error) { - if resolver := DefaultPackageNameResolver; resolver != nil { - return resolver(metadata) - } - return "", ErrPlatformNotSupport -} diff --git a/component/process/process_common.go b/component/process/process_common.go deleted file mode 100644 index fa7eeb9fc8..0000000000 --- a/component/process/process_common.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !(android && cmfa) - -package process - -import "github.com/metacubex/mihomo/constant" - -func FindPackageName(metadata *constant.Metadata) (string, error) { - return "", nil -} diff --git a/component/process/process_darwin.go b/component/process/process_darwin.go index 67d2e8332b..c02771ed78 100644 --- a/component/process/process_darwin.go +++ b/component/process/process_darwin.go @@ -46,12 +46,12 @@ func findProcessName(network string, ip netip.Addr, port int) (uint32, string, e isIPv4 := ip.Is4() - value, err := syscall.Sysctl(spath) + value, err := unix.SysctlRaw(spath) if err != nil { return 0, "", err } - buf := []byte(value) + buf := value itemSize := structSize if network == TCP { // rup8(sizeof(xtcpcb_n)) diff --git a/component/process/process_linux.go b/component/process/process_linux.go index 45c89e5a5a..3ce45ae816 100644 --- a/component/process/process_linux.go +++ b/component/process/process_linux.go @@ -2,23 +2,19 @@ package process import ( "bytes" - "context" "encoding/binary" "fmt" "net/netip" "os" + "path" "path/filepath" "runtime" "strings" - "sync" "syscall" "unicode" "unsafe" - "github.com/metacubex/mihomo/log" - "github.com/mdlayher/netlink" - tun "github.com/metacubex/sing-tun" "golang.org/x/sys/unix" ) @@ -63,25 +59,11 @@ type inetDiagResponse struct { INode uint32 } -type MyCallback struct{} - -var ( - packageManager tun.PackageManager - once sync.Once -) - -func (cb *MyCallback) OnPackagesUpdated(packageCount int, sharedCount int) {} - -func (cb *MyCallback) NewError(ctx context.Context, err error) { - log.Warnln("%s", err) -} - func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) { uid, inode, err := resolveSocketByNetlink(network, ip, srcPort) if err != nil { return 0, "", err } - pp, err := resolveProcessNameByProcSearch(inode, uid) return uid, pp, err } @@ -177,44 +159,38 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { if err != nil { continue } + if runtime.GOOS == "android" { if bytes.Equal(buffer[:n], socket) { - return findPackageName(uid), nil + cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) + if err != nil { + return "", err + } + + return splitCmdline(cmdline), nil } } else { if bytes.Equal(buffer[:n], socket) { return os.Readlink(filepath.Join(processPath, "exe")) } } - } } return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) } -func findPackageName(uid uint32) string { - once.Do(func() { - callback := &MyCallback{} - var err error - packageManager, err = tun.NewPackageManager(callback) - if err != nil { - log.Warnln("%s", err) - } - err = packageManager.Start() - if err != nil { - log.Warnln("%s", err) - return - } +func splitCmdline(cmdline []byte) string { + cmdline = bytes.Trim(cmdline, " ") + + idx := bytes.IndexFunc(cmdline, func(r rune) bool { + return unicode.IsControl(r) || unicode.IsSpace(r) }) - if sharedPackage, loaded := packageManager.SharedPackageByID(uid % 100000); loaded { - return sharedPackage - } - if packageName, loaded := packageManager.PackageByID(uid % 100000); loaded { - return packageName + if idx == -1 { + return filepath.Base(string(cmdline)) } - return "" + return filepath.Base(string(cmdline[:idx])) } func isPid(s string) bool { diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index e62912937d..c82a54a3d1 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -10,6 +10,7 @@ import ( types "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" + "github.com/sagernet/fswatch" "github.com/samber/lo" ) @@ -30,6 +31,7 @@ type Fetcher[V any] struct { parser Parser[V] interval time.Duration OnUpdate func(V) + watcher *fswatch.Watcher } func (f *Fetcher[V]) Name() string { @@ -113,7 +115,20 @@ func (f *Fetcher[V]) Initial() (V, error) { f.hash = md5.Sum(buf) // pull contents automatically - if f.interval > 0 { + if f.vehicle.Type() == types.File { + f.watcher, err = fswatch.NewWatcher(fswatch.Options{ + Path: []string{f.vehicle.Path()}, + Direct: true, + Callback: f.update, + }) + if err != nil { + return lo.Empty[V](), err + } + err = f.watcher.Start() + if err != nil { + return lo.Empty[V](), err + } + } else if f.interval > 0 { go f.pullLoop() } @@ -155,6 +170,9 @@ func (f *Fetcher[V]) Destroy() error { if f.interval > 0 { f.done <- struct{}{} } + if f.watcher != nil { + _ = f.watcher.Close() + } return nil } @@ -170,27 +188,31 @@ func (f *Fetcher[V]) pullLoop() { select { case <-timer.C: timer.Reset(f.interval) - elm, same, err := f.Update() - if err != nil { - log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error()) - continue - } - - if same { - log.Debugln("[Provider] %s's content doesn't change", f.Name()) - continue - } - - log.Infoln("[Provider] %s's content update", f.Name()) - if f.OnUpdate != nil { - f.OnUpdate(elm) - } + f.update(f.vehicle.Path()) case <-f.done: return } } } +func (f *Fetcher[V]) update(path string) { + elm, same, err := f.Update() + if err != nil { + log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error()) + return + } + + if same { + log.Debugln("[Provider] %s's content doesn't change", f.Name()) + return + } + + log.Infoln("[Provider] %s's content update", f.Name()) + if f.OnUpdate != nil { + f.OnUpdate(elm) + } +} + func safeWrite(path string, buf []byte) error { dir := filepath.Dir(path) diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 97bf162969..4438638dad 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -5,7 +5,6 @@ import ( "fmt" "net" "net/netip" - "sync" "time" "github.com/metacubex/mihomo/common/lru" @@ -30,7 +29,6 @@ type SnifferDispatcher struct { forceDomain *trie.DomainSet skipSNI *trie.DomainSet skipList *lru.LruCache[string, uint8] - rwMux sync.RWMutex forceDnsMapping bool parsePureIp bool } @@ -85,14 +83,11 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata return false } - sd.rwMux.RLock() dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) if count, ok := sd.skipList.Get(dst); ok && count > 5 { log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) - defer sd.rwMux.RUnlock() return false } - sd.rwMux.RUnlock() if host, err := sd.sniffDomain(conn, metadata); err != nil { sd.cacheSniffFailed(metadata) @@ -104,9 +99,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata return false } - sd.rwMux.RLock() sd.skipList.Delete(dst) - sd.rwMux.RUnlock() sd.replaceDomain(metadata, host, overrideDest) return true @@ -176,14 +169,13 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad } func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { - sd.rwMux.Lock() dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) - count, _ := sd.skipList.Get(dst) - if count <= 5 { - count++ - } - sd.skipList.Set(dst, count) - sd.rwMux.Unlock() + sd.skipList.Compute(dst, func(oldValue uint8, loaded bool) (newValue uint8, delete bool) { + if oldValue <= 5 { + oldValue++ + } + return oldValue, false + }) } func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { diff --git a/component/trie/domain.go b/component/trie/domain.go index 3decbb0255..db30402ede 100644 --- a/component/trie/domain.go +++ b/component/trie/domain.go @@ -123,16 +123,18 @@ func (t *DomainTrie[T]) Optimize() { t.root.optimize() } -func (t *DomainTrie[T]) Foreach(print func(domain string, data T)) { +func (t *DomainTrie[T]) Foreach(fn func(domain string, data T) bool) { for key, data := range t.root.getChildren() { - recursion([]string{key}, data, print) + recursion([]string{key}, data, fn) if data != nil && data.inited { - print(joinDomain([]string{key}), data.data) + if !fn(joinDomain([]string{key}), data.data) { + return + } } } } -func recursion[T any](items []string, node *Node[T], fn func(domain string, data T)) { +func recursion[T any](items []string, node *Node[T], fn func(domain string, data T) bool) bool { for key, data := range node.getChildren() { newItems := append([]string{key}, items...) if data != nil && data.inited { @@ -140,10 +142,15 @@ func recursion[T any](items []string, node *Node[T], fn func(domain string, data if domain[0] == domainStepByte { domain = complexWildcard + domain } - fn(domain, data.Data()) + if !fn(domain, data.Data()) { + return false + } + } + if !recursion(newItems, data, fn) { + return false } - recursion(newItems, data, fn) } + return true } func joinDomain(items []string) string { diff --git a/component/trie/domain_set.go b/component/trie/domain_set.go index 860d1235d5..7778d13379 100644 --- a/component/trie/domain_set.go +++ b/component/trie/domain_set.go @@ -28,8 +28,9 @@ type qElt struct{ s, e, col int } // NewDomainSet creates a new *DomainSet struct, from a DomainTrie. func (t *DomainTrie[T]) NewDomainSet() *DomainSet { reserveDomains := make([]string, 0) - t.Foreach(func(domain string, data T) { + t.Foreach(func(domain string, data T) bool { reserveDomains = append(reserveDomains, utils.Reverse(domain)) + return true }) // ensure that the same prefix is continuous // and according to the ascending sequence of length @@ -136,6 +137,41 @@ func (ss *DomainSet) Has(key string) bool { } +func (ss *DomainSet) keys(f func(key string) bool) { + var currentKey []byte + var traverse func(int, int) bool + traverse = func(nodeId, bmIdx int) bool { + if getBit(ss.leaves, nodeId) != 0 { + if !f(string(currentKey)) { + return false + } + } + + for ; ; bmIdx++ { + if getBit(ss.labelBitmap, bmIdx) != 0 { + return true + } + nextLabel := ss.labels[bmIdx-nodeId] + currentKey = append(currentKey, nextLabel) + nextNodeId := countZeros(ss.labelBitmap, ss.ranks, bmIdx+1) + nextBmIdx := selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nextNodeId-1) + 1 + if !traverse(nextNodeId, nextBmIdx) { + return false + } + currentKey = currentKey[:len(currentKey)-1] + } + } + + traverse(0, 0) + return +} + +func (ss *DomainSet) Foreach(f func(key string) bool) { + ss.keys(func(key string) bool { + return f(utils.Reverse(key)) + }) +} + func setBit(bm *[]uint64, i int, v int) { for i>>6 >= len(*bm) { *bm = append(*bm, 0) diff --git a/component/trie/domain_set_bin.go b/component/trie/domain_set_bin.go new file mode 100644 index 0000000000..27d15802e0 --- /dev/null +++ b/component/trie/domain_set_bin.go @@ -0,0 +1,115 @@ +package trie + +import ( + "encoding/binary" + "errors" + "io" +) + +func (ss *DomainSet) WriteBin(w io.Writer) (err error) { + // version + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + + // leaves + err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves))) + if err != nil { + return err + } + for _, d := range ss.leaves { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labelBitmap + err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap))) + if err != nil { + return err + } + for _, d := range ss.labelBitmap { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labels + err = binary.Write(w, binary.BigEndian, int64(len(ss.labels))) + if err != nil { + return err + } + _, err = w.Write(ss.labels) + if err != nil { + return err + } + + return nil +} + +func ReadDomainSetBin(r io.Reader) (ds *DomainSet, err error) { + // version + version := make([]byte, 1) + _, err = io.ReadFull(r, version) + if err != nil { + return nil, err + } + if version[0] != 1 { + return nil, errors.New("version is invalid") + } + + ds = &DomainSet{} + var length int64 + + // leaves + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.leaves = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.leaves[i]) + if err != nil { + return nil, err + } + } + + // labelBitmap + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.labelBitmap = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i]) + if err != nil { + return nil, err + } + } + + // labels + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 1 { + return nil, errors.New("length is invalid") + } + ds.labels = make([]byte, length) + _, err = io.ReadFull(r, ds.labels) + if err != nil { + return nil, err + } + + ds.init() + return ds, nil +} diff --git a/component/trie/domain_set_test.go b/component/trie/domain_set_test.go index 77106d5ffc..e343d11d1c 100644 --- a/component/trie/domain_set_test.go +++ b/component/trie/domain_set_test.go @@ -1,12 +1,29 @@ package trie_test import ( + "golang.org/x/exp/slices" "testing" "github.com/metacubex/mihomo/component/trie" "github.com/stretchr/testify/assert" ) +func testDump(t *testing.T, tree *trie.DomainTrie[struct{}], set *trie.DomainSet) { + var dataSrc []string + tree.Foreach(func(domain string, data struct{}) bool { + dataSrc = append(dataSrc, domain) + return true + }) + slices.Sort(dataSrc) + var dataSet []string + set.Foreach(func(key string) bool { + dataSet = append(dataSet, key) + return true + }) + slices.Sort(dataSet) + assert.Equal(t, dataSrc, dataSet) +} + func TestDomainSet(t *testing.T) { tree := trie.New[struct{}]() domainSet := []string{ @@ -33,6 +50,7 @@ func TestDomainSet(t *testing.T) { assert.True(t, set.Has("google.com")) assert.False(t, set.Has("qq.com")) assert.False(t, set.Has("www.baidu.com")) + testDump(t, tree, set) } func TestDomainSetComplexWildcard(t *testing.T) { @@ -55,6 +73,7 @@ func TestDomainSetComplexWildcard(t *testing.T) { assert.False(t, set.Has("google.com")) assert.True(t, set.Has("www.baidu.com")) assert.True(t, set.Has("test.test.baidu.com")) + testDump(t, tree, set) } func TestDomainSetWildcard(t *testing.T) { @@ -82,4 +101,5 @@ func TestDomainSetWildcard(t *testing.T) { assert.False(t, set.Has("a.www.google.com")) assert.False(t, set.Has("test.qq.com")) assert.False(t, set.Has("test.test.test.qq.com")) + testDump(t, tree, set) } diff --git a/component/trie/domain_test.go b/component/trie/domain_test.go index 4c5d8002d8..916f61076d 100644 --- a/component/trie/domain_test.go +++ b/component/trie/domain_test.go @@ -121,8 +121,9 @@ func TestTrie_Foreach(t *testing.T) { assert.NoError(t, tree.Insert(domain, localIP)) } count := 0 - tree.Foreach(func(domain string, data netip.Addr) { + tree.Foreach(func(domain string, data netip.Addr) bool { count++ + return true }) assert.Equal(t, 7, count) } diff --git a/config/config.go b/config/config.go index 5676f7aae4..ecd94c7dd9 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,7 @@ import ( T "github.com/metacubex/mihomo/tunnel" orderedmap "github.com/wk8/go-ordered-map/v2" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -96,6 +97,7 @@ type Controller struct { ExternalControllerTLS string `json:"-"` ExternalControllerUnix string `json:"-"` ExternalUI string `json:"-"` + ExternalDohServer string `json:"-"` Secret string `json:"-"` } @@ -322,6 +324,7 @@ type RawConfig struct { ExternalUI string `yaml:"external-ui"` ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` + ExternalDohServer string `yaml:"external-doh-server"` Secret string `yaml:"secret"` Interface string `yaml:"interface-name"` RoutingMark int `yaml:"routing-mark"` @@ -504,7 +507,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { }, Sniffer: RawSniffer{ Enable: false, - Sniffing: []string{}, + Sniff: map[string]RawSniffingConfig{}, ForceDomain: []string{}, SkipDomain: []string{}, Ports: []string{}, @@ -697,6 +700,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { Secret: cfg.Secret, ExternalControllerUnix: cfg.ExternalControllerUnix, ExternalControllerTLS: cfg.ExternalControllerTLS, + ExternalDohServer: cfg.ExternalDohServer, }, UnifiedDelay: cfg.UnifiedDelay, Mode: cfg.Mode, @@ -789,6 +793,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ AllProviders = append(AllProviders, name) } + slices.Sort(AllProxies) + slices.Sort(AllProviders) + // parse proxy group for idx, mapping := range groupsConfig { group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders) @@ -1087,13 +1094,16 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. case "tls": addr, err = hostWithDefaultPort(u.Host, "853") dnsNetType = "tcp-tls" // DNS over TLS - case "https": + case "http", "https": addr, err = hostWithDefaultPort(u.Host, "443") + dnsNetType = "https" // DNS over HTTPS + if u.Scheme == "http" { + addr, err = hostWithDefaultPort(u.Host, "80") + } if err == nil { proxyName = "" - clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path, User: u.User} + clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} addr = clearURL.String() - dnsNetType = "https" // DNS over HTTPS if len(u.Fragment) != 0 { for _, s := range strings.Split(u.Fragment, "&") { arr := strings.Split(s, "=") @@ -1566,7 +1576,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } } } else { - if sniffer.Enable { + if sniffer.Enable && len(snifferRaw.Sniffing) != 0 { // Deprecated: Use Sniff instead log.Warnln("Deprecated: Use Sniff instead") } diff --git a/constant/provider/interface.go b/constant/provider/interface.go index f7dfc9cc60..bd6b6e9470 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,6 +1,8 @@ package provider import ( + "fmt" + "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" ) @@ -110,9 +112,37 @@ func (rt RuleBehavior) String() string { } } +func (rt RuleBehavior) Byte() byte { + switch rt { + case Domain: + return 0 + case IPCIDR: + return 1 + case Classical: + return 2 + default: + return 255 + } +} + +func ParseBehavior(s string) (behavior RuleBehavior, err error) { + switch s { + case "domain": + behavior = Domain + case "ipcidr": + behavior = IPCIDR + case "classical": + behavior = Classical + default: + err = fmt.Errorf("unsupported behavior type: %s", s) + } + return +} + const ( YamlRule RuleFormat = iota TextRule + MrsRule ) type RuleFormat int @@ -123,11 +153,27 @@ func (rf RuleFormat) String() string { return "YamlRule" case TextRule: return "TextRule" + case MrsRule: + return "MrsRule" default: return "Unknown" } } +func ParseRuleFormat(s string) (format RuleFormat, err error) { + switch s { + case "", "yaml": + format = YamlRule + case "text": + format = TextRule + case "mrs": + format = MrsRule + default: + err = fmt.Errorf("unsupported format type: %s", s) + } + return +} + type Tunnel interface { Providers() map[string]ProxyProvider RuleProviders() map[string]RuleProvider diff --git a/dns/doh.go b/dns/doh.go index 54b8279657..97e01ea720 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -61,10 +61,12 @@ type dnsOverHTTPS struct { // for this upstream. quicConfig *quic.Config quicConfigGuard sync.Mutex - url *url.URL - httpVersions []C.HTTPVersion - dialer *dnsDialer - addr string + + url *url.URL + httpVersions []C.HTTPVersion + dialer *dnsDialer + addr string + skipCertVerify bool } // type check @@ -93,6 +95,10 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin httpVersions: httpVersions, } + if params["skip-cert-verify"] == "true" { + doh.skipCertVerify = true + } + runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) return doh @@ -102,6 +108,7 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin func (doh *dnsOverHTTPS) Address() string { return doh.addr } + func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { // Quote from https://www.rfc-editor.org/rfc/rfc8484.html: // In order to maximize HTTP cache friendliness, DoH clients using media @@ -178,19 +185,9 @@ func (doh *dnsOverHTTPS) closeClient(client *http.Client) (err error) { return nil } -// exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient. -func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { - resp, err = doh.exchangeHTTPSClient(ctx, client, req) - return resp, err -} - -// exchangeHTTPSClient sends the DNS query to a DoH resolver using the specified +// exchangeHTTPS sends the DNS query to a DoH resolver using the specified // http.Client instance. -func (doh *dnsOverHTTPS) exchangeHTTPSClient( - ctx context.Context, - client *http.Client, - req *D.Msg, -) (resp *D.Msg, err error) { +func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) { buf, err := req.Pack() if err != nil { return nil, fmt.Errorf("packing message: %w", err) @@ -204,24 +201,24 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( method = http3.MethodGet0RTT } - url := doh.url - url.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) - httpReq, err := http.NewRequestWithContext(ctx, method, url.String(), nil) + requestUrl := *doh.url // don't modify origin url + requestUrl.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) + httpReq, err := http.NewRequestWithContext(ctx, method, requestUrl.String(), nil) if err != nil { - return nil, fmt.Errorf("creating http request to %s: %w", url, err) + return nil, fmt.Errorf("creating http request to %s: %w", doh.url, err) } httpReq.Header.Set("Accept", "application/dns-message") httpReq.Header.Set("User-Agent", "") httpResp, err := client.Do(httpReq) if err != nil { - return nil, fmt.Errorf("requesting %s: %w", url, err) + return nil, fmt.Errorf("requesting %s: %w", doh.url, err) } defer httpResp.Body.Close() body, err := io.ReadAll(httpResp.Body) if err != nil { - return nil, fmt.Errorf("reading %s: %w", url, err) + return nil, fmt.Errorf("reading %s: %w", doh.url, err) } if httpResp.StatusCode != http.StatusOK { @@ -230,7 +227,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( "expected status %d, got %d from %s", http.StatusOK, httpResp.StatusCode, - url, + doh.url, ) } @@ -239,7 +236,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient( if err != nil { return nil, fmt.Errorf( "unpacking response from %s: body is %s: %w", - url, + doh.url, body, err, ) @@ -373,9 +370,21 @@ func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error) // HTTP3 is enabled in the upstream options). If this attempt is successful, // it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) { + transport := &http.Transport{ + DisableCompression: true, + DialContext: doh.dialer.DialContext, + IdleConnTimeout: transportDefaultIdleConnTimeout, + MaxConnsPerHost: dohMaxConnsPerHost, + MaxIdleConns: dohMaxIdleConns, + } + + if doh.url.Scheme == "http" { + return transport, nil + } + tlsConfig := ca.GetGlobalTLSConfig( &tls.Config{ - InsecureSkipVerify: false, + InsecureSkipVerify: doh.skipCertVerify, MinVersion: tls.VersionTLS12, SessionTicketsDisabled: false, }) @@ -384,6 +393,7 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp nextProtos = append(nextProtos, string(v)) } tlsConfig.NextProtos = nextProtos + transport.TLSClientConfig = tlsConfig if slices.Contains(doh.httpVersions, C.HTTPVersion3) { // First, we attempt to create an HTTP3 transport. If the probe QUIC @@ -402,18 +412,10 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream") } - transport := &http.Transport{ - TLSClientConfig: tlsConfig, - DisableCompression: true, - DialContext: doh.dialer.DialContext, - IdleConnTimeout: transportDefaultIdleConnTimeout, - MaxConnsPerHost: dohMaxConnsPerHost, - MaxIdleConns: dohMaxIdleConns, - // Since we have a custom DialContext, we need to use this field to - // make golang http.Client attempt to use HTTP/2. Otherwise, it would - // only be used when negotiated on the TLS level. - ForceAttemptHTTP2: true, - } + // Since we have a custom DialContext, we need to use this field to + // make golang http.Client attempt to use HTTP/2. Otherwise, it would + // only be used when negotiated on the TLS level. + transport.ForceAttemptHTTP2 = true // Explicitly configure transport to use HTTP/2. // diff --git a/dns/resolver.go b/dns/resolver.go index 28ffec6f15..fc228761a3 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -146,9 +146,12 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e }() q := m.Question[0] + domain := msgToDomain(m) + _, qTypeStr := msgToQtype(m) cacheM, expireTime, hit := r.cache.GetWithExpire(q.String()) if hit { - log.Debugln("[DNS] cache hit for %s, expire at %s", q.Name, expireTime.Format("2006-01-02 15:04:05")) + ips := msgToIP(cacheM) + log.Debugln("[DNS] cache hit %s --> %s %s, expire at %s", domain, ips, qTypeStr, expireTime.Format("2006-01-02 15:04:05")) now := time.Now() msg = cacheM.Copy() if expireTime.Before(now) { diff --git a/dns/util.go b/dns/util.go index e4ec5917cf..50459cc120 100644 --- a/dns/util.go +++ b/dns/util.go @@ -173,11 +173,20 @@ func msgToDomain(msg *D.Msg) string { return "" } +func msgToQtype(msg *D.Msg) (uint16, string) { + if len(msg.Question) > 0 { + qType := msg.Question[0].Qtype + return qType, D.Type(qType).String() + } + return 0, "" +} + func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) { cache = true fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) defer fast.Close() domain := msgToDomain(m) + qType, qTypeStr := msgToQtype(m) var noIpMsg *D.Msg for _, client := range clients { if _, isRCodeClient := client.(rcodeClient); isRCodeClient { @@ -186,7 +195,7 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M } client := client // shadow define client to ensure the value captured by the closure will not be changed in the next loop fast.Go(func() (*D.Msg, error) { - log.Debugln("[DNS] resolve %s from %s", domain, client.Address()) + log.Debugln("[DNS] resolve %s %s from %s", domain, qTypeStr, client.Address()) m, err := client.ExchangeContext(ctx, m) if err != nil { return nil, err @@ -195,20 +204,18 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M // so we would ignore RCode errors from RCode clients. return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode]) } - if ips := msgToIP(m); len(m.Question) > 0 { - qType := m.Question[0].Qtype - log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, D.Type(qType), client.Address()) - switch qType { - case D.TypeAAAA: - if len(ips) == 0 { - noIpMsg = m - return nil, resolver.ErrIPNotFound - } - case D.TypeA: - if len(ips) == 0 { - noIpMsg = m - return nil, resolver.ErrIPNotFound - } + ips := msgToIP(m) + log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, qTypeStr, client.Address()) + switch qType { + case D.TypeAAAA: + if len(ips) == 0 { + noIpMsg = m + return nil, resolver.ErrIPNotFound + } + case D.TypeA: + if len(ips) == 0 { + noIpMsg = m + return nil, resolver.ErrIPNotFound } } return m, nil diff --git a/docs/config.yaml b/docs/config.yaml index 9c51bc10b3..d7c686d01f 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -70,6 +70,10 @@ external-ui: /path/to/ui/folder/ external-ui-name: xd external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" +# 在RESTful API端口上开启DOH服务器 +# !!!该URL不会验证secret, 如果开启请自行保证安全问题 !!! +external-doh-server: /dns-query + # interface-name: en0 # 设置出口网卡 # 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint @@ -938,6 +942,24 @@ rule-providers: interval: 259200 path: /path/to/save/file.yaml type: file + rule3: + # mrs类型ruleset,目前仅支持domain和ipcidr(即不支持classical), + # + # 对于behavior=domain: + # - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式 + # - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式 + # - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回ymal格式) + # + # 对于behavior=ipcidr: + # - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式 + # - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式 + # - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回ymal格式) + # + type: http + url: "url" + format: mrs + behavior: domain + path: /path/to/save/file.mrs rules: - RULE-SET,rule1,REJECT - IP-ASN,1,PROXY diff --git a/go.mod b/go.mod index 4a96d14208..d5ac6beed6 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.20 require ( github.com/3andne/restls-client-go v0.1.6 - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/bahlo/generic-list-go v0.2.0 github.com/cilium/ebpf v0.12.3 github.com/coreos/go-iptables v0.7.0 @@ -15,17 +14,19 @@ require ( github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.2.0 github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 + github.com/klauspost/compress v1.17.9 github.com/klauspost/cpuid/v2 v2.2.8 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 + github.com/metacubex/chacha v0.1.0 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e github.com/metacubex/randv2 v0.2.0 github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 - github.com/metacubex/sing-shadowsocks v0.2.6 - github.com/metacubex/sing-shadowsocks2 v0.2.0 - github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e - github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f + github.com/metacubex/sing-shadowsocks v0.2.7 + github.com/metacubex/sing-shadowsocks2 v0.2.1 + github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d + github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 github.com/metacubex/utls v1.6.6 @@ -35,8 +36,9 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 github.com/puzpuzpuz/xsync/v3 v3.2.0 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a + github.com/sagernet/fswatch v0.1.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a - github.com/sagernet/sing v0.5.0-alpha.10 + github.com/sagernet/sing v0.5.0-alpha.13 github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 github.com/sagernet/sing-shadowtls v0.1.4 github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e @@ -45,13 +47,14 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/wk8/go-ordered-map/v2 v2.1.8 + gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 go.uber.org/automaxprocs v1.5.3 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.24.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 golang.org/x/sync v0.7.0 - golang.org/x/sys v0.21.0 + golang.org/x/sys v0.22.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.3.0 @@ -80,7 +83,6 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect @@ -111,4 +113,4 @@ require ( golang.org/x/tools v0.22.0 // indirect ) -replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240617013425-3e3bd9dab6a2 +replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 diff --git a/go.sum b/go.sum index 29a5d385de..7e9cd5d8a9 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4 github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= @@ -83,8 +81,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -99,6 +97,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= +github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= @@ -107,18 +107,18 @@ github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvW github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= -github.com/metacubex/sing v0.0.0-20240617013425-3e3bd9dab6a2 h1:N5tidgg/FRmkgPw/AjRwhLUinKDx/ODCSbvv9xqRoLM= -github.com/metacubex/sing v0.0.0-20240617013425-3e3bd9dab6a2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA= +github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= -github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= -github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= -github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= -github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= -github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc= -github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= +github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g= +github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= +github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg= +github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= +github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog= +github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= @@ -158,6 +158,8 @@ github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= @@ -212,6 +214,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= +gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= @@ -252,8 +256,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/hub/hub.go b/hub/hub.go index 38779e13b6..57c91aaef9 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -50,11 +50,12 @@ func Parse(options ...Option) error { if cfg.General.ExternalController != "" { go route.Start(cfg.General.ExternalController, cfg.General.ExternalControllerTLS, - cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.LogLevel == log.DEBUG) + cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.ExternalDohServer, + cfg.General.LogLevel == log.DEBUG) } if cfg.General.ExternalControllerUnix != "" { - go route.StartUnix(cfg.General.ExternalControllerUnix, cfg.General.LogLevel == log.DEBUG) + go route.StartUnix(cfg.General.ExternalControllerUnix, cfg.General.ExternalDohServer, cfg.General.LogLevel == log.DEBUG) } executor.ApplyConfig(cfg, true) diff --git a/hub/route/doh.go b/hub/route/doh.go new file mode 100644 index 0000000000..bb5416c436 --- /dev/null +++ b/hub/route/doh.go @@ -0,0 +1,63 @@ +package route + +import ( + "context" + "encoding/base64" + "io" + "net/http" + + "github.com/metacubex/mihomo/component/resolver" + + "github.com/go-chi/render" +) + +func dohRouter() http.Handler { + return http.HandlerFunc(dohHandler) +} + +func dohHandler(w http.ResponseWriter, r *http.Request) { + if resolver.DefaultResolver == nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, "DNS section is disabled") + return + } + + var dnsData []byte + var err error + switch r.Method { + case "GET": + dnsData, err = base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) + case "POST": + if r.Header.Get("Content-Type") != "application/dns-message" { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, "invalid content-type") + return + } + reader := io.LimitReader(r.Body, 65535) // according to rfc8484, the maximum size of the DNS message is 65535 bytes + dnsData, err = io.ReadAll(reader) + _ = r.Body.Close() + default: + render.Status(r, http.StatusMethodNotAllowed) + render.PlainText(w, r, "method not allowed") + return + } + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) + defer cancel() + + dnsData, err = resolver.RelayDnsPacket(ctx, dnsData, dnsData) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + w.Header().Set("Content-Type", "application/dns-message") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(dnsData) +} diff --git a/hub/route/server.go b/hub/route/server.go index c28782b9c3..165c7c6970 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -50,7 +50,7 @@ func SetUIPath(path string) { uiPath = C.Path.Resolve(path) } -func router(isDebug bool, withAuth bool) *chi.Mux { +func router(isDebug bool, withAuth bool, dohServer string) *chi.Mux { r := chi.NewRouter() corsM := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -104,11 +104,15 @@ func router(isDebug bool, withAuth bool) *chi.Mux { }) }) } + if len(dohServer) > 0 && dohServer[0] == '/' { + r.Mount(dohServer, dohRouter()) + } + return r } func Start(addr string, tlsAddr string, secret string, - certificate, privateKey string, isDebug bool) { + certificate, privateKey string, dohServer string, isDebug bool) { if serverAddr != "" { return } @@ -133,7 +137,7 @@ func Start(addr string, tlsAddr string, secret string, serverAddr = l.Addr().String() log.Infoln("RESTful API tls listening at: %s", serverAddr) tlsServe := &http.Server{ - Handler: router(isDebug, true), + Handler: router(isDebug, true, dohServer), TLSConfig: &tls.Config{ Certificates: []tls.Certificate{c}, }, @@ -152,13 +156,13 @@ func Start(addr string, tlsAddr string, secret string, serverAddr = l.Addr().String() log.Infoln("RESTful API listening at: %s", serverAddr) - if err = http.Serve(l, router(isDebug, true)); err != nil { + if err = http.Serve(l, router(isDebug, true, dohServer)); err != nil { log.Errorln("External controller serve error: %s", err) } } -func StartUnix(addr string, isDebug bool) { +func StartUnix(addr string, dohServer string, isDebug bool) { addr = C.Path.Resolve(addr) dir := filepath.Dir(addr) @@ -186,7 +190,7 @@ func StartUnix(addr string, isDebug bool) { serverAddr = l.Addr().String() log.Infoln("RESTful API unix listening at: %s", serverAddr) - if err = http.Serve(l, router(isDebug, false)); err != nil { + if err = http.Serve(l, router(isDebug, false, dohServer)); err != nil { log.Errorln("External controller unix serve error: %s", err) } } diff --git a/listener/http/client.go b/listener/http/client.go index dfd1985f8c..0f084fca7b 100644 --- a/listener/http/client.go +++ b/listener/http/client.go @@ -13,7 +13,7 @@ import ( "github.com/metacubex/mihomo/transport/socks5" ) -func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) *http.Client { +func newClient(srcConn net.Conn, tunnel C.Tunnel, additions []inbound.Addition) *http.Client { // additions using slice let caller can change its value (without size) after newClient return return &http.Client{ Transport: &http.Transport{ // from http.DefaultTransport @@ -21,6 +21,7 @@ func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, + DisableCompression: true, // prevents the Transport add "Accept-Encoding: gzip" DialContext: func(context context.Context, network, address string) (net.Conn, error) { if network != "tcp" && network != "tcp4" && network != "tcp6" { return nil, errors.New("unsupported network " + network) diff --git a/listener/http/proxy.go b/listener/http/proxy.go index c77f92307f..b2f312a578 100644 --- a/listener/http/proxy.go +++ b/listener/http/proxy.go @@ -10,10 +10,9 @@ import ( "sync" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" - authStore "github.com/metacubex/mihomo/listener/auth" "github.com/metacubex/mihomo/log" ) @@ -31,8 +30,10 @@ func (b *bodyWrapper) Read(p []byte) (n int, err error) { return n, err } -func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { - client := newClient(c, tunnel, additions...) +func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) { + additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser + inUserIdx := len(additions) - 1 + client := newClient(c, tunnel, additions) defer client.CloseIdleConnections() ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -41,7 +42,8 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], conn := N.NewBufferedConn(c) keepAlive := true - trusted := cache == nil // disable authenticate if lru is nil + trusted := authenticator == nil // disable authenticate if lru is nil + lastUser := "" for keepAlive { peekMutex.Lock() @@ -57,12 +59,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], var resp *http.Response - if !trusted { - var user string - resp, user = authenticate(request, cache) - additions = append(additions, inbound.WithInUser(user)) - trusted = resp == nil - } + var user string + resp, user = authenticate(request, authenticator) // always call authenticate function to get user + trusted = trusted || resp == nil + additions[inUserIdx] = inbound.WithInUser(user) if trusted { if request.Method == http.MethodConnect { @@ -89,6 +89,13 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], return // hijack connection } + // ensure there is a client with correct additions + // when the authenticated user changed, outbound client should close idle connections + if user != lastUser { + client.CloseIdleConnections() + lastUser = user + } + removeHopByHopHeaders(request.Header) removeExtraHTTPHostPort(request) @@ -138,34 +145,24 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], _ = conn.Close() } -func authenticate(request *http.Request, cache *lru.LruCache[string, bool]) (resp *http.Response, u string) { - authenticator := authStore.Authenticator() +func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) { if inbound.SkipAuthRemoteAddress(request.RemoteAddr) { authenticator = nil } - if authenticator != nil { - credential := parseBasicProxyAuthorization(request) - if credential == "" { - resp := responseWith(request, http.StatusProxyAuthRequired) - resp.Header.Set("Proxy-Authenticate", "Basic") - return resp, "" - } - - authed, exist := cache.Get(credential) - if !exist { - user, pass, err := decodeBasicProxyAuthorization(credential) - authed = err == nil && authenticator.Verify(user, pass) - u = user - cache.Set(credential, authed) - } - if !authed { - log.Infoln("Auth failed from %s", request.RemoteAddr) - - return responseWith(request, http.StatusForbidden), u - } + credential := parseBasicProxyAuthorization(request) + if credential == "" && authenticator != nil { + resp = responseWith(request, http.StatusProxyAuthRequired) + resp.Header.Set("Proxy-Authenticate", "Basic") + return } - - return nil, u + user, pass, err := decodeBasicProxyAuthorization(credential) + authed := authenticator == nil || (err == nil && authenticator.Verify(user, pass)) + if !authed { + log.Infoln("Auth failed from %s", request.RemoteAddr) + return responseWith(request, http.StatusForbidden), user + } + log.Debugln("Auth success from %s -> %s", request.RemoteAddr, user) + return } func responseWith(request *http.Request, statusCode int) *http.Response { diff --git a/listener/http/server.go b/listener/http/server.go index 8fc9da5962..77e10f0841 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -4,9 +4,10 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" + "github.com/metacubex/mihomo/component/auth" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" + authStore "github.com/metacubex/mihomo/listener/auth" ) type Listener struct { @@ -32,10 +33,20 @@ func (l *Listener) Close() error { } func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { - return NewWithAuthenticate(addr, tunnel, true, additions...) + return NewWithAuthenticator(addr, tunnel, authStore.Authenticator(), additions...) } +// NewWithAuthenticate +// never change type traits because it's used in CFMA func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { + authenticator := authStore.Authenticator() + if !authenticate { + authenticator = nil + } + return NewWithAuthenticator(addr, tunnel, authenticator, additions...) +} + +func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) (*Listener, error) { isDefault := false if len(additions) == 0 { isDefault = true @@ -50,11 +61,6 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi return nil, err } - var c *lru.LruCache[string, bool] - if authenticate { - c = lru.New[string, bool](lru.WithAge[string, bool](30)) - } - hl := &Listener{ listener: l, addr: addr, @@ -79,7 +85,7 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi continue } } - go HandleConn(conn, tunnel, c, additions...) + go HandleConn(conn, tunnel, authenticator, additions...) } }() diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 367b7a3683..773cabe3f1 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -4,9 +4,9 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" + authStore "github.com/metacubex/mihomo/listener/auth" "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/transport/socks4" @@ -16,7 +16,6 @@ import ( type Listener struct { listener net.Listener addr string - cache *lru.LruCache[string, bool] closed bool } @@ -53,7 +52,6 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener ml := &Listener{ listener: l, addr: addr, - cache: lru.New[string, bool](lru.WithAge[string, bool](30)), } go func() { for { @@ -70,14 +68,14 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener continue } } - go handleConn(c, tunnel, ml.cache, additions...) + go handleConn(c, tunnel, additions...) } }() return ml, nil } -func handleConn(conn net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { +func handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { N.TCPKeepAlive(conn) bufConn := N.NewBufferedConn(conn) @@ -92,6 +90,6 @@ func handleConn(conn net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool case socks5.Version: socks.HandleSocks5(bufConn, tunnel, additions...) default: - http.HandleConn(bufConn, tunnel, cache, additions...) + http.HandleConn(bufConn, tunnel, authStore.Authenticator(), additions...) } } diff --git a/listener/sing_tun/server_android.go b/listener/sing_tun/server_android.go index ac41282d51..bd5c4bd071 100644 --- a/listener/sing_tun/server_android.go +++ b/listener/sing_tun/server_android.go @@ -1,29 +1,80 @@ package sing_tun import ( + "errors" + "runtime" + "sync" + + "github.com/metacubex/mihomo/component/process" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" - tun "github.com/metacubex/sing-tun" + + "github.com/metacubex/sing-tun" "github.com/sagernet/netlink" "golang.org/x/sys/unix" - "runtime" ) -func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { - packageManager, err := tun.NewPackageManager(l.handler) +type packageManagerCallback struct{} + +func (cb *packageManagerCallback) OnPackagesUpdated(packageCount int, sharedCount int) {} + +func newPackageManager() (tun.PackageManager, error) { + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: &packageManagerCallback{}, + Logger: log.SingLogger, + }) if err != nil { - return err + return nil, err } err = packageManager.Start() + if err != nil { + return nil, err + } + return packageManager, nil +} + +var ( + globalPM tun.PackageManager + pmOnce sync.Once + pmErr error +) + +func getPackageManager() (tun.PackageManager, error) { + pmOnce.Do(func() { + globalPM, pmErr = newPackageManager() + }) + return globalPM, pmErr +} + +func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { + packageManager, err := getPackageManager() if err != nil { return err } - l.packageManager = packageManager tunOptions.BuildAndroidRules(packageManager, l.handler) return nil } -func (h *ListenerHandler) OnPackagesUpdated(packages int, sharedUsers int) { - return +func findPackageName(metadata *constant.Metadata) (string, error) { + packageManager, err := getPackageManager() + if err != nil { + return "", err + } + uid := metadata.Uid + if sharedPackage, loaded := packageManager.SharedPackageByID(uid % 100000); loaded { + return sharedPackage, nil + } + if packageName, loaded := packageManager.PackageByID(uid % 100000); loaded { + return packageName, nil + } + return "", errors.New("package not found") +} + +func init() { + if !features.CMFA { + process.DefaultPackageNameResolver = findPackageName + } } func (l *Listener) openAndroidHotspot(tunOptions tun.Options) { diff --git a/main.go b/main.go index 61f1d683bd..cd903ce6d9 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/rules/provider" "go.uber.org/automaxprocs/maxprocs" ) @@ -48,6 +49,12 @@ func init() { func main() { _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + + if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" { + provider.ConvertMain(os.Args[2:]) + return + } + if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) diff --git a/rules/provider/classical_strategy.go b/rules/provider/classical_strategy.go index 8353ebce40..205a8e5996 100644 --- a/rules/provider/classical_strategy.go +++ b/rules/provider/classical_strategy.go @@ -5,6 +5,7 @@ import ( "strings" C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" ) @@ -16,6 +17,10 @@ type classicalStrategy struct { parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) } +func (c *classicalStrategy) Behavior() P.RuleBehavior { + return P.Classical +} + func (c *classicalStrategy) Match(metadata *C.Metadata) bool { for _, rule := range c.rules { if m, _ := rule.Match(metadata); m { diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index c0787d5839..b893f038b2 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -1,9 +1,16 @@ package provider import ( + "errors" + "io" + "strings" + "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" + + "golang.org/x/exp/slices" ) type domainStrategy struct { @@ -12,6 +19,10 @@ type domainStrategy struct { domainSet *trie.DomainSet } +func (d *domainStrategy) Behavior() P.RuleBehavior { + return P.Domain +} + func (d *domainStrategy) ShouldFindProcess() bool { return false } @@ -35,6 +46,10 @@ func (d *domainStrategy) Reset() { } func (d *domainStrategy) Insert(rule string) { + if strings.ContainsRune(rule, '/') { + log.Warnln("invalid domain:[%s]", rule) + return + } err := d.domainTrie.Insert(rule, struct{}{}) if err != nil { log.Warnln("invalid domain:[%s]", rule) @@ -48,6 +63,45 @@ func (d *domainStrategy) FinishInsert() { d.domainTrie = nil } +func (d *domainStrategy) FromMrs(r io.Reader, count int) error { + domainSet, err := trie.ReadDomainSetBin(r) + if err != nil { + return err + } + d.count = count + d.domainSet = domainSet + return nil +} + +func (d *domainStrategy) WriteMrs(w io.Writer) error { + if d.domainSet == nil { + return errors.New("nil domainSet") + } + return d.domainSet.WriteBin(w) +} + +func (d *domainStrategy) DumpMrs(f func(key string) bool) { + if d.domainSet != nil { + var keys []string + d.domainSet.Foreach(func(key string) bool { + keys = append(keys, key) + return true + }) + slices.Sort(keys) + + for _, key := range keys { + if _, ok := slices.BinarySearch(keys, "+."+key); ok { + continue // ignore the rules added by trie internal processing + } + if !f(key) { + return + } + } + } +} + +var _ mrsRuleStrategy = (*domainStrategy)(nil) + func NewDomainStrategy() *domainStrategy { return &domainStrategy{} } diff --git a/rules/provider/ipcidr_strategy.go b/rules/provider/ipcidr_strategy.go index d0545c7cc2..9efffed96b 100644 --- a/rules/provider/ipcidr_strategy.go +++ b/rules/provider/ipcidr_strategy.go @@ -1,8 +1,13 @@ package provider import ( + "errors" + "io" + "net/netip" + "github.com/metacubex/mihomo/component/cidr" C "github.com/metacubex/mihomo/constant" + P "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/log" "go4.org/netipx" @@ -15,6 +20,10 @@ type ipcidrStrategy struct { //trie *trie.IpCidrTrie } +func (i *ipcidrStrategy) Behavior() P.RuleBehavior { + return P.IPCIDR +} + func (i *ipcidrStrategy) ShouldFindProcess() bool { return false } @@ -54,6 +63,34 @@ func (i *ipcidrStrategy) FinishInsert() { i.cidrSet.Merge() } +func (i *ipcidrStrategy) FromMrs(r io.Reader, count int) error { + cidrSet, err := cidr.ReadIpCidrSet(r) + if err != nil { + return err + } + i.count = count + i.cidrSet = cidrSet + if i.count > 0 { + i.shouldResolveIP = true + } + return nil +} + +func (i *ipcidrStrategy) WriteMrs(w io.Writer) error { + if i.cidrSet == nil { + return errors.New("nil cidrSet") + } + return i.cidrSet.WriteBin(w) +} + +func (i *ipcidrStrategy) DumpMrs(f func(key string) bool) { + if i.cidrSet != nil { + i.cidrSet.Foreach(func(prefix netip.Prefix) bool { + return f(prefix.String()) + }) + } +} + func (i *ipcidrStrategy) ToIpCidr() *netipx.IPSet { return i.cidrSet.ToIPSet() } diff --git a/rules/provider/mrs_converter.go b/rules/provider/mrs_converter.go new file mode 100644 index 0000000000..edc24e7eea --- /dev/null +++ b/rules/provider/mrs_converter.go @@ -0,0 +1,120 @@ +package provider + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "os" + + P "github.com/metacubex/mihomo/constant/provider" + + "github.com/klauspost/compress/zstd" +) + +func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) { + strategy := newStrategy(behavior, nil) + strategy, err = rulesParse(buf, strategy, format) + if err != nil { + return err + } + if strategy.Count() == 0 { + return errors.New("empty rule") + } + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + if format == P.MrsRule { // export to TextRule + _strategy.DumpMrs(func(key string) bool { + _, err = fmt.Fprintln(w, key) + if err != nil { + return false + } + return true + }) + return nil + } + + var encoder *zstd.Encoder + encoder, err = zstd.NewWriter(w) + if err != nil { + return err + } + defer func() { + zstdErr := encoder.Close() + if err == nil { + err = zstdErr + } + }() + + // header + _, err = encoder.Write(MrsMagicBytes[:]) + if err != nil { + return err + } + + // behavior + _behavior := []byte{behavior.Byte()} + _, err = encoder.Write(_behavior[:]) + if err != nil { + return err + } + + // count + count := int64(_strategy.Count()) + err = binary.Write(encoder, binary.BigEndian, count) + if err != nil { + return err + } + + // extra (reserved for future using) + var extra []byte + err = binary.Write(encoder, binary.BigEndian, int64(len(extra))) + if err != nil { + return err + } + _, err = encoder.Write(extra) + if err != nil { + return err + } + + return _strategy.WriteMrs(encoder) + } else { + return ErrInvalidFormat + } +} + +func ConvertMain(args []string) { + if len(args) > 3 { + behavior, err := P.ParseBehavior(args[0]) + if err != nil { + panic(err) + } + format, err := P.ParseRuleFormat(args[1]) + if err != nil { + panic(err) + } + source := args[2] + target := args[3] + + sourceFile, err := os.ReadFile(source) + if err != nil { + panic(err) + } + + targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + + err = ConvertToMrs(sourceFile, behavior, format, targetFile) + if err != nil { + panic(err) + } + + err = targetFile.Close() + if err != nil { + panic(err) + } + } else { + panic("Usage: convert-ruleset ") + } +} diff --git a/rules/provider/mrs_reader.go b/rules/provider/mrs_reader.go new file mode 100644 index 0000000000..66f62127c8 --- /dev/null +++ b/rules/provider/mrs_reader.go @@ -0,0 +1,72 @@ +package provider + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/klauspost/compress/zstd" +) + +var MrsMagicBytes = [4]byte{'M', 'R', 'S', 1} // MRSv1 + +func rulesMrsParse(buf []byte, strategy ruleStrategy) (ruleStrategy, error) { + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + reader, err := zstd.NewReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + defer reader.Close() + + // header + var header [4]byte + _, err = io.ReadFull(reader, header[:]) + if err != nil { + return nil, err + } + if header != MrsMagicBytes { + return nil, fmt.Errorf("invalid MrsMagic bytes") + } + + // behavior + var _behavior [1]byte + _, err = io.ReadFull(reader, _behavior[:]) + if err != nil { + return nil, err + } + if _behavior[0] != strategy.Behavior().Byte() { + return nil, fmt.Errorf("invalid behavior") + } + + // count + var count int64 + err = binary.Read(reader, binary.BigEndian, &count) + if err != nil { + return nil, err + } + + // extra (reserved for future using) + var length int64 + err = binary.Read(reader, binary.BigEndian, &length) + if err != nil { + return nil, err + } + if length < 0 { + return nil, errors.New("length is invalid") + } + if length > 0 { + extra := make([]byte, length) + _, err = io.ReadFull(reader, extra) + if err != nil { + return nil, err + } + } + + err = _strategy.FromMrs(reader, int(count)) + return strategy, err + } else { + return nil, ErrInvalidFormat + } +} diff --git a/rules/provider/parse.go b/rules/provider/parse.go index a20da28d5c..227debb3a0 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -32,28 +32,13 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t if err := decoder.Decode(mapping, schema); err != nil { return nil, err } - var behavior P.RuleBehavior - - switch schema.Behavior { - case "domain": - behavior = P.Domain - case "ipcidr": - behavior = P.IPCIDR - case "classical": - behavior = P.Classical - default: - return nil, fmt.Errorf("unsupported behavior type: %s", schema.Behavior) + behavior, err := P.ParseBehavior(schema.Behavior) + if err != nil { + return nil, err } - - var format P.RuleFormat - - switch schema.Format { - case "", "yaml": - format = P.YamlRule - case "text": - format = P.TextRule - default: - return nil, fmt.Errorf("unsupported format type: %s", schema.Format) + format, err := P.ParseRuleFormat(schema.Format) + if err != nil { + return nil, err } var vehicle P.Vehicle diff --git a/rules/provider/provider.go b/rules/provider/provider.go index 6c03c6e5f3..b9524c35e6 100644 --- a/rules/provider/provider.go +++ b/rules/provider/provider.go @@ -4,16 +4,17 @@ import ( "bytes" "encoding/json" "errors" + "io" "runtime" "strings" "time" - "gopkg.in/yaml.v3" - "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" + + "gopkg.in/yaml.v3" ) var tunnel P.Tunnel @@ -43,6 +44,7 @@ type RulePayload struct { } type ruleStrategy interface { + Behavior() P.RuleBehavior Match(metadata *C.Metadata) bool Count() int ShouldResolveIP() bool @@ -52,6 +54,13 @@ type ruleStrategy interface { FinishInsert() } +type mrsRuleStrategy interface { + ruleStrategy + FromMrs(r io.Reader, count int) error + WriteMrs(w io.Writer) error + DumpMrs(f func(key string) bool) +} + func (rp *ruleSetProvider) Type() P.ProviderType { return P.Rule } @@ -152,9 +161,13 @@ func newStrategy(behavior P.RuleBehavior, parse func(tp, payload, target string, } var ErrNoPayload = errors.New("file must have a `payload` field") +var ErrInvalidFormat = errors.New("invalid format") func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) { strategy.Reset() + if format == P.MrsRule { + return rulesMrsParse(buf, strategy) + } schema := &RulePayload{} @@ -228,6 +241,8 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStr if len(schema.Payload) > 0 { str = schema.Payload[0] } + default: + return nil, ErrInvalidFormat } if str == "" { diff --git a/transport/shadowsocks/core/cipher.go b/transport/shadowsocks/core/cipher.go index 44b2e8d42c..7cb12c45c3 100644 --- a/transport/shadowsocks/core/cipher.go +++ b/transport/shadowsocks/core/cipher.go @@ -34,6 +34,11 @@ const ( aeadAes256Gcm = "AEAD_AES_256_GCM" aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305" + aeadChacha8Poly1305 = "AEAD_CHACHA8_POLY1305" + aeadXChacha8Poly1305 = "AEAD_XCHACHA8_POLY1305" + aeadAes128Ccm = "AEAD_AES_128_CCM" + aeadAes192Ccm = "AEAD_AES_192_CCM" + aeadAes256Ccm = "AEAD_AES_256_CCM" ) // List of AEAD ciphers: key size in bytes and constructor @@ -46,6 +51,11 @@ var aeadList = map[string]struct { aeadAes256Gcm: {32, shadowaead.AESGCM}, aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, + aeadChacha8Poly1305: {32, shadowaead.Chacha8Poly1305}, + aeadXChacha8Poly1305: {32, shadowaead.XChacha8Poly1305}, + aeadAes128Ccm: {16, shadowaead.AESCCM}, + aeadAes192Ccm: {24, shadowaead.AESCCM}, + aeadAes256Ccm: {32, shadowaead.AESCCM}, } // List of stream ciphers: key size in bytes and constructor @@ -95,6 +105,16 @@ func PickCipher(name string, key []byte, password string) (Cipher, error) { name = aeadAes192Gcm case "AES-256-GCM": name = aeadAes256Gcm + case "CHACHA8-IETF-POLY1305": + name = aeadChacha8Poly1305 + case "XCHACHA8-IETF-POLY1305": + name = aeadXChacha8Poly1305 + case "AES-128-CCM": + name = aeadAes128Ccm + case "AES-192-CCM": + name = aeadAes192Ccm + case "AES-256-CCM": + name = aeadAes256Ccm } if choice, ok := aeadList[name]; ok { diff --git a/transport/shadowsocks/shadowaead/cipher.go b/transport/shadowsocks/shadowaead/cipher.go index 3cf7574974..7981d5b15a 100644 --- a/transport/shadowsocks/shadowaead/cipher.go +++ b/transport/shadowsocks/shadowaead/cipher.go @@ -7,6 +7,8 @@ import ( "io" "strconv" + "github.com/metacubex/chacha" + "gitlab.com/go-extension/aes-ccm" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) @@ -75,6 +77,25 @@ func AESGCM(psk []byte) (Cipher, error) { return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil } +func aesCCM(key []byte) (cipher.AEAD, error) { + blk, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return ccm.NewCCM(blk) +} + +// AESCCM creates a new Cipher with a pre-shared key. len(psk) must be +// one of 16, 24, or 32 to select AES-128/196/256-GCM. +func AESCCM(psk []byte) (Cipher, error) { + switch l := len(psk); l { + case 16, 24, 32: // AES 128/196/256 + default: + return nil, aes.KeySizeError(l) + } + return &metaCipher{psk: psk, makeAEAD: aesCCM}, nil +} + // Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk) // must be 32. func Chacha20Poly1305(psk []byte) (Cipher, error) { @@ -92,3 +113,21 @@ func XChacha20Poly1305(psk []byte) (Cipher, error) { } return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil } + +// Chacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func Chacha8Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha.NewChaCha8IETFPoly1305}, nil +} + +// XChacha8Poly1305 creates a new Cipher with a pre-shared key. len(psk) +// must be 32. +func XChacha8Poly1305(psk []byte) (Cipher, error) { + if len(psk) != chacha.KeySize { + return nil, KeySizeError(chacha.KeySize) + } + return &metaCipher{psk: psk, makeAEAD: chacha.NewXChaCha20IETFPoly1305}, nil +} diff --git a/transport/shadowsocks/shadowstream/old_chacha20.go b/transport/shadowsocks/shadowstream/old_chacha20.go index 65737fccd1..eb61232e30 100644 --- a/transport/shadowsocks/shadowstream/old_chacha20.go +++ b/transport/shadowsocks/shadowstream/old_chacha20.go @@ -2,7 +2,8 @@ package shadowstream import ( "crypto/cipher" - "github.com/aead/chacha20/chacha" + + "github.com/metacubex/chacha" ) type chacha20key []byte @@ -11,7 +12,7 @@ func (k chacha20key) IVSize() int { return chacha.NonceSize } func (k chacha20key) Encrypter(iv []byte) cipher.Stream { - c, _ := chacha.NewCipher(iv, k, 20) + c, _ := chacha.NewChaCha20(iv, k) return c } func (k chacha20key) Decrypter(iv []byte) cipher.Stream { diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 1a6f104dc9..5dd468f3b4 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -622,6 +622,10 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { metadata.Process = filepath.Base(path) metadata.ProcessPath = path metadata.Uid = uid + + if pkg, err := P.FindPackageName(metadata); err == nil { // for android (not CMFA) package names + metadata.Process = pkg + } } } else { // check package names