<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Vpn on hxmn.dev</title><link>https://hxmn.dev/tags/vpn/</link><description>Recent content in Vpn on hxmn.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 16 Apr 2026 10:00:00 +0300</lastBuildDate><atom:link href="https://hxmn.dev/tags/vpn/index.xml" rel="self" type="application/rss+xml"/><item><title>My Networking Setup</title><link>https://hxmn.dev/tools/my-networking-setup/</link><pubDate>Thu, 16 Apr 2026 10:00:00 +0300</pubDate><guid>https://hxmn.dev/tools/my-networking-setup/</guid><description>&lt;p&gt;I run machines in a few different places — a home lab, a cloud VPS, a laptop that travels with me. Keeping them all on one coherent private network, with stable names and encrypted transport, is the thing I did not want to think about every day. This page collects the pieces I landed on.&lt;/p&gt;
&lt;h2 id="service-connectivity"&gt;&lt;a href="#service-connectivity" class="header-anchor"&gt;&lt;/a&gt;Service Connectivity
&lt;/h2&gt;&lt;p&gt;For the private network I use &lt;a class="link" href="https://headscale.net/" target="_blank" rel="noopener"
 &gt;headscale&lt;/a&gt; — an open-source, self-hosted implementation of the Tailscale control server. Clients are the unmodified Tailscale clients; only the coordination server is mine.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 HS[Headscale&lt;br/&gt;control server]

 subgraph tailnet [Tailnet]
 L[Laptop]
 V[Cloud VPS]
 R[Subnet router&lt;br/&gt;home lab]
 end

 DB[(Postgres&lt;br/&gt;10.0.0.5)]
 D[DERP relay]

 L -. keys / ACLs .-&gt; HS
 V -. keys / ACLs .-&gt; HS
 R -. keys / ACLs .-&gt; HS

 L &lt;== WireGuard ==&gt; V
 L &lt;== WireGuard ==&gt; R
 R --&gt; DB

 L &lt;-. fallback .-&gt; D
 V &lt;-. fallback .-&gt; D&lt;/pre&gt;&lt;p&gt;The control plane (dotted lines) only hands out keys and policy; all service traffic (thick lines) is direct WireGuard between peers. DERP is a relay used only when a direct path cannot be established.&lt;/p&gt;
&lt;h3 id="why-headscale"&gt;&lt;a href="#why-headscale" class="header-anchor"&gt;&lt;/a&gt;Why headscale
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sovereignty.&lt;/strong&gt; Account, keys, and ACLs live on a box I control. No SaaS account to expire, no per-device limits, no upstream outage breaking my mesh re-keying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero-config client side.&lt;/strong&gt; Every node — macOS, Linux, iOS, router — runs the same Tailscale client I would use anyway. No custom builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WireGuard underneath.&lt;/strong&gt; Fast, modern, encrypted by default. I do not run a classic VPN gateway.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="typical-deployment"&gt;&lt;a href="#typical-deployment" class="header-anchor"&gt;&lt;/a&gt;Typical deployment
&lt;/h3&gt;&lt;p&gt;The server is a small VPS with a public IP. A single Go binary (&lt;code&gt;headscale serve&lt;/code&gt;) listens on &lt;code&gt;localhost:8080&lt;/code&gt; and a reverse proxy (Caddy, in my case) terminates TLS for &lt;code&gt;https://headscale.example.com&lt;/code&gt;. Configuration lives in &lt;code&gt;/etc/headscale/config.yaml&lt;/code&gt; — the defaults are close enough that I only touched a handful of fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server_url&lt;/code&gt; — the public URL.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listen_addr&lt;/code&gt; — loopback, since Caddy fronts it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_prefixes&lt;/code&gt; — the CGNAT range for the tailnet (the default &lt;code&gt;100.64.0.0/10&lt;/code&gt; is fine).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dns.magic_dns: true&lt;/code&gt; — so every node gets a stable DNS name inside the tailnet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;State is a single SQLite file; backing it up is &lt;code&gt;cp&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="onboarding-a-node"&gt;&lt;a href="#onboarding-a-node" class="header-anchor"&gt;&lt;/a&gt;Onboarding a node
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Create a user on the control server: &lt;code&gt;headscale users create ilya&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Issue a short-lived pre-auth key: &lt;code&gt;headscale preauthkeys create -u ilya --expiration 1h&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the new node: &lt;code&gt;tailscale up --login-server https://headscale.example.com --authkey &amp;lt;key&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is the whole provisioning flow. The node joins the mesh, picks up its tailnet address, and is reachable by MagicDNS name from every other node.&lt;/p&gt;
&lt;h3 id="reaching-services"&gt;&lt;a href="#reaching-services" class="header-anchor"&gt;&lt;/a&gt;Reaching services
&lt;/h3&gt;&lt;p&gt;With the mesh in place, service connectivity becomes boring in the best way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable names.&lt;/strong&gt; A Postgres instance on a home-lab host is just &lt;code&gt;homelab-db:5432&lt;/code&gt; from anywhere on the tailnet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACLs.&lt;/strong&gt; Headscale supports Tailscale-style policy files. I scope which tags can reach which ports — the laptop can reach &lt;code&gt;tag:dev&lt;/code&gt;, CI runners can reach &lt;code&gt;tag:infra&lt;/code&gt;, nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subnet routers.&lt;/strong&gt; A single node advertises &lt;code&gt;10.0.0.0/24&lt;/code&gt; from the home lab so I can reach devices that do not run Tailscale (a NAS, a switch&amp;rsquo;s management interface).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No inbound ports.&lt;/strong&gt; Services never need to be reachable on the public internet. WireGuard punches out; the control server only coordinates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="derp"&gt;&lt;a href="#derp" class="header-anchor"&gt;&lt;/a&gt;DERP
&lt;/h3&gt;&lt;p&gt;Two nodes that cannot reach each other directly (both behind strict NATs) fall back to a DERP relay. I use Tailscale&amp;rsquo;s public DERP pool for this — headscale lets me mix in my own later if I ever need guaranteed-path control, but I have not needed to.&lt;/p&gt;</description></item></channel></rss>