I use the starship shell prompt and I wanted to customize it to show whether I am connected to a VPN or not. Namely, Tailscale, which I use for accessing my homelab server remotely. Starship, alongside their extensive library of baked-in modules, supports creating your own modules. Within one, you define what you want to display (an icon, some text, a combination of the two) and when you want to display it (a certain file or directory present in your cwd or when a specific command returns 0). So my plan was to define a bash one-liner that checks if I am connected to my tailnet and, if so, display the \udb85\uddfc glyph (a nerd font icon that closely resembles Tailscale's actual logo).

My first thought for doing this connection check was the tailscale CLI. It has the tailscale status command, which prints "Tailscale is stopped." when disconnected and, if connected, prints your tailnet's information (IP addresses, hostnames, etc.). tailscale status piped to grep matching on "Tailscale is stopped." worked but was unfortunately far too slow. The status command opens a socket to the tailscaled daemon and takes anywhere from 50-150ms per check. And that would be 50-150ms of delay added to every prompt. So what if we skip the opening of the socket and just check if the tailscaled daemon is running? List all current processes and then grep for the tailscale daemon: ps aux | grep '[t]ailscaled' (We use [t]ailscaled since otherwise, the grep would match the grep process itself). This was faster at around 60ms but was still a bit too slow. It also had a false positive edge case when the Tailscale app was running in the background but the VPN wasn't connected. As for another approach, connecting to your tailnet creates /Library/Tailscale/ipnport (on macOS specifically), a temporary symlink targeting the localhost port used by Tailscale's local control API. So, we can simply check if that file exists with ls /Library/Tailscale/ipnport. This method benchmarked at 7ms. Getting better.

Instead of searching for processes or files, look at network interfaces. Tailscale uses an IPv4 range of 100.64.0.0/10. When you connect to its VPN, it creates a virtual network interface in that range. A bash one-liner is possible, using ifconfig to check the interfaces and then using awk to pattern match on Tailscale's IPv4 range, but this only offers marginal gains, clocking in at 6ms on average. Faster is possible if we check at the kernel level, bypassing any fork, exec, and piping overhead. We can do so using getifaddrs(3) inside a C program to check the kernel's network interface list directly:

#include <ifaddrs.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void) {
    struct ifaddrs *ifa, *p;
    if (getifaddrs(&ifa)) return 1;
    for (p = ifa; p; p = p->ifa_next) {
        if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
        uint32_t addr = ntohl(((struct sockaddr_in *)p->ifa_addr)->sin_addr.s_addr);
        if ((addr & 0xFFC00000) == 0x64400000) {
            freeifaddrs(ifa);
            return 0; 
        }
    }
    freeifaddrs(ifa);
    return 1; 
}

Here, we retrieve the network interface list with getifaddrs, skipping anything that isn't IPv4. The caveat is that addresses are returned in network byte order, so we have to convert them to host byte order to do any comparisons. We do so with ntohl(). Comparing the raw bits, we use 0xFFC00000 as a mask to keep only the first 10 bits of the IPv4 address, ignoring the remaining 22 bits. At this point in the for loop, the address we are checking is stored as a 32-bit integer, so we need to convert Tailscale's 100.64.0.0 IPv4 range to a 32-bit integer (0x64400000) for comparison. If the address we're checking starts with 100.64.0.0, we know it's from Tailscale and, by extension, we know that we are connected to our tailnet. Compiling with cc -O2 -o "$HOME/.local/bin/ts-check" main.c and testing with hyperfine, the average time of execution was 40 microseconds. That's 3,750x faster than our tailscale status approach and 175x faster than the one using ifconfig. Most importantly, it's fast enough to add no noticeable delay to our command prompt. This is important, since it will run on every prompt load.

The resulting custom starship module (at ~/.config/starship/starship.toml) is as follows:

[custom.tailscale]
symbol = '\udb85\uddfc'
when = 'ts-check'
format = '[$symbol]($style) '