Merge branch 'Alpha' into Meta

This commit is contained in:
github-actions[bot] 2024-07-28 05:50:11 +00:00
commit 0128a0bb1f
48 changed files with 1142 additions and 285 deletions

View File

@ -69,3 +69,5 @@ func WithDSCP(dscp uint8) Addition {
metadata.DSCP = dscp metadata.DSCP = dscp
} }
} }
func Placeholder(metadata *C.Metadata) {}

View File

@ -205,7 +205,6 @@ func strategyStickySessions(url string) strategyFn {
proxy := proxies[nowIdx] proxy := proxies[nowIdx]
if proxy.AliveForTestUrl(url) { if proxy.AliveForTestUrl(url) {
if nowIdx != idx { if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx) lruCache.Set(key, nowIdx)
} }
@ -215,7 +214,6 @@ func strategyStickySessions(url string) strategyFn {
} }
} }
lruCache.Delete(key)
lruCache.Set(key, 0) lruCache.Set(key, 0)
return proxies[0] return proxies[0]
} }

View File

@ -69,7 +69,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
} }
if groupOption.IncludeAllProviders { if groupOption.IncludeAllProviders {
groupOption.Use = append(groupOption.Use, AllProviders...) groupOption.Use = AllProviders
} }
if groupOption.IncludeAllProxies { if groupOption.IncludeAllProxies {
if groupOption.Filter != "" { if groupOption.Filter != "" {

View File

@ -223,6 +223,10 @@ func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.delete(key)
}
func (c *LruCache[K, V]) delete(key K) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.deleteElement(le) c.deleteElement(le)
} }
@ -255,6 +259,34 @@ func (c *LruCache[K, V]) Clear() error {
return nil 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 { type entry[K comparable, V any] struct {
key K key K
value V value V

View File

@ -59,8 +59,8 @@ func (q *Queue[T]) Copy() []T {
// Len returns the number of items in this queue. // Len returns the number of items in this queue.
func (q *Queue[T]) Len() int64 { func (q *Queue[T]) Len() int64 {
q.lock.Lock() q.lock.RLock()
defer q.lock.Unlock() defer q.lock.RUnlock()
return int64(len(q.items)) return int64(len(q.items))
} }

View File

@ -17,8 +17,8 @@ func NewCallback[T any]() *Callback[T] {
} }
func (c *Callback[T]) Register(item func(T)) io.Closer { func (c *Callback[T]) Register(item func(T)) io.Closer {
c.mutex.RLock() c.mutex.Lock()
defer c.mutex.RUnlock() defer c.mutex.Unlock()
element := c.list.PushBack(item) element := c.list.PushBack(item)
return &callbackCloser[T]{ return &callbackCloser[T]{
element: element, element: element,

View File

@ -57,6 +57,16 @@ func (set *IpCidrSet) Merge() error {
return nil 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 // ToIPSet not safe convert to *netipx.IPSet
// be careful, must be used after Merge // be careful, must be used after Merge
func (set *IpCidrSet) ToIPSet() *netipx.IPSet { func (set *IpCidrSet) ToIPSet() *netipx.IPSet {

View File

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

View File

@ -3,6 +3,8 @@ package process
import ( import (
"errors" "errors"
"net/netip" "net/netip"
C "github.com/metacubex/mihomo/constant"
) )
var ( var (
@ -19,3 +21,18 @@ const (
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) { func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, string, error) {
return findProcessName(network, srcIP, srcPort) 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
}

View File

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

View File

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

View File

@ -46,12 +46,12 @@ func findProcessName(network string, ip netip.Addr, port int) (uint32, string, e
isIPv4 := ip.Is4() isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath) value, err := unix.SysctlRaw(spath)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
buf := []byte(value) buf := value
itemSize := structSize itemSize := structSize
if network == TCP { if network == TCP {
// rup8(sizeof(xtcpcb_n)) // rup8(sizeof(xtcpcb_n))

View File

@ -2,23 +2,19 @@ package process
import ( import (
"bytes" "bytes"
"context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net/netip" "net/netip"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"unicode" "unicode"
"unsafe" "unsafe"
"github.com/metacubex/mihomo/log"
"github.com/mdlayher/netlink" "github.com/mdlayher/netlink"
tun "github.com/metacubex/sing-tun"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -63,25 +59,11 @@ type inetDiagResponse struct {
INode uint32 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) { func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) {
uid, inode, err := resolveSocketByNetlink(network, ip, srcPort) uid, inode, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
pp, err := resolveProcessNameByProcSearch(inode, uid) pp, err := resolveProcessNameByProcSearch(inode, uid)
return uid, pp, err return uid, pp, err
} }
@ -177,44 +159,38 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
if err != nil { if err != nil {
continue continue
} }
if runtime.GOOS == "android" { if runtime.GOOS == "android" {
if bytes.Equal(buffer[:n], socket) { 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 { } else {
if bytes.Equal(buffer[:n], socket) { if bytes.Equal(buffer[:n], socket) {
return os.Readlink(filepath.Join(processPath, "exe")) return os.Readlink(filepath.Join(processPath, "exe"))
} }
} }
} }
} }
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
} }
func findPackageName(uid uint32) string { func splitCmdline(cmdline []byte) string {
once.Do(func() { cmdline = bytes.Trim(cmdline, " ")
callback := &MyCallback{}
var err error idx := bytes.IndexFunc(cmdline, func(r rune) bool {
packageManager, err = tun.NewPackageManager(callback) return unicode.IsControl(r) || unicode.IsSpace(r)
if err != nil {
log.Warnln("%s", err)
}
err = packageManager.Start()
if err != nil {
log.Warnln("%s", err)
return
}
}) })
if sharedPackage, loaded := packageManager.SharedPackageByID(uid % 100000); loaded { if idx == -1 {
return sharedPackage return filepath.Base(string(cmdline))
} }
if packageName, loaded := packageManager.PackageByID(uid % 100000); loaded { return filepath.Base(string(cmdline[:idx]))
return packageName
}
return ""
} }
func isPid(s string) bool { func isPid(s string) bool {

View File

@ -10,6 +10,7 @@ import (
types "github.com/metacubex/mihomo/constant/provider" types "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/sagernet/fswatch"
"github.com/samber/lo" "github.com/samber/lo"
) )
@ -30,6 +31,7 @@ type Fetcher[V any] struct {
parser Parser[V] parser Parser[V]
interval time.Duration interval time.Duration
OnUpdate func(V) OnUpdate func(V)
watcher *fswatch.Watcher
} }
func (f *Fetcher[V]) Name() string { func (f *Fetcher[V]) Name() string {
@ -113,7 +115,20 @@ func (f *Fetcher[V]) Initial() (V, error) {
f.hash = md5.Sum(buf) f.hash = md5.Sum(buf)
// pull contents automatically // 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() go f.pullLoop()
} }
@ -155,6 +170,9 @@ func (f *Fetcher[V]) Destroy() error {
if f.interval > 0 { if f.interval > 0 {
f.done <- struct{}{} f.done <- struct{}{}
} }
if f.watcher != nil {
_ = f.watcher.Close()
}
return nil return nil
} }
@ -170,27 +188,31 @@ func (f *Fetcher[V]) pullLoop() {
select { select {
case <-timer.C: case <-timer.C:
timer.Reset(f.interval) timer.Reset(f.interval)
elm, same, err := f.Update() f.update(f.vehicle.Path())
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)
}
case <-f.done: case <-f.done:
return 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 { func safeWrite(path string, buf []byte) error {
dir := filepath.Dir(path) dir := filepath.Dir(path)

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"sync"
"time" "time"
"github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/lru"
@ -30,7 +29,6 @@ type SnifferDispatcher struct {
forceDomain *trie.DomainSet forceDomain *trie.DomainSet
skipSNI *trie.DomainSet skipSNI *trie.DomainSet
skipList *lru.LruCache[string, uint8] skipList *lru.LruCache[string, uint8]
rwMux sync.RWMutex
forceDnsMapping bool forceDnsMapping bool
parsePureIp bool parsePureIp bool
} }
@ -85,14 +83,11 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
return false return false
} }
sd.rwMux.RLock()
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
if count, ok := sd.skipList.Get(dst); ok && count > 5 { if count, ok := sd.skipList.Get(dst); ok && count > 5 {
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
defer sd.rwMux.RUnlock()
return false return false
} }
sd.rwMux.RUnlock()
if host, err := sd.sniffDomain(conn, metadata); err != nil { if host, err := sd.sniffDomain(conn, metadata); err != nil {
sd.cacheSniffFailed(metadata) sd.cacheSniffFailed(metadata)
@ -104,9 +99,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
return false return false
} }
sd.rwMux.RLock()
sd.skipList.Delete(dst) sd.skipList.Delete(dst)
sd.rwMux.RUnlock()
sd.replaceDomain(metadata, host, overrideDest) sd.replaceDomain(metadata, host, overrideDest)
return true return true
@ -176,14 +169,13 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
} }
func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) {
sd.rwMux.Lock()
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
count, _ := sd.skipList.Get(dst) sd.skipList.Compute(dst, func(oldValue uint8, loaded bool) (newValue uint8, delete bool) {
if count <= 5 { if oldValue <= 5 {
count++ oldValue++
} }
sd.skipList.Set(dst, count) return oldValue, false
sd.rwMux.Unlock() })
} }
func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) { func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) {

View File

@ -123,16 +123,18 @@ func (t *DomainTrie[T]) Optimize() {
t.root.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() { for key, data := range t.root.getChildren() {
recursion([]string{key}, data, print) recursion([]string{key}, data, fn)
if data != nil && data.inited { 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() { for key, data := range node.getChildren() {
newItems := append([]string{key}, items...) newItems := append([]string{key}, items...)
if data != nil && data.inited { 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 { if domain[0] == domainStepByte {
domain = complexWildcard + domain 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 { func joinDomain(items []string) string {

View File

@ -28,8 +28,9 @@ type qElt struct{ s, e, col int }
// NewDomainSet creates a new *DomainSet struct, from a DomainTrie. // NewDomainSet creates a new *DomainSet struct, from a DomainTrie.
func (t *DomainTrie[T]) NewDomainSet() *DomainSet { func (t *DomainTrie[T]) NewDomainSet() *DomainSet {
reserveDomains := make([]string, 0) 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)) reserveDomains = append(reserveDomains, utils.Reverse(domain))
return true
}) })
// ensure that the same prefix is continuous // ensure that the same prefix is continuous
// and according to the ascending sequence of length // 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) { func setBit(bm *[]uint64, i int, v int) {
for i>>6 >= len(*bm) { for i>>6 >= len(*bm) {
*bm = append(*bm, 0) *bm = append(*bm, 0)

View File

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

View File

@ -1,12 +1,29 @@
package trie_test package trie_test
import ( import (
"golang.org/x/exp/slices"
"testing" "testing"
"github.com/metacubex/mihomo/component/trie" "github.com/metacubex/mihomo/component/trie"
"github.com/stretchr/testify/assert" "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) { func TestDomainSet(t *testing.T) {
tree := trie.New[struct{}]() tree := trie.New[struct{}]()
domainSet := []string{ domainSet := []string{
@ -33,6 +50,7 @@ func TestDomainSet(t *testing.T) {
assert.True(t, set.Has("google.com")) assert.True(t, set.Has("google.com"))
assert.False(t, set.Has("qq.com")) assert.False(t, set.Has("qq.com"))
assert.False(t, set.Has("www.baidu.com")) assert.False(t, set.Has("www.baidu.com"))
testDump(t, tree, set)
} }
func TestDomainSetComplexWildcard(t *testing.T) { func TestDomainSetComplexWildcard(t *testing.T) {
@ -55,6 +73,7 @@ func TestDomainSetComplexWildcard(t *testing.T) {
assert.False(t, set.Has("google.com")) assert.False(t, set.Has("google.com"))
assert.True(t, set.Has("www.baidu.com")) assert.True(t, set.Has("www.baidu.com"))
assert.True(t, set.Has("test.test.baidu.com")) assert.True(t, set.Has("test.test.baidu.com"))
testDump(t, tree, set)
} }
func TestDomainSetWildcard(t *testing.T) { 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("a.www.google.com"))
assert.False(t, set.Has("test.qq.com")) assert.False(t, set.Has("test.qq.com"))
assert.False(t, set.Has("test.test.test.qq.com")) assert.False(t, set.Has("test.test.test.qq.com"))
testDump(t, tree, set)
} }

View File

@ -121,8 +121,9 @@ func TestTrie_Foreach(t *testing.T) {
assert.NoError(t, tree.Insert(domain, localIP)) assert.NoError(t, tree.Insert(domain, localIP))
} }
count := 0 count := 0
tree.Foreach(func(domain string, data netip.Addr) { tree.Foreach(func(domain string, data netip.Addr) bool {
count++ count++
return true
}) })
assert.Equal(t, 7, count) assert.Equal(t, 7, count)
} }

View File

@ -42,6 +42,7 @@ import (
T "github.com/metacubex/mihomo/tunnel" T "github.com/metacubex/mihomo/tunnel"
orderedmap "github.com/wk8/go-ordered-map/v2" orderedmap "github.com/wk8/go-ordered-map/v2"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -96,6 +97,7 @@ type Controller struct {
ExternalControllerTLS string `json:"-"` ExternalControllerTLS string `json:"-"`
ExternalControllerUnix string `json:"-"` ExternalControllerUnix string `json:"-"`
ExternalUI string `json:"-"` ExternalUI string `json:"-"`
ExternalDohServer string `json:"-"`
Secret string `json:"-"` Secret string `json:"-"`
} }
@ -322,6 +324,7 @@ type RawConfig struct {
ExternalUI string `yaml:"external-ui"` ExternalUI string `yaml:"external-ui"`
ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"` ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"`
ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"` ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"`
ExternalDohServer string `yaml:"external-doh-server"`
Secret string `yaml:"secret"` Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"` Interface string `yaml:"interface-name"`
RoutingMark int `yaml:"routing-mark"` RoutingMark int `yaml:"routing-mark"`
@ -504,7 +507,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
}, },
Sniffer: RawSniffer{ Sniffer: RawSniffer{
Enable: false, Enable: false,
Sniffing: []string{}, Sniff: map[string]RawSniffingConfig{},
ForceDomain: []string{}, ForceDomain: []string{},
SkipDomain: []string{}, SkipDomain: []string{},
Ports: []string{}, Ports: []string{},
@ -697,6 +700,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
Secret: cfg.Secret, Secret: cfg.Secret,
ExternalControllerUnix: cfg.ExternalControllerUnix, ExternalControllerUnix: cfg.ExternalControllerUnix,
ExternalControllerTLS: cfg.ExternalControllerTLS, ExternalControllerTLS: cfg.ExternalControllerTLS,
ExternalDohServer: cfg.ExternalDohServer,
}, },
UnifiedDelay: cfg.UnifiedDelay, UnifiedDelay: cfg.UnifiedDelay,
Mode: cfg.Mode, Mode: cfg.Mode,
@ -789,6 +793,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
AllProviders = append(AllProviders, name) AllProviders = append(AllProviders, name)
} }
slices.Sort(AllProxies)
slices.Sort(AllProviders)
// parse proxy group // parse proxy group
for idx, mapping := range groupsConfig { for idx, mapping := range groupsConfig {
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders) group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap, AllProxies, AllProviders)
@ -1087,13 +1094,16 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
case "tls": case "tls":
addr, err = hostWithDefaultPort(u.Host, "853") addr, err = hostWithDefaultPort(u.Host, "853")
dnsNetType = "tcp-tls" // DNS over TLS dnsNetType = "tcp-tls" // DNS over TLS
case "https": case "http", "https":
addr, err = hostWithDefaultPort(u.Host, "443") addr, err = hostWithDefaultPort(u.Host, "443")
dnsNetType = "https" // DNS over HTTPS
if u.Scheme == "http" {
addr, err = hostWithDefaultPort(u.Host, "80")
}
if err == nil { if err == nil {
proxyName = "" 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() addr = clearURL.String()
dnsNetType = "https" // DNS over HTTPS
if len(u.Fragment) != 0 { if len(u.Fragment) != 0 {
for _, s := range strings.Split(u.Fragment, "&") { for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.Split(s, "=") arr := strings.Split(s, "=")
@ -1566,7 +1576,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
} }
} }
} else { } else {
if sniffer.Enable { if sniffer.Enable && len(snifferRaw.Sniffing) != 0 {
// Deprecated: Use Sniff instead // Deprecated: Use Sniff instead
log.Warnln("Deprecated: Use Sniff instead") log.Warnln("Deprecated: Use Sniff instead")
} }

View File

@ -1,6 +1,8 @@
package provider package provider
import ( import (
"fmt"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/constant" "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 ( const (
YamlRule RuleFormat = iota YamlRule RuleFormat = iota
TextRule TextRule
MrsRule
) )
type RuleFormat int type RuleFormat int
@ -123,11 +153,27 @@ func (rf RuleFormat) String() string {
return "YamlRule" return "YamlRule"
case TextRule: case TextRule:
return "TextRule" return "TextRule"
case MrsRule:
return "MrsRule"
default: default:
return "Unknown" 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 { type Tunnel interface {
Providers() map[string]ProxyProvider Providers() map[string]ProxyProvider
RuleProviders() map[string]RuleProvider RuleProviders() map[string]RuleProvider

View File

@ -61,10 +61,12 @@ type dnsOverHTTPS struct {
// for this upstream. // for this upstream.
quicConfig *quic.Config quicConfig *quic.Config
quicConfigGuard sync.Mutex quicConfigGuard sync.Mutex
url *url.URL
httpVersions []C.HTTPVersion url *url.URL
dialer *dnsDialer httpVersions []C.HTTPVersion
addr string dialer *dnsDialer
addr string
skipCertVerify bool
} }
// type check // type check
@ -93,6 +95,10 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin
httpVersions: httpVersions, httpVersions: httpVersions,
} }
if params["skip-cert-verify"] == "true" {
doh.skipCertVerify = true
}
runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close)
return doh return doh
@ -102,6 +108,7 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin
func (doh *dnsOverHTTPS) Address() string { func (doh *dnsOverHTTPS) Address() string {
return doh.addr return doh.addr
} }
func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { 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: // Quote from https://www.rfc-editor.org/rfc/rfc8484.html:
// In order to maximize HTTP cache friendliness, DoH clients using media // 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 return nil
} }
// exchangeHTTPS logs the request and its result and calls exchangeHTTPSClient. // exchangeHTTPS sends the DNS query to a DoH resolver using the specified
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
// http.Client instance. // http.Client instance.
func (doh *dnsOverHTTPS) exchangeHTTPSClient( func (doh *dnsOverHTTPS) exchangeHTTPS(ctx context.Context, client *http.Client, req *D.Msg) (resp *D.Msg, err error) {
ctx context.Context,
client *http.Client,
req *D.Msg,
) (resp *D.Msg, err error) {
buf, err := req.Pack() buf, err := req.Pack()
if err != nil { if err != nil {
return nil, fmt.Errorf("packing message: %w", err) return nil, fmt.Errorf("packing message: %w", err)
@ -204,24 +201,24 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient(
method = http3.MethodGet0RTT method = http3.MethodGet0RTT
} }
url := doh.url requestUrl := *doh.url // don't modify origin url
url.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf)) requestUrl.RawQuery = fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(buf))
httpReq, err := http.NewRequestWithContext(ctx, method, url.String(), nil) httpReq, err := http.NewRequestWithContext(ctx, method, requestUrl.String(), nil)
if err != 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("Accept", "application/dns-message")
httpReq.Header.Set("User-Agent", "") httpReq.Header.Set("User-Agent", "")
httpResp, err := client.Do(httpReq) httpResp, err := client.Do(httpReq)
if err != nil { 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() defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body) body, err := io.ReadAll(httpResp.Body)
if err != nil { 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 { if httpResp.StatusCode != http.StatusOK {
@ -230,7 +227,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient(
"expected status %d, got %d from %s", "expected status %d, got %d from %s",
http.StatusOK, http.StatusOK,
httpResp.StatusCode, httpResp.StatusCode,
url, doh.url,
) )
} }
@ -239,7 +236,7 @@ func (doh *dnsOverHTTPS) exchangeHTTPSClient(
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"unpacking response from %s: body is %s: %w", "unpacking response from %s: body is %s: %w",
url, doh.url,
body, body,
err, 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, // HTTP3 is enabled in the upstream options). If this attempt is successful,
// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport. // it returns an HTTP3 transport, otherwise it returns the H1/H2 transport.
func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) { 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( tlsConfig := ca.GetGlobalTLSConfig(
&tls.Config{ &tls.Config{
InsecureSkipVerify: false, InsecureSkipVerify: doh.skipCertVerify,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
SessionTicketsDisabled: false, SessionTicketsDisabled: false,
}) })
@ -384,6 +393,7 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp
nextProtos = append(nextProtos, string(v)) nextProtos = append(nextProtos, string(v))
} }
tlsConfig.NextProtos = nextProtos tlsConfig.NextProtos = nextProtos
transport.TLSClientConfig = tlsConfig
if slices.Contains(doh.httpVersions, C.HTTPVersion3) { if slices.Contains(doh.httpVersions, C.HTTPVersion3) {
// First, we attempt to create an HTTP3 transport. If the probe QUIC // 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") return nil, errors.New("HTTP1/1 and HTTP2 are not supported by this upstream")
} }
transport := &http.Transport{ // Since we have a custom DialContext, we need to use this field to
TLSClientConfig: tlsConfig, // make golang http.Client attempt to use HTTP/2. Otherwise, it would
DisableCompression: true, // only be used when negotiated on the TLS level.
DialContext: doh.dialer.DialContext, transport.ForceAttemptHTTP2 = true
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,
}
// Explicitly configure transport to use HTTP/2. // Explicitly configure transport to use HTTP/2.
// //

View File

@ -146,9 +146,12 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e
}() }()
q := m.Question[0] q := m.Question[0]
domain := msgToDomain(m)
_, qTypeStr := msgToQtype(m)
cacheM, expireTime, hit := r.cache.GetWithExpire(q.String()) cacheM, expireTime, hit := r.cache.GetWithExpire(q.String())
if hit { 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() now := time.Now()
msg = cacheM.Copy() msg = cacheM.Copy()
if expireTime.Before(now) { if expireTime.Before(now) {

View File

@ -173,11 +173,20 @@ func msgToDomain(msg *D.Msg) string {
return "" 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) { func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) {
cache = true cache = true
fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout) fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout)
defer fast.Close() defer fast.Close()
domain := msgToDomain(m) domain := msgToDomain(m)
qType, qTypeStr := msgToQtype(m)
var noIpMsg *D.Msg var noIpMsg *D.Msg
for _, client := range clients { for _, client := range clients {
if _, isRCodeClient := client.(rcodeClient); isRCodeClient { 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 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) { 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) m, err := client.ExchangeContext(ctx, m)
if err != nil { if err != nil {
return nil, err 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. // so we would ignore RCode errors from RCode clients.
return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode]) return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode])
} }
if ips := msgToIP(m); len(m.Question) > 0 { ips := msgToIP(m)
qType := m.Question[0].Qtype log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, qTypeStr, client.Address())
log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, D.Type(qType), client.Address()) switch qType {
switch qType { case D.TypeAAAA:
case D.TypeAAAA: if len(ips) == 0 {
if len(ips) == 0 { noIpMsg = m
noIpMsg = m return nil, resolver.ErrIPNotFound
return nil, resolver.ErrIPNotFound }
} case D.TypeA:
case D.TypeA: if len(ips) == 0 {
if len(ips) == 0 { noIpMsg = m
noIpMsg = m return nil, resolver.ErrIPNotFound
return nil, resolver.ErrIPNotFound
}
} }
} }
return m, nil return m, nil

View File

@ -70,6 +70,10 @@ external-ui: /path/to/ui/folder/
external-ui-name: xd external-ui-name: xd
external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" 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 # 设置出口网卡 # interface-name: en0 # 设置出口网卡
# 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint # 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint
@ -938,6 +942,24 @@ rule-providers:
interval: 259200 interval: 259200
path: /path/to/save/file.yaml path: /path/to/save/file.yaml
type: file 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: rules:
- RULE-SET,rule1,REJECT - RULE-SET,rule1,REJECT
- IP-ASN,1,PROXY - IP-ASN,1,PROXY

20
go.mod
View File

@ -4,7 +4,6 @@ go 1.20
require ( require (
github.com/3andne/restls-client-go v0.1.6 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/bahlo/generic-list-go v0.2.0
github.com/cilium/ebpf v0.12.3 github.com/cilium/ebpf v0.12.3
github.com/coreos/go-iptables v0.7.0 github.com/coreos/go-iptables v0.7.0
@ -15,17 +14,19 @@ require (
github.com/gobwas/ws v1.4.0 github.com/gobwas/ws v1.4.0
github.com/gofrs/uuid/v5 v5.2.0 github.com/gofrs/uuid/v5 v5.2.0
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 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/klauspost/cpuid/v2 v2.2.8
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mdlayher/netlink v1.7.2 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/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72
github.com/metacubex/sing-shadowsocks v0.2.6 github.com/metacubex/sing-shadowsocks v0.2.7
github.com/metacubex/sing-shadowsocks2 v0.2.0 github.com/metacubex/sing-shadowsocks2 v0.2.1
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66
github.com/metacubex/utls v1.6.6 github.com/metacubex/utls v1.6.6
@ -35,8 +36,9 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 github.com/oschwald/maxminddb-golang v1.12.0
github.com/puzpuzpuz/xsync/v3 v3.2.0 github.com/puzpuzpuz/xsync/v3 v3.2.0
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a 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/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-mux v0.2.1-0.20240124034317-9bfb33698bb6
github.com/sagernet/sing-shadowtls v0.1.4 github.com/sagernet/sing-shadowtls v0.1.4
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e
@ -45,13 +47,14 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/wk8/go-ordered-map/v2 v2.1.8 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 go.uber.org/automaxprocs v1.5.3
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.24.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/net v0.26.0 golang.org/x/net v0.26.0
golang.org/x/sync v0.7.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 google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
lukechampine.com/blake3 v1.3.0 lukechampine.com/blake3 v1.3.0
@ -80,7 +83,6 @@ require (
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/josharian/native v1.1.0 // 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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
@ -111,4 +113,4 @@ require (
golang.org/x/tools v0.22.0 // indirect 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

35
go.sum
View File

@ -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/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 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= 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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 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.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 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 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 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/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 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 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 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= 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= 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/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 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= 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-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
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/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 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-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.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g=
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg=
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
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.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog=
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM= github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= 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 h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= 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= 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/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 h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= 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 h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= 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= 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/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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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 h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.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.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/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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=

View File

@ -50,11 +50,12 @@ func Parse(options ...Option) error {
if cfg.General.ExternalController != "" { if cfg.General.ExternalController != "" {
go route.Start(cfg.General.ExternalController, cfg.General.ExternalControllerTLS, 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 != "" { 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) executor.ApplyConfig(cfg, true)

63
hub/route/doh.go Normal file
View File

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

View File

@ -50,7 +50,7 @@ func SetUIPath(path string) {
uiPath = C.Path.Resolve(path) 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() r := chi.NewRouter()
corsM := cors.New(cors.Options{ corsM := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, 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 return r
} }
func Start(addr string, tlsAddr string, secret string, func Start(addr string, tlsAddr string, secret string,
certificate, privateKey string, isDebug bool) { certificate, privateKey string, dohServer string, isDebug bool) {
if serverAddr != "" { if serverAddr != "" {
return return
} }
@ -133,7 +137,7 @@ func Start(addr string, tlsAddr string, secret string,
serverAddr = l.Addr().String() serverAddr = l.Addr().String()
log.Infoln("RESTful API tls listening at: %s", serverAddr) log.Infoln("RESTful API tls listening at: %s", serverAddr)
tlsServe := &http.Server{ tlsServe := &http.Server{
Handler: router(isDebug, true), Handler: router(isDebug, true, dohServer),
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c}, Certificates: []tls.Certificate{c},
}, },
@ -152,13 +156,13 @@ func Start(addr string, tlsAddr string, secret string,
serverAddr = l.Addr().String() serverAddr = l.Addr().String()
log.Infoln("RESTful API listening at: %s", serverAddr) 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) 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) addr = C.Path.Resolve(addr)
dir := filepath.Dir(addr) dir := filepath.Dir(addr)
@ -186,7 +190,7 @@ func StartUnix(addr string, isDebug bool) {
serverAddr = l.Addr().String() serverAddr = l.Addr().String()
log.Infoln("RESTful API unix listening at: %s", serverAddr) 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) log.Errorln("External controller unix serve error: %s", err)
} }
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/metacubex/mihomo/transport/socks5" "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{ return &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
// from http.DefaultTransport // from http.DefaultTransport
@ -21,6 +21,7 @@ func newClient(srcConn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition)
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * 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) { DialContext: func(context context.Context, network, address string) (net.Conn, error) {
if network != "tcp" && network != "tcp4" && network != "tcp6" { if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, errors.New("unsupported network " + network) return nil, errors.New("unsupported network " + network)

View File

@ -10,10 +10,9 @@ import (
"sync" "sync"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/lru"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
@ -31,8 +30,10 @@ func (b *bodyWrapper) Read(p []byte) (n int, err error) {
return n, err return n, err
} }
func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool], additions ...inbound.Addition) { func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) {
client := newClient(c, tunnel, additions...) additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser
inUserIdx := len(additions) - 1
client := newClient(c, tunnel, additions)
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -41,7 +42,8 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool],
conn := N.NewBufferedConn(c) conn := N.NewBufferedConn(c)
keepAlive := true keepAlive := true
trusted := cache == nil // disable authenticate if lru is nil trusted := authenticator == nil // disable authenticate if lru is nil
lastUser := ""
for keepAlive { for keepAlive {
peekMutex.Lock() peekMutex.Lock()
@ -57,12 +59,10 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool],
var resp *http.Response var resp *http.Response
if !trusted { var user string
var user string resp, user = authenticate(request, authenticator) // always call authenticate function to get user
resp, user = authenticate(request, cache) trusted = trusted || resp == nil
additions = append(additions, inbound.WithInUser(user)) additions[inUserIdx] = inbound.WithInUser(user)
trusted = resp == nil
}
if trusted { if trusted {
if request.Method == http.MethodConnect { 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 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) removeHopByHopHeaders(request.Header)
removeExtraHTTPHostPort(request) removeExtraHTTPHostPort(request)
@ -138,34 +145,24 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, cache *lru.LruCache[string, bool],
_ = conn.Close() _ = conn.Close()
} }
func authenticate(request *http.Request, cache *lru.LruCache[string, bool]) (resp *http.Response, u string) { func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) {
authenticator := authStore.Authenticator()
if inbound.SkipAuthRemoteAddress(request.RemoteAddr) { if inbound.SkipAuthRemoteAddress(request.RemoteAddr) {
authenticator = nil authenticator = nil
} }
if authenticator != nil { credential := parseBasicProxyAuthorization(request)
credential := parseBasicProxyAuthorization(request) if credential == "" && authenticator != nil {
if credential == "" { resp = responseWith(request, http.StatusProxyAuthRequired)
resp := responseWith(request, http.StatusProxyAuthRequired) resp.Header.Set("Proxy-Authenticate", "Basic")
resp.Header.Set("Proxy-Authenticate", "Basic") return
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
}
} }
user, pass, err := decodeBasicProxyAuthorization(credential)
return nil, u 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 { func responseWith(request *http.Request, statusCode int) *http.Response {

View File

@ -4,9 +4,10 @@ import (
"net" "net"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/constant/features"
authStore "github.com/metacubex/mihomo/listener/auth"
) )
type Listener struct { type Listener struct {
@ -32,10 +33,20 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, 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) { 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 isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@ -50,11 +61,6 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi
return nil, err return nil, err
} }
var c *lru.LruCache[string, bool]
if authenticate {
c = lru.New[string, bool](lru.WithAge[string, bool](30))
}
hl := &Listener{ hl := &Listener{
listener: l, listener: l,
addr: addr, addr: addr,
@ -79,7 +85,7 @@ func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additi
continue continue
} }
} }
go HandleConn(conn, tunnel, c, additions...) go HandleConn(conn, tunnel, authenticator, additions...)
} }
}() }()

View File

@ -4,9 +4,9 @@ import (
"net" "net"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/lru"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/http"
"github.com/metacubex/mihomo/listener/socks" "github.com/metacubex/mihomo/listener/socks"
"github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks4"
@ -16,7 +16,6 @@ import (
type Listener struct { type Listener struct {
listener net.Listener listener net.Listener
addr string addr string
cache *lru.LruCache[string, bool]
closed bool closed bool
} }
@ -53,7 +52,6 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
ml := &Listener{ ml := &Listener{
listener: l, listener: l,
addr: addr, addr: addr,
cache: lru.New[string, bool](lru.WithAge[string, bool](30)),
} }
go func() { go func() {
for { for {
@ -70,14 +68,14 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
continue continue
} }
} }
go handleConn(c, tunnel, ml.cache, additions...) go handleConn(c, tunnel, additions...)
} }
}() }()
return ml, nil 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) N.TCPKeepAlive(conn)
bufConn := N.NewBufferedConn(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: case socks5.Version:
socks.HandleSocks5(bufConn, tunnel, additions...) socks.HandleSocks5(bufConn, tunnel, additions...)
default: default:
http.HandleConn(bufConn, tunnel, cache, additions...) http.HandleConn(bufConn, tunnel, authStore.Authenticator(), additions...)
} }
} }

View File

@ -1,29 +1,80 @@
package sing_tun package sing_tun
import ( 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" "github.com/metacubex/mihomo/log"
tun "github.com/metacubex/sing-tun"
"github.com/metacubex/sing-tun"
"github.com/sagernet/netlink" "github.com/sagernet/netlink"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"runtime"
) )
func (l *Listener) buildAndroidRules(tunOptions *tun.Options) error { type packageManagerCallback struct{}
packageManager, err := tun.NewPackageManager(l.handler)
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 { if err != nil {
return err return nil, err
} }
err = packageManager.Start() 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 { if err != nil {
return err return err
} }
l.packageManager = packageManager
tunOptions.BuildAndroidRules(packageManager, l.handler) tunOptions.BuildAndroidRules(packageManager, l.handler)
return nil return nil
} }
func (h *ListenerHandler) OnPackagesUpdated(packages int, sharedUsers int) { func findPackageName(metadata *constant.Metadata) (string, error) {
return 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) { func (l *Listener) openAndroidHotspot(tunOptions tun.Options) {

View File

@ -17,6 +17,7 @@ import (
"github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/rules/provider"
"go.uber.org/automaxprocs/maxprocs" "go.uber.org/automaxprocs/maxprocs"
) )
@ -48,6 +49,12 @@ func init() {
func main() { func main() {
_, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) _, _ = 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 { if version {
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
P "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log" "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) 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 { func (c *classicalStrategy) Match(metadata *C.Metadata) bool {
for _, rule := range c.rules { for _, rule := range c.rules {
if m, _ := rule.Match(metadata); m { if m, _ := rule.Match(metadata); m {

View File

@ -1,9 +1,16 @@
package provider package provider
import ( import (
"errors"
"io"
"strings"
"github.com/metacubex/mihomo/component/trie" "github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
P "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/exp/slices"
) )
type domainStrategy struct { type domainStrategy struct {
@ -12,6 +19,10 @@ type domainStrategy struct {
domainSet *trie.DomainSet domainSet *trie.DomainSet
} }
func (d *domainStrategy) Behavior() P.RuleBehavior {
return P.Domain
}
func (d *domainStrategy) ShouldFindProcess() bool { func (d *domainStrategy) ShouldFindProcess() bool {
return false return false
} }
@ -35,6 +46,10 @@ func (d *domainStrategy) Reset() {
} }
func (d *domainStrategy) Insert(rule string) { func (d *domainStrategy) Insert(rule string) {
if strings.ContainsRune(rule, '/') {
log.Warnln("invalid domain:[%s]", rule)
return
}
err := d.domainTrie.Insert(rule, struct{}{}) err := d.domainTrie.Insert(rule, struct{}{})
if err != nil { if err != nil {
log.Warnln("invalid domain:[%s]", rule) log.Warnln("invalid domain:[%s]", rule)
@ -48,6 +63,45 @@ func (d *domainStrategy) FinishInsert() {
d.domainTrie = nil 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 { func NewDomainStrategy() *domainStrategy {
return &domainStrategy{} return &domainStrategy{}
} }

View File

@ -1,8 +1,13 @@
package provider package provider
import ( import (
"errors"
"io"
"net/netip"
"github.com/metacubex/mihomo/component/cidr" "github.com/metacubex/mihomo/component/cidr"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
P "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"go4.org/netipx" "go4.org/netipx"
@ -15,6 +20,10 @@ type ipcidrStrategy struct {
//trie *trie.IpCidrTrie //trie *trie.IpCidrTrie
} }
func (i *ipcidrStrategy) Behavior() P.RuleBehavior {
return P.IPCIDR
}
func (i *ipcidrStrategy) ShouldFindProcess() bool { func (i *ipcidrStrategy) ShouldFindProcess() bool {
return false return false
} }
@ -54,6 +63,34 @@ func (i *ipcidrStrategy) FinishInsert() {
i.cidrSet.Merge() 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 { func (i *ipcidrStrategy) ToIpCidr() *netipx.IPSet {
return i.cidrSet.ToIPSet() return i.cidrSet.ToIPSet()
} }

View File

@ -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 <behavior> <format> <source file> <target file>")
}
}

View File

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

View File

@ -32,28 +32,13 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t
if err := decoder.Decode(mapping, schema); err != nil { if err := decoder.Decode(mapping, schema); err != nil {
return nil, err return nil, err
} }
var behavior P.RuleBehavior behavior, err := P.ParseBehavior(schema.Behavior)
if err != nil {
switch schema.Behavior { return nil, err
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)
} }
format, err := P.ParseRuleFormat(schema.Format)
var format P.RuleFormat if err != nil {
return nil, err
switch schema.Format {
case "", "yaml":
format = P.YamlRule
case "text":
format = P.TextRule
default:
return nil, fmt.Errorf("unsupported format type: %s", schema.Format)
} }
var vehicle P.Vehicle var vehicle P.Vehicle

View File

@ -4,16 +4,17 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"runtime" "runtime"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v3"
"github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/component/resource" "github.com/metacubex/mihomo/component/resource"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
P "github.com/metacubex/mihomo/constant/provider" P "github.com/metacubex/mihomo/constant/provider"
"gopkg.in/yaml.v3"
) )
var tunnel P.Tunnel var tunnel P.Tunnel
@ -43,6 +44,7 @@ type RulePayload struct {
} }
type ruleStrategy interface { type ruleStrategy interface {
Behavior() P.RuleBehavior
Match(metadata *C.Metadata) bool Match(metadata *C.Metadata) bool
Count() int Count() int
ShouldResolveIP() bool ShouldResolveIP() bool
@ -52,6 +54,13 @@ type ruleStrategy interface {
FinishInsert() 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 { func (rp *ruleSetProvider) Type() P.ProviderType {
return P.Rule 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 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) { func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) {
strategy.Reset() strategy.Reset()
if format == P.MrsRule {
return rulesMrsParse(buf, strategy)
}
schema := &RulePayload{} schema := &RulePayload{}
@ -228,6 +241,8 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStr
if len(schema.Payload) > 0 { if len(schema.Payload) > 0 {
str = schema.Payload[0] str = schema.Payload[0]
} }
default:
return nil, ErrInvalidFormat
} }
if str == "" { if str == "" {

View File

@ -34,6 +34,11 @@ const (
aeadAes256Gcm = "AEAD_AES_256_GCM" aeadAes256Gcm = "AEAD_AES_256_GCM"
aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305" aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305"
aeadXChacha20Poly1305 = "AEAD_XCHACHA20_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 // List of AEAD ciphers: key size in bytes and constructor
@ -46,6 +51,11 @@ var aeadList = map[string]struct {
aeadAes256Gcm: {32, shadowaead.AESGCM}, aeadAes256Gcm: {32, shadowaead.AESGCM},
aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305}, aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305},
aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305}, 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 // 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 name = aeadAes192Gcm
case "AES-256-GCM": case "AES-256-GCM":
name = aeadAes256Gcm 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 { if choice, ok := aeadList[name]; ok {

View File

@ -7,6 +7,8 @@ import (
"io" "io"
"strconv" "strconv"
"github.com/metacubex/chacha"
"gitlab.com/go-extension/aes-ccm"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
@ -75,6 +77,25 @@ func AESGCM(psk []byte) (Cipher, error) {
return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil 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) // Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32. // must be 32.
func Chacha20Poly1305(psk []byte) (Cipher, error) { func Chacha20Poly1305(psk []byte) (Cipher, error) {
@ -92,3 +113,21 @@ func XChacha20Poly1305(psk []byte) (Cipher, error) {
} }
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil 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
}

View File

@ -2,7 +2,8 @@ package shadowstream
import ( import (
"crypto/cipher" "crypto/cipher"
"github.com/aead/chacha20/chacha"
"github.com/metacubex/chacha"
) )
type chacha20key []byte type chacha20key []byte
@ -11,7 +12,7 @@ func (k chacha20key) IVSize() int {
return chacha.NonceSize return chacha.NonceSize
} }
func (k chacha20key) Encrypter(iv []byte) cipher.Stream { func (k chacha20key) Encrypter(iv []byte) cipher.Stream {
c, _ := chacha.NewCipher(iv, k, 20) c, _ := chacha.NewChaCha20(iv, k)
return c return c
} }
func (k chacha20key) Decrypter(iv []byte) cipher.Stream { func (k chacha20key) Decrypter(iv []byte) cipher.Stream {

View File

@ -622,6 +622,10 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
metadata.Process = filepath.Base(path) metadata.Process = filepath.Base(path)
metadata.ProcessPath = path metadata.ProcessPath = path
metadata.Uid = uid metadata.Uid = uid
if pkg, err := P.FindPackageName(metadata); err == nil { // for android (not CMFA) package names
metadata.Process = pkg
}
} }
} else { } else {
// check package names // check package names