#!/usr/bin/env python3
"""
Generate max-compression-depth.pcap — a DNS response with 7 answer RRs where
each successive RR's name is a compression pointer to the previous RR's name,
building a backward chain deep enough to exceed dns_max_compression_chain_depth.

Set dns_max_compression_chain_depth=5 in the test; the chain reaches depth 6.

Chain when resolving RR6's name:
  ExtractName(RR6.name, depth=0) -> ptr->RR5 -> ExtractName(depth=1)
    -> ptr->RR4 -> ExtractName(depth=2) -> ... -> ExtractName(depth=6) > 5 -> weird

All pointers target strictly earlier offsets so the forward-pointer check
(DNS_label_forward_compress_offset) is never hit.

Generated by Claude Sonnet 4.6 (Anthropic).
"""

import struct
from pathlib import Path

from scapy.all import IP, UDP, Ether, Raw, wrpcap

SRC = "1.2.3.4"
DST = "5.6.7.8"

# Packet layout (offsets from start of DNS payload):
#   [0..11]   DNS header (qdcount=1, ancount=7)
#   [12..14]  question name: \x01a\x00
#   [15..18]  qtype=A, qclass=IN
#   [19..35]  RR0: name=\x01a\x00 (3 bytes), type A, 4-byte rdata  (17 bytes)
#   [36..51]  RR1: name=ptr->19,  type A, 4-byte rdata             (16 bytes)
#   [52..67]  RR2: name=ptr->36, ...
#   [68..83]  RR3: name=ptr->52
#   [84..99]  RR4: name=ptr->68
#   [100..115] RR5: name=ptr->84
#   [116..131] RR6: name=ptr->100  <- resolving this fires the weird


def ptr(offset):
    """2-byte DNS compression pointer."""
    assert offset < 0x3FFF
    return bytes([0xC0 | (offset >> 8), offset & 0xFF])


def rr(name_bytes, rdata):
    """Build a DNS RR with type A, class IN, ttl 300."""
    return name_bytes + struct.pack("!HHIH", 1, 1, 300, len(rdata)) + rdata


def build_message():
    txid = 0x1234
    flags = 0x8180  # QR=1, AA=1, RD=1, RA=1
    ancount = 7

    header = struct.pack("!HHHHHH", txid, flags, 1, ancount, 0, 0)

    qname = b"\x01a\x00"
    question = qname + struct.pack("!HH", 1, 1)

    # RR0: real label at offset 19
    rr0 = rr(b"\x01a\x00", b"\x7f\x00\x00\x01")  # 17 bytes -> [19..35]
    # RR1..RR6: each name is a pointer to the previous RR's name field
    rr1 = rr(ptr(19), b"\x7f\x00\x00\x02")  # [36..51]
    rr2 = rr(ptr(36), b"\x7f\x00\x00\x03")  # [52..67]
    rr3 = rr(ptr(52), b"\x7f\x00\x00\x04")  # [68..83]
    rr4 = rr(ptr(68), b"\x7f\x00\x00\x05")  # [84..99]
    rr5 = rr(ptr(84), b"\x7f\x00\x00\x06")  # [100..115]
    rr6 = rr(ptr(100), b"\x7f\x00\x00\x07")  # [116..131]

    payload = header + question + rr0 + rr1 + rr2 + rr3 + rr4 + rr5 + rr6

    # Sanity-check offsets match what the pointers assume
    base = len(header) + len(question)  # 19
    assert base == 19
    for i, r in enumerate([rr0, rr1, rr2, rr3, rr4, rr5]):
        expected = base + sum(len(x) for x in [rr0, rr1, rr2, rr3, rr4, rr5][:i])
        assert expected == [19, 36, 52, 68, 84, 100][i], (
            f"RR{i} offset mismatch: {expected}"
        )

    return payload


def main():
    payload = build_message()

    pkt = (
        Ether() / IP(src=SRC, dst=DST) / UDP(sport=12345, dport=53) / Raw(load=payload)
    )

    out = Path(__file__).with_suffix("")
    wrpcap(str(out), [pkt])
    print(f"Wrote {out}")


if __name__ == "__main__":
    main()
