Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions docs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ proxies: # socks5
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.

reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
Expand Down Expand Up @@ -1195,7 +1195,7 @@ proxies: # socks5
- name: sudoku
type: sudoku
server: server_ip/domain # 1.2.3.4 or domain
port: 443
port: 443
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护)
padding-min: 2 # 最小填充率(0-100)
Expand Down Expand Up @@ -1428,11 +1428,16 @@ rule-providers:

rules:
- RULE-SET,rule1,REJECT
- IP-ASN,1,PROXY
- IP-ASN,13335,PROXY # 匹配 cloudflare ASN13335 IP 段
- IP-ASN,13335/396982,PROXY # 支持正斜杠(/)分割, 效果等同于 OR
- DOMAIN-REGEX,^abc,DIRECT
- DOMAIN-SUFFIX,baidu.com,DIRECT
- DOMAIN-KEYWORD,google,ss1
- DOMAIN-WILDCARD,test.*.mihomo.com,ss1
- GEOSITE,baidu,DIRECT # 匹配所有百度网站域名
- GEOSITE,google/youtube,PROXY # 支持正斜杠(/)分割, 效果等同于 OR
- GEOIP,cn,DIRECT # 匹配所有 CN IP 段
- GEOIP,gfw/cloudflare,PROXY # 支持正斜杠(/)分割, 效果等同于 OR
- IP-CIDR,1.1.1.1/32,ss1
- IP-CIDR6,2409::/64,DIRECT
# 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集
Expand Down
216 changes: 169 additions & 47 deletions rules/common/geoip.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/netip"
"strings"
"sync"

"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/geodata/router"
Expand All @@ -18,10 +19,39 @@ import (

type GEOIP struct {
Base
country string
countries []string
payload string
adapter string
noResolveIP bool
isSourceIP bool
matchers []namedGeoIPMatcher
matcher router.IPMatcher
matcherErr error
matcherOnce sync.Once
}

type namedGeoIPMatcher struct {
country string
matcher router.IPMatcher
}

type multiGeoIPMatcher []router.IPMatcher

func (m multiGeoIPMatcher) Match(ip netip.Addr) bool {
for _, matcher := range m {
if matcher.Match(ip) {
return true
}
}
return false
}

func (m multiGeoIPMatcher) Count() int {
count := 0
for _, matcher := range m {
count += matcher.Count()
}
return count
}

var _ C.Rule = (*GEOIP)(nil)
Expand All @@ -46,42 +76,50 @@ func (g *GEOIP) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, str
return false, ""
}

if g.country == "lan" {
return g.isLan(ip), g.adapter
if g.matchLan(ip) {
return true, g.adapter
}

if geodata.GeodataMode() {
if g.isSourceIP {
if slices.Contains(metadata.SrcGeoIP, g.country) {
if g.matchCountries(metadata.SrcGeoIP) {
return true, g.adapter
}
} else {
if slices.Contains(metadata.DstGeoIP, g.country) {
if g.matchCountries(metadata.DstGeoIP) {
return true, g.adapter
}
}
matcher, err := g.getIPMatcher()

matchers, err := g.getNamedIPMatchers()
if err != nil {
return false, ""
}
match := matcher.Match(ip)
if match {
if g.isSourceIP {
metadata.SrcGeoIP = append(metadata.SrcGeoIP, g.country)
} else {
metadata.DstGeoIP = append(metadata.DstGeoIP, g.country)
for _, matcher := range matchers {
if matcher.matcher.Match(ip) {
if g.isSourceIP {
metadata.SrcGeoIP = append(metadata.SrcGeoIP, matcher.country)
} else {
metadata.DstGeoIP = append(metadata.DstGeoIP, matcher.country)
}
return true, g.adapter
}
}
return match, g.adapter

return false, ""
}

if g.isSourceIP {
if metadata.SrcGeoIP != nil {
return slices.Contains(metadata.SrcGeoIP, g.country), g.adapter
if g.matchCountries(metadata.SrcGeoIP) {
return true, g.adapter
}
}
} else {
if metadata.DstGeoIP != nil {
return slices.Contains(metadata.DstGeoIP, g.country), g.adapter
if g.matchCountries(metadata.DstGeoIP) {
return true, g.adapter
}
}
}
codes := mmdb.IPInstance().LookupCode(ip.AsSlice())
Expand All @@ -90,7 +128,7 @@ func (g *GEOIP) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, str
} else {
metadata.DstGeoIP = codes
}
if slices.Contains(codes, g.country) {
if g.matchCountries(codes) {
return true, g.adapter
}
return false, ""
Expand All @@ -102,20 +140,25 @@ func (g *GEOIP) MatchIp(ip netip.Addr) bool {
return false
}

if g.country == "lan" {
return g.isLan(ip)
if g.matchLan(ip) {
return true
}

if geodata.GeodataMode() {
matcher, err := g.getIPMatcher()
matchers, err := g.getNamedIPMatchers()
if err != nil {
return false
}
return matcher.Match(ip)
for _, matcher := range matchers {
if matcher.matcher.Match(ip) {
return true
}
}
return false
}

codes := mmdb.IPInstance().LookupCode(ip.AsSlice())
return slices.Contains(codes, g.country)
return g.matchCountries(codes)
}

// MatchIp implements C.IpMatcher
Expand All @@ -128,20 +171,7 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
return false
}

if g.country == "lan" {
return !g.isLan(ip)
}

if geodata.GeodataMode() {
matcher, err := g.getIPMatcher()
if err != nil {
return false
}
return !matcher.Match(ip)
}

codes := mmdb.IPInstance().LookupCode(ip.AsSlice())
return !slices.Contains(codes, g.country)
return !g.GEOIP.MatchIp(ip)
}

type dnsFallbackFilter struct {
Expand All @@ -166,22 +196,69 @@ func (g *GEOIP) Adapter() string {
}

func (g *GEOIP) Payload() string {
return g.country
return g.payload
}

func (g *GEOIP) GetCountry() string {
return g.country
if len(g.countries) == 0 {
return ""
}
return g.countries[0]
}

func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) {
if !g.hasNonLanCountry() {
return nil, errors.New("geoip matcher has no data")
}

if geodata.GeodataMode() {
return g.getIPMatcher()
matchers, err := g.getNamedIPMatchers()
if err != nil {
return nil, err
}
if len(matchers) == 1 {
return matchers[0].matcher, nil
}
return g.matcher, nil
}
return nil, errors.New("not geodata mode")
}

func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) {
geoIPMatcher, err := geodata.LoadGeoIPMatcher(g.country)
func (g *GEOIP) getNamedIPMatchers() ([]namedGeoIPMatcher, error) {
g.matcherOnce.Do(func() {
matchers := make([]namedGeoIPMatcher, 0, len(g.countries))
combined := make(multiGeoIPMatcher, 0, len(g.countries))
for _, country := range g.countries {
if country == "lan" {
continue
}
matcher, err := g.getIPMatcher(country)
if err != nil {
g.matcherErr = err
return
}
matchers = append(matchers, namedGeoIPMatcher{country: country, matcher: matcher})
combined = append(combined, matcher)
}
g.matchers = matchers
switch len(combined) {
case 0:
g.matcherErr = errors.New("geoip matcher has no data")
case 1:
g.matcher = combined[0]
default:
g.matcher = combined
}
})

if g.matcherErr != nil {
return nil, g.matcherErr
}
return g.matchers, nil
}

func (g *GEOIP) getIPMatcher(country string) (router.IPMatcher, error) {
geoIPMatcher, err := geodata.LoadGeoIPMatcher(country)
if err != nil {
return nil, fmt.Errorf("[GeoIP] %w", err)
}
Expand All @@ -190,8 +267,7 @@ func (g *GEOIP) getIPMatcher() (router.IPMatcher, error) {
}

func (g *GEOIP) GetRecodeSize() int {
// skip pseudorule lan
if g.country == "lan" {
if !g.hasNonLanCountry() {
return 0
}

Expand All @@ -202,17 +278,29 @@ func (g *GEOIP) GetRecodeSize() int {
}

func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) {
country = strings.ToLower(country)
countries, err := parseSlashSeparatedPayload(country, "geoip country", strings.ToLower)
if err != nil {
return nil, err
}

geoip := &GEOIP{
Base: Base{},
country: country,
countries: countries,
payload: strings.Join(countries, "/"),
adapter: adapter,
noResolveIP: noResolveIP,
isSourceIP: isSrc,
}

if country == "lan" {
allLan := true
for _, country := range countries {
if country != "lan" {
allLan = false
break
}
}

if allLan {
return geoip, nil
}

Expand All @@ -222,14 +310,48 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
}

if geodata.GeodataMode() {
geoIPMatcher, err := geoip.getIPMatcher() // test load
records := 0
matchers, err := geoip.getNamedIPMatchers() // test load
if err != nil {
return nil, err
}
log.Infoln("Finished initial GeoIP rule %s => %s, records: %d", country, adapter, geoIPMatcher.Count())
for _, matcher := range matchers {
records += matcher.matcher.Count()
}
log.Infoln("Finished initial GeoIP rule %s => %s, records: %d", geoip.payload, adapter, records)
}

return geoip, nil
}

func (g *GEOIP) matchLan(ip netip.Addr) bool {
for _, country := range g.countries {
if country == "lan" {
return g.isLan(ip)
}
}
return false
}

func (g *GEOIP) matchCountries(codes []string) bool {
for _, country := range g.countries {
if country == "lan" {
continue
}
if slices.Contains(codes, country) {
return true
}
}
return false
}

func (g *GEOIP) hasNonLanCountry() bool {
for _, country := range g.countries {
if country != "lan" {
return true
}
}
return false
}

var _ C.Rule = (*GEOIP)(nil)
Loading