[PR #2] [CLOSED] [WIP] Refactor DNSBL response handling for detailed severity #3

Closed
opened 2026-04-08 13:54:02 +00:00 by thisisjaymehta · 0 comments

📋 Pull Request Information

Original PR: https://github.com/thisisjaymehta/maddy/pull/2
Author: @Copilot
Created: 1/10/2026
Status: Closed

Base: masterHead: copilot/update-dnsbl-response-handling


📝 Commits (1)

📄 Description

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.

Original prompt

Problem

Currently, Maddy's DNSBL implementation treats all response codes within a configured range equally. When using combined DNSBLs like Spamhaus ZEN (zen.spamhaus.org), different return codes indicate different listing types with different severity:

  • 127.0.0.2, 127.0.0.3 → SBL (known spam sources) - high severity
  • 127.0.0.4-127.0.0.7 → XBL (exploited/compromised hosts) - high severity
  • 127.0.0.10, 127.0.0.11 → PBL (policy block, dynamic IPs) - lower severity

Currently, Maddy:

  1. Counts any response within 127.0.0.1/24 as a single "hit"
  2. Applies the same score regardless of which specific code was returned
  3. Cannot provide response-code-specific rejection messages
  4. If multiple codes are returned (e.g., 127.0.0.3, 127.0.0.11, 127.0.0.4), they all count as one hit with one score

This means users who want different scores for different listing types must query separate lists (sbl.spamhaus.org, xbl.spamhaus.org, pbl.spamhaus.org), resulting in 3 DNS queries instead of 1.

Reference: https://docs.spamhaus.com/datasets/docs/source/40-real-world-usage/PublicMirrors/MTAs/020-Postfix.html

Proposed Solution

Add a new response configuration block that allows per-response-code scoring and custom messages:

check.dnsbl {
    reject_threshold 10
    quarantine_threshold 5

    zen.spamhaus.org {
        client_ipv4 yes
        client_ipv6 yes
        
        # SBL - Spamhaus Block List (known spam sources)
        response 127.0.0.2 127.0.0.3 {
            score 10
            message "Listed in Spamhaus SBL. See https://check.spamhaus.org/"
        }
        
        # XBL - Exploits Block List (compromised hosts)
        response 127.0.0.4 127.0.0.5 127.0.0.6 127.0.0.7 {
            score 10
            message "Listed in Spamhaus XBL. See https://check.spamhaus.org/"
        }
        
        # PBL - Policy Block List (dynamic IPs)
        response 127.0.0.10 127.0.0.11 {
            score 5
            message "Listed in Spamhaus PBL. See https://check.spamhaus.org/"
        }
    }
}

Implementation Details

1. Add new ResponseRule struct in internal/check/dnsbl/dnsbl.go:

type ResponseRule struct {
    Networks []net.IPNet
    Score    int
    Message  string // Custom rejection/quarantine message
}

2. Update List struct to include ResponseRules:

type List struct {
    Zone string
    ClientIPv4 bool
    ClientIPv6 bool
    EHLO     bool
    MAILFROM bool
    
    // Legacy: flat score for any response (used when ResponseRules is empty)
    ScoreAdj  int
    Responses []net.IPNet

    // New: per-response-code rules
    ResponseRules []ResponseRule
}

3. Update ListedErr in internal/check/dnsbl/common.go to include score and message:

type ListedErr struct {
    Identity string
    List     string
    Reason   string
    Score    int    // Score from matched response rule
    Message  string // Custom message from matched response rule
}

4. Update checkIP function in internal/check/dnsbl/common.go to handle ResponseRules:

  • If ResponseRules is configured, match each returned IP against rules and sum scores
  • If only legacy Responses is configured, use existing behavior for backwards compatibility
  • Split into checkIPWithRules (new) and checkIPLegacy (existing behavior)

5. Update checkLists in internal/check/dnsbl/dnsbl.go to use score from ListedErr:

  • Use ListedErr.Score when set (new behavior)
  • Fall back to list.ScoreAdj for backwards compatibility
  • Collect custom messages for rejection responses

6. Update readListCfg in internal/check/dnsbl/dnsbl.go to parse new response blocks:

  • Add parseResponseRule function to handle the new syntax
  • Support both CIDR notation and plain IPs in response arguments

7. Update documentation in docs/reference/checks/dnsbl.md

Files to Modify

  1. internal/check/dnsbl/dnsbl.go - Add ResponseRule struct, update List struct, update checkLists, update readListCfg, add parseResponseRule
  2. internal/check/dnsbl/common.go - Update ListedErr, update checkIP to handle ResponseRules
  3. internal/check/dnsbl/dnsbl_test.go - Add test cases for new functionality
  4. docs/reference/checks/dnsbl.md - Document the new response block syntax

Benefits

Feature Before After
DNS queries for ZEN 1 (but codes ignored) 1 (codes interpreted)
Per-code scoring
Custom rejection messages
Backwards compatible N/A (legacy responses + score still works)

Test Cases to Add

  1. Multiple return codes from single DNSBL with different scores → scores should sum
  2. Only low-severity code returned → quarantine but not reject
  3. High-severity code returned → reject
  4. Legacy configuration without response blocks → existing behavior preserved
  5. Response code not matching any rule → not counted

This pull request was created from Copilot chat.


Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/thisisjaymehta/maddy/pull/2 **Author:** [@Copilot](https://github.com/apps/copilot-swe-agent) **Created:** 1/10/2026 **Status:** ❌ Closed **Base:** `master` ← **Head:** `copilot/update-dnsbl-response-handling` --- ### 📝 Commits (1) - [`5091db7`](https://github.com/thisisjaymehta/maddy/commit/5091db788517855a63753b4a09fbd55df3ec10a3) Initial plan ### 📄 Description Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ## Problem Currently, Maddy's DNSBL implementation treats all response codes within a configured range equally. When using combined DNSBLs like Spamhaus ZEN (`zen.spamhaus.org`), different return codes indicate different listing types with different severity: - `127.0.0.2`, `127.0.0.3` → SBL (known spam sources) - high severity - `127.0.0.4`-`127.0.0.7` → XBL (exploited/compromised hosts) - high severity - `127.0.0.10`, `127.0.0.11` → PBL (policy block, dynamic IPs) - lower severity Currently, Maddy: 1. Counts any response within `127.0.0.1/24` as a single "hit" 2. Applies the same score regardless of which specific code was returned 3. Cannot provide response-code-specific rejection messages 4. If multiple codes are returned (e.g., `127.0.0.3`, `127.0.0.11`, `127.0.0.4`), they all count as one hit with one score This means users who want different scores for different listing types must query separate lists (sbl.spamhaus.org, xbl.spamhaus.org, pbl.spamhaus.org), resulting in 3 DNS queries instead of 1. Reference: https://docs.spamhaus.com/datasets/docs/source/40-real-world-usage/PublicMirrors/MTAs/020-Postfix.html ## Proposed Solution Add a new `response` configuration block that allows per-response-code scoring and custom messages: ``` check.dnsbl { reject_threshold 10 quarantine_threshold 5 zen.spamhaus.org { client_ipv4 yes client_ipv6 yes # SBL - Spamhaus Block List (known spam sources) response 127.0.0.2 127.0.0.3 { score 10 message "Listed in Spamhaus SBL. See https://check.spamhaus.org/" } # XBL - Exploits Block List (compromised hosts) response 127.0.0.4 127.0.0.5 127.0.0.6 127.0.0.7 { score 10 message "Listed in Spamhaus XBL. See https://check.spamhaus.org/" } # PBL - Policy Block List (dynamic IPs) response 127.0.0.10 127.0.0.11 { score 5 message "Listed in Spamhaus PBL. See https://check.spamhaus.org/" } } } ``` ## Implementation Details ### 1. Add new `ResponseRule` struct in `internal/check/dnsbl/dnsbl.go`: ```go type ResponseRule struct { Networks []net.IPNet Score int Message string // Custom rejection/quarantine message } ``` ### 2. Update `List` struct to include `ResponseRules`: ```go type List struct { Zone string ClientIPv4 bool ClientIPv6 bool EHLO bool MAILFROM bool // Legacy: flat score for any response (used when ResponseRules is empty) ScoreAdj int Responses []net.IPNet // New: per-response-code rules ResponseRules []ResponseRule } ``` ### 3. Update `ListedErr` in `internal/check/dnsbl/common.go` to include score and message: ```go type ListedErr struct { Identity string List string Reason string Score int // Score from matched response rule Message string // Custom message from matched response rule } ``` ### 4. Update `checkIP` function in `internal/check/dnsbl/common.go` to handle ResponseRules: - If `ResponseRules` is configured, match each returned IP against rules and sum scores - If only legacy `Responses` is configured, use existing behavior for backwards compatibility - Split into `checkIPWithRules` (new) and `checkIPLegacy` (existing behavior) ### 5. Update `checkLists` in `internal/check/dnsbl/dnsbl.go` to use score from `ListedErr`: - Use `ListedErr.Score` when set (new behavior) - Fall back to `list.ScoreAdj` for backwards compatibility - Collect custom messages for rejection responses ### 6. Update `readListCfg` in `internal/check/dnsbl/dnsbl.go` to parse new `response` blocks: - Add `parseResponseRule` function to handle the new syntax - Support both CIDR notation and plain IPs in response arguments ### 7. Update documentation in `docs/reference/checks/dnsbl.md` ## Files to Modify 1. `internal/check/dnsbl/dnsbl.go` - Add ResponseRule struct, update List struct, update checkLists, update readListCfg, add parseResponseRule 2. `internal/check/dnsbl/common.go` - Update ListedErr, update checkIP to handle ResponseRules 3. `internal/check/dnsbl/dnsbl_test.go` - Add test cases for new functionality 4. `docs/reference/checks/dnsbl.md` - Document the new response block syntax ## Benefits | Feature | Before | After | |---------|--------|-------| | DNS queries for ZEN | 1 (but codes ignored) | 1 (codes interpreted) | | Per-code scoring | ❌ | ✅ | | Custom rejection messages | ❌ | ✅ | | Backwards compatible | N/A | ✅ (legacy `responses` + `score` still works) | ## Test Cases to Add 1. Multiple return codes from single DNSBL with different scores → scores should sum 2. Only low-severity code returned → quarantine but not reject 3. High-severity code returned → reject 4. Legacy configuration without `response` blocks → existing behavior preserved 5. Response code not matching any rule → not counted </details> <!-- START COPILOT CODING AGENT SUFFIX --> *This pull request was created from Copilot chat.* > <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/thisisjaymehta/maddy/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
thisisjaymehta 2026-04-08 13:54:02 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thisisjaymehta/maddy#3
No description provided.