From 99b6ffa6f797f44cdb8650f0a0ca1ffd4c76fdec Mon Sep 17 00:00:00 2001 From: xishang0128 Date: Tue, 12 Mar 2024 01:53:03 +0800 Subject: [PATCH] feat: add `IP-ASN` rule --- component/geodata/init.go | 33 +++++++++++++-- component/mmdb/mmdb.go | 75 +++++++++++++++++++++++++-------- component/mmdb/patch_android.go | 10 ++--- component/mmdb/reader.go | 19 ++++++++- config/config.go | 3 ++ config/update_geo.go | 23 +++++++++- constant/geodata.go | 2 + constant/metadata.go | 3 +- constant/path.go | 20 +++++++++ constant/rule.go | 3 ++ dns/filters.go | 2 +- docs/config.yaml | 16 +++++++ rules/common/geoip.go | 2 +- rules/common/ipasn.go | 67 +++++++++++++++++++++++++++++ rules/parser.go | 3 ++ 15 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 rules/common/ipasn.go diff --git a/component/geodata/init.go b/component/geodata/init.go index 842efcc55d..834567a447 100644 --- a/component/geodata/init.go +++ b/component/geodata/init.go @@ -14,8 +14,11 @@ import ( "github.com/metacubex/mihomo/log" ) -var initGeoSite bool -var initGeoIP int +var ( + initGeoSite bool + initGeoIP int + initASN bool +) func InitGeoSite() error { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { @@ -113,7 +116,7 @@ func InitGeoIP() error { } if initGeoIP != 2 { - if !mmdb.Verify() { + if !mmdb.Verify(C.Path.MMDB()) { log.Warnln("MMDB invalid, remove and download") if err := os.Remove(C.Path.MMDB()); err != nil { return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) @@ -126,3 +129,27 @@ func InitGeoIP() error { } return nil } + +func InitASN() error { + if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { + log.Infoln("Can't find ASN.mmdb, start download") + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) + } + log.Infoln("Download ASN.mmdb finish") + initASN = false + } + if !initASN { + if !mmdb.Verify(C.Path.ASN()) { + log.Warnln("ASN invalid, remove and download") + if err := os.Remove(C.Path.ASN()); err != nil { + return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) + } + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN: %s", err.Error()) + } + } + initASN = true + } + return nil +} diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go index 66b632beb5..81156bc62d 100644 --- a/component/mmdb/mmdb.go +++ b/component/mmdb/mmdb.go @@ -25,56 +25,58 @@ const ( ) var ( - reader Reader - once sync.Once + IPreader IPReader + ASNreader ASNReader + IPonce sync.Once + ASNonce sync.Once ) func LoadFromBytes(buffer []byte) { - once.Do(func() { + IPonce.Do(func() { mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) } -func Verify() bool { - instance, err := maxminddb.Open(C.Path.MMDB()) +func Verify(path string) bool { + instance, err := maxminddb.Open(path) if err == nil { instance.Close() } return err == nil } -func Instance() Reader { - once.Do(func() { +func IPInstance() IPReader { + IPonce.Do(func() { mmdbPath := C.Path.MMDB() log.Infoln("Load MMDB file: %s", mmdbPath) mmdb, err := maxminddb.Open(mmdbPath) if err != nil { log.Fatalln("Can't load MMDB: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) - return reader + return IPreader } func DownloadMMDB(path string) (err error) { @@ -96,6 +98,43 @@ func DownloadMMDB(path string) (err error) { return err } -func Reload() { - mihomoOnce.Reset(&once) +func ASNInstance() ASNReader { + ASNonce.Do(func() { + ASNPath := C.Path.ASN() + log.Infoln("Load ASN file: %s", ASNPath) + asn, err := maxminddb.Open(ASNPath) + if err != nil { + log.Fatalln("Can't load ASN: %s", err.Error()) + } + ASNreader = ASNReader{Reader: asn} + }) + + return ASNreader +} + +func DownloadASN(path string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + if err != nil { + return + } + defer resp.Body.Close() + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + + return err +} + +func ReloadIP() { + mihomoOnce.Reset(&IPonce) +} + +func ReloadASN() { + mihomoOnce.Reset(&ASNonce) } diff --git a/component/mmdb/patch_android.go b/component/mmdb/patch_android.go index a994b75e3f..147a332434 100644 --- a/component/mmdb/patch_android.go +++ b/component/mmdb/patch_android.go @@ -5,14 +5,14 @@ package mmdb import "github.com/oschwald/maxminddb-golang" func InstallOverride(override *maxminddb.Reader) { - newReader := Reader{Reader: override} + newReader := IPReader{Reader: override} switch override.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } - reader = newReader + IPreader = newReader } diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go index 787bdfe8f5..e76e993986 100644 --- a/component/mmdb/reader.go +++ b/component/mmdb/reader.go @@ -14,12 +14,21 @@ type geoip2Country struct { } `maxminddb:"country"` } -type Reader struct { +type IPReader struct { *maxminddb.Reader databaseType } -func (r Reader) LookupCode(ipAddress net.IP) []string { +type ASNReader struct { + *maxminddb.Reader +} + +type ASNResult struct { + AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` + AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` +} + +func (r IPReader) LookupCode(ipAddress net.IP) []string { switch r.databaseType { case typeMaxmind: var country geoip2Country @@ -56,3 +65,9 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) } } + +func (r ASNReader) LookupASN(ip net.IP) ASNResult { + var result ASNResult + r.Lookup(ip, &result) + return result +} diff --git a/config/config.go b/config/config.go index 8fec0bab55..68ac491b9e 100644 --- a/config/config.go +++ b/config/config.go @@ -348,6 +348,7 @@ type RawConfig struct { type GeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` + ASN string `yaml:"asn" json:"asn"` GeoSite string `yaml:"geosite" json:"geosite"` } @@ -495,6 +496,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { }, GeoXUrl: GeoXUrl{ Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + ASN: "https://github.com/P3TERX/GeoLite.mmdb/releases/download/2024.03.10/GeoLite2-ASN.mmdb", GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", }, @@ -620,6 +622,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { C.GeoIpUrl = cfg.GeoXUrl.GeoIp C.GeoSiteUrl = cfg.GeoXUrl.GeoSite C.MmdbUrl = cfg.GeoXUrl.Mmdb + C.ASNUrl = cfg.GeoXUrl.ASN C.GeodataMode = cfg.GeodataMode C.UA = cfg.GlobalUA if cfg.KeepAliveInterval != 0 { diff --git a/config/update_geo.go b/config/update_geo.go index bf3d0810eb..43cac25c8d 100644 --- a/config/update_geo.go +++ b/config/update_geo.go @@ -34,7 +34,7 @@ func UpdateGeoDatabases() error { } } else { - defer mmdb.Reload() + defer mmdb.ReloadIP() data, err := downloadForBytes(C.MmdbUrl) if err != nil { return fmt.Errorf("can't download MMDB database file: %w", err) @@ -46,12 +46,31 @@ func UpdateGeoDatabases() error { } _ = instance.Close() - mmdb.Instance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file + mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file if err = saveFile(data, C.Path.MMDB()); err != nil { return fmt.Errorf("can't save MMDB database file: %w", err) } } + if C.ASNEnable { + defer mmdb.ReloadASN() + data, err := downloadForBytes(C.ASNUrl) + if err != nil { + return fmt.Errorf("can't download ASN database file: %w", err) + } + + instance, err := maxminddb.FromBytes(data) + if err != nil { + return fmt.Errorf("invalid ASN database file: %s", err) + } + _ = instance.Close() + + mmdb.ASNInstance().Reader.Close() + if err = saveFile(data, C.Path.ASN()); err != nil { + return fmt.Errorf("can't save ASN database file: %w", err) + } + } + data, err := downloadForBytes(C.GeoSiteUrl) if err != nil { return fmt.Errorf("can't download GeoSite database file: %w", err) diff --git a/constant/geodata.go b/constant/geodata.go index e93d56b305..cd3f74e30c 100644 --- a/constant/geodata.go +++ b/constant/geodata.go @@ -1,10 +1,12 @@ package constant var ( + ASNEnable bool GeodataMode bool GeoAutoUpdate bool GeoUpdateInterval int GeoIpUrl string MmdbUrl string GeoSiteUrl string + ASNUrl string ) diff --git a/constant/metadata.go b/constant/metadata.go index bf0fa28170..381e2dd44a 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -133,7 +133,8 @@ type Metadata struct { Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` - DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result + DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result + DstIPASN string `json:"destinationIPASN"` SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output InIP netip.Addr `json:"inboundIP"` diff --git a/constant/path.go b/constant/path.go index a920fbbc62..77f7d0ef0a 100644 --- a/constant/path.go +++ b/constant/path.go @@ -15,6 +15,7 @@ const Name = "mihomo" var ( GeositeName = "GeoSite.dat" GeoipName = "GeoIP.dat" + ASNName = "ASN.mmdb" ) // Path is used to get the configuration path @@ -112,6 +113,25 @@ func (p *path) MMDB() string { return P.Join(p.homeDir, "geoip.metadb") } +func (p *path) ASN() string { + files, err := os.ReadDir(p.homeDir) + if err != nil { + return "" + } + for _, fi := range files { + if fi.IsDir() { + // 目录则直接跳过 + continue + } else { + if strings.EqualFold(fi.Name(), "ASN.mmdb") { + ASNName = fi.Name() + return P.Join(p.homeDir, fi.Name()) + } + } + } + return P.Join(p.homeDir, ASNName) +} + func (p *path) OldCache() string { return P.Join(p.homeDir, ".cache") } diff --git a/constant/rule.go b/constant/rule.go index 9b03822198..fcefaba6ca 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -9,6 +9,7 @@ const ( GEOSITE GEOIP IPCIDR + IPASN SrcIPCIDR IPSuffix SrcIPSuffix @@ -49,6 +50,8 @@ func (rt RuleType) String() string { return "GeoIP" case IPCIDR: return "IPCIDR" + case IPASN: + return "IPASN" case SrcIPCIDR: return "SrcIPCIDR" case IPSuffix: diff --git a/dns/filters.go b/dns/filters.go index d8633e8bb3..138f3429bb 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -24,7 +24,7 @@ var geoIPMatcher *router.GeoIPMatcher func (gf *geoipFilter) Match(ip netip.Addr) bool { if !C.GeodataMode { - codes := mmdb.Instance().LookupCode(ip.AsSlice()) + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) for _, code := range codes { if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() { return true diff --git a/docs/config.yaml b/docs/config.yaml index dc257ee27d..d912eb6554 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -674,6 +674,8 @@ proxies: # socks5 type: hysteria2 server: server.com port: 443 + # ports: 1000,2000-3000,5000 # port 不可省略 + # hop-interval: 15 # up和down均不写或为0则使用BBR流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps @@ -767,6 +769,18 @@ proxies: # socks5 # protocol-param: "#" # udp: true + - name: "ssh-out" + type: ssh + + server: 127.0.0.1 + port: 22 + username: root + password: password + privateKey: path + +# dns出站会将请求劫持到内部dns模块,所有请求均在内部处理 + - name: "dns-out" + type: dns proxy-groups: # 代理链,目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic # wireguard目前不支持在relay中使用,请使用proxy中的dialer-proxy配置项 @@ -885,6 +899,8 @@ rule-providers: type: file rules: - RULE-SET,rule1,REJECT + - IP-ASN,1,PROXY + - DOMAIN-REGEX,^abc,DIRECT - DOMAIN-SUFFIX,baidu.com,DIRECT - DOMAIN-KEYWORD,google,ss1 - IP-CIDR,1.1.1.1/32,ss1 diff --git a/rules/common/geoip.go b/rules/common/geoip.go index 2a8913130b..223a79042d 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -52,7 +52,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { if metadata.DstGeoIP != nil { return false, g.adapter } - metadata.DstGeoIP = mmdb.Instance().LookupCode(ip.AsSlice()) + metadata.DstGeoIP = mmdb.IPInstance().LookupCode(ip.AsSlice()) for _, code := range metadata.DstGeoIP { if g.country == code { return true, g.adapter diff --git a/rules/common/ipasn.go b/rules/common/ipasn.go new file mode 100644 index 0000000000..1fce8af482 --- /dev/null +++ b/rules/common/ipasn.go @@ -0,0 +1,67 @@ +package common + +import ( + "strconv" + + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/mmdb" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +type ASN struct { + *Base + asn string + adapter string + noResolveIP bool +} + +func (a *ASN) Match(metadata *C.Metadata) (bool, string) { + ip := metadata.DstIP + if !ip.IsValid() { + return false, "" + } + + result := mmdb.ASNInstance().LookupASN(ip.AsSlice()) + + asnNumber := strconv.FormatUint(uint64(result.AutonomousSystemNumber), 10) + metadata.DstIPASN = asnNumber + " " + result.AutonomousSystemOrganization + + match := a.asn == asnNumber + return match, a.adapter +} + +func (a *ASN) RuleType() C.RuleType { + return C.IPASN +} + +func (a *ASN) Adapter() string { + return a.adapter +} + +func (a *ASN) Payload() string { + return a.asn +} + +func (a *ASN) ShouldResolveIP() bool { + return !a.noResolveIP +} + +func (a *ASN) GetASN() string { + return a.asn +} + +func NewIPASN(asn string, adapter string, noResolveIP bool) (*ASN, error) { + C.ASNEnable = true + if err := geodata.InitASN(); err != nil { + log.Errorln("can't initial ASN: %s", err) + return nil, err + } + + return &ASN{ + Base: &Base{}, + asn: asn, + adapter: adapter, + noResolveIP: noResolveIP, + }, nil +} diff --git a/rules/parser.go b/rules/parser.go index f7df5f4911..b69cc4fd9c 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -27,6 +27,9 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "IP-CIDR", "IP-CIDR6": noResolve := RC.HasNoResolve(params) parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) + case "IP-ASN": + noResolve := RC.HasNoResolve(params) + parsed, parseErr = RC.NewIPASN(payload, target, noResolve) case "SRC-IP-CIDR": parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) case "IP-SUFFIX":