mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-08 14:13:22 +00:00
DNS outbound: Add rules (matches qtype and domain, then action) (#5981)
https://github.com/XTLS/Xray-core/pull/5981#issuecomment-4279809648 Example: https://github.com/XTLS/Xray-core/pull/5981#issuecomment-4283200236 Closes https://github.com/XTLS/Xray-core/issues/5218
This commit is contained in:
@@ -1,19 +1,70 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type DNSOutboundRuleConfig struct {
|
||||
Action string `json:"action"`
|
||||
QType *PortList `json:"qtype"`
|
||||
Domain *StringList `json:"domain"`
|
||||
}
|
||||
|
||||
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
|
||||
rule := &dns.DNSRuleConfig{}
|
||||
|
||||
switch strings.ToLower(c.Action) {
|
||||
case "direct":
|
||||
rule.Action = dns.RuleAction_Direct
|
||||
case "drop":
|
||||
rule.Action = dns.RuleAction_Drop
|
||||
case "reject":
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
case "hijack":
|
||||
rule.Action = dns.RuleAction_Hijack
|
||||
default:
|
||||
return nil, errors.New("unknown action: ", c.Action)
|
||||
}
|
||||
|
||||
if c.QType != nil {
|
||||
for _, r := range c.QType.Range {
|
||||
if r.From > r.To {
|
||||
return nil, errors.New("invalid qtype range: ", r.String())
|
||||
}
|
||||
if r.To > 65535 {
|
||||
return nil, errors.New("dns rule qtype out of range: ", r.String())
|
||||
}
|
||||
for qtype := r.From; qtype <= r.To; qtype++ {
|
||||
rule.Qtype = append(rule.Qtype, int32(qtype))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Domain != nil {
|
||||
rules, err := geodata.ParseDomainRules(*c.Domain, geodata.Domain_Substr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule.Domain = rules
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
type DNSOutboundConfig struct {
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
NonIPQuery string `json:"nonIPQuery"`
|
||||
BlockTypes []int32 `json:"blockTypes"`
|
||||
Network Network `json:"network"`
|
||||
Address *Address `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
Rules []*DNSOutboundRuleConfig `json:"rules"`
|
||||
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
|
||||
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
|
||||
}
|
||||
|
||||
func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
@@ -27,12 +78,78 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
|
||||
if c.Address != nil {
|
||||
config.Server.Address = c.Address.Build()
|
||||
}
|
||||
switch c.NonIPQuery {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New(`unknown "nonIPQuery": `, c.NonIPQuery)
|
||||
|
||||
// todo: remove legacy
|
||||
if c.NonIPQuery != nil || c.BlockTypes != nil {
|
||||
if c.Rules != nil {
|
||||
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
|
||||
}
|
||||
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes" in DNS outbound`, `"rules"`)
|
||||
rules, err := c.buildLegacyDNSPolicy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = rules
|
||||
return config, nil
|
||||
}
|
||||
config.Non_IPQuery = c.NonIPQuery
|
||||
config.BlockTypes = c.BlockTypes
|
||||
|
||||
for _, r := range c.Rules {
|
||||
rule, err := r.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rule = append(config.Rule, rule)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) {
|
||||
rules := make([]*dns.DNSRuleConfig, 0, 3)
|
||||
|
||||
mode := "reject"
|
||||
if c.NonIPQuery != nil && *c.NonIPQuery != "" {
|
||||
mode = *c.NonIPQuery
|
||||
}
|
||||
switch mode {
|
||||
case "", "reject", "drop", "skip":
|
||||
default:
|
||||
return nil, errors.New("unknown nonIPQuery: ", mode)
|
||||
}
|
||||
|
||||
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
|
||||
if mode == "reject" {
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
}
|
||||
for _, qtype := range *c.BlockTypes {
|
||||
if qtype < 0 || qtype > 65535 {
|
||||
return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
|
||||
}
|
||||
rule.Qtype = append(rule.Qtype, qtype)
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
{
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
|
||||
rule.Qtype = append(rule.Qtype, 1)
|
||||
rule.Qtype = append(rule.Qtype, 28)
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
{
|
||||
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
|
||||
if mode == "reject" {
|
||||
rule.Action = dns.RuleAction_Reject
|
||||
} else if mode == "drop" {
|
||||
rule.Action = dns.RuleAction_Drop
|
||||
} else if mode == "skip" {
|
||||
rule.Action = dns.RuleAction_Direct
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
@@ -29,5 +31,208 @@ func TestDnsProxyConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": "1,3,23-24"
|
||||
}, {
|
||||
"action": "drop",
|
||||
"qtype": 28,
|
||||
"domain": ["domain:example.com", "full:example.com"]
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
Qtype: []int32{1, 3, 23, 24},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{28},
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Domain,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Full,
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "reject",
|
||||
"domain": "keyword:example"
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Domain: []*geodata.DomainRule{
|
||||
{
|
||||
Value: &geodata.DomainRule_Custom{
|
||||
Custom: &geodata.Domain{
|
||||
Type: geodata.Domain_Substr,
|
||||
Value: "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"rules": [{
|
||||
"action": "drop",
|
||||
"qtype": 257
|
||||
}]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{257},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": []
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"blockTypes": [1, 65]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
Qtype: []int32{1, 65},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Reject,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "drop",
|
||||
"blockTypes": [1]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{1},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: `{
|
||||
"nonIPQuery": "skip",
|
||||
"blockTypes": [65, 28]
|
||||
}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &dns.Config{
|
||||
Server: &net.Endpoint{},
|
||||
Rule: []*dns.DNSRuleConfig{
|
||||
{
|
||||
Action: dns.RuleAction_Drop,
|
||||
Qtype: []int32{65, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Hijack,
|
||||
Qtype: []int32{1, 28},
|
||||
},
|
||||
{
|
||||
Action: dns.RuleAction_Direct,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// todo: remove legacy
|
||||
func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(DNSOutboundConfig)
|
||||
}
|
||||
|
||||
_, err := loadJSON(creator)(`{
|
||||
"rules": [{
|
||||
"action": "direct",
|
||||
"qtype": 65
|
||||
}],
|
||||
"blockTypes": [65]
|
||||
}`)
|
||||
if err == nil || !strings.Contains(err.Error(), `legacy nonIPQuery and blockTypes cannot be mixed with rules`) {
|
||||
t.Fatal("expected mixed legacy/new config error, but got ", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user