Headscale is Just Better
For the longest time, I have been using Wireguard to connect all my devices together, with wg-quick it works sure, but its kind of a pain to register new devices to the network. First I have to generate a private and public key pair, then add that public key to my server's configuration and set its ip, then restart the interface, then assign the private key, ip address and dns to the client, and the server's public key as the peer with allowed ips set. If I want to tunnel the connection of the client through the server, I need to update the allowed ips parameter from 10.0.0.0/24 to 0.0.0.0/0 and restart. It's actually a hassle to do all that.
My friend was shilling me Tailscale for a while and I was telling him how I am too invested in Wireguard, how it will take me a long time to set it up all over again, and all the excuses I could find. But after a moment, I found some free time and finally caved in to his continuous praises. Setting it up wasn't too much of a problem, I just followed the guide which told me to download the .deb file and install it, which I did. The default configuration was pretty sane too, I only updated the server_url and base_domain. The extra thing I've done was to put it behind nginx reverse proxy, with following config:
/etc/nginx/sites-available/headscale
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name SERVERNAME;
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
And that was that, I now was running a Headscale server, and all I had to do was just register my clients by installing the appropriate Tailscale
software and connect to my server with: tailscale up --login-server https://SERVER_URL
which will give me a link to visit on my browser, which will tell me a command to run on my server running Headscale, all pretty trivial things.
The one major advantage that I have immediately noticed was that Tailscale uses DERP servers to connect clients directly to each other, so I don't have to use the server as a proxy to talk to other clients. This is a huge thing. You can set it up to use your own DERP server, but I wasn't too bothered to use public ones.
[furkan@klukai ~]$ tailscale ping sqlslave
pong from sqlslave (100.64.0.6) via DERP(fra) in 114ms
pong from sqlslave (100.64.0.6) via DERP(fra) in 111ms
pong from sqlslave (100.64.0.6) via DERP(fra) in 113ms
pong from sqlslave (100.64.0.6) via X.X.X.X:26299 in 22ms
Now, you can use every client as an exit node, but it involves a bit more steps. First, you need to get the client to advertise itself as an exit node:
tailscale set --advertise-exit-node
Then, in your server, you need to allow this client to be used as an exit node. For this, you list the available nodes on your server:
headscale nodes list-routes
Find the ID of the client advertising itself as exit, then approve it:
headscale nodes approve-routes --identifier ID --routes 0.0.0.0/0
Thats it, now you can use this client as an exit node on any other client. How convenient is that?
I had many clients to connect, and surprisingly Tailscale did not mess with the ongoing Wireguard setup that I had, even though it also uses Wireguard under the hood. So I just connected to my clients through Wireguard, set up Tailscale, ensured connectivity, then disabled Wireguard. It was a breeze. I'm not saying that Tailscale won't collide with Wireguard, but in my case it didn't, which I'm grateful for.
I also have 2 test servers for my work, that I want some other people to be able to connect to, and only limited to those 2 machines. While searching around, I have found out that Tailscale supports ACL, now I don't know much about this but there was an example config in the documentation that fit my exact needs. I just had to set the policy file location in Headscale config, and put the configuration in that path.
/etc/headscale/acl.hujson
{
"groups": {
"group:dev": [
"furkan@",
"ekp@"
]
},
"acls": [
{
"action": "accept",
"src": [
"autogroup:member"
],
"dst": [
"autogroup:self:*",
"autogroup:internet:*"
]
},
{
"action": "accept",
"src": [
"group:dev"
],
"dst": [
"tag:dev:*"
]
}
],
"tagOwners": {
"tag:dev": [
"group:dev"
]
}
}
With this config, all I had to do was to add the tag "dev" to the 2 machines that I wanted to be accessible by the members of this group ekp and furkan:
tailscale up --advertise-tags=tag:dev --login-server https://SERVER_URL
As far as I'm aware, you cannot advertise tags after having the connection up, so if you have the connection already, you have to take it down and back up again.
In one of my older blogs, I talked about port forwarding with Wireguard and UFW, for a minecraft server. This time, I wanted to do something similar, but wanted to try something different. I thought about using an Nginx reverse proxy, and have found out that Nginx supports streams, all I had to do was install the extension libnginx-mod-stream, or at least that's what its called under Debian. The stream directive is different than the http, and you need to reference it the same way as http in under /etc/nginx/nginx.conf.
/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
......
}
stream {
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/streams-enabled/*;
}
Then I just set up a 25565 TCP and 19132 UDP proxy pass under /etc/nginx/streams-available, which I symlinked to /etc/nginx/streams-enabled:
/etc/nginx/streams-enabled/minecraft
server {
listen 25565;
proxy_pass oasis:25565;
proxy_timeout 1h;
}
server {
listen 19132 udp;
proxy_pass oasis:19132;
proxy_timeout 1h;
}
The reason for both Java and Bedrock edition addresses is because I am running my server with GeyserMC, which allows Bedrock clients to connect to the Java server. It's a really handy thing.