<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>Lansenou · Writing</title><link>https://lansenou.com</link><description>Writing by Lans Camara</description><item><title>How to serialize HoloLens 2 anchors</title><link>https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/</link><guid isPermaLink="false">https://lansenou.com/?p=1090</guid><pubDate>Sun, 28 Jun 2026 12:21:49 GMT</pubDate><category>Anchors</category><category>C#</category><category>HoloLens</category><category>Unity</category><description>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#cross-platform&quot;&gt;Cross-platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#requirements&quot;&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#setup&quot;&gt;Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#exporting-the-anchor&quot;&gt;Exporting the anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#importing&quot;&gt;Importing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#loading-anchors-into-the-scene&quot;&gt;Loading Anchors into the scene&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#completed-file&quot;&gt;Completed file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#multiplayer&quot;&gt;Multiplayer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#projects-that-use-sharing-of-anchors&quot;&gt;Projects that use sharing of anchors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#troubleshooting&quot;&gt;Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/28/how-to-serialize-hololens-2-anchors/#license&quot;&gt;License&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article shows how to export and import spatial anchors in Unity using OpenXR, enabling persistence and sharing between HoloLens 2 devices. This resulting data can then be used to transfer the data. This allows you to persist the data either locally on a single HoloLens or to share the anchors between multiple devices.&lt;/p&gt;
&lt;p&gt;This uses XRAnchorTransferBatch for “offline” cross-device anchor persistence in Unity OpenXR without having to resort to Azure Spatial Anchors (which has been &lt;a href=&quot;https://learn.microsoft.com/en-us/lifecycle/products/azure-spatial-anchors&quot;&gt;retired &lt;/a&gt;as a cloud service as of November 2024), when cloud/online services are not an option or as a free alternative.&lt;/p&gt;
&lt;h2 id=&quot;cross-platform&quot;&gt;Cross-platform&lt;/h2&gt;
&lt;p&gt;My cross-platform anchor package, LansAnchor, is retired.&lt;/p&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HoloLens 2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unity 2020+&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenXR plugin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ARFoundation&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;The OpenXR plugin and ARFoundation can be installed via the &lt;a href=&quot;https://aka.ms/MRFeatureTool&quot;&gt;Mixed Reality Feature Tool&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Make sure you have the AR Anchor Manager component in the scene. I’ve got my scene setup as follows:&lt;/p&gt;
&lt;figure data-media=&quot;anchors-setup&quot;&gt;&lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://lansenou.com/img/8KeYDyjOAY-320.webp 320w, https://lansenou.com/img/8KeYDyjOAY-640.webp 640w, https://lansenou.com/img/8KeYDyjOAY-960.webp 960w&quot; sizes=&quot;(max-width: 760px) 100vw, 960px&quot;&gt;&lt;img alt=&quot;Unity AR Anchor Manager setup&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://lansenou.com/img/8KeYDyjOAY-320.png&quot; width=&quot;960&quot; height=&quot;301&quot; srcset=&quot;https://lansenou.com/img/8KeYDyjOAY-320.png 320w, https://lansenou.com/img/8KeYDyjOAY-640.png 640w, https://lansenou.com/img/8KeYDyjOAY-960.png 960w&quot; sizes=&quot;(max-width: 760px) 100vw, 960px&quot;&gt;&lt;/picture&gt;&lt;figcaption class=&quot;photo-credit&quot;&gt;(c) Lans Camara · &lt;a href=&quot;https://lansenou.com/wp-content/uploads/2025/01/image.png&quot;&gt;source&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/media/anchors-setup.png&quot;&gt;View the full-resolution setup screenshot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then you need an ARFoundation &lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@3.0/api/UnityEngine.XR.ARFoundation.ARAnchor.html&quot;&gt;ARAnchor&lt;/a&gt; attached on the object on the location that you want to save.&lt;/p&gt;
&lt;h2 id=&quot;exporting-the-anchor&quot;&gt;Exporting the anchor&lt;/h2&gt;
&lt;p&gt;This can be used to grab the data from the anchors. This data can be saved locally, offline or shared with your networking solution of choice.&lt;/p&gt;
&lt;p&gt;Save as &lt;code&gt;ExportAnchor.cs (Pseudocode)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.MixedReality.OpenXR;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public async Task&amp;lt;byte[]&amp;gt; ExportAnchorsAsync(
    IEnumerable&amp;lt;ARAnchor&amp;gt; anchors)
{
    var batch = new XRAnchorTransferBatch();

    foreach (var anchor in anchors)
    {
        // You usually store a mapping between ARAnchor and a string id yourself
        batch.AddAnchor(anchor.trackableId, anchor.name);
    }

    Stream stream = await XRAnchorTransferBatch.ExportAsync(batch);

    using var ms = new MemoryStream();
    await stream.CopyToAsync(ms);

    return ms.ToArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;importing&quot;&gt;Importing&lt;/h2&gt;
&lt;p&gt;This is how you should load the data onto the HoloLens.&lt;/p&gt;
&lt;p&gt;Save as &lt;code&gt;ExportAnchor.cs (Pseudocode)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Microsoft.MixedReality.OpenXR;
using System.IO;
using System.Threading.Tasks;

public async Task&amp;lt;XRAnchorTransferBatch&amp;gt; ImportAnchorsAsync(byte[] data)
{
    using var stream = new MemoryStream(data);

    XRAnchorTransferBatch batch =
        await XRAnchorTransferBatch.ImportAsync(stream);

    return batch;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;loading-anchors-into-the-scene&quot;&gt;Loading Anchors into the scene&lt;/h2&gt;
&lt;p&gt;After import, you explicitly request each anchor to be recreated in the Unity/XR session. You can also replace existing anchors.&lt;/p&gt;
&lt;p&gt;Save as &lt;code&gt;LoadAnchors.cs (Pseudocode)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public void LoadImportedAnchors(XRAnchorTransferBatch batch, string anchorName)
{
    batch.LoadAnchor(anchorName);
}

public void ReplaceAnchor(
    XRAnchorTransferBatch batch,
    string anchorName,
    TrackableId existingAnchorId)
{
    batch.LoadAndReplaceAnchor(anchorName, existingAnchorId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;completed-file&quot;&gt;Completed file&lt;/h2&gt;
&lt;p&gt;Github gist: &lt;a href=&quot;https://gist.github.com/Lansenou/caf479981f5757e36d86c684635e2303&quot;&gt;https://gist.github.com/Lansenou/caf479981f5757e36d86c684635e2303&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/downloads/AnchorImporterExporter.cs&quot;&gt;Download AnchorImporterExporter.cs&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;multiplayer&quot;&gt;Multiplayer&lt;/h2&gt;
&lt;p&gt;If you want players to see the information based around the Persistent Spatial Anchor you will need to make sure that the shared content in the scene is all placed relative to the anchor.
For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Anchor (Same world position, parent of all objects)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Object with a position relative to the anchor&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This will show all the objects at the same world position.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;To summarize the steps to correctly serialize a HoloLens anchor.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Collect known anchors (&lt;code&gt;AddAnchor&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Serialize them into an opaque stream (&lt;code&gt;ExportAsync&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can save these anchors for later usage or instantly transfer it to different HoloLens devices.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rebuild the anchor mapping on another device (&lt;code&gt;ImportAsync&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Let Unity resolve them back into live and local anchors (&lt;code&gt;LoadAnchor&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The anchor data itself is intentionally opaque by Microsoft. You’re not supposed to inspect or modify the batch, the code is there to send the anchor data between devices or sessions.&lt;/p&gt;
&lt;h2 id=&quot;projects-that-use-sharing-of-anchors&quot;&gt;Projects that use sharing of anchors&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/portfolio/artex/&quot;&gt;Artex&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/portfolio/ar-in-remote-airtraffic-tower-control/&quot;&gt;AR in ATC&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/portfolio/maintenance-training/&quot;&gt;Nuveon – Maintenance Training&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/portfolio/engines/&quot;&gt;Nuveon – Engines&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;Let me know if this information helped you! If you couldn’t make it work. Please &lt;a href=&quot;https://lansenou.com/contact-me/&quot;&gt;contact&lt;/a&gt; me!&lt;/p&gt;
&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;
&lt;p&gt;If you found this code useful, I’d appreciate a mention or a link back to this blog.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MIT License

Copyright (c) 2026 Lansenou.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the &amp;quot;Software&amp;quot;), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED &amp;quot;AS IS&amp;quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
&lt;/code&gt;&lt;/pre&gt;
</description></item><item><title>Split DNS on macOS while using WireGuard</title><link>https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/</link><guid isPermaLink="false">https://lansenou.com/?p=1025</guid><pubDate>Sat, 27 Jun 2026 19:30:33 GMT</pubDate><category>DNS</category><category>Dnsmasq</category><category>Experience</category><category>MacOS</category><category>Tutorial</category><category>Wireguard</category><description>&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#point-dnsmasq-to-original-dns-nameservers&quot;&gt;Point Dnsmasq to original DNS nameservers&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#restoring-dns-settings&quot;&gt;Restoring DNS settings&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#grabbing-original-dns-values-from-scutil&quot;&gt;Grabbing original DNS values from scutil&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#running-a-background-script&quot;&gt;Running a background script&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#load-the-dns-update-script&quot;&gt;Load the dns update script&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#setting-up-wireguard-to-point-to-the-internal-resolver&quot;&gt;Setting up wireguard to point to the internal resolver&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#finishing-it-all&quot;&gt;Finishing it all&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lansenou.com/blog/2026/06/27/split-dns-on-macos-while-using-wireguard/#troubleshooting&quot;&gt;Troubleshooting&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My goal was to have a Split-Tunnel WireGuard connection which grants access to some private services, including a DNS server, and to serve some DNS results over the WireGuard connection. I wanted the rest of the requests to be restored to the original DNS of the WIFI connection. Without this, internal hostnames either fail or leak to public DNS. To do this we need to setup a &lt;a href=&quot;https://gist.github.com/dferg/0472269333be4aca6aaa21cf3b165c02&quot;&gt;DNS resolver&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By creating a resolver that is only responsible for your internal domain, queries for names such as &lt;code&gt;host.internal&lt;/code&gt; are sent through the DNS server reachable over the WireGuard tunnel. All other DNS queries continue to use your normal network’s DNS servers, so public traffic behaves exactly as it would without the VPN.&lt;/p&gt;
&lt;p&gt;This approach is often referred to as split DNS. Instead of routing &lt;em&gt;all&lt;/em&gt; DNS traffic through the VPN, only the domains that actually exist on the remote network use the VPN’s DNS server. This avoids unnecessary latency for public lookups while still allowing private hostnames to resolve correctly.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/opt/homebrew&lt;/code&gt; path is the captured Apple Silicon setup. Check &lt;code&gt;brew --prefix&lt;/code&gt; on other Macs before choosing the configuration path.&lt;/p&gt;
&lt;h2 id=&quot;point-dnsmasq-to-original-dns-nameservers&quot;&gt;Point Dnsmasq to original DNS nameservers&lt;/h2&gt;
&lt;p&gt;I followed &lt;a href=&quot;https://gist.github.com/dferg/0472269333be4aca6aaa21cf3b165c02&quot;&gt;dferg’s&lt;/a&gt; guide on how to configure splitdns on macos. Instead of having &lt;code&gt;no-resolve&lt;/code&gt; the goal is to point towards a custom updated resolve file.&lt;/p&gt;
&lt;p&gt;First install Dnsmasq via homebrew (install homebrew via &lt;a href=&quot;https://brew.sh/&quot;&gt;https://brew.sh/&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install dnsmasq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save as &lt;code&gt;/opt/homebrew/etc/dnsmasq.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# https://lansenou.com/blog/2026/06/27/split-dns-on-macos/
# Edited from: https://gist.github.com/dferg/0472269333be4aca6aaa21cf3b165c02
# Ignore /etc/resolv.conf - Keep this disabled to make resolv-file work
# no-resolv
resolv-file=/opt/dnsmasq/upstream.conf

# For queries *.internal and *.box, forward to the specified DNS server
# Servers are queried in order (if the previous fails)
# -- Note: These are EXAMPLES. Replace with your desired config.
server=/internal/box/10.0.0.1
server=/internal/box/10.0.0.2

# Forward all other requests to Google&#39;s public DNS server - Keep this disabled since we use our own resolv-file
# server=8.8.8.8

# Only listen for DNS queries on localhost
listen-address=127.0.0.1

# Required due to macOS limitations
bind-interfaces
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;restoring-dns-settings&quot;&gt;Restoring DNS settings&lt;/h2&gt;
&lt;p&gt;Now I needed a way to put the original DNS settings that DHCP is handing out into the &lt;code&gt;upstream.conf&lt;/code&gt; file.&lt;/p&gt;
&lt;h3 id=&quot;grabbing-original-dns-values-from-scutil&quot;&gt;Grabbing original DNS values from scutil&lt;/h3&gt;
&lt;p&gt;This file grabs all upstream servers via the scutil command, and filters out loopback addresses (127.x), including the own resolver address 127.0.0.1. It outputs the result into the &lt;code&gt;upstream.conf&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mkdir -p /opt/dnsmasq
sudo chown -R $(whoami) /opt/dnsmasq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save as &lt;code&gt;/opt/dnsmasq/update-dns.sh&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# https://lansenou.com/blog/2026/06/27/split-dns-on-macos/

OUT=&amp;quot;/opt/dnsmasq/upstream.conf&amp;quot;

# Get macOS DNS entries
scutil --dns | awk &#39;/nameserver\[[0-9]+\]/{print $3}&#39; \
  | grep -E &#39;^([0-9]{1,3}\.){3}[0-9]{1,3}$&#39; \
  | grep -v &#39;^127\.&#39; \
  | sort -u \
  | sed &#39;s/^/nameserver /&#39; \
  &amp;gt; &amp;quot;$OUT&amp;quot;

# Fallback (never leave empty)
if [ ! -s &amp;quot;$OUT&amp;quot; ]; then
  echo &amp;quot;127.0.0.1&amp;quot; &amp;gt; &amp;quot;$OUT&amp;quot;
fi

# restart dnsmasq safely
killall -HUP dnsmasq 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo chmod +x /opt/dnsmasq/update-dns.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;running-a-background-script&quot;&gt;Running a background script&lt;/h3&gt;
&lt;p&gt;I’m using launchctl to make sure the &lt;code&gt;update-dns.sh&lt;/code&gt; is ran at load time and also when settings change.
The &lt;code&gt;/Library/Preferences/SystemConfiguration&lt;/code&gt; directory contains macOS’s network configuration state. Changes to the system will cause the script to update again:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;VPN connect/disconnect&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Joining a different Wi-Fi network&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ethernet connecting/disconnecting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DNS changes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save as &lt;code&gt;~/Library/LaunchAgents/local.dnsmasq.dnsupdate.plist&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;plist version=&amp;quot;1.0&amp;quot;&amp;gt;
&amp;lt;dict&amp;gt;
  &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
  &amp;lt;string&amp;gt;local.dnsmasq.dnsupdate&amp;lt;/string&amp;gt;

  &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
  &amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;/opt/dnsmasq/update-dns.sh&amp;lt;/string&amp;gt;
  &amp;lt;/array&amp;gt;

  &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;
  &amp;lt;true/&amp;gt;

  &amp;lt;key&amp;gt;WatchPaths&amp;lt;/key&amp;gt;
  &amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;/Library/Preferences/SystemConfiguration&amp;lt;/string&amp;gt;
  &amp;lt;/array&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;load-the-dns-update-script&quot;&gt;Load the dns update script&lt;/h3&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.dnsmasq.dnsupdate.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;setting-up-wireguard-to-point-to-the-internal-resolver&quot;&gt;Setting up wireguard to point to the internal resolver&lt;/h2&gt;
&lt;p&gt;To make sure your local dnsmasq resolver is used, point towards it in the wireguard configuration. In my wireguard setup I want to have access to the whole subnet.&lt;/p&gt;
&lt;p&gt;Save as &lt;code&gt;wireguard.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;[Interface]
DNS = 127.0.0.1
#... rest of the interface

[Peer]
AllowedIPs = 10.0.0.0/24 # Exposes the entire network range 10.0.0.0-10.0.0.255
#... rest of the peer setup

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you should activate the configuration.&lt;/p&gt;
&lt;h2 id=&quot;finishing-it-all&quot;&gt;Finishing it all&lt;/h2&gt;
&lt;p&gt;Restart the dnsmasq to use make use of the upstream config file.&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo brew services restart dnsmasq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The DNS resolver should now correctly be setup for wireguard and support network switching. After reconnecting the VPN, internal domains resolves through the VPN DNS while public domains continue using the system resolver.&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ nslookup google.com
Server:		127.0.0.1
Address:	127.0.0.1#53

Non-authoritative answer:
Name:	google.com
Address: 172.217.23.238
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ nslookup nas.internal
Server:		127.0.0.1
Address:	127.0.0.1#53

Non-authoritative answer:
Name:	nas.internal
Address: 10.0.0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks for reading! This my first blog post, please let me know if my experience helped you, or if I should have described some steps in more depth.&lt;/p&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;Check launched processes&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;launchctl list | grep dnsmasq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unloading the script&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/local.dnsmasq.dnsupdate.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Manually check the logs of dnsmasq&lt;/p&gt;
&lt;p&gt;In Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo brew services stop dnsmasq
sudo dnsmasq --no-daemon --log-queries --log-facility=-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don’t forget to start dnsmasq again after this.&lt;/p&gt;
</description></item></channel></rss>