A year of Rust and DNS
bluejekyll.github.io
bluejekyll.github.io
But BIND isn't just failing because "it's written in C", it's failing because it's written in terrible C. That said, "terrible C" is probably most every C routine written by someone with less than 10 years of solid low level experience, so "writing good C code" is not very scalable. There are active, solid projects with very few security exploits that are written entirely in C. Nginx comes to mind.
The article makes a brief reference to DJBDNS, which is written in C, but has suffered zero security exploits [1], and is extremely performant. And it is being used in production, so presumably if it had any exploits they would have been exposed by now.
But...DJB's code is (usually?) released under a rather unfriendly (though mostly open) license, and DJBDNS hasn't been updated in some time, so a more modern project isn't a bad idea. And Rust developers can probably write solid code with only a few years of programming experience, which makes it easier to extend without adding security holes on a weekly basis (coughBINDcoughOPENSSLcough)...
That's not an issue for me at my $corporation because they are sane, but apparently that's the argument.
This changed in 2007 when he decided to release ALL his software under a public domain license (that includes djbdns obviously)
I'm on a mobile now so I won't lookup the sources for this statement, but I'm sure it's not hard to google...
>2007.12.28: I hereby place the djbdns package (in particular, djbdns-1.05.tar.gz, with MD5 checksum 3147c5cd56832aa3b41955c7a51cbeb2) into the public domain. The package is no longer copyrighted.
It's obviously been a while since I've run my own Qmail or DNS server. :)
Worth remembering that until the real crypto libraries arrive in pure rust, openssl is still being used internally.
Yup. This has been the case for virtually all of the "core" open-source C projects I've opened up the covers on. In particular, bash's code base is terrifying. The more I look, the more I believe that nearly everything we build on top of needs to be thrown out and rewritten with a safer language (e.g., Rust) and with a focus on incorporating software development best practices we've learned since the past forty years.
How so? If it ain't broke...
C code doesn't have to be terrible. And "terrible" often is in the eye of the beholder.
I've heard people decry OpenSSL and it's various warts (it has many), but as a code base, it's worked well for years across an incredible range of platforms.
Some claim the architecture and quality is crap (often, it's more about style), some just know that's how it was developed and stick to the style and extend / fix / etc. A lot of the OpenSSL issues stem from the wide range of use and the lack of giveback by companies working with it (including donations, etc. This goes back nearly 15+ years).
A new and hot language does not make a new implementation any more secure (maybe it has different issues) than one that has been around awhile, had multiple eyes, etc.
That... really depends on what you consider "worked well". Being a cryptography library, I think I could make a strong argument that working well includes a very good track record with regard to exploits. I don't believe OpenSSL exhibits that[1]. Note the number of items that include overflows and memory corruption in that reference. Also note that the first page (50 items, but some just DoS) only takes us back to mid 2014.
> A new and hot language does not make a new implementation any more secure (maybe it has different issues) than one that has been around awhile, had multiple eyes, etc.
That's true. Unfortunately, OpenSSL has proven itself to be a poor choice for a cryptography library for those that value security over performance. We used it because it's what we had that was free and supported what we needed, but we need more competition (which we are finally getting) so people can make decisions based on their priorities, not just the small pool of what's available.
New implementations (by knowledgeable security professionals) are what we need. Whether those are done in C/C++ using more modern techniques, or some other language that can eliminate certain classes of error in some other way is somewhat irrelevant, as long as it works as a library. I suspect that the vast majority of people would take a 50% performance hit if it yielded a library that had only half as many major flaws as we've seen. That we might be able to approach that with much smaller performance loss is exciting.
1: https://www.cvedetails.com/vulnerability-list/vendor_id-217/...
How would one know whether there are any major flaws in a code base?
Note that there turned out to be a distressing number of obvious bugs in Openssl, and that it's doubtful that all serious bugs in a full, real-world crypto stack (especially perhaps if you demand support for x509) could ever be obvious.
Ed: see also: http://www.openbsd.org/papers/bsdcan14-libressl/
True, but I hardly have seen any codebase at enterprise level that isn't so.
C doesn't scale when you have projects where modules are written all the time by different people that got hired to work just on piece A and then leave.
They hardly can keep on their head all the nuances of memory management and UB of the company's codebase they have been asked to develop the feature A for in a month.
Languages with more strict type safety help keep people on the right path, even though they still stumble every now and then.
That is true for any language, C, Java, Rust, Go. Continuity from team members is important.
For example, the use of automatic memory management or bounds checking, not only allows to remove an whole class of errors they remove the need to have a deep understanding of memory allocation patterns in the code.
Similarly the way one can make use of strong type definitions like in Ada, ML languages to only allow the code to compile if certain rules are followed.
We all hate to be dealt as cogs, but that is how the enterprise world works, the majority of the jobs are consulting gigs for specific features in short to mid term projects.
Of course there are other types of errors that they still cannot avoid.
About 14 years ago I worked on a project that was a fork of qmail. I got to know DJB's code quite well, to say the least it is awe-inspiring. The level of understanding and craftsmanship he has in C is honestly something I am certain I will never achieve. At the same time, it is some of (for me) the most dense and obtuse code I've had to read. I'm not a fan of loop unrolling, I think that's a thing for the compiler to optimize personally.
Anyway, in researching before starting this project, I was very aware of DJBDNS and all of the tools that make up that suite of tools. It is solid, like a rock. But like a rock, it is also inflexible. If you want to read an interesting post from DJB, this is excellent one on AXFR/IXFR: http://cr.yp.to/djbdns/axfr-notes.html
In reading this I realized that we have different goals for DNS. I want DNS to be flexible and secure, he clearly wants DNS to be secure and hardened. We have different goals, this is not to say my goals are better or worse, but I do think they differ fundamentally from that of DJB's.
You can't exploit something that you can't change.
https://people.csail.mit.edu/nickolai/papers/bangert-nail-la...
Nail, btw is parser written in the spirit of "language theoretic security", which is a poweful new way of looking at security problems.
Please do remember that many of these codebases are ancient.
If you rewrote many of these in C today, they would be much better--especially if you also added a test suite around them.
I'm happy to see this in Rust as I'd like Rust to get some real, hostile-environment usage to see what breaks.
I'd agree the C code is easier to read than Bind's heavy use of macros.
Probably the best part of djb's dns was the source port randomization. The logic was key to responding NS eviction attacks (Kaminsky's attack and its variations).
Modern tooling like Haskell, Dafny and SPARK Ada make this way easier than it use to be. I've recommended doing it in (preferred language) side-by-side with one or more of those in most equivalent way possible so analysis prevents errors.
Want to develop a DNS library or server? Read the RFCs and download the public test suite (which may get regular updates as people fill out corner cases, put in tests for common bugs/exploits, etc). Occasionally run public test suite plus any of your own tests you have added to confirm things function as you expect. There's so much duplication of effort in this area currently, and i'm sure the majority of test suites for related applications have at least a few tests that would be useful to others.
Note: sklogic did mention doing something like that by making sure all the programs' had a scriptable API then using basic, shell input and output to test them. There were acceptance, testing tools I used a long time ago that did something similar using XML. Not sure the efficiency of this method but it should work.
It's because it's written in C. Writing it in average Java, Go, or Ada would've prevented most of those problems because they're not allowed to do the damage by default. That's because the C language, by design and default, adds risk into common operations that doesn't have to be there. Especially today where PDP-11's no longer dominate hardware constraints. That you had to cite the DNS of an elite cryptographer and programmer to make the point supports ours that writing security-critical software in C is the first mistake. Not spending years of practice, significant time, and using available QA tools in the individual project to correctly write the C is the second.
Contrast the situation to IRONSIDES DNS where a decent, but not elite, coder wrote his DNS in SPARK Ada. It can prove absence of most errors that lead to code injection if you can express the app within its limitations. So, just by avoiding C in favor of SPARK, he gets to say it's immune to single packet DOS plus a number of code injections. Similarly, one would at least have memory-safety (50% less errors than BIND) if they used a Wirth language. Still fast & easy to code, too. BlueJekyll, who I assume is average to above average, used Rust to similarly knock out plenty of memory-safety problems.
As in Orange Book days, the language choice should be considered one of the assurance activities (or lack of assurance). Its ability to securely express the problem, be easily analyzed by verification tools, safely optimized, and securely compiled to ASM are important. The fewer risks in each the better. That C poses anywhere from a high to grand challenge across the board argues against it being a default & for its use being an explicit, security risk.
C's dominance today is about platform and architectural support, not hardware constraints. It's the JavaScript of systems programming.
Note: Many safer languages can also compile down to C and integrate it with FFI with type-checked wrappers. Classic strategy for dealing with ecosystem problem. I can't recall if Rust has one. It should.
Note 2: You look to maybe have over-constrained yourself with supported platforms, too. Only four on that list are critical.
C++ is available almost everywhere C is available, so it's an acceptable compromise. We also fuzz any "C-like" parser code, of which there is not much.
I am really optimistic about Rust. I think it's the only thing with a chance to displace C in systems coding. (Go is too intrinsically heavy for tiny devices and has other issues.) But it has a long road ahead to work its way into all the corners of computing that we need to target in practice. I wasn't hating on it, just pointing out how hard it is to displace an inferior language with a monstrous install base. Look at JavaScript.
Edit: here's an idea: what about a Rust-to-C transplier? How practical would it be to implement such a thing well enough to allow it to be used for low-level systems programming?
Well, that's good given I previously told you that your best approach to memory safety... given C++ use... was to convert it to C to run through Softbound+CETS or something similar. Alternatively, a C++ to C compiler then that. I wonder what the state of C++-to-C compilers is these days in terms of what they take in and quality of output.
> I want to get a DNS fuzzer running against it to really pound on it, and then get some benchmark and comparison tests against other servers.
Even if the benchmarks turn out to be not in favor of Trust-DNS, the added safety overhead (if any) should be worth it.
> (I’ll try to post more regularly on progress)
Please do. I thoroughly enjoyed the content as well as the writing style!
Thanks!
http://ironsides.martincarlisle.com/
"Take a look at the full list sometime, I think roughly 50% of those could have been avoided by using (safe) Rust."
This is true for most vulnerabilities I see in C-related apps. We also know there's techniques to prevent that with acceptable performance. So, outside hobbyist or non-critical stuff, I tell people to use a different language to get the baseline of quality/security up. DNS is kind of example that makes it more true.
Now, I haven't learned Rust yet, so I won't be able to fully appreciate the article or spot coding ideas until I do. What I did spot was that you slogged through all the RFC's implementing and testing what you thought should be in the DNS system. Aside from a DNS server/client, I think one of most valuable things you could do in this project is create a single specification of various things in DNS that cites parts of RFC's or advice from real-world implementations to justify each part. Kind of a walkthrough for other people creating DNS's that gives them high-level view and/or drill-down into details of something.
"While in the pit of dispair" "Then I climbed saviors peak"
Haha. Good job getting through all the work as most people quit that I can tell.
"It’s currently not used in production (as far as I know). I’ve put a lot of work into validating correctness of what is going on"
It's good you have gone through the specs and have plenty of features. You did hit on a risk area that might need to be in this section: difference between the specs and real-world implementations that you have to work with. The differences could cause your server to fail. If those exist in DNS, too, there might already be write-ups on common ones out there. Biggest way to find them, though, is running combo's of popular clients and servers against each other in testing. So, that's a possible consideration for the future.
I looked at that a while ago, I should take another. Thanks for pointing that out.
> I think one of most valuable things you could do in this project is create a single specification of various things in DNS that cites parts of RFC's or advice from real-world implementations to justify each part. Kind of a walkthrough for other people creating DNS's that gives them high-level view and/or drill-down into details of something.
I was thinking of doing something like this, but you have an even better idea here. Actual references back to the code. I might start trying to do that. (Though that's probably as much work, if not more, as writing the software ;)
> Biggest way to find them, though, is running combo's of popular clients and servers against each other in testing. So, that's a possible consideration for the future.
Yeah, I've wanted to stand up some automated tests with the most popular systems out there, but there's a time issue here. If others are inspired and want to join the project, I'd love help in this area!
Wireshark's DNS dissector is exceptionally useful here.
I agree that it's a project in and of itself.
Literate programming (in the Knuth sense): yay!
> (Though that's probably as much work, if not more, as writing the software ;)
Yep. Hence one of the few big projects I'm aware of that use lp (apart from Knuth) is:
Axiom:
http://axiom.axiom-developer.org/axiom-website/books.html
Ed: from the post: > (And writing this post helped uncover a bug, so even if no one reads this, it was worth it).
Literate programming: yay!
Or perhaps narrative-driven design: ndd, to go with tdd and bdd...
And just to note: I separated the network portion from the encoder/decoder functions. It's easier to integrate that way.
I was also thinking of getting my feet wet in ring with some of the easy issues on the github page.
If you are an IRC kind of person, maybe also consider joining the #rust-crypto channel on irc.mozilla.org.
At least for the client/server code, I think you'd end up with 90% of all the same dependencies, so I don't know exactly how I'd divide it up. If you have ideas, definitely let me know!
>Implementing rfc1035 was deceivingly easy
This stuck out to me - RFC1035 is the first RFC I've read all the way through and had a shot at implementing. Then you realise it's RFCs all the way down and there's no easy way to navigate through all the overridden / deprecated parts.
Zone master files are a great example of this. There's no official grammar and so there are subtle differences in the way different DNS servers parse them.
Its also easy to under-estimate and not notice for example, until you've written a lexer or two, that the grammar is context sensitive and ambiguous... so I'd almost guarantee that trust-dns can be made to interpret one differently to BIND, Knot, or unbound.
let recursion_available = (0b1000_0000 & r_z_ad_cd_rcod) == 0b1000_0000
That just seems redundant. If you declared the var as a Boolean, would the == 0bxxxx part have been necessary?Rust is deliberately explicit about conversions between types; it’s less error-prone, though often more verbose. It’s a part of the language’s philosophy.
That would already be a good improvement, both in terms of DRY and readability, without the need of adding another dependency.
Also, remember mem::forget() is not unsafe
I'm pointing this out because I don't want people to have wrong expectation of Rust. Rust doesn't prevent memory leak. Rust doesn't consider memory leak to be "unsafe". Even the Rust team themselves have pointed this out multiple times.
Somehow people are still repeating that Rust can rid you of memory leaks. And I'm downvoted just because I'm pointing out facts. Sigh.
(It's been a while, but IIRC the issues they had were more about not understanding what exacty was available and how to use them, at the time. The project has been going really well since then; Amit has been giving a number of cool talks about it at various Rust user groups, and they recently started their own "This week in", though it's called "Talking Tock" because that's a better name, ha!)
,----
| %% extract dns-header fields from a raw packet.
| parse_dns_header(raw_packet) ->
| <<
| ID:16,
| QR:1, OPCODE:4, AA:1, TC:1, RD:1, RA:1, Z:3, RCODE:4,
| QDCOUNT:16,
| ANCOUNT:16,
| NSCOUNT:16, ARCOUNT:16,
| Tail/binary
| >> = raw_packet,
|
| {#dns_header_record {
| id = ID,
| qr = QR, opcode = OPCODE, aa = AA, tc = TC, rd = RD, ra = RA, z = Z, rcode = RCODE,
| qdcount = QDCOUNT,
| ancount = ANCOUNT,
| nscount = NSCOUNT,
| arcount = ARCOUNT
| }, Tail}.
|
`----
almost verbatim 'transliteration' of sec:4.1.1 of 1035 :)
I wish more programming languages would make an effort like this to optimize readability and be self-documenting.
yup. getting from an ascii art representation of the pdu, to something like this should be fairly trivial :) only if writing fsm's were that easy ;)
parse_dns_header(raw_packet) ->
<<
ID:16,
QR:1, OPCODE:4, AA:1, TC:1, RD:1, RA:1, Z:3, RCODE:4,
QDCOUNT:16,
ANCOUNT:16,
NSCOUNT:16, ARCOUNT:16,
Tail/binary
>> = raw_packet,
Also for readers, this does depend on customisable (per-segment/field) defaults: `ID:16` is a shorthand for `ID:16/big-unsigned-integer-unit:1` aka "16-bits wide segment parsed as an unsigned integer in big endian"ah thank you ! that is very good to know.
will it be possible to have some formatting guidelines / examples on the site somewhere ? similar to what we see on reddit ?
https://news.ycombinator.com/formatdoc
Sadly HN has jack shit formatting-wise (the most annoying part being you can't escape asterisks so half the time it's going to emphasise a comment section and remove your asterisks despite that not being what you wanted)
it is 'spartan' :)
#[packet]
struct Dns {
id: u16be,
qr: u1,
opcode: u4,
aa: u1,
tc: u1,
...
#[payload]
data: Vec<u8>,
}<3
It would be interesting to see how the way Rust manages memory and its current optimizer match against OCaml in a production TCP/IP stack.
The question you pose though is awkward. MirageOS is a unikernel, Trust-DNS is an application, a consumer of the TCP stack.
I have seen Rust apps built into rumprun, but as of right now this isn't something on my list, in theory it's possible.
You can build a DNS server with MirageOS and it is used like that in the context to the new Docker versions for Mac OS X and Windows, which is one example of being used in production.
This is correct as my colleagues have recently relearned. Moving from one endianness to another seriously complicated their lives for a few weeks (time to initial discovery of error, time to add in new definitions, time to test the updated system).
This is true, but for many of us in the embedded world, we use the specific packing anyway. I know my endianness, and I know my compiler--so my structs map straight across.