Skip to content
Merged
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
249 changes: 148 additions & 101 deletions app/dns/config.pb.go

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions app/dns/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ message NameServer {
}

repeated PriorityDomain prioritized_domain = 2;
repeated xray.app.router.GeoIP geoip = 3;
repeated xray.app.router.GeoIP expected_geoip = 3;
repeated OriginalRule original_rules = 4;
QueryStrategy query_strategy = 7;
bool allowUnexpectedIPs = 8;
bool actPrior = 8;
string tag = 9;
uint64 timeoutMs = 10;
bool disableCache = 11;
bool finalQuery = 12;
repeated xray.app.router.GeoIP unexpected_geoip = 13;
bool actUnprior = 14;
}

enum DomainMatchingType {
Expand All @@ -44,6 +48,7 @@ enum QueryStrategy {
USE_IP = 0;
USE_IP4 = 1;
USE_IP6 = 2;
USE_SYS = 3;
}

message Config {
Expand Down
45 changes: 42 additions & 3 deletions app/dns/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type DNS struct {
ctx context.Context
domainMatcher strmatcher.IndexMatcher
matcherInfos []*DomainMatcherInfo
checkSystem bool
}

// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
Expand All @@ -47,13 +48,21 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
}

var ipOption dns.IPOption
checkSystem := false
switch config.QueryStrategy {
case QueryStrategy_USE_IP:
ipOption = dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}
case QueryStrategy_USE_SYS:
ipOption = dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}
checkSystem = true
case QueryStrategy_USE_IP4:
ipOption = dns.IPOption{
IPv4Enable: true,
Expand Down Expand Up @@ -108,7 +117,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
myClientIP = net.IP(ns.ClientIp)
}

disableCache := config.DisableCache
disableCache := config.DisableCache || ns.DisableCache

var tag = defaultTag
if len(ns.Tag) > 0 {
Expand All @@ -118,6 +127,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable {
return nil, errors.New("no QueryStrategy available for ", ns.Address)
}

client, err := NewClient(ctx, ns, myClientIP, disableCache, tag, clientIPOption, &matcherInfos, updateDomain)
if err != nil {
return nil, errors.New("failed to create client").Base(err)
Expand All @@ -139,6 +149,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
matcherInfos: matcherInfos,
disableFallback: config.DisableFallback,
disableFallbackIfMatch: config.DisableFallbackIfMatch,
checkSystem: checkSystem,
}, nil
}

Expand Down Expand Up @@ -179,8 +190,14 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
return nil, 0, errors.New("empty domain name")
}

option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable
if s.checkSystem {
supportIPv4, supportIPv6 := checkSystemNetwork()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable
}

if !option.IPv4Enable && !option.IPv6Enable {
return nil, 0, dns.ErrEmptyResponse
Expand Down Expand Up @@ -227,6 +244,9 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
}
errs = append(errs, err)

if client.IsFinalQuery() {
break
}
}

if len(errs) > 0 {
Expand Down Expand Up @@ -318,3 +338,22 @@ func init() {
return New(ctx, config.(*Config))
}))
}

func checkSystemNetwork() (supportIPv4 bool, supportIPv6 bool) {
conn4, err4 := net.Dial("udp4", "8.8.8.8:53")
if err4 != nil {
supportIPv4 = false
} else {
supportIPv4 = true
conn4.Close()
}

conn6, err6 := net.Dial("udp6", "[2001:4860:4860::8888]:53")
if err6 != nil {
supportIPv6 = false
} else {
supportIPv6 = true
conn6.Close()
}
return
}
14 changes: 7 additions & 7 deletions app/dns/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ func TestIPMatch(t *testing.T) {
},
Port: uint32(port),
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{
CountryCode: "local",
Cidr: []*router.CIDR{
Expand All @@ -563,7 +563,7 @@ func TestIPMatch(t *testing.T) {
},
Port: uint32(port),
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{
CountryCode: "test",
Cidr: []*router.CIDR{
Expand Down Expand Up @@ -667,7 +667,7 @@ func TestLocalDomain(t *testing.T) {
// Equivalent of dotless:localhost
{Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"},
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{ // Will match localhost, localhost-a and localhost-b,
CountryCode: "local",
Cidr: []*router.CIDR{
Expand Down Expand Up @@ -897,7 +897,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Domain: "google.com",
},
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{ // Will only match 8.8.8.8 and 8.8.4.4
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
Expand All @@ -922,7 +922,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Domain: "google.com",
},
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{ // Will match 8.8.8.8 and 8.8.8.7, etc
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 8, 7}, Prefix: 24},
Expand All @@ -946,7 +946,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Domain: "api.google.com",
},
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{ // Will only match 8.8.7.7 (api.google.com)
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 7}, Prefix: 32},
Expand All @@ -970,7 +970,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
Domain: "v2.api.google.com",
},
},
Geoip: []*router.GeoIP{
ExpectedGeoip: []*router.GeoIP{
{ // Will only match 8.8.7.8 (v2.api.google.com)
Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 8}, Prefix: 32},
Expand Down
113 changes: 76 additions & 37 deletions app/dns/nameserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ type Server interface {

// Client is the interface for DNS client.
type Client struct {
server Server
skipFallback bool
domains []string
expectedIPs []*router.GeoIPMatcher
allowUnexpectedIPs bool
tag string
timeoutMs time.Duration
ipOption *dns.IPOption
server Server
skipFallback bool
domains []string
expectedIPs []*router.GeoIPMatcher
unexpectedIPs []*router.GeoIPMatcher
actPrior bool
actUnprior bool
tag string
timeoutMs time.Duration
finalQuery bool
ipOption *dns.IPOption
checkSystem bool
}

// NewServer creates a name server object according to the network destination url.
Expand Down Expand Up @@ -150,13 +154,23 @@ func NewClient(
}

// Establish expected IPs
var matchers []*router.GeoIPMatcher
for _, geoip := range ns.Geoip {
var expectedMatchers []*router.GeoIPMatcher
for _, geoip := range ns.ExpectedGeoip {
matcher, err := router.GlobalGeoIPContainer.Add(geoip)
if err != nil {
return errors.New("failed to create ip matcher").Base(err).AtWarning()
return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
}
matchers = append(matchers, matcher)
expectedMatchers = append(expectedMatchers, matcher)
}

// Establish unexpected IPs
var unexpectedMatchers []*router.GeoIPMatcher
for _, geoip := range ns.UnexpectedGeoip {
matcher, err := router.GlobalGeoIPContainer.Add(geoip)
if err != nil {
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
}
unexpectedMatchers = append(unexpectedMatchers, matcher)
}

if len(clientIP) > 0 {
Expand All @@ -173,14 +187,20 @@ func NewClient(
timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond
}

checkSystem := ns.QueryStrategy == QueryStrategy_USE_SYS

client.server = server
client.skipFallback = ns.SkipFallback
client.domains = rules
client.expectedIPs = matchers
client.allowUnexpectedIPs = ns.AllowUnexpectedIPs
client.expectedIPs = expectedMatchers
client.unexpectedIPs = unexpectedMatchers
client.actPrior = ns.ActPrior
client.actUnprior = ns.ActUnprior
client.tag = tag
client.timeoutMs = timeoutMs
client.finalQuery = ns.FinalQuery
client.ipOption = &ipOption
client.checkSystem = checkSystem
return nil
})
return client, err
Expand All @@ -191,10 +211,21 @@ func (c *Client) Name() string {
return c.server.Name()
}

func (c *Client) IsFinalQuery() bool {
return c.finalQuery
}

// QueryIP sends DNS query to the name server with the client's IP.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) {
option.IPv4Enable = option.IPv4Enable && c.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && c.ipOption.IPv6Enable
if c.checkSystem {
supportIPv4, supportIPv6 := checkSystemNetwork()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
option.IPv4Enable = option.IPv4Enable && c.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && c.ipOption.IPv6Enable
}

if !option.IPv4Enable && !option.IPv6Enable {
return nil, 0, dns.ErrEmptyResponse
}
Expand All @@ -212,39 +243,47 @@ func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption
return nil, 0, dns.ErrEmptyResponse
}

if len(c.expectedIPs) > 0 {
newIps := c.MatchExpectedIPs(domain, ips)
if len(newIps) == 0 {
if !c.allowUnexpectedIPs {
return nil, 0, dns.ErrEmptyResponse
}
} else {
ips = newIps
if len(c.expectedIPs) > 0 && !c.actPrior {
ips = router.MatchIPs(c.expectedIPs, ips, false)
errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", ips, " matched at server ", c.Name())
if len(ips) == 0 {
return nil, 0, dns.ErrEmptyResponse
}
}

return ips, ttl, nil
}
if len(c.unexpectedIPs) > 0 && !c.actUnprior {
ips = router.MatchIPs(c.unexpectedIPs, ips, true)
errors.LogDebug(context.Background(), "domain ", domain, " unexpectedIPs ", ips, " matched at server ", c.Name())
if len(ips) == 0 {
return nil, 0, dns.ErrEmptyResponse
}
}

// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.
func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) []net.IP {
var newIps []net.IP
for _, ip := range ips {
for _, matcher := range c.expectedIPs {
if matcher.Match(ip) {
newIps = append(newIps, ip)
break
}
if len(c.expectedIPs) > 0 && c.actPrior {
ipsNew := router.MatchIPs(c.expectedIPs, ips, false)
if len(ipsNew) > 0 {
ips = ipsNew
errors.LogDebug(context.Background(), "domain ", domain, " priorIPs ", ips, " matched at server ", c.Name())
}
}
errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", newIps, " matched at server ", c.Name())
return newIps

if len(c.unexpectedIPs) > 0 && c.actUnprior {
ipsNew := router.MatchIPs(c.unexpectedIPs, ips, true)
if len(ipsNew) > 0 {
ips = ipsNew
errors.LogDebug(context.Background(), "domain ", domain, " unpriorIPs ", ips, " matched at server ", c.Name())
}
}

return ips, ttl, nil
}

func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) dns.IPOption {
switch queryStrategy {
case QueryStrategy_USE_IP:
return ipOption
case QueryStrategy_USE_SYS:
return ipOption
case QueryStrategy_USE_IP4:
return dns.IPOption{
IPv4Enable: ipOption.IPv4Enable,
Expand Down
26 changes: 26 additions & 0 deletions app/router/condition_geoip.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,29 @@ func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
}

var GlobalGeoIPContainer GeoIPMatcherContainer

func MatchIPs(matchers []*GeoIPMatcher, ips []net.IP, reverse bool) []net.IP {
if len(matchers) == 0 {
panic("GeoIP matchers should not be empty to avoid ambiguity")
}
newIPs := make([]net.IP, 0, len(ips))
var isFound bool
for _, ip := range ips {
isFound = false
for _, matcher := range matchers {
if matcher.Match(ip) {
isFound = true
break
}
}
if isFound && !reverse {
newIPs = append(newIPs, ip)
continue
}
if !isFound && reverse {
newIPs = append(newIPs, ip)
continue
}
}
return newIPs
}
Loading