Well, hi there, friends!
Today, a calm Sunday, I decided to spend some time with a project that has been on my mind for a while. Ever since I found out that Brad Fitzpatrick, an engineer I have great respect for, left Google and joined Tailscale - I have been looking for an opportunity to try the Tailscale product. I couldn’t carve out time for this until recently, when I needed to setup No-IP for a computer at home so I can access it while away. The pain of reconfiguring home routers, installing the dynamic IP update client (DUC), and exposing a machine to the Internet wilderness seemed like a significant time commitment and a security risk. Then I remembered something I had used 15 years ago - a great product called Hamachi. I was pleasantly surprised to discover that Hamachi is still around and has a great domain - VPN.net. Then I remembered reading about Tailscale and thinking how similar it is to Hamachi. Time to learn about Tailscale and read its code!
No surprise - Brad and team have done an incredible job. Tailscale is a breeze to install on macOS, OpenBSD, and Windows. The product solved my remote access problem immediately, without exposing my machines to the Internet.
Tailscale is built on top of a new VPN software, called Wireguard, which was created by Jason Donenfeld. I’ve heard great things about it. I decided to dedicate this Sunday afternoon to casually perusing through the source code of the Go implementation of Wireguard.
There is a great amount of complexity in the Wireguard repository inherent to encryption, networking, and systems software. This despite the fact that Wireguard prides itself on being simple as compared to incumbent VPN tech.
For this post we'll look at the beautiful simplicity of the entry point into wireguard-go. The main.go file of the wireguard-go project is an excellent primer on writing minimal Go CLIs and also daemons.
I've used Go for many a year and yet I found some interesting new tricks I had not run into before. The very first line, for instance, is something I had not seen in the past:
// +build !windows
This is self-explanatory, but nevertheless - a good thing to have in one’s toolbox when dealing with multi-OS projects. Adding this build constraint (or build tag) is done when one needs to exclude a file when building on Windows.
Scrolling through the source code, I found it interesting that the license Jason had chosen for this repo is MIT, which differs from the GPLv2 license used for the Linux kernel implementation of Wireguard. Later browsing wireguard.com I noticed the License section, where the multi-license use is explained: "The kernel components are released under the GPLv2, as is the Linux kernel itself. Other projects are licensed[...] depending on context."
Continuing my linear walk through the Go code, I found the warning function really interesting. It is such great news to know that Jason added wireguard to the 5.6 version of the Linux kernel. The warning function shares w/ the Linux user the fact that Wireguard may already be in their kernel. You’ll notice, as of this writing, there is no check for the version of the Linux kernel. The message may be inaccurate for folks running older kernels. I have not needed to check the OS within a Go binary, and so the "runtime.GOOS != "linux" statement (runtime.GOOS in particular) is something new I learned today and will use in a project I am working on.
I love the attention to detail and the usage of unicode characters (“┌” (U+250C) for instance) around the message. The border definitely makes the message stand out and brought nostalgic memories of the ncurses library - a topic for a future post.
Another interesting choice is the exclusive use of the os.Args to parse the CLI arguments. In Go there is a plethora of libraries and methods to parse command line arguments and flags. The author of wireguard-go chose the minimal rudimentary option available - a choice I appreciate.
I find the block related to daemonization of the process also interesting and worth looking into to learn from. We'll save the deeper analysis of this block for another time, but it it is definitely worth examining the use of os.Executable(), os.StartProcess(), and process.Release().
If you haven't been exposed to handling operating system signals with Go, the term channel would be interesting to learn from:
term := make(chan os.Signal, 1)
Once the channel is initialized, wireguard-go registers with the OS, which will send SIGTERM and interrupt (Control-C) to this channel. A few lines below wireguard-go will wait and block on either one of these, or for a device release before it cleans up and shuts down:
select {
case <-term:
case <-errs:
case <-device.Wait():
}
The minimalistic approach adopted in creating the wireguard-go CLI and daemon is a great primer and I encourage you to bookmark this repository and leverage it when creating simple Go CLI tools or daemons. One area of improvement, in my opinion, would be to further carve out code out of main() and into smaller functions. This would make the code testable and main() smaller and easier to comprehend. I encourage you, and will attempt this myself, to submit patches to the repository with this and any other improvements possible.
See you next time!