Metadata-Version: 2.4
Name: dns-rfc-lint
Version: 1.0.0
Summary: RFC Compliance Checker for DNS Protocol Implementations
License: AGPL-3.0-or-later
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: scapy>=2.5.0
Requires-Dist: click>=8.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# rfc-lint

**RFC Compliance Checker for DNS Protocol Implementations**

Feed it a pcap. It checks every DNS packet against MUST/SHOULD/MAY requirements from RFC 1035 and RFC 6891, flags violations, and produces structured compliance reports.

## Quick Start

```bash
# Install
pip install -e ".[dev]"

# Lint a pcap
rfc-lint capture.pcap

# Filter by RFC, output JSON
rfc-lint capture.pcap --rfc 1035 --format json -o report.json
```

## What It Checks

### RFC 1035 — DNS Message Format (17 rules)

| Rule ID | Section | Level | What It Checks |
|---|---|---|---|
| `RFC1035-4.1.1-OPCODE` | §4.1.1 | MUST | OPCODE is a defined value (0,1,2,4,5) |
| `RFC1035-4.1.1-RCODE` | §4.1.1 | MUST | Response RCODE is defined (0-5) |
| `RFC1035-4.1.1-ZBITS` | §4.1.1 | MUST | Z bits are zero in non-EDNS messages |
| `RFC1035-4.1.1-QR` | §4.1.1 | MUST | QR bit is 0 or 1 |
| `RFC1035-4.1.1-TC` | §4.1.1 | MUST | TC bit not set in queries |
| `RFC1035-4.1.1-AA` | §4.1.1 | SHOULD | AA bit only in responses |
| `RFC1035-4.1.1-RA` | §4.1.1 | SHOULD | RA bit only in responses |
| `RFC1035-4.1.2-QDCOUNT` | §4.1.2 | MUST | Standard query has QDCOUNT=1 |
| `RFC1035-4.1.2-QCLASS` | §4.1.2 | SHOULD | QCLASS is a recognized value |
| `RFC1035-4.1-QDCOUNT-RESP` | §4.1 | SHOULD | Response echoes question section |
| `RFC1035-4.1-COUNTS` | §4.1 | MUST | Header counts match actual records |
| `RFC1035-4.1-NOERROR-ANS` | §4.1 | SHOULD | NODATA detection (informational) |
| `RFC1035-2.3.4-LABEL-LEN` | §2.3.4 | MUST | Label <= 63 octets |
| `RFC1035-2.3.4-NAME-LEN` | §2.3.4 | MUST | Domain name <= 253 characters |
| `RFC1035-2.3.1-LDH` | §2.3.1 | SHOULD | LDH syntax (underscore-prefix exempt) |
| `RFC1035-4.2.1-UDP-SIZE` | §4.2.1 | MUST | UDP message <= 512 bytes without EDNS |
| `RFC1035-3.6.2-CNAME-ONLY` | §3.6.2 | MUST | CNAME doesn't coexist with other types |

### RFC 6891 — EDNS(0) (8 rules)

| Rule ID | Section | Level | What It Checks |
|---|---|---|---|
| `RFC6891-6.1.1-OPT-COUNT` | §6.1.1 | MUST | At most one OPT record |
| `RFC6891-6.1.1-OPT-SECTION` | §6.1.1 | MUST | OPT in additional section only |
| `RFC6891-6.1.1-OPT-NAME` | §6.1.1 | MUST | OPT NAME is root ('.') |
| `RFC6891-6.1.3-VERSION` | §6.1.3 | MUST | EDNS version is 0 |
| `RFC6891-6.2.3-UDP-SIZE` | §6.2.3 | SHOULD | UDP payload size >= 512, != 0 |
| `RFC6891-6.1.1-EDNS-Z` | §6.1.1 | MUST | EDNS Z reserved bits are zero |
| `RFC6891-7-EDNS-RESP` | §7 | MUST | EDNS response requires EDNS query (placeholder) |
| `RFC6891-7-FORMERR-OPT` | §7 | MUST | FORMERR response has no OPT |

### Transaction-Level Rules (4 rules)

These pair queries with responses by (IP, TXID):

| Rule ID | What It Checks |
|---|---|
| `RFC1035-4.1.1-TXID-MATCH` | Response TXID matches query |
| `RFC1035-4.1-QUESTION-ECHO` | Response question matches query question |
| `RFC6891-7-EDNS-PAIR` | EDNS in response only if query had EDNS |
| `RFC1035-4.1.1-TC-TRUNCATION` | Truncated response flagged for TCP retry |

**Total: 29 rules across 2 RFCs + stateful transaction analysis.**

## Architecture

```
rfc_lint/
├── _scapy.py          # Scapy compatibility shim
├── models.py          # Severity, Verdict, RuleMetadata, Violation, LintReport
├── parser.py          # DNS packet parser (Scapy → structured dict)
├── engine.py          # BaseRule, RuleRegistry
├── transaction.py     # Stateful query↔response pairing + cross-packet rules
├── linter.py          # Orchestrator + text/JSON report formatters
├── cli.py             # Click CLI
└── rules/
    ├── rfc1035.py     # 17 RFC 1035 rules
    └── rfc6891.py     # 8 RFC 6891 rules
tests/
├── fixtures.py        # 30 crafted pcap generators
└── test_rules.py      # 75 tests (positive + negative for every rule)
```

**Design principles:**
- Each rule is a self-contained class with metadata (RFC, section, severity) and a `check()` method
- Rules are declarative — adding new RFCs means adding rule files, not modifying the engine
- Stateful analysis (transaction pairing) is separate from per-packet rules
- MUST violations → FAIL, SHOULD violations → WARN

## Adding New Rules

```python
from rfc_lint.engine import BaseRule
from rfc_lint.models import RuleMetadata, Severity, Violation

class MyNewRule(BaseRule):
    @property
    def metadata(self):
        return RuleMetadata(
            rule_id="RFC7858-4.2-TLS-VERSION",
            rfc=7858, section="4.2", severity=Severity.MUST,
            title="DoT must use TLS 1.2+",
            description="DNS over TLS connections must use TLS 1.2 or later.")

    def check(self, p: dict) -> list:
        # Your check logic here
        return []
```

## Output Formats

**Text** — human-readable report with violation details and summary by rule.

**JSON** — machine-parseable with full violation metadata for CI/CD integration.

## Test Suite

```bash
pytest tests/ -v    # 75 tests, ~1 second
```

Every rule has both positive (violation detected) and negative (clean packet passes) tests. Edge cases include IPv6, TCP DNS, EDNS DO bit, underscore service labels, NOTIFY/UPDATE opcodes, NXDOMAIN/SERVFAIL responses, and multi-violation packets.

## License

MIT
