{"id":747,"date":"2024-09-04T11:54:56","date_gmt":"2024-09-04T06:24:56","guid":{"rendered":"https:\/\/www.lguruprasad.in\/blog\/?p=747"},"modified":"2024-09-04T11:55:03","modified_gmt":"2024-09-04T06:25:03","slug":"seamlessly-access-local-services-on-lan-and-tailnet","status":"publish","type":"post","link":"https:\/\/www.lguruprasad.in\/blog\/2024\/09\/04\/seamlessly-access-local-services-on-lan-and-tailnet\/","title":{"rendered":"Seamlessly access local services on LAN and Tailnet"},"content":{"rendered":"\n<p>As I am passionate about self-hosting, I have been setting up various services in my homelab, in addition to those on my cloud servers. I have also been using <a href=\"https:\/\/tailscale.com\/\">Tailscale<\/a> to access my devices and services while not at home. So I have wanted to have a seamless way to access the services, irrespective of whether I am on my home local area network (LAN) or connected to it via Tailscale. Below are my requirements for such a setup.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>All the devices\/services should be accessible using a fully-qualified domain name (FQDN), under a domain that I own and control. This rules out the auto-generated Tailscale subdomains.<\/li>\n\n\n\n<li>I have a <a href=\"https:\/\/docs.linuxserver.io\/general\/swag\/\">LinuxServer.io SWAG reverse proxy<\/a> in front of all the services in my homelab, and it provides TLS termination. So I would like to access the existing services using TLS at all times.<\/li>\n\n\n\n<li>While I could set up a <a href=\"https:\/\/tailscale.com\/kb\/1019\/subnets\">Tailscale subnet router<\/a> that allows access to my LAN, I do not want to allow the devices on my <a href=\"https:\/\/tailscale.com\/kb\/1136\/tailnet\">Tailnet<\/a> full access to my LAN. And I do not want to redo my home LAN setup to isolate things to be able to do this.<\/li>\n\n\n\n<li>The FQDNs of the exposed services should resolve to a LAN IP address when I am in my home LAN and to a Tailnet-specific address when I am not at home and connected to my Tailnet.<\/li>\n\n\n\n<li>It should be possible to expose more services using this setup in the future, even if they are not behind the SWAG reverse proxy.<\/li>\n\n\n\n<li>The base domain that I want to use for this should not have any publicly accessible DNS records pointing to private IP addresses for this setup to work.<\/li>\n\n\n\n<li>The resulting setup should integrate into my existing <code>docker-compose<\/code> configuration.<\/li>\n<\/ul>\n\n\n\n<p>The <a href=\"https:\/\/tailscale.com\/kb\/1282\/docker\">Tailscale docker documentation<\/a> illustrates a way to expose LAN services on a Tailnet, but the example on that page causes the service(s) to be accessibly only over the Tailnet. So it doesn&#8217;t work for me.<\/p>\n\n\n\n<p>To start, I added a Tailscale docker container to my <code>compose.yaml<\/code> file using a configuration like<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\n  tailscale:\n    image: tailscale\/tailscale\n    container_name: tailscale\n    hostname: &lt;tailnet device name&gt;\n    environment:\n      - TS_ACCEPT_DNS=true\n      - TS_AUTHKEY=&lt;authkey or OAuth2 client secret&gt;\n      - TS_EXTRA_ARGS=--advertise-tags=tag:docker\n      - TS_ROUTES=172.21.0.0\/24\n    volumes:\n      - .\/config\/tailscale\/state:\/var\/lib\/tailscale\n      - \/dev\/net\/tun:\/dev\/net\/tun\n    cap_add:\n      - net_admin\n      - sys_module\n    networks:\n      tailnet-subnet:\n        ipv4_address: 172.21.0.11\n    restart: unless-stopped\nnetworks:\n  tailnet-subnet:\n    ipam:\n      config:\n        - subnet: 172.21.0.0\/24\n<\/pre><\/div>\n\n\n<p>For this to work, I had to define a tag named <code>docker<\/code> and add it to my Tailscale ACLs. I also added an ACL to auto-approve the routes advertised by this container.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\n{\n    \/\/ other configuration\n\t&quot;tagOwners&quot;: {\n\t\t&quot;tag:docker&quot;: &#x5B;&quot;autogroup:admin&quot;],\n\t},\n    &quot;autoApprovers&quot;: {\n\t\t&quot;routes&quot;: {\n\t\t\t&quot;172.21.0.0\/24&quot;: &#x5B;&quot;tag:docker&quot;],\n\t\t},\n\t},\n    \/\/ other configuration\n}\n<\/pre><\/div>\n\n\n<p>With this, all the containers that get added to the <code>tailnet-subnet<\/code> network and have an IP address in the <code>172.21.0.0\/24<\/code> subnet will be accessible over my Tailnet. So I updated the configuration of the <code>swag<\/code> container to add it to the <code>tailnet-subnet<\/code> network.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\n  swag:\n    image: lscr.io\/linuxserver\/swag\n    container_name: swag\n    cap_add:\n      - NET_ADMIN\n    environment:\n      - var1=value1\n      - var2=value2\n    volumes:\n      - .\/config\/swag:\/config\n    ports:\n      - 443:443\n      - 80:80\n    networks:\n      tailnet-subnet:\n        ipv4_address: 172.21.0.12\n      default:\n    restart: unless-stopped\n<\/pre><\/div>\n\n\n<p>In the above snippet, I added the <code>tailnet-subnet<\/code> network to the <code>networks<\/code> key and assigned it a static IP address in its subnet, <code>172.21.0.12<\/code>. Since the <code>default<\/code> network was implicitly included before and adding a different network will remove the implicit inclusion, I have also explicitly added the <code>default<\/code> network.<\/p>\n\n\n\n<p>With these configuration changes, the <code>swag<\/code> container was accessible at the <code>172.21.0.12<\/code> IP address over my Tailnet. But I still needed to set up DNS to access the services by domain name.<\/p>\n\n\n\n<p>Tailscale provides a way to add a <a href=\"https:\/\/tailscale.com\/kb\/1054\/dns#restricted-nameservers\">restricted nameserver<\/a> for a specific domain using split DNS. So I needed a DNS server that resolved the domains of the services hosted on the <code>swag<\/code> container to its Tailnet subnet IP address, <code>172.21.0.12<\/code>.<\/p>\n\n\n\n<p>For this, I took inspiration from <a href=\"https:\/\/github.com\/jpillora\/docker-dnsmasq\">jpillora\/dnsmasq<\/a> and created a custom <code>Dockerfile<\/code> that set up a <code>dnsmasq<\/code> resolver.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nFROM alpine:latest\nLABEL maintainer=&quot;email@domain.tld&quot;\nRUN apk update \\\n    &amp;amp;&amp;amp; apk --no-cache add dnsmasq\nRUN mkdir -p \/etc\/default \\\n    &amp;amp;&amp;amp; echo -e &quot;ENABLED=1\\nIGNORE_RESOLVCONF=yes&quot; &gt; \/etc\/default\/dnsmasq\nCOPY dnsmasq.conf \/etc\/dnsmasq.conf\nEXPOSE 53\/udp\nENTRYPOINT &#x5B;&quot;dnsmasq&quot;, &quot;--no-daemon&quot;]\n<\/pre><\/div>\n\n\n<p>Then I created a <code>dnsmasq.conf<\/code> configuration file that looks like the following snippet.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nlog-queries\nno-resolv\naddress=\/domain1.fqdn\/172.21.0.12\naddress=\/domain2.fqdn\/172.21.0.12\n<\/pre><\/div>\n\n\n<p>Then I added the following snippet to my <code>compose.yaml<\/code> file to add the <code>dnsmasq<\/code> container.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\n  dnsmasq:\n    build: &quot;.\/build\/dnsmasq&quot;\n    container_name: dnsmasq\n    restart: unless-stopped\n    volumes:\n      - .\/config\/dnsmasq\/dnsmasq.conf:\/etc\/dnsmasq.conf\n    networks:\n      tailnet-subnet:\n        ipv4_address: 172.21.0.3\n<\/pre><\/div>\n\n\n<p>Then I ran <code>docker compose build<\/code> to build the container, and <code>docker compose up -d dnsmasq<\/code> to start it. With that, I had a DNS resolver to resolve my domain names in the Tailnet.<\/p>\n\n\n\n<p>You might notice error messages in the <code>dnsmasq<\/code> container&#8217;s logs that look like <code>dnsmasq: config error is REFUSED (EDE: not ready)<\/code>. This happens because we have not defined any upstream servers that <code>dnsmasq<\/code> can use. But since we want this <code>dnsmasq<\/code> instance to resolve only our domain names, this is okay and the error can be ignored.<\/p>\n\n\n\n<p>Then on my Tailscale admin dashboard, I added a custom nameserver for my domain name and configured <code>172.21.0.3<\/code>, the IP address of the <code>dnsmasq<\/code> container, as the address of the server to use. Now, all the devices on my Tailnet could access the services on my <code>swag<\/code> container by domain name.<\/p>\n\n\n\n<p>I have an existing DNS setup on my home LAN that resolves the same domain names to the LAN IP addresses. So now, with this setup for Tailscale, my devices can seamlessly access the private services on my LAN and Tailnet.<\/p>\n\n\n\n<p>If I want to add a new service to this setup, it is as easy as adding the <code>tailscale-subnet<\/code> network to it, and adding the DNS records to <code>dnsmasq<\/code> docker container&#8217;s configuration file and the resolver in my home LAN.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As I am passionate about self-hosting, I have been setting up various services in my homelab, in addition to those on my cloud servers. I have also been using Tailscale to access my devices and services while not at home. So I have wanted to have a seamless way to access the services, irrespective of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_s2mail":"","activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":4,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"federated","footnotes":""},"categories":[321],"tags":[357,314,325,356,355],"class_list":["post-747","post","type-post","status-publish","format-standard","hentry","category-self-hosting","tag-dnsmasq","tag-docker","tag-docker-compose","tag-tailnet","tag-tailscale"],"_links":{"self":[{"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/posts\/747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/comments?post=747"}],"version-history":[{"count":18,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/posts\/747\/revisions"}],"predecessor-version":[{"id":765,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/posts\/747\/revisions\/765"}],"wp:attachment":[{"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/media?parent=747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/categories?post=747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lguruprasad.in\/blog\/wp-json\/wp\/v2\/tags?post=747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}