Let's code a TCP/IP stack, 1: Ethernet and ARP
saminiir.com
saminiir.com
I would suggest being a little more careful with this:
> unsigned char dmac[6];
because there are several platforms, all of them high-performance, non-legacy processors, where unsigned char is not 8 bits :-). E.g. on AD's SHARC, it's 32. You're declaring the struct with __attribute__((packed)), so I assume you're going to want to fill it automatically through DMA at some point in the future. On such a platform, it won't have the expected results. A colleague got bit by this, on a device that's sitting on my desk right now, and it's not a vintage piece of equipment from the 60s.
I would suggest using uint8_t instead, just like you're using uint16_t a few lines below.
It is impossible for unsigned char to be 32 bits and for uint8_t to exist, on a conformant C/C++ implementation. The sizeof char / unsigned char is 1 ("byte" is defined this way). So if 1 byte is more than 8 bits, there cannot be an uint8_t type, since that would make sizeof uint8_t less than 1.
Actually, combined with the requirement for char to have at least 8 bits, is follows that uint8_t can only possibly exist if char has 8 bits.
Edit: quoting C99, to show that uint8_t must not have padding bits (an assumption in this reasoning). The requirement for uintN_t to have no padding seems omitted in the second paragraph (I assume by mistake), but the third one suggests it really should have no padding.
The typedef name intN_t designates a signed integer type with width N, no padding bits, and a two’s complement representation. Thus, int8_t denotes a signed integer type with a width of exactly 8 bits.
The typedef name uintN_t designates an unsigned integer type with width N. Thus, uint24_t denotes an unsigned integer type with a width of exactly 24 bits.
These types are optional. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names.
At the moment, each one of the two codebases I'm maintaining has about a dozen workarounds for compiler bugs. One of the compilers is already at its 6th major version.
It's things like these that bring a smile to my face whenever I see people valiantly calling for rewriting everything in Rust :-).
TUN/TAP: https://github.com/songgao/water (Linux only for now)
Raw Ethernet: https://github.com/songgao/ether (Linux, FreeBSD, Darwin)
A few things I'm missing for this to be a more complete ARP implementation: 1. Answer Ip to Mac conversion requests from the above stack
2. Send an ARP Request if the mac is missing (+Retries)
3. Static ARP configurations (To prevent ARP spoofing attacks)
4. ARP entry aging. Otherwise you'll run really quickly into "ERR: No free space in ARP translation table\n" (And of course a much larger cache than 32 entries)
5. Ignore unsolicited ARP replies (Helps prevent ARP spoofing attacks as well)
6. use uint8_t instead of "unsigned char" for packet structures.
Awesome work!
Anyway, I linked the book here because it’s actually quite well-written, and the introductory section in particular actually does a good job of describing basic networking concepts.
――――――
¹ — https://web.archive.org/web/20040626025717/http://developer....
² — https://en.wikipedia.org/wiki/Open_Transport
³ — https://developer.apple.com/legacy/library/documentation/Car...
⁴ — https://web.archive.org/web/20041113084430/http://developer....
Facebook does data centers that internally only use IPv6 using these techniques: http://blog.ipspace.net/2014/03/facebook-is-close-to-having-...
TUN/TAP is very useful for doing nonstandard things with networks, implementing your own VPN, etc.
typedef union {
uint32_t net_order;
uint8_t byte[4];
} ipv4_address;
_Static_assert( sizeof(ipv4_address) == 4, "ipv4_address is not 4 bytes long");
static inline __attribute__((overloadable)) uint32_t as_host( ipv4_address a) {
return other_order_32(a.net_order);
}
// more functions follow, like
// bool equal(a,b)
// int compare(a,b)
// ipv4_address ipv4_address_from_host( uint32_t)
The nice part of this is that it would take an act of willful ignorance on my part to accidentally miss or over include a conversion from network order to host order.The compiler is still happy to slam these around in registers like integers so I haven't changed the runtime performance. The downside is that C won't let you compare structs or unions with ==, so I have to have that equal() function which dirties the source code a bit.
That __attribute__((overloadable)) makes as_host() an overloadable function so I don't have to have a giant family of incredibly_long_function_names to convert all my different types to host byte order or to compare them with themselves.
That _Static_assert isn't terribly useful in this case, but there is one on all of the structures where I expect to have explicitly specified a layout. That way when the compiler tries to pull a fast one on me because some language lawyer spent too long reading the spec, I'll find out at build time.
Doing this in Rust would be a good exercise.
The 1970s string functions, such as "strcpy", that don't have a destination length were deprecated years ago, but they've never been removed from libraries. Microsoft's C compiler has warned about this for a decade.[3] This is one of the major causes of buffer overflows in C, and a large number of exploits involve it.. There's a CERT advisory from Homeland Security about "strcpy".[4] Seeing a "strcpy" in new code is a big red flag. If you see that in an interview, don't hire that programmer. They're dangerous.
Since C99, you've been able to write
for (int i = 0; i < 10; ++i) ...
in C code, and that was in C++ from the early years. The trend in programming is strongly towards declaring and initializing variables at the same time. Remember, in C, local variables declared but not initialized have junk values until assigned. The "all variables declared at the top of a function" style is obsolete. Pointer variables declared without initialization are especially bad.There's a long and painful history of classic C bugs, and the newer versions of C help, just a little, to avoid them.
[1] http://icube-icps.unistra.fr/img_auth.php/d/db/ModernC.pdf [2] https://news.ycombinator.com/item?id=9018247 [3] http://stackoverflow.com/questions/4012222/c-strcpy-gives-a-... [4] https://buildsecurityin.us-cert.gov/articles/knowledge/codin...
> Since C99, you've been able to write
>
> for (int i = 0; i < 10; ++i)
To be fair, Microsoft's compiler only began supporting that recently, so for cross-platform compatible code, people often declared the index outside the loop.And having all variables declared at the top of the function can make it easier to visualize how much stack space the function is using, which can be useful when your stack is limited to a few kilobytes.
Microsoft's _s functions don't help if you don't know what the lengths should be, and if you do, they just make it more confusing.
In good code it spells out in one glance that there exists a guarantee on the size of the destination buffer.
Supplying the string's length just brings redundant code and, with it, ambiguity.
Discouraging strcpy() use is fine and understood; I'm aware of all the caveats and bugs and security implications.
But the kind of black-and-white thinking that says "always use strncpy instead of strcpy" is bad; the idea that truncating a string magically absolves us of any security implications or the need for exit paths.
And then let's look at how much code has made a mess of strncpy() whilst thinking it was doing the right thing.
I can probably count on one hand the number of times I've wanted to copy a string but been happy for it to be quietly trimmed, even in extreme cases. Whereas anything from alloca to flexible array member, or copying a string back to its original buffer, are all very appropriate use of strcpy().
Using strcpy is a big no-no -- it's both detrimental, and out of style. Its manpage gives this warning (on FreeBSD -- the one you'll get if you type man strcpy may differ, but all man pages have had a similar warning since 1990something):
> The strcpy() function is easily misused in a manner which enables malicious users to arbitrarily change a running program's functionality through a buffer overflow attack.
That's because strcpy(dst, src) will basically do something like this:
int i = 0;
while (src[i] != '\0') {
dst[i] = src[i];
i++;
}
(the real implementation is usually more terse for reasons I won't go into right now, but this is what it basically does in more verbose terms).without bothering to check if dst[i] is not beyond the end of dst. This allows you to write past the end of the buffer that holds dst, and who knows what's there...
Nowadays, all that usually happens (on sane operating systems running on machines with a MMU) is that your application crashes due to something called ASLR. It used to be a big problem before that, though, and it still is (on insane operating systems, on machines that don't have a MMU and so on).
Either way, it's not a good idea, and it's considered to be a pretty big code smell. strncpy is the encouraged version. strlcpy is the sane one, but it's not available on all platforms (and even when it is, it's not always sanely implemented).
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
will not necessarily NULL terminate ifr.ifr_name for all values of dev variable. NULL terminated input is assumed later when ifr.ifr_name is used in strcpy.https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux....
Old C code declared variables at the beginning of a function because it made compilers easier to implement, in a single pass. If you interleave variables and statements, then calculating the amount of stack space needed for the function call requires multiple passes. Knowing this, defenses of "variables at the top" often seem like rationalizations to me.
There's a reason that other programming languages don't share this convention, and that's because compilers had evolved enough in sophistication to allow interleaved variables and statements from their beginnings; and they had no legacy code bases anchoring their code style, like C does.
Declaring variables at the top is suboptimal compared to declaring them at their usage. It requires less working memory to understand, and it's fewer lines of code, and the resulting code can often be easier to understand due to context. Standalone variables typically either require more documentation to make sense, or else they don't make sense until you see them used in context.
The C language has a lot of bad conventions and semantics that have led to thousands of bugs over the years. Variables whose scope span entire functions is one of them. There's no articulable advantage and the downside is greater demands on working memory, and greater risk of defects (variables being visible before they're initialized or outside places where they make sense). Rust is on the right track allowing variable lifetimes to be defined and managed in a precise way.
Like I said, it's a matter of style. Defenses of one or another that span more than one paragraph often seem like a waste of time to me.
Declaring variables at the top keeps variable declarations close to each other; declaring variables close to where they're used keeps, obviously, declarations close to use.
And that one is not very useful to a human. Declaring variables near their usage is useful to a human. Understanding the code requires less (human) working memory.
The reason that old C code declared all variables at the top is because it made the earliest compilers easier to implement in a single pass: the compiler could tell up front how much stack space was necessary for the function call before encountering statements. The early compilers could scan all of the statements, tabulate the stack space required, and then emit the machine code to reserve that amount of stack space, before proceeding to parse and compile the following statements. Modern compilers are multi-pass and can scan the function and compute the stack space before beginning to generate machine code.
Writing code in that style is an example of humans optimizing for the machine, rather than vice versa. Modern compilers don't have this limitation. I haven't read an argument that code written in this style is actually preferable for humans, and in my opinion and experience, it is not.
well, take a look at the latest tun_alloc(...) from linux-kernel-git then, and you would see, that sore points that you have raised are still there :)
https://github.com/iCepa/tun2tor/blob/6cc56a2f6c0b578aed9277...
Unfortunately, I haven't had time to work on it in a little while :(
However, please note that this is not production-capable code. Casting directly a struct directly into the data as a way to "parse" is easy and convenient, but ignores issues like endianness, alignment, etc. By all means, use a struct, but for production code, pick off the bits bit-by-by or word-by-word using appropriate bit operators and assign them to the struct members. Also, someone already mentioned the `char` issue with certain architectures. I'm sure I'm missing some other issues, since writing safe, portable C code isn't always obvious.
I look forward to reading the next part of these series.
Hopefully not too off-topic but I would love to learn how we would have implemented a state-of-the-art global interconnect protocol today. There's of course efforts like http://named-data.net but what about the lower levels like about congestion control. Is dropping packages really the best paradigm?
EDIT: to be clear, I mean if starting from scratch in a world where IP never existed (and no backwards compatibility mattered).
The good part is you catch your mistakes. Developing networking code on a big endian machine can get you a surprise later.