|
|
@@ -0,0 +1,502 @@
|
|
|
+package conf
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "v2ray.com/core/app/router"
|
|
|
+ "v2ray.com/core/common/net"
|
|
|
+ "v2ray.com/core/common/platform/filesystem"
|
|
|
+
|
|
|
+ "github.com/golang/protobuf/proto"
|
|
|
+)
|
|
|
+
|
|
|
+type RouterRulesConfig struct {
|
|
|
+ RuleList []json.RawMessage `json:"rules"`
|
|
|
+ DomainStrategy string `json:"domainStrategy"`
|
|
|
+}
|
|
|
+
|
|
|
+type BalancingRule struct {
|
|
|
+ Tag string `json:"tag"`
|
|
|
+ Selectors StringList `json:"selector"`
|
|
|
+}
|
|
|
+
|
|
|
+func (r *BalancingRule) Build() (*router.BalancingRule, error) {
|
|
|
+ if len(r.Tag) == 0 {
|
|
|
+ return nil, newError("empty balancer tag")
|
|
|
+ }
|
|
|
+ if len(r.Selectors) == 0 {
|
|
|
+ return nil, newError("empty selector list")
|
|
|
+ }
|
|
|
+
|
|
|
+ return &router.BalancingRule{
|
|
|
+ Tag: r.Tag,
|
|
|
+ OutboundSelector: []string(r.Selectors),
|
|
|
+ }, nil
|
|
|
+}
|
|
|
+
|
|
|
+type RouterConfig struct {
|
|
|
+ Settings *RouterRulesConfig `json:"settings"` // Deprecated
|
|
|
+ RuleList []json.RawMessage `json:"rules"`
|
|
|
+ DomainStrategy *string `json:"domainStrategy"`
|
|
|
+ Balancers []*BalancingRule `json:"balancers"`
|
|
|
+}
|
|
|
+
|
|
|
+func (c *RouterConfig) getDomainStrategy() router.Config_DomainStrategy {
|
|
|
+ ds := ""
|
|
|
+ if c.DomainStrategy != nil {
|
|
|
+ ds = *c.DomainStrategy
|
|
|
+ } else if c.Settings != nil {
|
|
|
+ ds = c.Settings.DomainStrategy
|
|
|
+ }
|
|
|
+
|
|
|
+ switch strings.ToLower(ds) {
|
|
|
+ case "alwaysip":
|
|
|
+ return router.Config_UseIp
|
|
|
+ case "ipifnonmatch":
|
|
|
+ return router.Config_IpIfNonMatch
|
|
|
+ case "ipondemand":
|
|
|
+ return router.Config_IpOnDemand
|
|
|
+ default:
|
|
|
+ return router.Config_AsIs
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (c *RouterConfig) Build() (*router.Config, error) {
|
|
|
+ config := new(router.Config)
|
|
|
+ config.DomainStrategy = c.getDomainStrategy()
|
|
|
+
|
|
|
+ rawRuleList := c.RuleList
|
|
|
+ if c.Settings != nil {
|
|
|
+ rawRuleList = append(c.RuleList, c.Settings.RuleList...)
|
|
|
+ }
|
|
|
+ for _, rawRule := range rawRuleList {
|
|
|
+ rule, err := ParseRule(rawRule)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ config.Rule = append(config.Rule, rule)
|
|
|
+ }
|
|
|
+ for _, rawBalancer := range c.Balancers {
|
|
|
+ balancer, err := rawBalancer.Build()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ config.BalancingRule = append(config.BalancingRule, balancer)
|
|
|
+ }
|
|
|
+ return config, nil
|
|
|
+}
|
|
|
+
|
|
|
+type RouterRule struct {
|
|
|
+ Type string `json:"type"`
|
|
|
+ OutboundTag string `json:"outboundTag"`
|
|
|
+ BalancerTag string `json:"balancerTag"`
|
|
|
+}
|
|
|
+
|
|
|
+func ParseIP(s string) (*router.CIDR, error) {
|
|
|
+ var addr, mask string
|
|
|
+ i := strings.Index(s, "/")
|
|
|
+ if i < 0 {
|
|
|
+ addr = s
|
|
|
+ } else {
|
|
|
+ addr = s[:i]
|
|
|
+ mask = s[i+1:]
|
|
|
+ }
|
|
|
+ ip := net.ParseAddress(addr)
|
|
|
+ switch ip.Family() {
|
|
|
+ case net.AddressFamilyIPv4:
|
|
|
+ bits := uint32(32)
|
|
|
+ if len(mask) > 0 {
|
|
|
+ bits64, err := strconv.ParseUint(mask, 10, 32)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid network mask for router: ", mask).Base(err)
|
|
|
+ }
|
|
|
+ bits = uint32(bits64)
|
|
|
+ }
|
|
|
+ if bits > 32 {
|
|
|
+ return nil, newError("invalid network mask for router: ", bits)
|
|
|
+ }
|
|
|
+ return &router.CIDR{
|
|
|
+ Ip: []byte(ip.IP()),
|
|
|
+ Prefix: bits,
|
|
|
+ }, nil
|
|
|
+ case net.AddressFamilyIPv6:
|
|
|
+ bits := uint32(128)
|
|
|
+ if len(mask) > 0 {
|
|
|
+ bits64, err := strconv.ParseUint(mask, 10, 32)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid network mask for router: ", mask).Base(err)
|
|
|
+ }
|
|
|
+ bits = uint32(bits64)
|
|
|
+ }
|
|
|
+ if bits > 128 {
|
|
|
+ return nil, newError("invalid network mask for router: ", bits)
|
|
|
+ }
|
|
|
+ return &router.CIDR{
|
|
|
+ Ip: []byte(ip.IP()),
|
|
|
+ Prefix: bits,
|
|
|
+ }, nil
|
|
|
+ default:
|
|
|
+ return nil, newError("unsupported address for router: ", s)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func loadGeoIP(country string) ([]*router.CIDR, error) {
|
|
|
+ return loadIP("geoip.dat", country)
|
|
|
+}
|
|
|
+
|
|
|
+func loadIP(filename, country string) ([]*router.CIDR, error) {
|
|
|
+ geoipBytes, err := filesystem.ReadAsset(filename)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to open file: ", filename).Base(err)
|
|
|
+ }
|
|
|
+ var geoipList router.GeoIPList
|
|
|
+ if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, geoip := range geoipList.Entry {
|
|
|
+ if geoip.CountryCode == country {
|
|
|
+ return geoip.Cidr, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, newError("country not found: " + country)
|
|
|
+}
|
|
|
+
|
|
|
+func loadSite(filename, country string) ([]*router.Domain, error) {
|
|
|
+ geositeBytes, err := filesystem.ReadAsset(filename)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to open file: ", filename).Base(err)
|
|
|
+ }
|
|
|
+ var geositeList router.GeoSiteList
|
|
|
+ if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, site := range geositeList.Entry {
|
|
|
+ if site.CountryCode == country {
|
|
|
+ return site.Domain, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, newError("country not found: " + country)
|
|
|
+}
|
|
|
+
|
|
|
+type AttributeMatcher interface {
|
|
|
+ Match(*router.Domain) bool
|
|
|
+}
|
|
|
+
|
|
|
+type BooleanMatcher string
|
|
|
+
|
|
|
+func (m BooleanMatcher) Match(domain *router.Domain) bool {
|
|
|
+ for _, attr := range domain.Attribute {
|
|
|
+ if attr.Key == string(m) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+type AttributeList struct {
|
|
|
+ matcher []AttributeMatcher
|
|
|
+}
|
|
|
+
|
|
|
+func (al *AttributeList) Match(domain *router.Domain) bool {
|
|
|
+ for _, matcher := range al.matcher {
|
|
|
+ if !matcher.Match(domain) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+func (al *AttributeList) IsEmpty() bool {
|
|
|
+ return len(al.matcher) == 0
|
|
|
+}
|
|
|
+
|
|
|
+func parseAttrs(attrs []string) *AttributeList {
|
|
|
+ al := new(AttributeList)
|
|
|
+ for _, attr := range attrs {
|
|
|
+ lc := strings.ToLower(attr)
|
|
|
+ al.matcher = append(al.matcher, BooleanMatcher(lc))
|
|
|
+ }
|
|
|
+ return al
|
|
|
+}
|
|
|
+
|
|
|
+func loadGeositeWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) {
|
|
|
+ parts := strings.Split(siteWithAttr, "@")
|
|
|
+ if len(parts) == 0 {
|
|
|
+ return nil, newError("empty site")
|
|
|
+ }
|
|
|
+ country := strings.ToUpper(parts[0])
|
|
|
+ attrs := parseAttrs(parts[1:])
|
|
|
+ domains, err := loadSite(file, country)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if attrs.IsEmpty() {
|
|
|
+ return domains, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ filteredDomains := make([]*router.Domain, 0, len(domains))
|
|
|
+ for _, domain := range domains {
|
|
|
+ if attrs.Match(domain) {
|
|
|
+ filteredDomains = append(filteredDomains, domain)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to load geosite: ", country).Base(err)
|
|
|
+ }
|
|
|
+ return domains, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if strings.HasPrefix(domain, "ext:") {
|
|
|
+ kv := strings.Split(domain[4:], ":")
|
|
|
+ if len(kv) != 2 {
|
|
|
+ return nil, newError("invalid external resource: ", domain)
|
|
|
+ }
|
|
|
+ filename := kv[0]
|
|
|
+ country := kv[1]
|
|
|
+ domains, err := loadGeositeWithAttr(filename, country)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to load external sites: ", country, " from ", filename).Base(err)
|
|
|
+ }
|
|
|
+ return domains, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ domainRule := new(router.Domain)
|
|
|
+ switch {
|
|
|
+ case strings.HasPrefix(domain, "regexp:"):
|
|
|
+ domainRule.Type = router.Domain_Regex
|
|
|
+ domainRule.Value = domain[7:]
|
|
|
+ case strings.HasPrefix(domain, "domain:"):
|
|
|
+ domainRule.Type = router.Domain_Domain
|
|
|
+ domainRule.Value = domain[7:]
|
|
|
+ case strings.HasPrefix(domain, "full:"):
|
|
|
+ domainRule.Type = router.Domain_Full
|
|
|
+ domainRule.Value = domain[5:]
|
|
|
+ default:
|
|
|
+ domainRule.Type = router.Domain_Plain
|
|
|
+ domainRule.Value = domain
|
|
|
+ }
|
|
|
+ return []*router.Domain{domainRule}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func toCidrList(ips StringList) ([]*router.GeoIP, error) {
|
|
|
+ var geoipList []*router.GeoIP
|
|
|
+ var customCidrs []*router.CIDR
|
|
|
+
|
|
|
+ for _, ip := range ips {
|
|
|
+ if strings.HasPrefix(ip, "geoip:") {
|
|
|
+ country := ip[6:]
|
|
|
+ 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,
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if strings.HasPrefix(ip, "ext:") {
|
|
|
+ kv := strings.Split(ip[4:], ":")
|
|
|
+ if len(kv) != 2 {
|
|
|
+ return nil, newError("invalid external resource: ", ip)
|
|
|
+ }
|
|
|
+
|
|
|
+ filename := kv[0]
|
|
|
+ country := kv[1]
|
|
|
+ geoip, err := loadGeoIP(strings.ToUpper(country))
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to load IPs: ", country, " from ", filename).Base(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ geoipList = append(geoipList, &router.GeoIP{
|
|
|
+ CountryCode: strings.ToUpper(filename + "_" + country),
|
|
|
+ Cidr: geoip,
|
|
|
+ })
|
|
|
+
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ ipRule, err := ParseIP(ip)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid IP: ", ip).Base(err)
|
|
|
+ }
|
|
|
+ customCidrs = append(customCidrs, ipRule)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(customCidrs) > 0 {
|
|
|
+ geoipList = append(geoipList, &router.GeoIP{
|
|
|
+ Cidr: customCidrs,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return geoipList, nil
|
|
|
+}
|
|
|
+
|
|
|
+func parseFieldRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
|
|
+ type RawFieldRule struct {
|
|
|
+ RouterRule
|
|
|
+ Domain *StringList `json:"domain"`
|
|
|
+ IP *StringList `json:"ip"`
|
|
|
+ Port *PortRange `json:"port"`
|
|
|
+ Network *NetworkList `json:"network"`
|
|
|
+ SourceIP *StringList `json:"source"`
|
|
|
+ User *StringList `json:"user"`
|
|
|
+ InboundTag *StringList `json:"inboundTag"`
|
|
|
+ Protocols *StringList `json:"protocol"`
|
|
|
+ }
|
|
|
+ rawFieldRule := new(RawFieldRule)
|
|
|
+ err := json.Unmarshal(msg, rawFieldRule)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ rule := new(router.RoutingRule)
|
|
|
+ if len(rawFieldRule.OutboundTag) > 0 {
|
|
|
+ rule.TargetTag = &router.RoutingRule_Tag{
|
|
|
+ Tag: rawFieldRule.OutboundTag,
|
|
|
+ }
|
|
|
+ } else if len(rawFieldRule.BalancerTag) > 0 {
|
|
|
+ rule.TargetTag = &router.RoutingRule_BalancingTag{
|
|
|
+ BalancingTag: rawFieldRule.BalancerTag,
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return nil, newError("neither outboundTag nor balancerTag is specified in routing rule")
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.Domain != nil {
|
|
|
+ for _, domain := range *rawFieldRule.Domain {
|
|
|
+ rules, err := parseDomainRule(domain)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to parse domain rule: ", domain).Base(err)
|
|
|
+ }
|
|
|
+ rule.Domain = append(rule.Domain, rules...)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.IP != nil {
|
|
|
+ geoipList, err := toCidrList(*rawFieldRule.IP)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ rule.Geoip = geoipList
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.Port != nil {
|
|
|
+ rule.PortRange = rawFieldRule.Port.Build()
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.Network != nil {
|
|
|
+ rule.Networks = rawFieldRule.Network.Build()
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.SourceIP != nil {
|
|
|
+ geoipList, err := toCidrList(*rawFieldRule.SourceIP)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ rule.SourceGeoip = geoipList
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.User != nil {
|
|
|
+ for _, s := range *rawFieldRule.User {
|
|
|
+ rule.UserEmail = append(rule.UserEmail, s)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.InboundTag != nil {
|
|
|
+ for _, s := range *rawFieldRule.InboundTag {
|
|
|
+ rule.InboundTag = append(rule.InboundTag, s)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if rawFieldRule.Protocols != nil {
|
|
|
+ for _, s := range *rawFieldRule.Protocols {
|
|
|
+ rule.Protocol = append(rule.Protocol, s)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rule, nil
|
|
|
+}
|
|
|
+
|
|
|
+func ParseRule(msg json.RawMessage) (*router.RoutingRule, error) {
|
|
|
+ rawRule := new(RouterRule)
|
|
|
+ err := json.Unmarshal(msg, rawRule)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid router rule").Base(err)
|
|
|
+ }
|
|
|
+ if 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" {
|
|
|
+ chinaiprule, err := parseChinaIPRule(msg)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid chinaip rule").Base(err)
|
|
|
+ }
|
|
|
+ return chinaiprule, nil
|
|
|
+ }
|
|
|
+ if rawRule.Type == "chinasites" {
|
|
|
+ chinasitesrule, err := parseChinaSitesRule(msg)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid chinasites rule").Base(err)
|
|
|
+ }
|
|
|
+ return chinasitesrule, nil
|
|
|
+ }
|
|
|
+ return nil, newError("unknown router rule type: ", rawRule.Type)
|
|
|
+}
|
|
|
+
|
|
|
+func parseChinaIPRule(data []byte) (*router.RoutingRule, error) {
|
|
|
+ rawRule := new(RouterRule)
|
|
|
+ err := json.Unmarshal(data, rawRule)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid router rule").Base(err)
|
|
|
+ }
|
|
|
+ chinaIPs, err := loadGeoIP("CN")
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to load geoip:cn").Base(err)
|
|
|
+ }
|
|
|
+ return &router.RoutingRule{
|
|
|
+ TargetTag: &router.RoutingRule_Tag{
|
|
|
+ Tag: rawRule.OutboundTag,
|
|
|
+ },
|
|
|
+ Cidr: chinaIPs,
|
|
|
+ }, nil
|
|
|
+}
|
|
|
+
|
|
|
+func parseChinaSitesRule(data []byte) (*router.RoutingRule, error) {
|
|
|
+ rawRule := new(RouterRule)
|
|
|
+ err := json.Unmarshal(data, rawRule)
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("invalid router rule").Base(err).AtError()
|
|
|
+ }
|
|
|
+ domains, err := loadGeositeWithAttr("geosite.dat", "CN")
|
|
|
+ if err != nil {
|
|
|
+ return nil, newError("failed to load geosite:cn.").Base(err)
|
|
|
+ }
|
|
|
+ return &router.RoutingRule{
|
|
|
+ TargetTag: &router.RoutingRule_Tag{
|
|
|
+ Tag: rawRule.OutboundTag,
|
|
|
+ },
|
|
|
+ Domain: domains,
|
|
|
+ }, nil
|
|
|
+}
|