.arpa, rDNS and a few magical ICMP hacks
Through Project SERVFAIL, I became aware that there are a few individuals, not just ISPs, who host their own in-addr.arpa. and ip6.arpa. zones. It never occurred to me that I could ask bgp.wtf, my beloved ISP, to delegate me a zone like this - until one faithful late-night chat.
ARPA zones are usually totally out of reach for individuals, so I was absolutely hyped when one of our netadmins agreed to delegate me the ip6.arpa. zone for my whole /48 IPv6 range. Thank you so much <3!!
What even is .arpa?
The story begins in the late 1960s, a long time before the Internet itself. Several experimental networks existed back then, ARPANET being one of the most prominent ones.
Picture 1 - ARPANET logical map circa 1977. Mirrored from Wikimedia Commons, Public Domain
In its original form, ARPANET connected four universities in the US; By mid 1970s, that expanded to every major uni in the US, plus a few participants from abroad, several connected by satellite links. ARPANET is relevant to the history of the internet because a lot of the core protocols in use today have originated within it - most notably IP (RFC760, RFC791), ICMP (RFC777) and the concept of Name Servers (RFC883 et al). ARPANET also pioneered dynamic routing, a technology without which Internet as we know it couldn't exist.
Looking at those old RFCs with the power of hindsight is quite an experience: Some concepts stuck, others were complete misses that look hilariously wrong today. ARPANET was a period of rapid evolution - there were virtually no legacy systems, and thanks to the academic deployment base, backwards compatibility wasn't a major concern. Protocols changed rapidly, things were tried, applied and ratified. This is a stark contrast to how the commercial internet ended up being, with many things set in stone purely out of fear of breaking compat.
That state didn't last for long, as ARPANET was already scheduled for closure in 1985. It was finally shut down in 1990, being superseded by NSFNET and the slowly forming commercial Internet3.
The .arpa zone has a somewhat convoluted history; In RFC920 "Domain Requirements", it's defined as one of the first non-country domains, and the only one in the "Temporary" category. At that point, all old ARPANET domains got moved under .arpa as a "legacy" stopgap until everyone reconfigured their servers. However, there also was one new subdomain used for a contact mail address (see the same RFC). Wikipedia cites that mailserver as the reason why .arpa didn't get decommissioned; That statement, along with early .arpa history in general, is hard to verify. IANA was actively pushing for deprecating the zone, instead placing all metadata services within the .int domain (RFC1886, 1995). Later on, they seemed to revert that (RFC3152, 2001), dedicating .int for international organizations only, and .arpa to all the <meta> services, like reverse DNS.
Temporary solutions truly are the most permanent ones.
How does it .arpa?
Perhaps the biggest uses of .arpa are the in-addr.arpa. and ip6.arpa. domains, used for IPv4 and IPv6 reverse DNS, respectively. If you're not familiar with reverse DNS - it's used to map IPs to domain names (as opposed to "forwards" DNS, which maps domains to IPs). Given an IP address, you can look up its corresponding PTR record, which - if it exists - should return you a domain name.
Picture 2 - pinging sdomi.pl, you'll get replies from an IP with rDNS sakamoto.pl.
Specifically, rDNS works by querying the PTR record for a specific subdomain (in this case, 103.240.236.185.in-addr.arpa.), which returns a string, usually matching the "main" domain used on that IP. However, a lot of software is quite lentient at accepting weird responses - a thought we'll come back to later :3
Having never seen a domain like the one above, it may be a bit hard to grasp what's the meaning of all the numbers; The following things are important to know:
- Reverse DNS uses the same underlying protocols and systems as forward DNS
- DNS is a hierarchical system. This means that it can be divided into smaller and smaller zones, with different entities administering each one
- IP is also a hierarchy: someone owns an address range, leases it to an ISP who then divides the range into multiple smaller ranges for their users. Each step ends up with a smaller chunk.
- DNS hierarchy goes left, with subdomains. IP hierarchy goes right, with subnets.
- To make them fit together, something had to be reversed, so they both go in one direction. Hence, the last part of the IP from the right will become the first subdomain from the left.
The same rules apply to IPv6, except that for technical reasons each subdomain is just one character.
Of note, while you can use domains and raw IPs interchangeably with the vast majority of tools, an application needs to be explicitly made revDNS-aware to use it. A lot of tools aren't, by negligence or design. This is why I consider those zones to be both least and most used; technically all IPs have it, yet it isn't actually used that much.
But what fun can you use it for?
See, normally you'd only see PTR records being used within revDNS zones. There's also the odd CNAME, if someone wanted to copy another entry verbatim. However, what if I told you that there's nothing stopping you from adding other record types to it? RFCs suggest a specific use for those zones (revDNS), but they don't explicitly forbid any other uses; And even if they did, it's not like most software would care about rules like this!
Picture 5 - oh gawd. it's not even an AAAA record!
So we can use reverse DNS for forward DNS, which then can also do reverse DNS for (proper) name resolution. Cursed, right? It's about to get much better!
$ curl 'http://1023.1023.0.6.0.0.8.0.0.b.e.d.0.a.2.ip6.arpa/'
i opened my nginx config for this
Figure 1 - There's nothing limiting us to just pings.This can be considered a regular domain... almost.
As nginx (the web server hosting this page) doesn't do any specific validations on vhosts (because why would it?), we can set the server_name to serve an arpa domain under our zone. Check this out: the post you're reading can be also accessed through http://meow.6.0.0.8.0.0.b.e.d.0.a.2.ip6.arpa/weblog?id=24 - cool, right?
Unfortunately, getting a TLS certificate issued has proven to be more difficult than anticipated. Let's Encrypt, which I use for all my other HTTPS needs, has a rule prohibiting domains with more than 10 parts (subdomains). Even if I got around that, they blanket-forbid all .arpa zones anyways. I've tried all other free TLS providers, and all I got was errors, and weird hangs within the flow. ZeroDNS even got to a point when they were somehow stating that I need to wait for verification to conclude, and that they succeeded, and will generate me a certificate shortly (both at the same time, on one webpage). Ultimately, nothing came of it after over a few days of waiting. I was even considering paying for a certificate, but I didn't trust that a provider wouldn't just eat my money and run away, proudly saying that they don't serve arpa domains.
I had one last idea, which was to leverage cloudflare's "DDoS protection" (which is in fact just a global MitM attack - but I digress). If you didn't know - when using any domain with clownflare, they take the liberty to generate TLS certificates for themselves, even if you're not "securing" your domain with their proxy. CF was on my radar, because they issue the vast majority of certs for various arpa subdomains. And I mean vast majority: it's around 100:1 cloudflare to all other issuers. See the crt.sh reports for in-addr.arpa and ip6.arpa (IA links to not accidentally DoS crt.sh ^^). As previously stated, this just means that cloudflare has generated certs for themselves, not that anyone is actually using them.
But what more fun can a TLS certificate get us?
Picture 6 - one of my first ideas which strictly required a certificate: a fedi instance
Contrary to other TLS providers, CF just worked - at the cost of me not actually having the certificate itself. Anyhoo, I setup a GoToSocial instance, because cursed GTS setups became somewhat of a custom, at least around my parts. For the unaware: GoToSocial is one of the smaller Fediverse servers, filling a particular niche, often used for single-user instances.
I stood up, looked around to see if anyone would try to stop me. As expected, there was nobody else in the flat, nor anyone screaming from the ouside. I hesitated a bit, but finally clicked Post. I copied the full URL to boost it from Akkoma. It worked.
Picture 7 - first post on the instance
Registrations are closed, plus this is not a forever instance. Please don't ask if you can get let in. I don't have enough hands to moderate and care for the instance as it already stands - sorry :/
Important thing to mention: while CF worked just fine for me, I've seen others struggling to reproduce my results and needing to pay CF to change the default TLS provider. My account went through either by pure luck, or because it had an old provider option grandfathered in. Just, know that it may be harder for you to pull this off, should you attempt so.
MX server
For the GTS server, I also needed a mail account, and hosting it on .arpa seemed fitting. Since I already had a mail server for all my other domains, this wasn't a particularly involved operation.
Once again, my expectations were very low: I thought it would just explode upon the first interaction with any major e-mail provider, as is common.
Picture 8 - thanks to ari for the gmail screenshot
But the e-mails, of course, arrived anyways - because mail also doesn't care what humans think is impossible. Some of it lands in spam (as expected, tbh), but it generally arrives just fine - a welcome surprise, for once.
illegal ICMP hacks
As a network nerd, the place where I see revDNS used the most is perhaps ICMP in various flavors: ping, traceroute, the like. Some nicer tools resolve the revDNS to make the output more readable. Is this something?
Consider this: ICMP Echo requests (colloquially known as "pings") contain a few unexpected fields, most notably the Sequence number (icmp_seq) which increases with every consequent ping, and an Identifier, a random per-session 16-bit value. Some time ago I wondered whether one could use those two to send different mock responses, simulating a fake traceroute. Identifier could match a specific user running a traceroute, while the sequence number could be used as to customize the response based on how much time has elapsed since the start.
Unfortunately, I arrived at the lack of tools:
- ICMP is largely stateless. There isn't a concept of "port" and "listening", your kernel just gets a packet, and responds to it with another packet
- If I wanted to reply with different data to multiple clients at once, I couldn't just use a funky static IP route, as that introduces state.
To make this stateless, I'd need to write an entire IP stack2, which is a lot of effort for a shitpost.
So let's do it.Writing an IPv6 stack
Fortunately, our IP stack will be relatively simple: we only need ICMP(v6), no TCP or UDP in sight. Unfortunately, this means I can't name it Trumpet Bashsock (see Trumpet Winsock, but this is a sacrifice I'm willing to live with (...FOR NOW).
First roadblock: Picking a way to interface with the network. I thought of TUN/TAP virtual interfaces; Sadly, those require the use of ioctls, which are simply impossible to send out with pure Bash (and, for now, I drew the line at adding builtins).
Mentioning my problem in a conversation with Wanda, she suggested I use SLIP, and I loved the idea. Initially, I couldn't get slattach(8) to bind to any ptys I made with mknod(1). Finally, I found out that socat(1) can make me a proper PTY, then connect it to a program's stdio on the other end. Poking around, figuring out the "protocol" (or lack thereof, it's just raw IPv4 packets) and scribbling out a minimum implementation, I got hit with a brick wall: apparently there's no way to transmit IPv6 over SLIP, even though the first nibble in each packet is the IP version. It seems like an oversight and I don't buy that it can't be done. However, I accepted defeat rather quickly, as I noticed something much, much more interesting while reading through socat -h.
spooky scary socat TUN
Remember when I was complaining about ioctls? Apparently you can just use socat to make up tun interfaces!? All sources I've seen describe the suggested serving as L3 bridging over TCP/UDP, some even going as far as stringing SSH into the mix. Regardless, the beauty of socat is that it doesn't guard me from my own terrible ideas:
doas socat -d -d EXEC:./ip.sh TUN:127.21.37.1/32,up
Figure 2 - because why not. why shouldn't I make my stdio into a TUN?
Through some manual experimentation around the interface, I arrived at the following facts:
- even if we never intend to use IPv4, socat requires some dummy address. Sadly, IPv6 routes have to be added manually.
- each frame starts with 2 nullbytes, folowed by another 2 bytes for the protocol (standard EtherType: 0x0800 for IPv4, 0x86dd for IPv6)
- a standard IP packet is transmitted afterwards
- there appears to be no way to discern bounds between packets, so you have to look for a length field inside, and only read so many bytes at once
- the interface needs to be primed; it only starts transferring data after you transmit something yourself
- ... to not mess up the framing, we should actually send a proper (but dummy) packet
I already had some experience with IPv6 from my networking adventures with SerenityOS; I decided to skip IPv4 altogether for simplicity, as what I planned didn't exactly need it, and it's a bit more painful to interface.
For working on raw data, I used my messy, but slowly shaping up binary manipulation library. This got me from an idea to a basic implementation within maybe 30 minutes. My code reads the IPv6 header, populates some variables, reads the payload, and calls the handle_icmp6 function if the next_header value matches 0x3a.
handle_icmp6 was surprisingly simple to write. I initially expected to need to respond to Neighbor/Router Advertisements (outside of standard Echo Requests), but socat's TUN interface doesn't expose MAC addresses, so I got to skip that. My function parses some of the header data, then calls icmp6_resp, which glues together a packet, computes a checksum and actually sends it off.
Picture 9 - checksum functions. they look kinda similar if you squint.C++ "inspiration" on the left, Bash on the right
ICMP uses the "Internet Checksum" algorithm, which is a one's complement of all payload bytes, plus some extra steps. I got a bit lucky, because I've reworked exactly that algo within SerenityOS last year. My Bash rewrite matches C++ surprisingly closely.
After struggling a bit with some embarrasing bugs, I got my first matching checksums (and therefore, first accepted responses). Now, I could easily add more logic for whatever I wanted.
Picture 10 - "hey it works! i'm replying to pings from bash! :D"
I added a check, returning a different IP when the Hop Limit (TTL) field is lower than 12. That results in 12 fake hops along the way; If that doesn't make any sense, I suspect you may not be familiar with how traceroute works:
- traceroute is a Hack. It's not a separate protocol, it works on top of ICMP (at least traditionally). Gravis has a wonderful blogpost about it (see link).
- In IP networks, each packet has a Hop Limit (also named TTL, Time-To-Live) field in the header
- Every host ("hop") along the way decreases it by 1
- If it ever reaches 0, packet is dropped, and a response is generated with the hop's IP address as source
- We can leverage this behavior: by checking if that value is n or lower and responding with a different source IP, traceroute will see n bogus hosts along the way.
Picture 11 - welcome to my laptop, my laptop, my laptop (...) AND my laptop
As seen above, the route doesn't have to make logical sense; repetitions are OK, as long as only the last hop replies with the "correct" address (traceroute uses that as a marker to end the trace).
OK, Now what?
A few sections ago I've firmly buried the lede as to why I would even want to have a custom per-client response. My plan? Play an animation stored within PTR records, through careful manipulation of ICMP Echo Reply packets!
Picture 12 - same screenshot as above, but with revDNS resolution enabled
As outlined before, PTR responses can contain almost anything. Two largest limitations1 include absolutely no Unicode, and no spaces - but you can use characters which are otherwise forbidden in domains, like underscore, or an octothorp.
I opted for automating the conversion (instead of manually making an ASCII animation, however much fun doing a conversion akin to towel.blinkenlights.nl might have been). The pipeline has three parts:
ffmpeg -i input.mkv -vf scale=24x12 -r 1 'out/%06d.bmp'
# intentional bad scaling to account for non-square fonts
# dec.sh from https://git.sakamoto.pl/domi/88x31/src/branch/meow/dec.sh
for i in out/*; do
./dec.sh < $i \
| sed -E 's/(.).{5}/\1/g;s/f/#/g;s/0/_/g;s/[12]/,/g;s/[45]/\!/g;s/[67]/~/g;s/[89]/=/g;s/[abc]/%/g;s/[de]/#/g' \
| tac
done > frames
Figure 3 - first part of the conversion process, from MKV to ASCII
This snippet takes any video file, scales it down, outputs raw BMP frames @ 1fps, decodes those one by one, samples the most significant nibble (out of a 24bit RGB value), and finally outputs ASCII characters roughly corresponding to the pixel brightness. Result is a list of frames, each separated by an empty newline.
This intermediate representation is only really useful as a preview. The next snippets both work on a lines file which is generated by cat frames | sort | uniq > lines.
#!/bin/bash
source ~/projects/libsh/bin/bin.sh
IFS=$'\n'
lines=($(cat lines))
while read line; do
if [[ "$line" == "$prev" ]]; then
echo "$(u16 "$i")"
continue
fi
for (( i=0; i<${#lines[@]}; i++ )); do
if [[ "$line" == "${lines[i]}" ]]; then
echo "$(u16 "$i")"
break
fi
done
prev="$line"
done < frames
Figure 4 - second part of the conversion, dumping a sequential list of indexes
This one iterates over frames, and outputs an index of each line into the deduplicated file. This will be used later on by the IP stack to determine the fake hop order.
#!/bin/bash
source ~/projects/libsh/bin/bin.sh
IFS=$'\n'
lines=($(cat lines))
for (( i=0; i<${#lines[@]}; i++ )); do
echo "$(u16 "$i").0.0.0.0.0.0.0.0.b.a.c.a.f.0.0.0.6.0.0.8.0.0.b.e.d.0.a.2.ip6.arpa. 3600 IN PTR ${lines[i]}."
done | sed -E 's/(.)(.)(.)(.)/\4.\3.\2.\1/'
Figure 5 - final part. outputs the animation in a BIND-compatible format
This outputs a list which can be easily imported by BIND, or a million other compatible DNS servers (in my case, servfail-web's raw record view). And that's all there is to it!
...Well, okay, that's a lie. That's all there is to it if you want just video; Audio is a separate issue, but I'll handle it in a way equivalent to how we did it two posts ago: lots of TXT records. I also thought about making it into a series of PTR records, resolved with a similar traceroute, but this has proven to be infeasible, as PTRs are limited to somewhere between 60 and 99 characters by PowerDNS. In contrast, TXT records are quite flexible: Each one can store up to around 42000 characters (theoretical limit being a few short of 65535, but resolvers tend to explode before we get there); Even counting base64 overhead, that's still around 30-32K per record. Through some quick napkin math, this is about 21 seconds of audio per entry, given that we use a heavily compressed OPUS @ 12kbps. Not audiophile grade by any means, but enough for this demo :p
Suddenly: Firewalls
My traceroute worked quite well locally, as well as from some servers with public IPv6 addressing. But then I tested it on a residential connection, and I received no responses. What gives?
Picture 13 - wireshark packet dump, showing some Unforeseen Behavior
I assumed that ICMPv6 packets can be returned for TTL-exceeded reasons with a regular ICMP Echo Reply, just by modifying the source address. Investigating this issue, I learnt about ICMPv6 Time Exceeded, error code 3. Turns out that my previous version only worked incidentally: routers on the way will reject the packet if they're set up to only let "related" packets go through (which is the case for the majority of end users).
Anyhoo, I added a branch which would send the ICMPv6 Time Exceeded message for all mock hops, yet answer with a proper Echo Reply for the last hop. It's rather ugly, but who cares! it works!
Picture 14 - Packet losses are unfortunately beyond my control.However, those patterns are quite interesting.
(sleep 1.5; dig +short +tcp @2a0d:eb00:8006::acab TXT {0..10}.0.0.f.0.0.0.0.0.0.0.0.b.a.c.a.f.0.0.0.6.0.0.8.0.0.b.e.d.0.a.2.ip6.arpa. | sed 's/[" ]//g' | base64 -d | mpv - 2>/dev/null >/dev/null) & mtr --displaymode 1 -i 1 -m 30 -s 28 2a0d:eb00:8006:dead::acab
Figure 6 - The command invocation. Click once to select all.
The cool thing is that this just works with mtr, all you need is a specific switch (and a local IPv6 address, which is still surprisingly hard to come by).
For transparency: I ended up rewriting the Bash IPv6 stack into Raku. The bash stack is functional, but much slower; On my laptop, frames would start dropping after ~3 clients connected at once, while the Raku version was able to handle 15+ clients (likely much more), hitting barely 24% CPU load.
Picture 15 - MikroTiks hate them: ONE SIMPLE TRICK against the DNS cache
Addendum #1: Please, sanitize your inputs!
Working on this post got me thinking - since in reality domains are only a serving suggestion for contents of a PTR, can they be used as an "unexpected" attack vector? Thinking of the numerous webpages which offer various DNS resolution tools, maybe some of those made bad design assumptions?
3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.@ 3600 IN PTR <script>alert(1)</script>sakamoto.pl.
4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.@ 3600 IN PTR $(sleep${IFS}15).
Figure 7 - Those may look fake, but it's completely real:XSS payload, and a timing shell injection payload
I went through the first 3 pages of duckduckgo and google results for "online reverse DNS lookup". In the sea of SEO garbage, I found roughly 10 webpages offering actual lookups; Four of them have turned out vulnerable, but only to the XSS payload. Thankfully, no page actually executed the shell injection (nor variants of it).
Interestingly, almost all of those pages can be divided into two categories:
- displays the record verbatim, is vulnerable to this attack or a variant
- refuses to display a record if it isn't a valid domain, isn't vulnerable
Picture 16 - who's "root" NOW? ha!
The odd-one-out was the googleapps dig page, which mangled the record, removing the <script> tag - from both sides, even. Turns out that they're doing sanitizations client-side, using a slightly inapropriate library for the job. It even lets through some basic tags like <b> and <br>. It is unknown to me why they didn't just use innerText instead of innerHTML. TXT record lookups worked just the same, so I doubt I'm the first to look at this. Anyhoo, beyond visual oddities, NOTABUG.
All the actual vulnerabilities that I discovered have been reported, and this post has been delayed to give them time to patch. To my best knowledge, all of the pages are patched by now. Also, no hate to any parties involved: vulnerable code happens, and it's hard to write something that's secure against all kinds of attacks. Everyone who I wrote to was really responsive, with the quickest ACKs getting to me within 15 minutes. A pleasant surprise!
Addendum #2: Other ARPA subdomains
There are several other subdomains outside in-addr and ip6; Wikipedia even has a nice list. Of those, perhaps the most interesting one is e164.arpa., which relates to the E.164 public numbering plan for the public telephone network. In very short terms, the dream was to have a subdomain with a reversed phone number (same reason as revDNS), pointing to an IP address on which one could receive a telephone call. This telephone-to-IP mapping could be used in various scenarios, such as roaming between physical locations.
Unfortunately, e164 never caught on, and multiple regional registrars have already deprecated the supporting infra. Still, if after reading this article you're in a dire need of an ARPA subdomain, this may be your easiest way of getting ahold one. See below for an easier method!
Addendum #3: Do It Yourself (no please don't)
Through the adventures of yet another entity hosting an ARPA fedi instance I found out that Hurricane Electric's tunnelbroker.net, besides giving out free IPv6 ranges, also gives out free ip6.arpa zones! HE 💚
Greetz to paige, and thanks for sharing your method! Also greetz to caramel for their writeup (linked earlier)!
Should you welcome this into your own home, I published all the source under a BSD license - feel free to steal any part. also click here for a free ipv6 shirt.
Notes:
1. Technically, you're supposed to put a domain in there. Practically, the protocol is very malleable: with a custom DNS server, you could likely put anything in a PTR record and see how clients reject your response, or burn in undefined behavior. However! For simplicity, I wanted to keep with PowerDNS, so for the purpose of this article I'm considering limits of pdns as limits of the protocol.
2. Yes, yes, I know, SOCK_RAW is a thing, and you can handle ICMP with it. Bash doesn't have a single way to open RAW sockets tho, so this was out of the question - and even if it wasn't, I find it not much less painful than what I ended up doing.
3. The "superseded" part is a bit vague. In reality, links joining ARPANET and the internet existed for some time before the final closure. For a lack of a better word, this shall do.
Big thanks to kleines Filmröllchen, Lili, famfo and Linus for proofreading this post!

Comments:
what.
very cool, i love reading longer blogposts such as this.