Skip to content

Commit c29208c

Browse files
Add ResponseRules support for checkDomain (domain-based lookups)
Co-authored-by: thisisjaymehta <31812582+thisisjaymehta@users.noreply.github.com>
1 parent 66339dd commit c29208c

3 files changed

Lines changed: 145 additions & 12 deletions

File tree

docs/reference/checks/dnsbl.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ Defines per-response-code rules for scoring and custom messages. This is useful
207207
for combined DNSBLs like Spamhaus ZEN that return different codes for different
208208
listing types.
209209

210+
This works for both IP-based lookups (client_ipv4, client_ipv6) and domain-based
211+
lookups (ehlo, mailfrom).
212+
210213
Each `response` block takes one or more IP addresses or CIDR ranges as arguments
211214
and contains the following directives:
212215

internal/check/dnsbl/common.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,25 +72,54 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, cfg List, domain st
7272
return nil
7373
}
7474

75-
// Attempt to extract explanation string.
76-
txts, err := resolver.LookupTXT(context.Background(), query)
77-
if err != nil || len(txts) == 0 {
78-
// Not significant, include addresses as reason. Usually they are
79-
// mapped to some predefined 'reasons' by BL.
80-
return ListedErr{
81-
Identity: domain,
82-
List: cfg.Zone,
83-
Reason: strings.Join(addrs, "; "),
75+
var score int
76+
var customMessage string
77+
var filteredAddrs []string
78+
79+
// If ResponseRules is configured, use new behavior
80+
if len(cfg.ResponseRules) > 0 {
81+
// Convert string addresses to IPAddr for matching
82+
ipAddrs := make([]net.IPAddr, 0, len(addrs))
83+
for _, addr := range addrs {
84+
if ip := net.ParseIP(addr); ip != nil {
85+
ipAddrs = append(ipAddrs, net.IPAddr{IP: ip})
86+
}
8487
}
88+
89+
matchedScore, matchedMessages, matchedReasons, matched := matchResponseRules(ipAddrs, cfg.ResponseRules)
90+
if !matched {
91+
return nil
92+
}
93+
score = matchedScore
94+
95+
// Use first matched message if available
96+
if len(matchedMessages) > 0 {
97+
customMessage = matchedMessages[0]
98+
}
99+
100+
filteredAddrs = matchedReasons
101+
} else {
102+
// Legacy behavior: accept all addresses
103+
filteredAddrs = addrs
85104
}
86105

87-
// Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so
88-
// don't mangle them by joining with "", instead join with "; ".
106+
// Attempt to extract explanation string from TXT records (shared by both paths)
107+
txts, err := resolver.LookupTXT(ctx, query)
108+
var reason string
109+
if err == nil && len(txts) > 0 {
110+
reason = strings.Join(txts, "; ")
111+
} else {
112+
// Not significant, include addresses as reason. Usually they are
113+
// mapped to some predefined 'reasons' by BL.
114+
reason = strings.Join(filteredAddrs, "; ")
115+
}
89116

90117
return ListedErr{
91118
Identity: domain,
92119
List: cfg.Zone,
93-
Reason: strings.Join(txts, "; "),
120+
Reason: reason,
121+
Score: score,
122+
Message: customMessage,
94123
}
95124
}
96125

internal/check/dnsbl/common_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,104 @@ func TestCheckIP(t *testing.T) {
236236
Reason: "127.0.0.1",
237237
})
238238
}
239+
240+
func TestCheckDomainWithResponseRules(t *testing.T) {
241+
test := func(zones map[string]mockdns.Zone, cfg List, domain string, expectedErr error) {
242+
t.Helper()
243+
resolver := mockdns.Resolver{Zones: zones}
244+
err := checkDomain(context.Background(), &resolver, cfg, domain)
245+
if expectedErr == nil {
246+
if err != nil {
247+
t.Errorf("expected no error, got '%#v'", err)
248+
}
249+
} else {
250+
if err == nil {
251+
t.Errorf("expected err to be '%#v', got nil", expectedErr)
252+
} else {
253+
expectedLE, okExpected := expectedErr.(ListedErr)
254+
actualLE, okActual := err.(ListedErr)
255+
if !okExpected || !okActual {
256+
t.Errorf("expected err to be '%#v', got '%#v'", expectedErr, err)
257+
} else {
258+
if expectedLE.Identity != actualLE.Identity ||
259+
expectedLE.List != actualLE.List ||
260+
expectedLE.Score != actualLE.Score ||
261+
expectedLE.Message != actualLE.Message {
262+
t.Errorf("expected err to be '%#v', got '%#v'", expectedErr, err)
263+
}
264+
}
265+
}
266+
}
267+
}
268+
269+
// Test domain with single response code and custom message
270+
test(map[string]mockdns.Zone{
271+
"spam.example.com.dnsbl.example.org.": {
272+
A: []string{"127.0.0.2"},
273+
},
274+
}, List{
275+
Zone: "dnsbl.example.org",
276+
ResponseRules: []ResponseRule{
277+
{
278+
Networks: []net.IPNet{
279+
{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},
280+
},
281+
Score: 10,
282+
Message: "Domain listed as spam source",
283+
},
284+
},
285+
}, "spam.example.com", ListedErr{
286+
Identity: "spam.example.com",
287+
List: "dnsbl.example.org",
288+
Score: 10,
289+
Message: "Domain listed as spam source",
290+
})
291+
292+
// Test domain with multiple response codes - scores should sum
293+
test(map[string]mockdns.Zone{
294+
"multi.example.com.dnsbl.example.org.": {
295+
A: []string{"127.0.0.2", "127.0.0.11"},
296+
},
297+
}, List{
298+
Zone: "dnsbl.example.org",
299+
ResponseRules: []ResponseRule{
300+
{
301+
Networks: []net.IPNet{
302+
{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},
303+
},
304+
Score: 10,
305+
Message: "High severity",
306+
},
307+
{
308+
Networks: []net.IPNet{
309+
{IP: net.IPv4(127, 0, 0, 11), Mask: net.IPv4Mask(255, 255, 255, 255)},
310+
},
311+
Score: 5,
312+
Message: "Low severity",
313+
},
314+
},
315+
}, "multi.example.com", ListedErr{
316+
Identity: "multi.example.com",
317+
List: "dnsbl.example.org",
318+
Score: 15, // 10 + 5
319+
Message: "High severity",
320+
})
321+
322+
// Test domain with no matching response codes
323+
test(map[string]mockdns.Zone{
324+
"unknown.example.com.dnsbl.example.org.": {
325+
A: []string{"127.0.0.99"},
326+
},
327+
}, List{
328+
Zone: "dnsbl.example.org",
329+
ResponseRules: []ResponseRule{
330+
{
331+
Networks: []net.IPNet{
332+
{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},
333+
},
334+
Score: 10,
335+
Message: "Listed",
336+
},
337+
},
338+
}, "unknown.example.com", nil)
339+
}

0 commit comments

Comments
 (0)