diff --git a/conf/input.dns_query/dns_query.toml b/conf/input.dns_query/dns_query.toml index ee5f5bf48..4dc12e3bd 100644 --- a/conf/input.dns_query/dns_query.toml +++ b/conf/input.dns_query/dns_query.toml @@ -29,4 +29,12 @@ servers = [] # port = 53 ## Query timeout in seconds. -# timeout = 2 \ No newline at end of file +# timeout = 2 + +## 监控域名劫持 +## record_type必须为A/AAA, expect_query_ips中必须包含对应的域名和IP列表(ip列表越全越好) +## 对比响应中的IP与如下配置的IP,会生成两个新的指标 dns_query_status_change 与 dns_query_status_change_detail +## 当diff 不为空的时候 dns_query_status_change 为1 , diff 为空的时候 dns_query_status_change=0 +## dns_query_status_change_detail{ips="响应的IP列表", diff="响应IP列表-期望IP列表"} +## dns_query_status_change_detail 只有当dns_query_status_change为1 时才会有这个指标 +# expect_query_ips={"baidu.com"=["182.61.244.181","182.61.201.211"]} \ No newline at end of file diff --git a/inputs/dns_query/README.md b/inputs/dns_query/README.md index 3e668514b..d75f65411 100644 --- a/inputs/dns_query/README.md +++ b/inputs/dns_query/README.md @@ -29,6 +29,10 @@ It is not necessary to enable this plugin on every machine. We recommend enablin ## Query timeout in seconds timeout = 5 + + ## Expected IP check for domain hijacking + ## record_type must be A/AAAA. Configure the expected IPs in expect_query_ips + # expect_query_ips={"baidu.com"=["182.61.244.181","182.61.201.211"]} ``` If you need to query different record types (e.g., `A` records and `CNAME` records), you can configure multiple `[[instances]]` blocks. @@ -38,8 +42,10 @@ If you need to query different record types (e.g., `A` records and `CNAME` recor - `dns_query_query_time_ms`: The latency/response time of the DNS resolution in milliseconds. - `dns_query_result_code`: The result code of the probe execution (0 means success, non-zero indicates an exception like timeout or connection failure). - `dns_query_rcode_value`: The standard DNS protocol response code (e.g., NOERROR, NXDOMAIN, SERVFAIL). +- `dns_query_status_change`: When `expect_query_ips` is configured, this value is 1 if any expected IP is missing from the result, otherwise 0. +- `dns_query_status_change_detail`: Reported only when there is a diff (value is 1). Includes a `diff` tag for missing IPs, and an `ips` tag for all returned IPs. -All metrics include tags such as `server`, `domain`, and `record_type`, allowing for granular analysis per DNS server or domain. +All metrics include tags such as `server`, `domain`, and `record_type`, allowing for granular analysis per DNS server or domain. Normal `dns_query` metrics will also include an `ips` tag with comma-separated IPs. ## Alerting Recommendations diff --git a/inputs/dns_query/README_CN.md b/inputs/dns_query/README_CN.md index a5b891985..ead0feac3 100644 --- a/inputs/dns_query/README_CN.md +++ b/inputs/dns_query/README_CN.md @@ -29,6 +29,10 @@ DNS Query 采集插件用于对 DNS 服务器的响应质量进行持续监测 ## DNS 查询的超时时间 (秒) timeout = 5 + + ## 监控域名劫持 + ## record_type 必须为 A/AAAA,expect_query_ips 中需包含预期的 IP 列表 + # expect_query_ips={"baidu.com"=["182.61.244.181","182.61.201.211"]} ``` 如果需要拨测不同类型的记录(如 `A` 记录和 `CNAME` 记录),可以配置多个 `[[instances]]` 块。 @@ -38,8 +42,10 @@ DNS Query 采集插件用于对 DNS 服务器的响应质量进行持续监测 - `dns_query_query_time_ms`: DNS 解析延迟时间 (毫秒) - `dns_query_result_code`: 探测过程的结果码 (0 为成功,非 0 为异常,如超时、无法连接等) - `dns_query_rcode_value`: DNS 协议标准返回的响应码 (如 NOERROR, NXDOMAIN, SERVFAIL 等) +- `dns_query_status_change`: 当配置了预期 IP 校验时,如果实际查询到的 IP 中缺失了预期 IP,则该值为 1,否则为 0。 +- `dns_query_status_change_detail`: 当开启 IP 校验且存在差异时才会上报此指标(值为 1)。带有 `diff` 标签记录缺失的 IP 列表,`ips` 标签记录所有实际返回的 IP 列表。 -所有指标都会带上 `server`, `domain`, `record_type` 等标签,方便按照特定 DNS 服务器或域名进行聚合分析。 +所有指标都会带上 `server`, `domain`, `record_type` 等标签,方便按照特定 DNS 服务器或域名进行聚合分析。基础监控指标中也会追加 `ips` 标签来记录实际解析得到的所有 IP。 ## 告警建议 diff --git a/inputs/dns_query/dashboard.json b/inputs/dns_query/dashboard.json index 1d65dc501..3c1d5dea7 100644 --- a/inputs/dns_query/dashboard.json +++ b/inputs/dns_query/dashboard.json @@ -1,52 +1,985 @@ { - "title": "DNS Query Quality", - "uid": "82f9567e", - "tags": [ - "dns query quality" + "version": "3.0.0", + "var": [ + { + "name": "datasource", + "type": "datasource", + "definition": "prometheus", + "defaultValue": 1, + "label": "Datasource", + "hide": false + }, + { + "name": "domain", + "type": "query", + "datasource": { + "cate": "prometheus", + "value": "${datasource}" + }, + "definition": "label_values(dns_query_result_code, domain)", + "reg": "", + "multi": false, + "hide": false + }, + { + "name": "server", + "type": "query", + "datasource": { + "cate": "prometheus", + "value": "${datasource}" + }, + "definition": "label_values(dns_query_result_code{domain=\"$domain\"}, server)", + "reg": "", + "multi": false, + "hide": false + } ], - "timezone": "browser", - "schemaVersion": 30, "panels": [ { - "title": "DNS Query Time (ms)", - "type": "timeseries", - "gridPos": { + "name": "全局巡检", + "type": "row", + "layout": { + "h": 1, + "w": 24, "x": 0, "y": 0, + "i": "dns-query-row-global", + "isResizable": true + }, + "id": "dns-query-row-global", + "version": "3.4.0", + "overrides": [] + }, + { + "name": "所有域名解析状态", + "type": "hexbin", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_result_code) by (domain, server, record_type)", + "legend": "{{domain}} on {{server}} ({{record_type}})", + "maxDataPoints": 480 + } + ], + "custom": { + "textMode": "valueAndName", + "calc": "lastNotNull", + "valueField": "Value", + "colorRange": [ + "#2c9d3d", + "#f5a623", + "#ce4f52" + ], + "reverseColorOrder": false, + "colorDomainAuto": false, + "colorDomain": [ + 0, + 1 + ] + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "UP", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 1 + }, + "result": { + "text": "TIMEOUT", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "ERROR", + "color": "#ce4f52" + } + } + ], + "standardOptions": {}, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#ce4f52", + "value": 1 + } + ] + } + }, + "layout": { + "h": 8, + "w": 12, + "x": 0, + "y": 1, + "i": "dns-query-global-health", + "isResizable": true + }, + "maxPerRow": 6, + "id": "dns-query-global-health", + "overrides": [] + }, + { + "name": "所有域名解析耗时", + "type": "barGauge", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_query_time_ms) by (domain, server, record_type)", + "legend": "{{domain}} on {{server}} ({{record_type}})", + "maxDataPoints": 480 + } + ], + "custom": { + "calc": "lastNotNull", + "valueField": "Value", + "serieWidth": 20, + "sortOrder": "desc", + "displayMode": "basic", + "valueMode": "color", + "otherPosition": "none" + }, + "options": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#f5a623", + "value": 200 + }, + { + "color": "#ce4f52", + "value": 1000 + } + ] + }, + "standardOptions": { + "unit": "ms" + } + }, + "layout": { + "h": 8, "w": 12, - "h": 8 + "x": 12, + "y": 1, + "i": "dns-query-global-latency", + "isResizable": true + }, + "maxPerRow": 4, + "id": "dns-query-global-latency", + "overrides": [] + }, + { + "name": "DNS 响应码", + "type": "barGauge", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_rcode_value) by (domain, server, record_type)", + "legend": "{{domain}} on {{server}} ({{record_type}})", + "maxDataPoints": 480 + } + ], + "custom": { + "calc": "lastNotNull", + "valueField": "Value", + "serieWidth": 20, + "sortOrder": "desc", + "displayMode": "basic", + "valueMode": "color", + "otherPosition": "none" + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "NOERROR", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "SERVFAIL", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 3 + }, + "result": { + "text": "NXDOMAIN", + "color": "#f5a623" + } + }, + { + "type": "special", + "match": { + "special": 5 + }, + "result": { + "text": "REFUSED", + "color": "#f5a623" + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#f5a623", + "value": 1 + }, + { + "color": "#ce4f52", + "value": 2 + } + ] + }, + "standardOptions": { + "unit": "none" + } + }, + "layout": { + "h": 6, + "w": 8, + "x": 0, + "y": 9, + "i": "dns-query-global-rcode", + "isResizable": true }, - "id": 1, + "id": "dns-query-global-rcode", + "overrides": [] + }, + { + "name": "当前 DNS 查询明细", + "type": "table", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", "targets": [ { - "expr": "dns_query_query_time_ms", - "legendFormat": "{{domain}} on {{server}} ({{record_type}})", - "refId": "A" + "refId": "A", + "expr": "max(dns_query_result_code) by (domain, server, record_type)", + "legend": "result_code", + "maxDataPoints": 480 + }, + { + "refId": "B", + "expr": "max(dns_query_rcode_value) by (domain, server, record_type)", + "legend": "rcode", + "maxDataPoints": 480 + }, + { + "refId": "C", + "expr": "max(dns_query_query_time_ms) by (domain, server, record_type)", + "legend": "latency_ms", + "maxDataPoints": 480 + } + ], + "custom": { + "showHeader": true, + "calc": "lastNotNull", + "displayMode": "labelValuesToRows", + "aggrDimension": "domain,server,record_type", + "tableLayout": "fixed", + "nowrap": false + }, + "options": { + "valueMappings": [], + "standardOptions": {} + }, + "overrides": [ + { + "matcher": { + "value": "A" + }, + "properties": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "UP", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 1 + }, + "result": { + "text": "TIMEOUT", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "ERROR", + "color": "#ce4f52" + } + } + ], + "standardOptions": {} + } + }, + { + "matcher": { + "value": "B" + }, + "properties": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "NOERROR", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "SERVFAIL", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 3 + }, + "result": { + "text": "NXDOMAIN", + "color": "#f5a623" + } + }, + { + "type": "special", + "match": { + "special": 5 + }, + "result": { + "text": "REFUSED", + "color": "#f5a623" + } + } + ], + "standardOptions": {} + } + }, + { + "matcher": { + "value": "C" + }, + "properties": { + "standardOptions": { + "unit": "ms" + } + } } - ] + ], + "layout": { + "h": 6, + "w": 16, + "x": 8, + "y": 9, + "i": "dns-query-global-table", + "isResizable": true + }, + "id": "dns-query-global-table" + }, + { + "name": "解析 IP 校验", + "type": "row", + "layout": { + "h": 1, + "w": 24, + "x": 0, + "y": 15, + "i": "dns-query-row-expect", + "isResizable": true + }, + "id": "dns-query-row-expect", + "version": "3.4.0", + "overrides": [] }, { - "title": "DNS Result Code", + "name": "非预期 IP 状态", + "type": "hexbin", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_status_change) by (domain, server, record_type)", + "legend": "{{domain}} on {{server}} ({{record_type}})", + "maxDataPoints": 480 + } + ], + "custom": { + "textMode": "valueAndName", + "calc": "lastNotNull", + "valueField": "Value", + "colorRange": [ + "#2c9d3d", + "#f5a623", + "#ce4f52" + ], + "reverseColorOrder": false, + "colorDomainAuto": false, + "colorDomain": [ + 0, + 1 + ] + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "OK", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 1 + }, + "result": { + "text": "UNEXPECTED", + "color": "#ce4f52" + } + } + ], + "standardOptions": {}, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#ce4f52", + "value": 1 + } + ] + } + }, + "layout": { + "h": 6, + "w": 8, + "x": 0, + "y": 16, + "i": "dns-query-expect-status", + "isResizable": true + }, + "maxPerRow": 6, + "id": "dns-query-expect-status", + "overrides": [] + }, + { + "name": "非预期 IP 明细", + "type": "table", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_status_change_detail) by (domain, server, record_type, diff, ips)", + "legend": "mismatch", + "maxDataPoints": 480 + } + ], + "custom": { + "showHeader": true, + "calc": "lastNotNull", + "displayMode": "labelValuesToRows", + "aggrDimension": "domain,server,record_type,diff,ips", + "tableLayout": "fixed", + "nowrap": false + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "OK", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 1 + }, + "result": { + "text": "UNEXPECTED", + "color": "#ce4f52" + } + } + ], + "standardOptions": {} + }, + "layout": { + "h": 6, + "w": 16, + "x": 8, + "y": 16, + "i": "dns-query-expect-detail", + "isResizable": true + }, + "id": "dns-query-expect-detail", + "overrides": [] + }, + { + "name": "单域名详情", + "type": "row", + "layout": { + "h": 1, + "w": 24, + "x": 0, + "y": 22, + "i": "dns-query-row-domain", + "isResizable": true + }, + "id": "dns-query-row-domain", + "version": "3.4.0", + "overrides": [] + }, + { + "name": "当前健康状态", + "type": "stat", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_result_code{domain=\"$domain\",server=\"$server\"})", + "legend": "result_code", + "maxDataPoints": 480 + } + ], + "custom": { + "textMode": "value", + "colorMode": "background", + "calc": "lastNotNull", + "valueField": "Value", + "colSpan": 1, + "textSize": {} + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "UP", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 1 + }, + "result": { + "text": "TIMEOUT", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "ERROR", + "color": "#ce4f52" + } + } + ], + "standardOptions": {}, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#ce4f52", + "value": 1 + } + ] + } + }, + "layout": { + "h": 4, + "w": 8, + "x": 0, + "y": 23, + "i": "dns-query-domain-health", + "isResizable": true + }, + "id": "dns-query-domain-health", + "overrides": [] + }, + { + "name": "当前 DNS 响应码", + "type": "stat", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_rcode_value{domain=\"$domain\",server=\"$server\"})", + "legend": "rcode", + "maxDataPoints": 480 + } + ], + "custom": { + "textMode": "valueAndName", + "colorMode": "value", + "calc": "lastNotNull", + "valueField": "Value", + "colSpan": 1, + "textSize": {} + }, + "options": { + "valueMappings": [ + { + "type": "special", + "match": { + "special": 0 + }, + "result": { + "text": "NOERROR", + "color": "#2c9d3d" + } + }, + { + "type": "special", + "match": { + "special": 2 + }, + "result": { + "text": "SERVFAIL", + "color": "#ce4f52" + } + }, + { + "type": "special", + "match": { + "special": 3 + }, + "result": { + "text": "NXDOMAIN", + "color": "#f5a623" + } + }, + { + "type": "special", + "match": { + "special": 5 + }, + "result": { + "text": "REFUSED", + "color": "#f5a623" + } + } + ], + "standardOptions": {}, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#f5a623", + "value": 1 + }, + { + "color": "#ce4f52", + "value": 2 + } + ] + } + }, + "layout": { + "h": 4, + "w": 8, + "x": 8, + "y": 23, + "i": "dns-query-domain-rcode", + "isResizable": true + }, + "id": "dns-query-domain-rcode", + "overrides": [] + }, + { + "name": "当前解析耗时", + "type": "stat", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "max(dns_query_query_time_ms{domain=\"$domain\",server=\"$server\"})", + "legend": "latency_ms", + "maxDataPoints": 480 + } + ], + "custom": { + "textMode": "valueAndName", + "colorMode": "value", + "calc": "lastNotNull", + "valueField": "Value", + "colSpan": 1, + "textSize": {} + }, + "options": { + "standardOptions": { + "unit": "ms" + }, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + }, + { + "color": "#f5a623", + "value": 200 + }, + { + "color": "#ce4f52", + "value": 1000 + } + ] + } + }, + "layout": { + "h": 4, + "w": 8, + "x": 16, + "y": 23, + "i": "dns-query-domain-latency", + "isResizable": true + }, + "id": "dns-query-domain-latency", + "overrides": [] + }, + { + "name": "解析耗时趋势", "type": "timeseries", - "gridPos": { - "x": 12, - "y": 0, + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", + "targets": [ + { + "refId": "A", + "expr": "dns_query_query_time_ms{domain=\"$domain\",server=\"$server\"}", + "legend": "{{record_type}} latency_ms", + "maxDataPoints": 480 + } + ], + "custom": { + "drawStyle": "lines", + "lineInterpolation": "linear", + "spanNulls": false, + "lineWidth": 2, + "fillOpacity": 0.15, + "gradientMode": "none", + "stack": "off", + "scaleDistribution": { + "type": "linear" + } + }, + "options": { + "tooltip": { + "mode": "all", + "sort": "desc" + }, + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "standardOptions": { + "unit": "ms" + }, + "thresholds": { + "steps": [ + { + "color": "#2c9d3d", + "value": null, + "type": "base" + } + ] + } + }, + "layout": { + "h": 7, "w": 12, - "h": 8 + "x": 0, + "y": 27, + "i": "dns-query-domain-latency-trend", + "isResizable": true }, - "id": 2, + "id": "dns-query-domain-latency-trend", + "overrides": [] + }, + { + "name": "状态变化趋势", + "type": "timeseries", + "version": "3.4.0", + "datasourceCate": "prometheus", + "datasourceValue": "${datasource}", "targets": [ { - "expr": "dns_query_result_code", - "legendFormat": "{{domain}} on {{server}} ({{record_type}})", - "refId": "A" + "refId": "A", + "expr": "dns_query_result_code{domain=\"$domain\",server=\"$server\"}", + "legend": "result_code", + "maxDataPoints": 480 + }, + { + "refId": "B", + "expr": "dns_query_rcode_value{domain=\"$domain\",server=\"$server\"}", + "legend": "rcode", + "maxDataPoints": 480 + }, + { + "refId": "C", + "expr": "dns_query_status_change{domain=\"$domain\",server=\"$server\"}", + "legend": "expected_ip_change", + "maxDataPoints": 480 } - ] + ], + "custom": { + "drawStyle": "lines", + "lineInterpolation": "smooth", + "spanNulls": false, + "lineWidth": 2, + "fillOpacity": 0.01, + "gradientMode": "none", + "stack": "off", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "none", + "pointSize": 5 + }, + "options": { + "tooltip": { + "mode": "single" + }, + "legend": { + "displayMode": "list", + "placement": "bottom", + "behaviour": "showItem", + "selectMode": "single" + }, + "standardOptions": {}, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(44, 157, 61)", + "value": null, + "type": "base" + } + ] + }, + "thresholdsStyle": { + "mode": "dashed" + } + }, + "layout": { + "h": 7, + "w": 12, + "x": 12, + "y": 27, + "i": "dns-query-domain-status-trend", + "isResizable": true + }, + "id": "dns-query-domain-status-trend", + "overrides": [] } - ], - "refresh": "1m", - "time": { - "from": "now-1h", - "to": "now" - } -} \ No newline at end of file + ] +} diff --git a/inputs/dns_query/dns_query.go b/inputs/dns_query/dns_query.go index 6926c1c70..5eb5f12e6 100644 --- a/inputs/dns_query/dns_query.go +++ b/inputs/dns_query/dns_query.go @@ -1,11 +1,14 @@ package dns_query import ( + "bytes" "fmt" "log" "net" "os" + "sort" "strconv" + "strings" "sync" "time" @@ -73,6 +76,8 @@ type Instance struct { // Dns query timeout in seconds. 0 means no timeout Timeout int `toml:"timeout"` + + ExpectQueryIps map[string][]string `toml:"expect_query_ips"` } func (ins *Instance) Init() error { @@ -119,6 +124,24 @@ func (ins *Instance) Init() error { return nil } +// set(B) - set(A) +func diff(ips1, ips2 []string) (diff []string, match []string) { + ips1Set := make(map[string]bool) + for _, ip := range ips1 { + ips1Set[ip] = true + } + + for _, ip := range ips2 { + if !ips1Set[ip] { + diff = append(diff, ip) + } else { + match = append(match, ip) + } + } + + return diff, match +} + func (ins *Instance) Gather(slist *types.SampleList) { var wg sync.WaitGroup @@ -133,11 +156,33 @@ func (ins *Instance) Gather(slist *types.SampleList) { "record_type": ins.RecordType, } - dnsQueryTime, rcode, err := ins.getDNSQueryTime(domain, server) + dnsQueryTime, rcode, ips, err := ins.getDNSQueryTime(domain, server) if rcode >= 0 { fields["rcode_value"] = rcode } + if v, ok := ins.ExpectQueryIps[domain]; ok && v != nil { + sort.Slice(ips, func(i, j int) bool { + ip1 := net.ParseIP(ips[i]) + ip2 := net.ParseIP(ips[j]) + return bytes.Compare(ip1, ip2) < 0 + }) + minus, _ := diff(v, ips) + status := 0 + if len(minus) > 0 { + status = 1 + + diffTags := make(map[string]string) + for k, v := range tags { + diffTags[k] = v + } + diffTags["diff"] = strings.Join(minus, ",") + diffTags["ips"] = strings.Join(ips, ",") + slist.PushSample(inputName, "status_change_detail", status, diffTags) + } + slist.PushSample(inputName, "status_change", status, tags) + } + if err == nil { setResult(Success, fields) fields["query_time_ms"] = dnsQueryTime @@ -147,6 +192,9 @@ func (ins *Instance) Gather(slist *types.SampleList) { setResult(Error, fields) log.Println("E!", err) } + if len(ips) > 0 { + tags["ips"] = strings.Join(ips, ",") + } slist.PushSamples("dns_query", fields, tags) wg.Done() @@ -157,7 +205,7 @@ func (ins *Instance) Gather(slist *types.SampleList) { wg.Wait() } -func (ins *Instance) getDNSQueryTime(domain string, server string) (float64, int, error) { +func (ins *Instance) getDNSQueryTime(domain string, server string) (float64, int, []string, error) { dnsQueryTime := float64(0) c := new(dns.Client) @@ -167,20 +215,36 @@ func (ins *Instance) getDNSQueryTime(domain string, server string) (float64, int m := new(dns.Msg) recordType, err := ins.parseRecordType() if err != nil { - return dnsQueryTime, -1, err + return dnsQueryTime, -1, nil, err } m.SetQuestion(dns.Fqdn(domain), recordType) m.RecursionDesired = true r, rtt, err := c.Exchange(m, net.JoinHostPort(server, strconv.Itoa(ins.Port))) if err != nil { - return dnsQueryTime, -1, err + return dnsQueryTime, -1, nil, err } if r.Rcode != dns.RcodeSuccess { - return dnsQueryTime, r.Rcode, fmt.Errorf("invalid answer (%s) from %s after %s query for %s", dns.RcodeToString[r.Rcode], server, ins.RecordType, domain) + return dnsQueryTime, r.Rcode, nil, fmt.Errorf("invalid answer (%s) from %s after %s query for %s", dns.RcodeToString[r.Rcode], server, ins.RecordType, domain) } dnsQueryTime = float64(rtt.Nanoseconds()) / 1e6 - return dnsQueryTime, r.Rcode, nil + ips := make([]string, 0) + for _, record := range r.Answer { + if ip, found := extractIP(record); found { + ips = append(ips, ip) + } + } + return dnsQueryTime, r.Rcode, ips, nil +} + +func extractIP(record dns.RR) (string, bool) { + if r, ok := record.(*dns.A); ok { + return r.A.String(), true + } + if r, ok := record.(*dns.AAAA); ok { + return r.AAAA.String(), true + } + return "", false } func (ins *Instance) parseRecordType() (uint16, error) { @@ -219,4 +283,4 @@ func (ins *Instance) parseRecordType() (uint16, error) { func setResult(result ResultType, fields map[string]interface{}) { fields["result_code"] = uint64(result) -} +} \ No newline at end of file