diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 12cf781..c5271a9 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -62,7 +62,7 @@ func (p *prog) serveDNS(listenerNum string) error { t := time.Now() ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) ctrld.Log(ctx, mainLog.Load().Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain) - upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain) + upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, ci.Mac, domain) var answer *dns.Msg if !matched && listenerConfig.Restricted { answer = new(dns.Msg) @@ -146,7 +146,7 @@ func (p *prog) serveDNS(listenerNum string) error { // Though domain policy has higher priority than network policy, it is still // processed later, because policy logging want to know whether a network rule // is disregarded in favor of the domain level rule. -func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) { +func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, srcMac, domain string) ([]string, bool) { upstreams := []string{upstreamPrefix + defaultUpstreamNum} matchedPolicy := "no policy" matchedNetwork := "no network" @@ -202,6 +202,19 @@ networkRules: } } +macRules: + for _, rule := range lc.Policy.Macs { + for source, targets := range rule { + if source != "" && strings.EqualFold(source, srcMac) { + matchedPolicy = lc.Policy.Name + matchedNetwork = source + networkTargets = targets + matched = true + break macRules + } + } + } + for _, rule := range lc.Policy.Rules { // There's only one entry per rule, config validation ensures this. for source, targets := range rule { diff --git a/cmd/cli/dns_proxy_test.go b/cmd/cli/dns_proxy_test.go index 674d486..c3b6c96 100644 --- a/cmd/cli/dns_proxy_test.go +++ b/cmd/cli/dns_proxy_test.go @@ -81,6 +81,7 @@ func Test_prog_upstreamFor(t *testing.T) { tests := []struct { name string ip string + mac string defaultUpstreamNum string lc *ctrld.ListenerConfig domain string @@ -88,11 +89,14 @@ func Test_prog_upstreamFor(t *testing.T) { matched bool testLogMsg string }{ - {"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""}, - {"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""}, - {"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""}, - {"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""}, - {"unenforced loging", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"}, + {"Policy map matches", "192.168.0.1:0", "", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""}, + {"Policy split matches", "192.168.0.1:0", "", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""}, + {"Policy map for other network matches", "192.168.1.2:0", "", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""}, + {"No policy map for listener", "192.168.1.2:0", "", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""}, + {"unenforced loging", "192.168.1.2:0", "", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"}, + {"Policy Macs matches upper", "192.168.0.1:0", "14:45:A0:67:83:0A", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:45:a0:67:83:0a"}, + {"Policy Macs matches lower", "192.168.0.1:0", "14:54:4a:8e:08:2d", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:54:4a:8e:08:2d"}, + {"Policy Macs matches case-insensitive", "192.168.0.1:0", "14:54:4A:8E:08:2D", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:54:4a:8e:08:2d"}, } for _, tc := range tests { @@ -111,7 +115,7 @@ func Test_prog_upstreamFor(t *testing.T) { require.NoError(t, err) require.NotNil(t, addr) ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, requestID()) - upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain) + upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.mac, tc.domain) assert.Equal(t, tc.matched, matched) assert.Equal(t, tc.upstreams, upstreams) if tc.testLogMsg != "" { diff --git a/config.go b/config.go index 21d636c..c9c1acc 100644 --- a/config.go +++ b/config.go @@ -253,6 +253,7 @@ type ListenerPolicyConfig struct { Name string `mapstructure:"name" toml:"name,omitempty"` Networks []Rule `mapstructure:"networks" toml:"networks,omitempty,inline,multiline" validate:"dive,len=1"` Rules []Rule `mapstructure:"rules" toml:"rules,omitempty,inline,multiline" validate:"dive,len=1"` + Macs []Rule `mapstructure:"macs" toml:"macs,omitempty,inline,multiline" validate:"dive,len=1"` FailoverRcodes []string `mapstructure:"failover_rcodes" toml:"failover_rcodes,omitempty" validate:"dive,dnsrcode"` FailoverRcodeNumbers []int `mapstructure:"-" toml:"-"` } diff --git a/docs/config.md b/docs/config.md index 35fbda5..29dff8d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -386,7 +386,15 @@ If set to `true` makes the listener `REFUSE` DNS queries from all source IP addr Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to. If no `policy` is defined or the requests do not match any policy rules, it will be forwarded to corresponding upstream of the listener. For example, the request to `listener.0` will be forwarded to `upstream.0`. -The policy `rule` syntax is a simple `toml` inline table with exactly one key/value pair per rule. `key` is either the `network` or a domain. Value is the list of the upstreams. For example: +The policy `rule` syntax is a simple `toml` inline table with exactly one key/value pair per rule. `key` is either: + + - Network. + - Domain. + - Mac Address. + +Value is the list of the upstreams. + +For example: ```toml [listener.0.policy] @@ -400,12 +408,18 @@ rules = [ {"*.local" = ["upstream.1"]}, {"test.com" = ["upstream.2", "upstream.1"]}, ] + +macs = [ + {"14:54:4a:8e:08:2d" = ["upstream.3"]}, +] ``` Above policy will: -- Forward requests on `listener.0` from `network.0` to `upstream.1`. + - Forward requests on `listener.0` for `.local` suffixed domains to `upstream.1`. - Forward requests on `listener.0` for `test.com` to `upstream.2`. If timeout is reached, retry on `upstream.1`. +- Forward requests on `listener.0` from client with Mac `14:54:4a:8e:08:2d` to `upstream.3`. +- Forward requests on `listener.0` from `network.0` to `upstream.1`. - All other requests on `listener.0` that do not match above conditions will be forwarded to `upstream.0`. An empty upstream would not route the request to any defined upstreams, and use the OS default resolver. @@ -419,6 +433,18 @@ rules = [ ] ``` +--- + +Note that the order of matching preference: + +``` +rules => macs => networks +``` + +And within each policy, the rules are processed from top to bottom. + +--- + #### name `name` is the name for the policy. @@ -440,6 +466,13 @@ rules = [ - Required: no - Default: [] +### macs: +`macs` is the list of mac rules within the policy. Mac address value is case-insensitive. + +- Type: array of macs +- Required: no +- Default: [] + ### failover_rcodes For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`. diff --git a/testhelper/config.go b/testhelper/config.go index 5c2e5f4..6199424 100644 --- a/testhelper/config.go +++ b/testhelper/config.go @@ -82,4 +82,8 @@ rules = [ {"*.ru" = ["upstream.1"]}, {"*.local.host" = ["upstream.2", "upstream.0"]}, ] +macs = [ + {"14:45:A0:67:83:0A" = ["upstream.2"]}, + {"14:54:4a:8e:08:2d" = ["upstream.2"]}, +] `