package geodata import ( "errors" "strings" xraygeodata "github.com/xtls/xray-core/common/geodata" ) // DefaultSiteFile and DefaultIPFile are the databases the geosite: and geoip: // shorthands expand to. const ( DefaultSiteFile = xraygeodata.DefaultGeoSiteDat DefaultIPFile = xraygeodata.DefaultGeoIPDat ) var ( // ErrInvalidToken reports a routing token that names a database but does not // resolve to a file and category. ErrInvalidToken = errors.New("invalid geodata routing token") // ErrWrongKind reports a token carrying the other rule kind's prefix, such // as geoip: typed into a domain field. ErrWrongKind = errors.New("geodata token belongs to the other rule kind") ) var ( sitePrefixes = []string{"ext:", "ext-domain:", "ext-site:"} ipPrefixes = []string{"ext:", "ext-ip:"} ) // Reference is the database file and category a routing token points at. // An empty File means a plain domain or CIDR, which needs no database. type Reference struct { File string Code string Attributes []string Reverse bool } // ParseReference resolves a routing token the way xray-core does, expanding the // geosite:/geoip: shorthands to their ext: form. The core's own parser is not // reusable here: it opens the database from disk as part of parsing, which // would both duplicate this package's cache and fail whenever the file is // merely absent — exactly the case the panel needs to report. func ParseReference(token string, kind GeoKind) (Reference, error) { token = strings.TrimSpace(token) if token == "" { return Reference{}, ErrInvalidToken } var reference Reference if kind == KindIP { // The core strips one "!" before the prefix and another before the code, // each flipping the match, so "!!geoip:cn" is an ordinary geoip:cn. token, reference.Reverse = cutNegation(token) } shorthand, defaultFile, prefixes := "geosite:", DefaultSiteFile, sitePrefixes if kind == KindIP { shorthand, defaultFile, prefixes = "geoip:", DefaultIPFile, ipPrefixes } if rest, found := strings.CutPrefix(token, shorthand); found { token = "ext:" + defaultFile + ":" + rest } // A geoip: token in a domain field (or the reverse) parses as a plain // domain and would be waved through, yet the core cannot resolve it as one. // The field knows its own kind, so say so instead. if strings.HasPrefix(token, otherShorthand(kind)) { return Reference{}, ErrWrongKind } rest, matched := "", false for _, prefix := range prefixes { if trimmed, found := strings.CutPrefix(token, prefix); found { rest, matched = trimmed, true break } } if !matched { return Reference{}, nil } if rest == "" { return Reference{}, ErrInvalidToken } file, code, found := strings.Cut(rest, ":") if !found || file == "" { return Reference{}, ErrInvalidToken } if kind == KindIP { var negated bool code, negated = cutNegation(code) reference.Reverse = reference.Reverse != negated } reference.File = file // Attribute filters exist for domain rules only; in an ip rule the core // treats "@" as part of the category code and fails to resolve it, so the // panel must not quietly strip it either. // Whitespace inside the token is significant: the core matches the code and // the attributes verbatim, so "geosite: cn" and "geosite:cn@ ads" are its // problems to report, not ours to silently repair. Only the case is folded, // which the core does too. if kind == KindSite { parts := strings.Split(code, "@") code = parts[0] for _, attribute := range parts[1:] { // The core rejects an empty attribute outright ("geosite:cn@"), // so accepting it here would hide a config it will not start with. if attribute == "" { return Reference{}, ErrInvalidToken } reference.Attributes = append(reference.Attributes, strings.ToLower(attribute)) } } reference.Code = strings.ToLower(code) if reference.Code == "" { return Reference{}, ErrInvalidToken } return reference, nil } // cutNegation strips leading "!" markers, reporting whether an odd number of // them was present — the core folds a double negation back into a plain match. func cutNegation(value string) (string, bool) { negated := false for { rest, found := strings.CutPrefix(value, "!") if !found { return value, negated } value = rest negated = !negated } } func otherShorthand(kind GeoKind) string { if kind == KindIP { return "geosite:" } return "geoip:" }