README.md

Have you seen DNS TYPE0 CLASS256?

we run a recursive resolver for historical reason. these recent months, we started seeing messages like this in unbound.log, with different names:

info: resolving cctv-api.cdn20.com.cd23f.com. TYPE0 CLASS256
info: resolving cctv-api.cdn20.com.chnc.cloudcsp.com. TYPE0 CLASS256
info: resolving cctv-api.wswebpic.com.chnc.cloudcsp.com. TYPE0 CLASS256
info: resolving overseatest.livehttp.speedcdns.com. TYPE0 CLASS256
info: resolving wangsu-1.ttyuyin.com.cdn20.com. TYPE0 CLASS256
info: resolving wangsu-3.ttyuyin.com.cdn20.com. TYPE0 CLASS256

afaik we have no relationship with such infrastructure (CCTV in some Chinese cloud?). we blocked the most queried labels, and move on to other things. however i have a nagging question: what is that?

i still have no details about the chinese parties, but after some searching & reproduction, i think there is a defect-attractor in DNS-packet construction. in order to avoid search-suffix behaviour, people write .-ending names in config, and some software will construct an invalid DNS query:

  • haproxy, before 2020 https://github.com/haproxy/haproxy/issues/435
  • gnupg dirmngr, in 2017 https://dev.gnupg.org/T3517#106524
  • nongnu LWIP, in 2020 https://savannah.nongnu.org/bugs/?58473#comment3

the sequence is potentially like this:

  1. split name by ..
  2. for each item, construct ${length}${label} segments. the final empty label gets converted to its own 0x0 byte.
  3. concat the segments together, then append the empty label 0x0, regardless of previous step.
  4. append the DNS type & class (always IN/0x1 for Internet)

Thus, now we have a name that ends with \0\0 (or 00 00 in hexdump view). The question now appends usual Type A (00 01), Class IN (00 01), but those become misaligned:

Okay:
03 ?? ?? ?? 00|  00 01|    00 01
ll  c  o  m . | TYPE A|CLASS  IN

Wrong:
03 ?? ?? ?? 00|  00 00|    01 00 01
ll  c  o  m . | TYPE 0|CLASS 256 FORMAT ERROR, i hope the server answer

for more background, check out jvns' blog or view responses in various formats with DNS institute tool.

there is a different case from 2001, where a (windows-based?) client sent TYPE & CLASS as little-endian, not big-endian, resulting in TYPE256 CLASS256 query.

Reproduction

a single ping, comrade. just one, for range. -- Marko Ramius

catching in wireshark/tshark:

dns.qry.class > 1

or with tcpdump/pcap filter:

udp port 53 and dns.query.class > 1

i want to generate those wrong Questions on-demand, to observe and quote the various DNS servers' log lines about them, as well as to put all kind of TYPEs in the Q and see what they get intepreted as.

$ declare -a TYPES=(type1 type2 cname type6 mx txt aaaa srv dname type65 type255 caa)

The core code is:

fn naughty_question(qtype: Rtype) -> Question<Dname<Vec<u8>>> {
    let name = Dname::vec_from_str("example.").unwrap();
    let mut bytes = name.into_octets();
    bytes.push(0);
    Question::new_in(
        // Safety: I want to do it wrong
        unsafe { Dname::from_octets_unchecked(bytes) },
        qtype,
    )
}
#[test]
fn it_is_different() {
    assert!(
        !Question::from((Dname::vec_from_str("example.").unwrap(), Rtype::Any))
            .eq(&naughty_question(Rtype::Any))
    );
}

i chose NLnet Labs' domain crate, because it is self-contained, comes with a resolver. fortunately it exposes the above hand-off function to give users' direct access to parsed domain bytes. i haven't wrapped my head around how to properly print out all kinds of answer data yet, so i resigned to asserting that the queried server ignores/refuses the bad questions. i can construct a bad Dname, pinky-promise to domain that it is good, and let it help me with the rest of DNS+UDP wrapping & handling!

an alternative would be trust-dns's proto & client crates. i don't think its server-side implementation is ready yet.

I queried some usual resource types against an unbound server:

$ declare -a TYPES=(type1 type2 cname type6 mx txt aaaa srv dname type65 type255 caa)
$ for type in ${TYPES[*]}; do cargo r -q -- @unbound.address $type; done

A: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x01\0"
NS: server ignored us, perhaps: Custom { kind: TimedOut, error: "all timed out" }
CNAME: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x05\0"
SOA: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x06\0"
MX: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x0f\0"
TXT: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x10\0"
AAAA: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\x1c\0"
SRV: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0!\0"
DNAME: server ignored us, perhaps: Custom { kind: TimedOut, error: "all timed out" }
HTTPS: server ignored us, perhaps: Custom { kind: TimedOut, error: "all timed out" }
ANY: b"\0\0\x81\x85\0\x01\0\0\0\0\0\0\x07example\0\0\0\xff\0"
CAA: server ignored us, perhaps: Custom { kind: TimedOut, error: "all timed out" }

"perhaps" is because, as we see below, the program sometimes doesn't receive response data. unbound logs clear messages:

unbound[1626:0] info: resolving example. TYPE0 CLASS256
unbound[1626:4] info: resolving example. TYPE0 CLASS512
unbound[1626:2] info: resolving example. TYPE0 CLASS1280
unbound[1626:3] info: resolving example. TYPE0 CLASS1536
unbound[1626:1] info: resolving example. TYPE0 CLASS3840
unbound[1626:5] info: resolving example. TYPE0 CLASS4096
unbound[1626:2] info: resolving example. TYPE0 CLASS7168
unbound[1626:4] info: resolving example. TYPE0 CLASS8448
unbound[1626:3] info: resolving example. TYPE0 CLASS9984
unbound[1626:3] info: resolving example. TYPE0 CLASS65280

Here is tcpdump's translation of the traffic:

0+ [1au] Type0 (Class 256)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 256)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 512)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 512)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 1280)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 1280)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 1536)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 1536)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 3840)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 3840)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 4096)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 4096)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 7168)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 7168)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 8448)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 8448)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 9984)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 9984)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 16640)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 16640)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] Type0 (Class 65280)? example. (37)
0 FormErr 0/0/1 (36)
0+ Type0 (Class 65280)? example. (26)
0 Refused 0/0/0 (25)
0+ [1au] A (Class 256)? example. (37)
0 FormErr 0/0/1 (36)

(you see why i said perhaps). this last one A (Class 256)? is from a CAA rtype. its high byte gets reinterpreted as type A again! but the Class 256 is never fixable, and the whole packet is malformed beyond repair anyhow.

i hope someone out there come across this & figure out whatever is happening with the clients sending these queries.

i tested again ISC bind, but it says only:

client: warning: client 4.5.6.7#33870: message parsing failed: unexpected end of input

CoreDNS, NLnet NSD, systemd-resolved doesn't log anything about these at all in my testing.

of the cloudflare addresses, .1 and .3 replies quickly & i can print the response, but i miss .2 traffic consistently:

$ for s in 1 2 3; do cargo r -q -- @1.1.1.$s aaaa; done
AAAA: b"\0\0\x81\x84\0\x01\0\0\0\0\0\0\x07example\0\0\0\x1c\0"
AAAA: server ignored us, perhaps: Custom { kind: TimedOut, error: "all timed out" }
AAAA: b"\0\0\x81\x84\0\x01\0\0\0\0\0\0\x07example\0\0\0\x1c\0"

nothing comes back from .2:

0+ [1au] Type0 (Class 7168)? example. (37)
0 NotImp 0/0/0 (25)
0+ [1au] Type0 (Class 7168)? example. (37)
0+ [1au] Type0 (Class 7168)? example. (37)
0 NotImp 0/0/0 (25)