Support using MX records for DNS discovery (#10099)

It's currently possible to use blackbox_exporter to probe MX records
themselves. However it's not possible to do an end-to-end test, like is
possible with SRV records. This makes it possible to use MX records as a
source of hostnames in the same way as SRV records.

Signed-off-by: David Leadbeater <dgl@dgl.cx>
This commit is contained in:
David Leadbeater 2022-08-03 19:19:26 +10:00 committed by GitHub
parent a7f19b5775
commit d677ec489e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 5 deletions

View File

@ -40,6 +40,8 @@ const (
dnsSrvRecordPrefix = model.MetaLabelPrefix + "dns_srv_record_" dnsSrvRecordPrefix = model.MetaLabelPrefix + "dns_srv_record_"
dnsSrvRecordTargetLabel = dnsSrvRecordPrefix + "target" dnsSrvRecordTargetLabel = dnsSrvRecordPrefix + "target"
dnsSrvRecordPortLabel = dnsSrvRecordPrefix + "port" dnsSrvRecordPortLabel = dnsSrvRecordPrefix + "port"
dnsMxRecordPrefix = model.MetaLabelPrefix + "dns_mx_record_"
dnsMxRecordTargetLabel = dnsMxRecordPrefix + "target"
// Constants for instrumentation. // Constants for instrumentation.
namespace = "prometheus" namespace = "prometheus"
@ -100,7 +102,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
switch strings.ToUpper(c.Type) { switch strings.ToUpper(c.Type) {
case "SRV": case "SRV":
case "A", "AAAA": case "A", "AAAA", "MX":
if c.Port == 0 { if c.Port == 0 {
return errors.New("a port is required in DNS-SD configs for all record types except SRV") return errors.New("a port is required in DNS-SD configs for all record types except SRV")
} }
@ -136,6 +138,8 @@ func NewDiscovery(conf SDConfig, logger log.Logger) *Discovery {
qtype = dns.TypeAAAA qtype = dns.TypeAAAA
case "SRV": case "SRV":
qtype = dns.TypeSRV qtype = dns.TypeSRV
case "MX":
qtype = dns.TypeMX
} }
d := &Discovery{ d := &Discovery{
names: conf.Names, names: conf.Names,
@ -195,7 +199,7 @@ func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targ
} }
for _, record := range response.Answer { for _, record := range response.Answer {
var target, dnsSrvRecordTarget, dnsSrvRecordPort model.LabelValue var target, dnsSrvRecordTarget, dnsSrvRecordPort, dnsMxRecordTarget model.LabelValue
switch addr := record.(type) { switch addr := record.(type) {
case *dns.SRV: case *dns.SRV:
@ -206,6 +210,13 @@ func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targ
addr.Target = strings.TrimRight(addr.Target, ".") addr.Target = strings.TrimRight(addr.Target, ".")
target = hostPort(addr.Target, int(addr.Port)) target = hostPort(addr.Target, int(addr.Port))
case *dns.MX:
dnsMxRecordTarget = model.LabelValue(addr.Mx)
// Remove the final dot from rooted DNS names to make them look more usual.
addr.Mx = strings.TrimRight(addr.Mx, ".")
target = hostPort(addr.Mx, d.port)
case *dns.A: case *dns.A:
target = hostPort(addr.A.String(), d.port) target = hostPort(addr.A.String(), d.port)
case *dns.AAAA: case *dns.AAAA:
@ -222,6 +233,7 @@ func (d *Discovery) refreshOne(ctx context.Context, name string, ch chan<- *targ
dnsNameLabel: model.LabelValue(name), dnsNameLabel: model.LabelValue(name),
dnsSrvRecordTargetLabel: dnsSrvRecordTarget, dnsSrvRecordTargetLabel: dnsSrvRecordTarget,
dnsSrvRecordPortLabel: dnsSrvRecordPort, dnsSrvRecordPortLabel: dnsSrvRecordPort,
dnsMxRecordTargetLabel: dnsMxRecordTarget,
}) })
} }

View File

@ -80,6 +80,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_name": "web.example.com.", "__meta_dns_name": "web.example.com.",
"__meta_dns_srv_record_target": "", "__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "", "__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "",
}, },
}, },
}, },
@ -110,6 +111,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_name": "web.example.com.", "__meta_dns_name": "web.example.com.",
"__meta_dns_srv_record_target": "", "__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "", "__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "",
}, },
}, },
}, },
@ -140,12 +142,14 @@ func TestDNS(t *testing.T) {
"__meta_dns_name": "_mysql._tcp.db.example.com.", "__meta_dns_name": "_mysql._tcp.db.example.com.",
"__meta_dns_srv_record_target": "db1.example.com.", "__meta_dns_srv_record_target": "db1.example.com.",
"__meta_dns_srv_record_port": "3306", "__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
}, },
{ {
"__address__": "db2.example.com:3306", "__address__": "db2.example.com:3306",
"__meta_dns_name": "_mysql._tcp.db.example.com.", "__meta_dns_name": "_mysql._tcp.db.example.com.",
"__meta_dns_srv_record_target": "db2.example.com.", "__meta_dns_srv_record_target": "db2.example.com.",
"__meta_dns_srv_record_port": "3306", "__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
}, },
}, },
}, },
@ -175,6 +179,7 @@ func TestDNS(t *testing.T) {
"__meta_dns_name": "_mysql._tcp.db.example.com.", "__meta_dns_name": "_mysql._tcp.db.example.com.",
"__meta_dns_srv_record_target": "db1.example.com.", "__meta_dns_srv_record_target": "db1.example.com.",
"__meta_dns_srv_record_port": "3306", "__meta_dns_srv_record_port": "3306",
"__meta_dns_mx_record_target": "",
}, },
}, },
}, },
@ -195,6 +200,45 @@ func TestDNS(t *testing.T) {
}, },
}, },
}, },
{
name: "MX record query",
config: SDConfig{
Names: []string{"example.com."},
Type: "MX",
Port: 25,
RefreshInterval: model.Duration(time.Minute),
},
lookup: func(name string, qtype uint16, logger log.Logger) (*dns.Msg, error) {
return &dns.Msg{
Answer: []dns.RR{
&dns.MX{Preference: 0, Mx: "smtp1.example.com."},
&dns.MX{Preference: 10, Mx: "smtp2.example.com."},
},
},
nil
},
expected: []*targetgroup.Group{
{
Source: "example.com.",
Targets: []model.LabelSet{
{
"__address__": "smtp1.example.com:25",
"__meta_dns_name": "example.com.",
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "smtp1.example.com.",
},
{
"__address__": "smtp2.example.com:25",
"__meta_dns_name": "example.com.",
"__meta_dns_srv_record_target": "",
"__meta_dns_srv_record_port": "",
"__meta_dns_mx_record_target": "smtp2.example.com.",
},
},
},
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {

View File

@ -961,8 +961,8 @@ A DNS-based service discovery configuration allows specifying a set of DNS
domain names which are periodically queried to discover a list of targets. The domain names which are periodically queried to discover a list of targets. The
DNS servers to be contacted are read from `/etc/resolv.conf`. DNS servers to be contacted are read from `/etc/resolv.conf`.
This service discovery method only supports basic DNS A, AAAA and SRV record This service discovery method only supports basic DNS A, AAAA, MX and SRV
queries, but not the advanced DNS-SD approach specified in record queries, but not the advanced DNS-SD approach specified in
[RFC6763](https://tools.ietf.org/html/rfc6763). [RFC6763](https://tools.ietf.org/html/rfc6763).
The following meta labels are available on targets during [relabeling](#relabel_config): The following meta labels are available on targets during [relabeling](#relabel_config):
@ -970,13 +970,14 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_dns_name`: the record name that produced the discovered target. * `__meta_dns_name`: the record name that produced the discovered target.
* `__meta_dns_srv_record_target`: the target field of the SRV record * `__meta_dns_srv_record_target`: the target field of the SRV record
* `__meta_dns_srv_record_port`: the port field of the SRV record * `__meta_dns_srv_record_port`: the port field of the SRV record
* `__meta_dns_mx_record_target`: the target field of the MX record
```yaml ```yaml
# A list of DNS domain names to be queried. # A list of DNS domain names to be queried.
names: names:
[ - <string> ] [ - <string> ]
# The type of DNS query to perform. One of SRV, A, or AAAA. # The type of DNS query to perform. One of SRV, A, AAAA or MX.
[ type: <string> | default = 'SRV' ] [ type: <string> | default = 'SRV' ]
# The port number used if the query type is not SRV. # The port number used if the query type is not SRV.