Skip to content
Closed
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
29 changes: 22 additions & 7 deletions app/router/condition_geoip.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ type ipv6 struct {
}

type GeoIPMatcher struct {
countryCode string
ip4 []uint32
prefix4 []uint8
ip6 []ipv6
prefix6 []uint8
countryCode string
reverseMatch bool
ip4 []uint32
prefix4 []uint8
ip6 []ipv6
prefix6 []uint8
}

func normalize4(ip uint32, prefix uint8) uint32 {
Expand Down Expand Up @@ -80,6 +81,10 @@ func (m *GeoIPMatcher) Init(cidrs []*CIDR) error {
return nil
}

func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) {
m.reverseMatch = isReverseMatch
}

func (m *GeoIPMatcher) match4(ip uint32) bool {
if len(m.ip4) == 0 {
return false
Expand Down Expand Up @@ -147,8 +152,17 @@ func (m *GeoIPMatcher) match6(ip ipv6) bool {
func (m *GeoIPMatcher) Match(ip net.IP) bool {
switch len(ip) {
case 4:
if m.reverseMatch {
return !m.match4(binary.BigEndian.Uint32(ip))
}
return m.match4(binary.BigEndian.Uint32(ip))
case 16:
if m.reverseMatch {
return !m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
}
return m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
Expand All @@ -168,14 +182,15 @@ type GeoIPMatcherContainer struct {
func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
if len(geoip.CountryCode) > 0 {
for _, m := range c.matchers {
if m.countryCode == geoip.CountryCode {
if m.countryCode == geoip.CountryCode && m.reverseMatch == geoip.ReverseMatch {
return m, nil
}
}
}

m := &GeoIPMatcher{
countryCode: geoip.CountryCode,
countryCode: geoip.CountryCode,
reverseMatch: geoip.ReverseMatch,
}
if err := m.Init(geoip.Cidr); err != nil {
return nil, err
Expand Down
36 changes: 36 additions & 0 deletions app/router/condition_geoip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,39 @@ func BenchmarkGeoIPMatcher6US(b *testing.B) {
_ = matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP())
}
}

func TestGeoIPReverseMatcher(t *testing.T) {
cidrList := router.CIDRList{
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher := &router.GeoIPMatcher{}
matcher.SetReverseMatch(true) // Reverse match
common.Must(matcher.Init(cidrList))

testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "91.108.255.254",
Output: false,
},
}

for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
196 changes: 103 additions & 93 deletions app/router/config.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/router/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ message CIDR {
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}

message GeoIPList {
Expand Down
11 changes: 6 additions & 5 deletions infra/conf/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import (
"path/filepath"
"testing"

"github.com/golang/protobuf/proto"
"github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
. "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/infra/conf"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/runtime/protoiface"
)

func init() {
Expand Down Expand Up @@ -52,9 +53,9 @@ func TestDNSConfigParsing(t *testing.T) {
os.Unsetenv("xray.location.asset")
}()

parserCreator := func() func(string) (proto.Message, error) {
return func(s string) (proto.Message, error) {
config := new(DNSConfig)
parserCreator := func() func(string) (protoiface.MessageV1, error) {
return func(s string) (protoiface.MessageV1, error) {
config := new(conf.DNSConfig)
if err := json.Unmarshal([]byte(s), config); err != nil {
return nil, err
}
Expand Down
117 changes: 89 additions & 28 deletions infra/conf/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
}

switch strings.ToLower(ds) {
case "alwaysip":
case "alwaysip", "always_ip", "always-ip":
return router.Config_UseIp
case "ipifnonmatch":
case "ipifnonmatch", "ip_if_non_match", "ip-if-non-match":
return router.Config_IpIfNonMatch
case "ipondemand":
case "ipondemand", "ip_on_demand", "ip-on-demand":
return router.Config_IpOnDemand
default:
return router.Config_AsIs
Expand Down Expand Up @@ -261,7 +261,7 @@ type BooleanMatcher string

func (m BooleanMatcher) Match(domain *router.Domain) bool {
for _, attr := range domain.Attribute {
if attr.Key == string(m) {
if strings.EqualFold(attr.GetKey(), string(m)) {
return true
}
}
Expand All @@ -288,47 +288,69 @@ func (al *AttributeList) IsEmpty() bool {
func parseAttrs(attrs []string) *AttributeList {
al := new(AttributeList)
for _, attr := range attrs {
lc := strings.ToLower(attr)
al.matcher = append(al.matcher, BooleanMatcher(lc))
trimmedAttr := strings.ToLower(strings.TrimSpace(attr))
if len(trimmedAttr) == 0 {
continue
}
al.matcher = append(al.matcher, BooleanMatcher(trimmedAttr))
}
return al
}

func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
parts := strings.Split(siteWithAttr, "@")
if len(parts) == 0 {
return nil, newError("empty site")
return nil, newError("empty rule")
}
list := strings.TrimSpace(parts[0])
attrVal := parts[1:]

if len(list) == 0 {
return nil, newError("empty listname in rule: ", siteWithAttr)
}
country := strings.ToUpper(parts[0])
attrs := parseAttrs(parts[1:])
domains, err := loadSite(file, country)

domains, err := loadSite(file, list)
if err != nil {
return nil, err
}

attrs := parseAttrs(attrVal)
if attrs.IsEmpty() {
if strings.Contains(siteWithAttr, "@") {
newError("empty attribute list: ", siteWithAttr)
}
return domains, nil
}

filteredDomains := make([]*router.Domain, 0, len(domains))
hasAttrMatched := false
for _, domain := range domains {
if attrs.Match(domain) {
hasAttrMatched = true
filteredDomains = append(filteredDomains, domain)
}
}

if !hasAttrMatched {
newError("attribute match no rule: geosite:", siteWithAttr)
}

return filteredDomains, nil
}

func parseDomainRule(domain string) ([]*router.Domain, error) {
if strings.HasPrefix(domain, "geosite:") {
country := strings.ToUpper(domain[8:])
domains, err := loadGeositeWithAttr("geosite.dat", country)
list := domain[8:]
if len(list) == 0 {
return nil, newError("empty listname in rule: ", domain)
}
domains, err := loadGeositeWithAttr("geosite.dat", list)
if err != nil {
return nil, newError("failed to load geosite: ", country).Base(err)
return nil, newError("failed to load geosite: ", list).Base(err)
}
return domains, nil
}

var isExtDatFile = 0
{
const prefix = "ext:"
Expand All @@ -340,37 +362,55 @@ func parseDomainRule(domain string) ([]*router.Domain, error) {
isExtDatFile = len(prefixQualified)
}
}

if isExtDatFile != 0 {
kv := strings.Split(domain[isExtDatFile:], ":")
if len(kv) != 2 {
return nil, newError("invalid external resource: ", domain)
}
filename := kv[0]
country := kv[1]
domains, err := loadGeositeWithAttr(filename, country)
list := kv[1]
domains, err := loadGeositeWithAttr(filename, list)
if err != nil {
return nil, newError("failed to load external sites: ", country, " from ", filename).Base(err)
return nil, newError("failed to load external geosite: ", list, " from ", filename).Base(err)
}

return domains, nil
}

domainRule := new(router.Domain)
switch {
case strings.HasPrefix(domain, "regexp:"):
regexpVal := domain[7:]
if len(regexpVal) == 0 {
return nil, newError("empty regexp type of rule: ", domain)
}
domainRule.Type = router.Domain_Regex
domainRule.Value = domain[7:]
domainRule.Value = regexpVal

case strings.HasPrefix(domain, "domain:"):
domainName := domain[7:]
if len(domainName) == 0 {
return nil, newError("empty domain type of rule: ", domain)
}
domainRule.Type = router.Domain_Domain
domainRule.Value = domain[7:]
domainRule.Value = domainName

case strings.HasPrefix(domain, "full:"):
fullVal := domain[5:]
if len(fullVal) == 0 {
return nil, newError("empty full domain type of rule: ", domain)
}
domainRule.Type = router.Domain_Full
domainRule.Value = domain[5:]
domainRule.Value = fullVal

case strings.HasPrefix(domain, "keyword:"):
keywordVal := domain[8:]
if len(keywordVal) == 0 {
return nil, newError("empty keyword type of rule: ", domain)
}
domainRule.Type = router.Domain_Plain
domainRule.Value = domain[8:]
domainRule.Value = keywordVal

case strings.HasPrefix(domain, "dotless:"):
domainRule.Type = router.Domain_Regex
Expand All @@ -397,17 +437,28 @@ func toCidrList(ips StringList) ([]*router.GeoIP, error) {
for _, ip := range ips {
if strings.HasPrefix(ip, "geoip:") {
country := ip[6:]
isReverseMatch := false
if strings.HasPrefix(ip, "geoip:!") {
country = ip[7:]
isReverseMatch = true
}
if len(country) == 0 {
return nil, newError("empty country name in rule")
}
geoip, err := loadGeoIP(strings.ToUpper(country))
if err != nil {
return nil, newError("failed to load GeoIP: ", country).Base(err)
}

geoipList = append(geoipList, &router.GeoIP{
CountryCode: strings.ToUpper(country),
Cidr: geoip,
CountryCode: strings.ToUpper(country),
Cidr: geoip,
ReverseMatch: isReverseMatch,
})

continue
}

var isExtDatFile = 0
{
const prefix = "ext:"
Expand All @@ -427,14 +478,24 @@ func toCidrList(ips StringList) ([]*router.GeoIP, error) {

filename := kv[0]
country := kv[1]
if len(filename) == 0 || len(country) == 0 {
return nil, newError("empty filename or empty country in rule")
}

isReverseMatch := false
if strings.HasPrefix(country, "!") {
country = country[1:]
isReverseMatch = true
}
geoip, err := loadIP(filename, strings.ToUpper(country))
if err != nil {
return nil, newError("failed to load IPs: ", country, " from ", filename).Base(err)
return nil, newError("failed to load geoip: ", country, " from ", filename).Base(err)
}

geoipList = append(geoipList, &router.GeoIP{
CountryCode: strings.ToUpper(filename + "_" + country),
Cidr: geoip,
CountryCode: strings.ToUpper(filename + "_" + country),
Cidr: geoip,
ReverseMatch: isReverseMatch,
})

continue
Expand Down Expand Up @@ -570,21 +631,21 @@ func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) {
if err != nil {
return nil, newError("invalid router rule").Base(err)
}
if rawRule.Type == "field" {
if strings.EqualFold(rawRule.Type, "field") {
fieldrule, err := parseFieldRule(msg)
if err != nil {
return nil, newError("invalid field rule").Base(err)
}
return fieldrule, nil
}
if rawRule.Type == "chinaip" {
if strings.EqualFold(rawRule.Type, "chinaip") {
chinaiprule, err := parseChinaIPRule(msg)
if err != nil {
return nil, newError("invalid chinaip rule").Base(err)
}
return chinaiprule, nil
}
if rawRule.Type == "chinasites" {
if strings.EqualFold(rawRule.Type, "chinasites") {
chinasitesrule, err := parseChinaSitesRule(msg)
if err != nil {
return nil, newError("invalid chinasites rule").Base(err)
Expand Down
Loading