feat: add IP-ASN rule

This commit is contained in:
xishang0128 2024-03-12 03:14:25 +08:00
parent 7ad37ca0e3
commit 44d8a14629
No known key found for this signature in database
GPG Key ID: 44A1E10B5ADF68CB
15 changed files with 248 additions and 33 deletions

View File

@ -14,8 +14,11 @@ import (
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
var initGeoSite bool var (
var initGeoIP int initGeoSite bool
initGeoIP int
initASN bool
)
func InitGeoSite() error { func InitGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
@ -113,7 +116,7 @@ func InitGeoIP() error {
} }
if initGeoIP != 2 { if initGeoIP != 2 {
if !mmdb.Verify() { if !mmdb.Verify(C.Path.MMDB()) {
log.Warnln("MMDB invalid, remove and download") log.Warnln("MMDB invalid, remove and download")
if err := os.Remove(C.Path.MMDB()); err != nil { if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
@ -126,3 +129,27 @@ func InitGeoIP() error {
} }
return nil 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
}

View File

@ -25,56 +25,58 @@ const (
) )
var ( var (
reader Reader IPreader IPReader
once sync.Once ASNreader ASNReader
IPonce sync.Once
ASNonce sync.Once
) )
func LoadFromBytes(buffer []byte) { func LoadFromBytes(buffer []byte) {
once.Do(func() { IPonce.Do(func() {
mmdb, err := maxminddb.FromBytes(buffer) mmdb, err := maxminddb.FromBytes(buffer)
if err != nil { if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error()) log.Fatalln("Can't load mmdb: %s", err.Error())
} }
reader = Reader{Reader: mmdb} IPreader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType { switch mmdb.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
reader.databaseType = typeSing IPreader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
reader.databaseType = typeMetaV0 IPreader.databaseType = typeMetaV0
default: default:
reader.databaseType = typeMaxmind IPreader.databaseType = typeMaxmind
} }
}) })
} }
func Verify() bool { func Verify(path string) bool {
instance, err := maxminddb.Open(C.Path.MMDB()) instance, err := maxminddb.Open(path)
if err == nil { if err == nil {
instance.Close() instance.Close()
} }
return err == nil return err == nil
} }
func Instance() Reader { func IPInstance() IPReader {
once.Do(func() { IPonce.Do(func() {
mmdbPath := C.Path.MMDB() mmdbPath := C.Path.MMDB()
log.Infoln("Load MMDB file: %s", mmdbPath) log.Infoln("Load MMDB file: %s", mmdbPath)
mmdb, err := maxminddb.Open(mmdbPath) mmdb, err := maxminddb.Open(mmdbPath)
if err != nil { if err != nil {
log.Fatalln("Can't load MMDB: %s", err.Error()) log.Fatalln("Can't load MMDB: %s", err.Error())
} }
reader = Reader{Reader: mmdb} IPreader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType { switch mmdb.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
reader.databaseType = typeSing IPreader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
reader.databaseType = typeMetaV0 IPreader.databaseType = typeMetaV0
default: default:
reader.databaseType = typeMaxmind IPreader.databaseType = typeMaxmind
} }
}) })
return reader return IPreader
} }
func DownloadMMDB(path string) (err error) { func DownloadMMDB(path string) (err error) {
@ -96,6 +98,43 @@ func DownloadMMDB(path string) (err error) {
return err return err
} }
func Reload() { func ASNInstance() ASNReader {
mihomoOnce.Reset(&once) 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)
} }

View File

@ -5,14 +5,14 @@ package mmdb
import "github.com/oschwald/maxminddb-golang" import "github.com/oschwald/maxminddb-golang"
func InstallOverride(override *maxminddb.Reader) { func InstallOverride(override *maxminddb.Reader) {
newReader := Reader{Reader: override} newReader := IPReader{Reader: override}
switch override.Metadata.DatabaseType { switch override.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
reader.databaseType = typeSing IPreader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
reader.databaseType = typeMetaV0 IPreader.databaseType = typeMetaV0
default: default:
reader.databaseType = typeMaxmind IPreader.databaseType = typeMaxmind
} }
reader = newReader IPreader = newReader
} }

View File

@ -14,12 +14,21 @@ type geoip2Country struct {
} `maxminddb:"country"` } `maxminddb:"country"`
} }
type Reader struct { type IPReader struct {
*maxminddb.Reader *maxminddb.Reader
databaseType 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 { switch r.databaseType {
case typeMaxmind: case typeMaxmind:
var country geoip2Country var country geoip2Country
@ -56,3 +65,9 @@ func (r Reader) LookupCode(ipAddress net.IP) []string {
panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) 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
}

View File

@ -348,6 +348,7 @@ type RawConfig struct {
type GeoXUrl struct { type GeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"` GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"` Mmdb string `yaml:"mmdb" json:"mmdb"`
ASN string `yaml:"asn" json:"asn"`
GeoSite string `yaml:"geosite" json:"geosite"` GeoSite string `yaml:"geosite" json:"geosite"`
} }
@ -495,6 +496,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
}, },
GeoXUrl: GeoXUrl{ GeoXUrl: GeoXUrl{
Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
ASN: "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", 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", 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.GeoIpUrl = cfg.GeoXUrl.GeoIp
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
C.MmdbUrl = cfg.GeoXUrl.Mmdb C.MmdbUrl = cfg.GeoXUrl.Mmdb
C.ASNUrl = cfg.GeoXUrl.ASN
C.GeodataMode = cfg.GeodataMode C.GeodataMode = cfg.GeodataMode
C.UA = cfg.GlobalUA C.UA = cfg.GlobalUA
if cfg.KeepAliveInterval != 0 { if cfg.KeepAliveInterval != 0 {

View File

@ -34,7 +34,7 @@ func UpdateGeoDatabases() error {
} }
} else { } else {
defer mmdb.Reload() defer mmdb.ReloadIP()
data, err := downloadForBytes(C.MmdbUrl) data, err := downloadForBytes(C.MmdbUrl)
if err != nil { if err != nil {
return fmt.Errorf("can't download MMDB database file: %w", err) return fmt.Errorf("can't download MMDB database file: %w", err)
@ -46,12 +46,31 @@ func UpdateGeoDatabases() error {
} }
_ = instance.Close() _ = 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 { if err = saveFile(data, C.Path.MMDB()); err != nil {
return fmt.Errorf("can't save MMDB database file: %w", err) 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) data, err := downloadForBytes(C.GeoSiteUrl)
if err != nil { if err != nil {
return fmt.Errorf("can't download GeoSite database file: %w", err) return fmt.Errorf("can't download GeoSite database file: %w", err)

View File

@ -1,10 +1,12 @@
package constant package constant
var ( var (
ASNEnable bool
GeodataMode bool GeodataMode bool
GeoAutoUpdate bool GeoAutoUpdate bool
GeoUpdateInterval int GeoUpdateInterval int
GeoIpUrl string GeoIpUrl string
MmdbUrl string MmdbUrl string
GeoSiteUrl string GeoSiteUrl string
ASNUrl string
) )

View File

@ -133,7 +133,8 @@ type Metadata struct {
Type Type `json:"type"` Type Type `json:"type"`
SrcIP netip.Addr `json:"sourceIP"` SrcIP netip.Addr `json:"sourceIP"`
DstIP netip.Addr `json:"destinationIP"` 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 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 DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output
InIP netip.Addr `json:"inboundIP"` InIP netip.Addr `json:"inboundIP"`

View File

@ -15,6 +15,7 @@ const Name = "mihomo"
var ( var (
GeositeName = "GeoSite.dat" GeositeName = "GeoSite.dat"
GeoipName = "GeoIP.dat" GeoipName = "GeoIP.dat"
ASNName = "ASN.mmdb"
) )
// Path is used to get the configuration path // Path is used to get the configuration path
@ -112,6 +113,25 @@ func (p *path) MMDB() string {
return P.Join(p.homeDir, "geoip.metadb") 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 { func (p *path) OldCache() string {
return P.Join(p.homeDir, ".cache") return P.Join(p.homeDir, ".cache")
} }

View File

@ -9,6 +9,7 @@ const (
GEOSITE GEOSITE
GEOIP GEOIP
IPCIDR IPCIDR
IPASN
SrcIPCIDR SrcIPCIDR
IPSuffix IPSuffix
SrcIPSuffix SrcIPSuffix
@ -49,6 +50,8 @@ func (rt RuleType) String() string {
return "GeoIP" return "GeoIP"
case IPCIDR: case IPCIDR:
return "IPCIDR" return "IPCIDR"
case IPASN:
return "IPASN"
case SrcIPCIDR: case SrcIPCIDR:
return "SrcIPCIDR" return "SrcIPCIDR"
case IPSuffix: case IPSuffix:

View File

@ -24,7 +24,7 @@ var geoIPMatcher *router.GeoIPMatcher
func (gf *geoipFilter) Match(ip netip.Addr) bool { func (gf *geoipFilter) Match(ip netip.Addr) bool {
if !C.GeodataMode { if !C.GeodataMode {
codes := mmdb.Instance().LookupCode(ip.AsSlice()) codes := mmdb.IPInstance().LookupCode(ip.AsSlice())
for _, code := range codes { for _, code := range codes {
if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() { if !strings.EqualFold(code, gf.code) && !ip.IsPrivate() {
return true return true

View File

@ -674,6 +674,8 @@ proxies: # socks5
type: hysteria2 type: hysteria2
server: server.com server: server.com
port: 443 port: 443
# ports: 1000,2000-3000,5000 # port 不可省略
# hop-interval: 15
# up和down均不写或为0则使用BBR流控 # up和down均不写或为0则使用BBR流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps # up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps
@ -767,6 +769,18 @@ proxies: # socks5
# protocol-param: "#" # protocol-param: "#"
# udp: true # 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: proxy-groups:
# 代理链目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic # 代理链目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic
# wireguard目前不支持在relay中使用请使用proxy中的dialer-proxy配置项 # wireguard目前不支持在relay中使用请使用proxy中的dialer-proxy配置项
@ -885,6 +899,8 @@ rule-providers:
type: file type: file
rules: rules:
- RULE-SET,rule1,REJECT - RULE-SET,rule1,REJECT
- IP-ASN,1,PROXY
- DOMAIN-REGEX,^abc,DIRECT
- DOMAIN-SUFFIX,baidu.com,DIRECT - DOMAIN-SUFFIX,baidu.com,DIRECT
- DOMAIN-KEYWORD,google,ss1 - DOMAIN-KEYWORD,google,ss1
- IP-CIDR,1.1.1.1/32,ss1 - IP-CIDR,1.1.1.1/32,ss1

View File

@ -52,7 +52,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) {
if metadata.DstGeoIP != nil { if metadata.DstGeoIP != nil {
return false, g.adapter return false, g.adapter
} }
metadata.DstGeoIP = mmdb.Instance().LookupCode(ip.AsSlice()) metadata.DstGeoIP = mmdb.IPInstance().LookupCode(ip.AsSlice())
for _, code := range metadata.DstGeoIP { for _, code := range metadata.DstGeoIP {
if g.country == code { if g.country == code {
return true, g.adapter return true, g.adapter

67
rules/common/ipasn.go Normal file
View File

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

View File

@ -27,6 +27,9 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string]
case "IP-CIDR", "IP-CIDR6": case "IP-CIDR", "IP-CIDR6":
noResolve := RC.HasNoResolve(params) noResolve := RC.HasNoResolve(params)
parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) 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": case "SRC-IP-CIDR":
parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true))
case "IP-SUFFIX": case "IP-SUFFIX":