<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
  <title>willnorris.com</title>
  <id>https://willnorris.com/atom.xml</id>
  <link href="https://willnorris.com/" rel="alternate" type="text/html"/>
  <link href="https://willnorris.com/atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://creativecommons.org/licenses/by/4.0/" rel="license" type="text/html"/>
  <link href="https://willnorris.superfeedr.com/" rel="hub"/>
  <rights>
    Copyright Will Norris.  Unless noted otherwise,
    text content in this feed is licensed under a Creative Commons
    Attribution 4.0 International License and code under an MIT License.
  </rights>
  <updated>2023-12-27T15:48:24-08:00</updated>

  <entry>
    <id>https://willnorris.com/2023/golinks-across-tailnets/</id>
    <title>Accessing go links across tailnets</title>
    <published>2023-11-02T22:31:11-07:00</published>
    <updated>2023-12-03T04:10:51-08:00</updated>
    <content type="html" xml:base="https://willnorris.com/2023/golinks-across-tailnets/"><![CDATA[
      
      <p>One of the more fun projects I&rsquo;ve worked on at Tailscale is <a href="https://tailscale.com/blog/golink/">golink</a>,
which provide simple, private shortcuts that you can share with others on your tailnet.
We have hundreds of go links at Tailscale that we use on a daily basis.</p>
<p>But I also run a personal golink server in my homelab with some links that don&rsquo;t really make sense to add to our corporate golink instance.
I&rsquo;d really like to be able to access my personal go links, even when I&rsquo;m logged in to my work Tailscale profile.
And it turns out, it&rsquo;s incredibly simple to do.</p>
<p>Tailscale allows you to <a href="https://tailscale.com/kb/1084/sharing/">share devices</a> to individuals on other tailnets.
You can control exactly what level of access those users have in your <a href="https://tailscale.com/kb/1018/acls/">ACLs</a> just like any other user.
For the share recipient, they see the device in their list of machines with a &ldquo;shared in&rdquo; label.
Because the device is in a different tailnet, they can&rsquo;t use the MagicDNS short name to access it,
but they can still use the fully qualified <code>host.tailnetXXXX.ts.net</code> address.</p>
<p>As I noted previously, I&rsquo;ve also <a href="/2023/tailscale-custom-domain/">setup DNS</a> so that I can access my devices on a custom domain.
So my personal golink server is at <code>go.willnorris.net</code>, but still only accessible on my tailnet.
So once I&rsquo;ve shared my golink server to my work account, I can access all of my personal go links using URLs like <code>go.willnorris.net/deploy</code>.
But that&rsquo;s still a lot of typing, and I&rsquo;d like to have something a little closer to the convenience of the short <code>go</code> hostname.</p>
<h2 id="chaining-go-links">Chaining go links</h2>
<p>What I ended up doing is creating a chain of go links on our corporate go link server
which allows any employee to access their personal go links.
All go links have a short name and a destination URL.
The destination URL can actually use <a href="https://pkg.go.dev/text/template">go templates</a> to do dynamic resolution.
One of the variables that the template has access to is <code>.User</code>,
which provides the username (typically an email address) of the user resolving the link.</p>
<p>So for example, we have a link named <code>go/me</code>, which resolves as:</p>
<pre tabindex="0"><code>go/me  =&gt;  http://who/{{TrimSuffix .User &#34;tailscale.com&#34;}}
</code></pre><p>This will take the username of the person visiting <code>go/me</code>,
trim off the &ldquo;tailscale.com&rdquo; from the end of their email address, and send them to our <code>who</code> service.
So when I visit <code>go/me</code>, it sends me to <code>http://who/will@</code>, which shows my personal profile in our company directory.
(This was one of the go links I brought over from my time at Twitter.)</p>
<p>So back to accessing my personal go link server.
We have a very similarly named go link, <code>go/my</code>, which resolves as:</p>
<pre tabindex="0"><code>go/my  =&gt;  /{{TrimSuffix .User &#34;@tailscale.com&#34;}}-go{{with .Path}}/{{.}}{{end}}
</code></pre><p>Let&rsquo;s break this down:</p>
<ul>
<li>
<p><code>{{TrimSuffix .User &quot;@tailscale.com&quot;}}</code> is almost identical to our <code>go/me</code> link but it strips off the <code>@</code> as well.
So when I visit this link, this portion will simply resolve to <code>will</code>.</p>
</li>
<li>
<p><code>-go</code> means that we just add the literal string <code>-go</code>, so now we have <code>will-go</code></p>
</li>
<li>
<p><code>{{with .Path}}/{{.}}{{end}}</code> means that if I added an additional path, we&rsquo;ll add a slash and then whatever path was specified.
So if I visited <code>go/my/deploy</code>, then the <code>deploy</code> would be the extra path that gets added to the end.</p>
</li>
</ul>
<p>There&rsquo;s one more thing to call out: this destination is a relative URL.
It doesn&rsquo;t have a scheme or a host, it just starts with a <code>/</code>.
That means that it gets resolved relative to the current host, which is <code>http://go/</code>.
This is how you chain multiple go links together, and it&rsquo;s actually important that you do it this way.
So if I visit <code>http://go/my</code>, using the expansion explained above,
I would be sent to <code>/will-go</code>, which then expands to the absolute URL <code>http://go/will-go</code>.</p>
<p>So where does <code>/will-go</code> resolve to? Well, to my personal go link server of course!
Any Tailscale employee can create a link named <code>{user}-go</code> with their username, and point that at their personal golink server.
So for example, I have:</p>
<pre tabindex="0"><code>go/will-go  =&gt;  http://go.willnorris.net/
</code></pre><p>I don&rsquo;t need to use any <code>.Path</code> template variables, since golink will append any extra path by default.
And if I hadn&rsquo;t setup a custom domain, this could just as easily be <code>http://go.tailXXXX.ts.net</code>.</p>
<p>So now this means when I visit <code>go/my/deploy</code>, it ends up resolving to <code>http://go.willnorris.net/deploy</code>
as you can see in this truncated curl output:</p>
<pre tabindex="0"><code>% curl -isL http://go/my/deploy

HTTP/1.1 302 Found
Location: /will-go/deploy

HTTP/1.1 302 Found
Location: http://go.willnorris.net/deploy

HTTP/1.1 302 Found
Location: https://github.com/willnorris/willnorris.com/actions/workflows/deploy.yml
</code></pre><h2 id="why-relative-links-matter">Why relative links matter</h2>
<p>This approach for accessing personal go links involved chaining multiple go links together to get to the final destination.
This is also commonly done to create alias go links.
For example, you might have <code>go/bugs</code> that links to your bug tracker.
But you may also want to have <code>go/b</code>, <code>go/bug</code>, and <code>go/issues</code> link there.
You could copy the same destination URL to all of the links, or you could just have the aliases link to the first.</p>
<pre tabindex="0"><code>go/bugs  =&gt;  http://bugtracker/

go/b  =&gt;  /bugs
go/bug  =&gt;  /bugs
go/issues  =&gt;  /bugs
</code></pre><p>Then, if you ever move your bug tracker, you only need to update the main <code>go/bugs</code> link.
This is also helpful to do for go links that have common misspellings.</p>
<p>So imagine I had created an alias on my personal golink server for <code>go/b</code>.
But instead of using a relative link <code>/bugs</code>, I used the absolute URL <code>http://go/bugs</code>.
Now what happens when I resolve that from my work account using <code>go/my/b</code>?</p>
<pre tabindex="0"><code>% curl -isL http://go/my/b

HTTP/1.1 302 Found
Location: /will-go/b

HTTP/1.1 302 Found
Location: http://go.willnorris.net/b

HTTP/1.1 302 Found
Location: http://go/bugs

HTTP/1.1 302 Found
Location: http://bugs.corp.example.com
</code></pre><p>When I resolved <code>http://go.willnorris.net/b</code>, it redirected to <code>http://go/bugs</code>.
But because I&rsquo;m logged into my company account, <code>http://go/</code> points to my company golink server,
which then redirects <code>http://go/bugs</code> to the company bug tracker, not my own.
Using relative links ensures that chained links are always resolved by the same server.
This is also helpful if you name your server something other than <code>go</code>, or you decide to rename it at some point.</p>
<p>Finally, because I&rsquo;ve gotten accustomed to using <code>go/my</code> links for my personal links,
I&rsquo;ve also setup a <code>go/my</code> link on my personal golink server.
Since those links should just resolve locally, the destination URL is literally just a slash:</p>
<pre tabindex="0"><code>go/my  =&gt;  /
</code></pre><p>So now, if I use <code>go/my/deploy</code> when I&rsquo;m on my personal Tailscale account,
even though I could have just used <code>go/deploy</code>, it still gets me there.</p>
<h2 id="nothing-extra-to-build">Nothing extra to build</h2>
<p>What&rsquo;s particularly neat about this approach is that it didn&rsquo;t require building anything extra.
Device sharing, MagicDNS, user identity, and access controls are all just core features of Tailscale.
They&rsquo;re just building blocks you can use to build and access all kinds of services.
And once I had those, it was just a matter of setting up a few go links.</p>

    ]]></content>
    <link href="https://willnorris.com/2023/golinks-across-tailnets/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2023/tailscale-custom-domain/</id>
    <title>Tailscale devices with a custom domain</title>
    <published>2023-11-01T00:10:53-07:00</published>
    <updated>2023-12-01T20:56:41-08:00</updated>
    <content type="html" xml:base="https://willnorris.com/2023/tailscale-custom-domain/"><![CDATA[
      
      <p>One of the things that <a href="/tweets/1532881581475368960">kinda blew my mind</a> my first week at Tailscale was <a href="https://tailscale.com/kb/1081/magicdns/">MagicDNS</a>.
I had been using Tailscale at home for a while, but just hadn&rsquo;t actually used MagicDNS at that point.
It runs a local DNS server in every Tailscale client that can answer queries about other devices in your network.
Every tailnet is given a name of the form <code>tailnetNNNN.ts.net</code>,
and so every device can be addressed as <code>&lt;device&gt;.tailnetNNNN.ts.net</code>.
If you want, you can instead choose a <a href="https://tailscale.com/kb/1217/tailnet-name/">fun tailnet name</a> which is randomly picked
from a list of things with <a href="https://github.com/tailscale/tailscale/blob/main/words/tails.txt">tails</a>, and a list of things with <a href="https://github.com/tailscale/tailscale/blob/main/words/scales.txt">scales</a>.
So you might end up with something like <code>&lt;device&gt;.orca-lizard.ts.net</code>.</p>
<p>While the fun tailnet names are cute and all, I really wanted to use my own domain.
For quite a while, I just manually maintained DNS records for the handful of hosts I cared about.
Tailscale IP addresses don&rsquo;t change, so this wasn&rsquo;t actually too much work.
But I recently got around to switching to a new tailnet using my own domain with <a href="https://tailscale.com/kb/1240/sso-custom-oidc/">custom OIDC</a>,
which meant I needed to reregister all of my devices.</p>
<p>I decided to take this opportunity to try and sort out my DNS properly.
What I found was <a href="https://github.com/damomurf/coredns-tailscale">coredns-tailscale</a>, a plugin for <a href="https://github.com/coredns/coredns">coredns</a>
that effectively maps Tailscale device names onto a custom domain.
The coredns-tailscale project has been around for about a year,
and I later discovered that it had been mentioned in the Tailscale newsletter from <a href="https://tailscale.com/blog/2022-10-newsletter/">October 2022</a>.
I guess I either missed seeing it or just wasn&rsquo;t looking for a tool like that at the time.</p>
<h2 id="delegating-dns">Delegating DNS</h2>
<p>When I started manually maintaining DNS records for my Tailscale devices,
I chose the zone <code>ipn.willnorris.net</code>.
(IPN was the abbreviation for a Tailscale network before it was called a &ldquo;tailnet&rdquo;,
and is <a href="https://pkg.go.dev/tailscale.com/ipn">still present</a> in parts of the code base.)
So I basically wanted to delegate the entire <code>ipn.willnorris.net</code> zone to my coredns server.
I use <a href="https://porkbun.com/">Porkbun</a> for domain registration and DNS hosting, so it was a simple matter of adding NS records.
I already knew I wanted to host coredns on <a href="https://fly.io/">Fly</a>, so I created the Fly app and got a public IP address.</p>
<p>I didn&rsquo;t have to, but I decided to go ahead and add names for my nameservers rather than bare IPs.
I cleverly chose <code>ns1.ipn.willnorris.net</code> and <code>ns2.ipn.willnorris.net</code>.
I added A records pointing each hostname to my Fly IP address, and
added NS records for <code>ipn.willnorris.net</code> pointing to those two hosts.</p>
<pre tabindex="0"><code>ns1.ipn.willnorris.net.	600	IN	A	37.16.12.98
ns2.ipn.willnorris.net.	600	IN	A	37.16.12.98

ipn.willnorris.net.	600	IN	NS	ns1.ipn.willnorris.net.
ipn.willnorris.net.	600	IN	NS	ns2.ipn.willnorris.net.
</code></pre><h2 id="tailscale-configuration">Tailscale configuration</h2>
<p>I needed the coredns server to join my Tailnet (explained below), so I created an <a href="https://tailscale.com/kb/1085/auth-keys/">auth key</a> for that purpose.
I made one that is reusable, ephemeral, pre-approved, and tagged with <code>tag:dns</code>.
I also added an ACL entry to my policy file to make sure that all of the devices on my network can do DNS queries.
This same entry also causes the DNS server to be aware of all of the other devices on the network,
which is needed to populate its internal mappings.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;acls&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;accept&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;src&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;*&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;dst&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;tag:dns:53&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;tagOwners&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;tag:dns&#34;</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="build-and-configure-coredns">Build and configure coredns</h2>
<p>The source for my personal coredns server can be found at <a href="https://github.com/willnorris/ipn-dns">https://github.com/willnorris/ipn-dns</a>.
There&rsquo;s really not a whole lot to it.
My <a href="https://github.com/willnorris/ipn-dns/blob/main/main.go">main.go</a> simply registers the tailscale plugin and starts coredns.
My <a href="https://github.com/willnorris/ipn-dns/blob/main/Dockerfile">Dockerfile</a> builds everything in a wolfi build image and copies the final binary and config to a static image.
(Don&rsquo;t miss calling <code>setcap cap_net_bind_service=+ep</code> so that you can listen on port 53).
My <a href="https://github.com/willnorris/ipn-dns/blob/main/fly.toml">fly config</a> is also pretty boring, adding a single volume mount for Tailscale state files and listening on port 53.
I also set my Tailscale auth key to the <code>TS_AUTHKEY</code> secrets variable using <code>fly secrets</code>.</p>
<p>The only interesting bit is the <a href="https://github.com/willnorris/ipn-dns/blob/main/Corefile">coredns config</a> itself:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="gh">ipn.willnorris.net</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">hosts</span> <span class="p">{</span><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">    # some resolvers will recheck the entries for DNS glue records at the delegate nameserver.
</span></span></span><span class="line"><span class="cl"><span class="c1">    # Manually specify these hosts, since they won&#39;t appear in the Tailscale node list.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">37.16.12.98</span> <span class="s">ns1.ipn.willnorris.net</span> <span class="s">ns2.ipn.willnorris.net</span>
</span></span><span class="line"><span class="cl">    <span class="k">fallthrough</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">tailscale</span> <span class="s">ipn.willnorris.net</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">authkey</span> <span class="se">{$TS_AUTHKEY}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">log</span>
</span></span><span class="line"><span class="cl">  <span class="k">errors</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I manually respecify records for my nameservers since some resolvers will check for that.
I then configure the coredns-tailscale plugin to use my <code>ipn.willnorris.net</code> zone,
and register itself with my Tailscale auth key.</p>
<p>Now this auth key is the one really non-standard bit, and relies on a <a href="https://github.com/damomurf/coredns-tailscale/pull/54">local change</a> I made to coredns-tailscale.
Normally, it requires that a Tailscale client be running on the host system (the docker image in my case).
I added support for having coredns join the tailnet directly using <a href="https://tailscale.com/blog/tsnet-virtual-private-services/">tsnet</a>,
so that everything can be self-contained in the single coredns binary, including the Tailscale client itself.
I also made <a href="https://github.com/damomurf/coredns-tailscale/pull/53">another change</a> to respond to tailnet changes more quickly.
If you want to try those changes out yourself, see the <code>replace</code> directive in my <a href="https://github.com/willnorris/ipn-dns/blob/main/go.mod">go.mod</a>.</p>
<p>Once deployed, you can see that DNS queries for my MagicDNS hostname and my custom hostname match.
Though in practice, I typically create a CNAME without the <code>ipn</code> component
and use that for actually accessing services when I need to:</p>
<pre tabindex="0"><code>% dig +short go.tail27e07.ts.net
100.69.62.103

% dig +short go.ipn.willnorris.net
100.69.62.103

% dig +short go.willnorris.net
go.ipn.willnorris.net.
100.69.62.103
</code></pre><h2 id="whats-missing-and-why-bother">What&rsquo;s missing and why bother?</h2>
<p>There are a few additional things that MagicDNS gets you that is missing here.
First, MagicDNS also automatically sets up a DNS search path so that you can typically just use bare hostnames.
This is what makes <a href="https://tailscale.com/blog/golink">go links</a> like <a href="http://go/meet">go/meet</a> work without needing the fully qualified domain name.
You can also have Tailscale automatically get certificates for your ts.net hostname,
even for private services that can&rsquo;t typically get Let&rsquo;s Encrypt certs using the HTTP challenge.
This is possible because Tailscale uses the DNS challenge on the ts.net domain.
And Tailscale <a href="https://tailscale.com/blog/reintroducing-serve-funnel/">serve and funnel</a> build on top of this HTTPS support
to make services available to your tailnet or even publicly on the internet.
None of these things work with the custom DNS approach I&rsquo;ve described here.</p>
<p>However, there are still reasons why you might want custom names as a supplement to your ts.net hostnames.
I often share some devices between my personal and work tailnet.
While bare hostnames work for devices in your own tailnet, they don&rsquo;t work for shared devices.
For that, you have to use the fully qualified hostname,
and I can never remember (or want to type) my full ts.net name.
If I want to access a personal go link while logged into my work tailnet,
it&rsquo;s much simpler to remember <em>go.willnorris.net</em>.
(Actually, I have an even simpler method with go links <a href="/2023/golinks-across-tailnets/">I&rsquo;ll talk about later</a>.)</p>
<p>Or you may have existing hostnames that you&rsquo;ve been using for a while and want to migrate them to a private Tailscale network.
Or you&rsquo;re possibly migrating from a different VPN product that was using a custom domain.
Setting up a DNS server like this could help keep those old hostnames active with their new Tailscale IP addresses.</p>
<p>It&rsquo;s also worth noting that I&rsquo;m serving my custom DNS server publicly.
That means anyone can poke around to discover my Tailscale device names as well as their Tailscale IPs.
But those hostnames already end up getting written to public transparency logs whenever HTTPS certs are issued,
so I&rsquo;m not too worried about that.
And Tailscale IP addresses themselves are generally pretty useless,
though they do theoretically make certain types of attacks a little easier.
So depending on the network setup and what you&rsquo;re trying to do,
you could just host this DNS server privately instead.</p>

    ]]></content>
    <link href="https://willnorris.com/2023/tailscale-custom-domain/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2023/caddy-snippets/</id>
    <title>Caddy snippets for static sites</title>
    <published>2023-10-27T21:20:28-07:00</published>
    <updated>2023-10-27T22:31:25-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2023/caddy-snippets/"><![CDATA[
      
      <p>I moved my website from WordPress <a href="/2014/one-step-forward-two-steps-back/">to a static site generator</a> in 2014,
and over the next few months, I wrote several posts about how I achieved
certain dynamic behavior using custom nginx configurations.
However, I <a href="https://github.com/willnorris/willnorris.com/commit/6f2f7445c1242a531d7d9efe60f41e8b0f33a92a">switched over to Caddy</a> as my web server in 2017,
but I never updated how I adapted my server configuration.
In basically all cases, I find the Caddy config much simpler and easier to read,
though that may be because it&rsquo;s all I ever use anymore.
So here is my long overdue updates to a few old blog posts about adding
some custom web server behavior for static sites.</p>
<h2 id="supporting-webfinger">Supporting WebFinger</h2>
<p>I July 2014, I wrote <a href="/2014/webfinger-with-static-files-nginx/">Supporting WebFinger with Static Files and Nginx</a>.
I still use Webfinger, now primarily for my custom Mastodon server and most recently with <a href="https://tailscale.com/kb/1240/sso-custom-oidc/">OpenID Connect for Tailscale</a>.
My old nginx config required lua support to be compiled in, which wasn&rsquo;t awful, but kind of annoying.
My Caddy configuration is mostly equivalent, though I didn&rsquo;t bother to return
the proper <code>400</code> and <code>405</code> status codes on an incorrect resource parameter or HTTP method.
Instead, they just return a <code>404</code> which suits me just fine.</p>
<p>I define a <a href="https://caddyserver.com/docs/caddyfile/matchers#named-matchers">named matcher</a> that matches on the webfinger well-known URL,
the HTTP methods I want to support, and one of several valid resource values.
Then I rewrite the request to a static file like before and set some response headers.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddy" data-lang="caddy"><span class="line"><span class="cl"><span class="gh">@webfinger</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">path</span> <span class="nd">/.well-known/webfinger</span>
</span></span><span class="line"><span class="cl">  <span class="k">method</span> <span class="s">GET</span> <span class="s">HEAD</span>
</span></span><span class="line"><span class="cl">  <span class="k">query</span> <span class="s">resource=acct:will@willnorris.com</span>
</span></span><span class="line"><span class="cl">  <span class="k">query</span> <span class="s">resource=mailto:will@willnorris.com</span>
</span></span><span class="line"><span class="cl">  <span class="k">query</span> <span class="s">resource=https://willnorris.com</span>
</span></span><span class="line"><span class="cl">  <span class="k">query</span> <span class="s">resource=https://willnorris.com/</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="gh">rewrite</span> <span class="gh">@webfinger</span> <span class="gh">/webfinger.json</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="k">header</span> <span class="nd">@webfinger</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">Content-Type</span> <span class="s">&#34;application/jrd+json&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">Access-Control-Allow-Origin</span> <span class="s">&#34;*&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">X-Robots-Tag</span> <span class="s">&#34;noindex&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="proxying-webmentions">Proxying webmentions</h2>
<p>In August 2014, I wrote <a href="/2014/proxying-webmentions-with-nginx/">Proxying webmentions with nginx</a>.
I still proxy my webmentions to an external service, though I now use webmention.io.
The config requires a tiny bit more work because my URL path didn&rsquo;t match where I needed to send it,
but it is still pretty straightforward.</p>
<p>Like before, I use a named matcher to match the relevant requests,
then use Caddy&rsquo;s reverse_proxy directive to send them to webmention.io.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddy" data-lang="caddy"><span class="line"><span class="cl"><span class="gh">@webmention</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">method</span> <span class="s">POST</span>
</span></span><span class="line"><span class="cl">  <span class="k">path</span> <span class="nd">/api/webmention/</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="gh">handle</span> <span class="gh">@webmention</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">uri</span> <span class="no">replace</span> <span class="nd">/api/webmention/</span> <span class="s">/willnorris.com/webmention</span>
</span></span><span class="line"><span class="cl">  <span class="k">reverse_proxy</span> <span class="s">https://webmention.io</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">header_up</span> <span class="s">Host</span> <span class="se">{upstream_hostport}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="fetching-go-packages">Fetching go packages</h2>
<p>In February 2015, I wrote <a href="/2015/go-get-subpackages-nginx/">Fetching Go Sub-Packages on Static Sites</a>.
Unsurprisingly, I still use my own domain in the import path of all of my go packages.
I currently use Hugo to generate my site, so I have a <a href="https://github.com/willnorris/willnorris.com/blob/main/layouts/go/single.html">custom layout</a> for my go package files
which reads relevant metadata from the page front matter and populates the necessary meta tags.</p>
<p>To serve the right page on <code>go get</code> requests for sub-packages, the Caddy config is quite minimal.
A named matcher is used to match requests for go sub-packages that include the <code>go-get</code> parameter,
and then serve the contents of the top-level go package file without the sub-package.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddy" data-lang="caddy"><span class="line"><span class="cl"><span class="gh">@gopkg</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">path_regexp</span> <span class="s">gopkg</span> <span class="s">(/go/\w+/).+</span>
</span></span><span class="line"><span class="cl">  <span class="k">query</span> <span class="s">go-get=*</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="gh">rewrite</span> <span class="gh">@gopkg</span> <span class="se">{re.gopkg.1}</span><span class="p">
</span></span></span></code></pre></div><h2 id="do-more-with-custom-caddy-modules">Do more with custom Caddy modules</h2>
<p>I&rsquo;ve also done a lot more interesting things with custom Caddy modules
like embedding <a href="/2014/a-self-hosted-alternative-to-jetpacks-photon-service/">my imageproxy service</a> as well as a Tailscale node directly into the Caddy binary.
But that will be a topic for another day.</p>

    ]]></content>
    <link href="https://willnorris.com/2023/caddy-snippets/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2023/logo/</id>
    <title>A fun little personal logo</title>
    <published>2023-10-27T00:33:18-07:00</published>
    <updated>2023-12-01T23:06:26-08:00</updated>
    <content type="html" xml:base="https://willnorris.com/2023/logo/"><![CDATA[
      
      <p>Like many kids, I remember drawing out the letters of my name
in interesting ways in the margin of my notebook or on random scrap paper.
I always liked the symmetry of the W and M in &ldquo;William&rdquo;,
and how the two letters could continuously flow into one another.
The N in &ldquo;Norris&rdquo; had a similar shape, and so I would doodle my initials
in different geometric designs, sometimes 3-dimensional or with celtic knots.</p>
<figure class="alignleft">
<svg width="80" height="80" viewBox="0 0 300 300"><path fill="#132448" stroke="#132448" d="M31.554 86.643c3.799.142 23.85-21.491 26.791-23.703 21.438 29.347 22.032 39.505 71.449 84.088-2.975-20.317-38.262-80.601-56.562-99.327 8.932-9.029 27.56-27.152 32.744-33.296 5.491 15.799 25.006 77.316 45.252 114.563 15.479-29.347 42.52-103.844 42.271-115.128 8.849 8.192 22.393 25.94 32.153 34.427-19.056 14.673-51.801 81.83-63.711 113.435-3.451 9.167-.593 14.673-.593 14.673 10.674 8.192 46.44 41.762 52.991 47.97-13.096-84.652 14.291-141.088 33.343-161.405 3.053 3.219 12.271 10.933 22.03 20.881-10.12 13.544-30.136 48.92-30.96 85.782-1.192 53.049 7.738 76.188 22.027 97.069-6.391 7.835-11.179 7.321-20.241 18.06-14.338-13.164-68.987-62.478-78.596-71.109l1.79 40.069c.456 2.339.969 23.352 10.12 33.299-20.102.026-23.964.028-44.059.563 6.71-9.071 8.778-28.845 8.932-34.989l-.598-63.773c-7.932-7.607-47.121-43.191-50.606-46.842 4.165 21.446 4.764 88.04-25.004 138.27-2.443-3.514-27.299-33.074-26.797-33.863 9.15-14.338 25.295-46.615 25.601-76.752.559-55.033-20.922-69.503-29.767-82.962"/></svg>
<figcaption>New York Yankees logo</figcaption>
</figure>
<p>I was really into baseball as a kid, and so I really enjoyed the way
some teams would overlay their city initials as their team logo.
The most famous, of course, being the stacked &ldquo;NY&rdquo; for the New York Yankees
(which apparently came from a <a href="https://www.mlb.com/news/yankees-new-york-logo-origin">Tiffany-designed NYPD medal</a>),
but also &ldquo;SD&rdquo; for the San Diego Padres, and &ldquo;LA&rdquo; for the Los Angeles Dodgers.</p>
<figure class="alignright">
  <svg width="60" viewBox="0 0 516 560"><path fill-rule="evenodd" d="M515.581 157.65h-3.243c-52.639 0-105.277-.005-157.916.01-4.254.001-3.134-.577-5.413 3.047-29.802 47.386-59.584 94.783-89.373 142.177-.463.736-.949 1.458-1.632 2.504-.635-.929-1.17-1.659-1.65-2.424-19.447-30.941-38.889-61.887-58.333-92.83-10.509-16.725-21.049-33.43-31.494-50.194-1.061-1.704-2.184-2.336-4.192-2.334-52.958.055-105.917.044-158.876.044H.467C-.093 155.599-.169 2.473.347 0h515.127c.461 1.749.598 154.398.107 157.65zm-164.71 357.815h-185.66V323.999c31.098 46.323 61.768 92.876 92.434 139.324.233 0 .309.013.378-.003a.451.451 0 0 0 .216-.094c.166-.169.342-.335.472-.531 30.49-46.175 60.978-92.353 91.468-138.528.041-.063.122-.103.184-.152.077.014.161.013.229.045.061.029.145.093.148.146.051.714.126 1.428.126 2.143.006 62.938.005 125.879.005 189.116z" clip-rule="evenodd" fill="var(--color-text)" /></svg>
  <figcaption>Personal logo of <a href="http://terrymun.com/">Terry Mun</a>.</figcaption>
</figure>
<p>In more recent years, I&rsquo;ve seen a few of these types of logos that really stood out.
Probably the most memorable for me is <a href="http://terrymun.com/">Terry Mun</a>&rsquo;s &ldquo;TM&rdquo; logo using the negative space for the M.
Terry also does some really tasteful animations with his logo and the rest of his site,
but even the static logo is quite something.</p>
<figure class="alignleft">
  <svg viewBox="0 0 40 35" width="60" fill="#666666"><path d="M20 .528L0 34.973h12.392L20 21.87l7.608 13.103H40L20 .528"></path></svg>
  <figcaption>Personal logo of <a href="https://andy-bell.co.uk/">Andy Bell</a>.</figcaption>
</figure>
<p>I had also recently rediscovered some of <a href="https://andy-bell.co.uk/">Andy Bell</a>&rsquo;s CSS work
(notably his <a href="https://andy-bell.co.uk/my-favourite-3-lines-of-css/">method for managing flow</a>),
and was struck by the simplicity of his triangular &ldquo;A&rdquo; logo on his website.
It certainly fits with the minimalist aesthetic of the rest of his site,
and it inspired me to start doodling again.</p>
<p>I started with the same equilateral triangle, notched on one side using a second triangle one-third the base size.
I added the notch on the top to form a &ldquo;V&rdquo;, then created a second one and combined them to form a &ldquo;W&rdquo;.
Finally, I separated the left arm of the &ldquo;W&rdquo; to allow it to also be read as a slanted &ldquo;N&rdquo;.</p>
<style>
  .logo-progression svg {
    margin-inline: 1em;
  }
  .logo-progression svg path {
    fill: var(--color-text);
    &.red {
      fill: var(--color-red);
      opacity: 70%;
    }
    &.blue {
      fill: var(--color-blue);
      opacity: 80%;
    }
  }
</style>
<figure class="logo-progression aligncenter">
  <svg width="60" height="51.96">
    <path d="M0 0 H60 L30 51.96 Z" />
  </svg>
  <svg width="60" height="51.96">
    <path d="M0 0 H60 L30 51.96 Z" class="red" />
    <path d="M20 0 H40 L30 17.32 Z" class="blue" />
  </svg>
  <svg width="60" height="51.96">
    <path d="M0 0 h20 l10 17.32 l10 -17.32 h20 l-30 51.96 Z" />
  </svg>
  <svg width="120" height="51.96">
    <path d="M0 0 h20 l10 17.32 l10 -17.32 h20 l-30 51.96 Z" class="red" />
    <path d="M60 0 h20 l10 17.32 l10 -17.32 h20 l-30 51.96 Z" class="blue" />
  </svg>
  <svg width="80" height="51.96">
    <path d="M20 0 h20 l10 17.32 l10 -17.32 h20 l-30 51.96 Z" class="blue" />
    <path d="M0 0 h20 l10 17.32 l10 -17.32 h20 l-30 51.96 Z" class="red" />
  </svg>
  <svg width="80" height="51.96">
    <path d="M0 0 H20 L30 17.32 L40 0 L50 17.32 L60 0 H80 L50 51.96 L40 34.64 L30 51.96 Z" />
  </svg>
  <svg width="80" height="51.96">
    <path d="M0 0 H20 L27 12.12 L17 29.44 Z M40 0 L50 17.32 L60 0 H80 L50 51.96 L40 34.64 L30 51.96 L20 34.64 Z" />
  </svg>
  <figcaption>Progression from simple equilateral triangle to final WN logo</figcaption>
</figure>
<figure class="alignright">
<svg width="150" height="69.796"><path d="M975.734 603.361H692.611a46.706 46.706 0 0 0-1.634-.023c-8.736 0-53.757 2.692-81.987 55.733l-34.739 63.247-60.891-110.92-13.364-24.342-13.374 24.341-60.899 110.921-34.633-63.07c-28.338-53.218-73.36-55.91-82.104-55.91-.702 0-1.252.01-1.619.023H0l10.517 21.858c1.375 2.86 13.836 28.344 27.352 42.466 9.57 10.007 19.448 14.44 26.467 16.415l4.04 8.391c1.376 2.86 13.848 28.34 27.353 42.47 11.614 12.147 23.684 16.092 30.518 17.394l3.145 6.523c1.38 2.847 13.847 28.331 27.357 42.453 16.312 17.061 33.575 17.996 35.891 18.04h85.934c.815.023 19.997.797 31.014 21.786l69.823 127.57 32.435 59.227 13.358 24.39 13.373-24.377 32.48-59.19 28.925-52.691 28.938 52.69 32.48 59.192 13.374 24.394 13.36-24.408 32.422-59.226 69.956-127.805c10.893-20.755 30.076-21.53 30.692-21.552h86.53c1.922-.044 19.183-.979 35.5-18.04 13.507-14.122 25.972-39.606 27.354-42.453l3.126-6.5c6.66-1.267 18.828-5.173 30.54-17.417 13.498-14.13 25.963-39.61 27.336-42.47l4.03-8.36c6.947-1.966 16.87-6.39 26.488-16.446 13.506-14.122 25.972-39.606 27.353-42.466L1000 603.36h-24.266" style="display:inline;fill:#000;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.15 0 0 .15 0 -88.058)"/><path d="M305.785 737.742h-174.16s-12.444-.31-24.879-13.32c-12.222-12.788-24.638-38.542-24.638-38.542H285.45s25.108.615 36.227 22.845c11.128 22.233 21.928 41.372 21.928 41.372s-16.517-12.355-37.82-12.355M694.188 737.742h174.168s12.434-.31 24.868-13.32c12.23-12.788 24.643-38.542 24.643-38.542h-203.34s-25.108.615-36.232 22.845c-11.124 22.233-21.928 41.372-21.928 41.372s16.526-12.355 37.82-12.355" style="display:inline;fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.15 0 0 .15 0 -88.058)"/><path d="M692.186 618.603s-42.909-2.776-69.845 47.81l-48.103 87.567-74.247-135.257-74.257 135.257-48.1-87.566c-26.938-50.587-69.848-47.811-69.848-47.811H24.25s12.413 25.759 24.642 38.547c12.435 13.019 24.872 13.324 24.872 13.324h213.28s28.613 0 44.19 29.709l62.042 112.926 32.48 59.125 32.459-59.138 41.775-76.089 41.767 76.09 32.462 59.137 32.48-59.125 62.03-112.926c15.587-29.709 44.198-29.709 44.198-29.709h213.286s12.43-.305 24.873-13.324c12.226-12.788 24.634-38.547 24.634-38.547H692.186" style="display:inline;fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.15 0 0 .15 0 -88.058)"/><path d="M700.467 752.262s-42.914-2.78-69.85 47.802l-55.919 102.153-74.707-136.09-74.717 136.09-55.918-102.153c-26.93-50.583-69.845-47.802-69.845-47.802H143.12s12.418 25.75 24.64 38.538c12.442 13.01 24.879 13.33 24.879 13.33h86.135s28.604 0 44.184 29.703l69.833 127.57 32.423 59.227 32.487-59.191 42.29-77.032 42.279 77.032 32.49 59.191 32.423-59.227 69.831-127.57c15.583-29.704 44.189-29.704 44.189-29.704h86.136s12.426-.319 24.873-13.329c12.226-12.788 24.638-38.538 24.638-38.538H700.467" style="display:inline;fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(.15 0 0 .15 0 -88.058)"/></svg>
  <figcaption>Wonder Woman logo</figcaption>
</figure>
<p>I ended up with something that is most certainly inspired by Andy&rsquo;s logo,
but with some additional character I really like for combining the W and N.
The final result also reminds me a bit of the Wonder Woman logo,
which was not intentional but I&rsquo;m kind of okay with.
I certainly don&rsquo;t need a personal logo, and it&rsquo;s somewhat of a vanity project,
but it was certainly fun to design and build.</p>

    ]]></content>
    <link href="https://willnorris.com/2023/logo/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2023/tailscale-pinewood-derby/</id>
    <title>Tailscale at the Pinewood Derby</title>
    <published>2023-05-05T23:03:20-07:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2023/tailscale-pinewood-derby/"><![CDATA[
      <img src="derby-timer.jpg" alt="" />
      <p>Earlier this year, I organized and ran the Pinewood Derby for my son&rsquo;s <a href="https://www.pack263.org/">Cub Scout Pack</a>.
I had always participated in the Pinewood Derby when I was a Scout, and attended a few as an adult,
but I&rsquo;d never actually organized or run one myself.
This is an overview of the software I used, how I set it up, and how <a href="https://tailscale.com/">Tailscale</a> brought it all together.
(Disclaimer up front: I also work at Tailscale.)</p>
<figure class="alignright" style="max-width: 300px">
  <a href="optimus-prime.jpg">

<img src="/api/imageproxy/600x0/2023/tailscale-pinewood-derby/optimus-prime.jpg" alt="A pinewood derby car shaped and painted like the character Optimus Prime from Transformers, a semi-truck with a red cab and gray trailer." width="300">
</a>
  <figcaption>My five year old son's Optimus Prime car from this year. (Dad helped.)</figcaption>
</figure>
<p>The <a href="https://en.wikipedia.org/wiki/Pinewood_derby">Pinewood Derby</a> has been a favorite scouting event for over 70 years,
with scouts designing, building, and racing a model car made from a block of pine wood.
Awards are given for the fastest cars, but typically also for most creative designs.
It&rsquo;s no exaggeration to say that some kids stay in scouts just for the pinewood derby.</p>
<p>The complexity of a scout pack&rsquo;s pinewood derby setup can vary pretty wildly.
Our pack races on a 6 lane, 40 foot aluminum track from <a href="https://www.besttrack.com/">BestTrack</a> with a <a href="https://www.besttrack.com/champ_timer.htm">Champ Timer</a>.
The timer interfaces with race management software specifically designed for these types of races
that manages the racing brackets, records the times for each heat, and calculates the rankings.</p>
<h2 id="derbynet">DerbyNet</h2>
<p>In the past, our pack has used <a href="http://www.grandprix-software-central.com/gprm/">GrandPrix Race Manager</a> which,
as best as I can tell, is one of the more popular software options.
However, this year I chose to instead use <a href="https://derbynet.org/">DerbyNet</a>, which is an open source alternative.
Besides being open source (which was great because I did make some small customizations),
I also really liked how DerbyNet is architected.
The application itself is a simple web application written in PHP with a SQLite database.
System requirements are minimal and everything is managed through the browser,
even interfacing with the race timer using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API">Web Serial API</a>.</p>
<p>You do need somewhere to actually run the application,
which can be on a local laptop, a raspberry pi, or on a remote cloud server.
And you do need at least one client that can access that server to serve as the primary coordinator for the race.
Then, any number of additional devices can be used in different roles,
such as a checking in scouts, kiosks to display race results, or set up with a camera to provide instant replays.</p>
<h2 id="our-setup">Our Setup</h2>
<p>We held our pinewood derby at the <a href="https://www.coastsidefire.org/fire-stations">local fire station</a>,
which is a lot of fun because the kids get to hang out in the apparatus bay and look at the trucks.
The battalion chief was very gracious and accommodating,
but we weren&rsquo;t completely sure whether we&rsquo;d be able to use the station&rsquo;s wireless network.
I opted to run DerbyNet directly on my laptop so that in a worst case scenario,
I could do everything locally from a single machine without any network connection.
That ended up just being a local Caddy web server (after I gave up trying to run it in Docker),
PHP to run DerbyNet, and connecting to our race timer over a USB-to-serial adapter.</p>
<p>The Caddy config was very simple:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="gh">:8080</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">root</span> <span class="s">website</span>
</span></span><span class="line"><span class="cl">  <span class="k">file_server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">index</span> <span class="s">index.html</span> <span class="s">index.php</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">php_fastcgi</span> <span class="s">unix//opt/homebrew/var/run/php-fpm.sock</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">env</span> <span class="s">DERBYNET_CONFIG_DIR</span> <span class="s">/var/lib/derbynet</span>
</span></span><span class="line"><span class="cl">    <span class="k">env</span> <span class="s">DERBYNET_DATA_DIR</span> <span class="s">/var/lib/derbynet</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><figure class="alignright" style="max-width: 300px">
  <a href="endeavour.jpg">

<img src="/api/imageproxy/600x0/2023/tailscale-pinewood-derby/endeavour.jpg" alt="A pinewood derby car shaped and painted like the space shuttle Endeavour, white with black edging and an American flag an NASA logo." width="300">
</a>
  <figcaption>My eight year old son's Space Shuttle Endeavour car from this year.</figcaption>
</figure>
<p>Once we had that working, we added:</p>
<ul>
<li>several volunteers&rsquo; phones used to check-in Scouts as they arrived</li>
<li>an iPad used by the track manager to know which cars to place on which lanes</li>
<li>a Pixelbook set up on a table showing times and standings at the end of each run
(ideally this would have been connected to a large monitor)</li>
</ul>
<p>DerbyNet also allows parents to vote on the design awards,
and we knew that was something we wanted to support if possible.
If the network wasn&rsquo;t cooperative, the leaders could always just select winners.</p>
<p>Unfortunately, there were indeed issues with fire station&rsquo;s wifi,
so we ended up having to tether all of the devices off of cell phone hotspots.
My Macbook and Pixelbook were connected to my phone, the track manager had his iPad connected to his phone,
and the volunteers and parents were on their individual phones.
But we needed all of them to be able to reach the DerbyNet server running on my laptop; tethered to a phone.</p>
<p>Our device setup looked a little something like this:</p>
<figure>
  <a href="devices.png"><img src="devices.png" width="600" alt="Network diagram showing two laptops
    tethered to a phone, an iPad tethered to a separate phone, and a collection of other phones."></a>
</figure>
<h2 id="tailscale-makes-the-connection">Tailscale makes the connection</h2>
<figure class="alignright" style="max-width: 300px">
  <a href="will-gabriel.jpg">

<img src="/api/imageproxy/600x0/2023/tailscale-pinewood-derby/will-gabriel.jpg" alt="Me and my seven year old son Gabriel in our scout uniforms." width="300">
</a>
  <figcaption>My son Gabriel and me at last year's pinewood derby. I forgot to take a picture together this year.</figcaption>
</figure>
<p>So we have a dozen or so different devices on disparate networks that we need to all connect to each other.
Fortunately, this is exactly what <a href="https://tailscale.com/">Tailscale</a> is designed for:
providing secure access between remote devices and resources.
Of course, if I had all of these devices on the same tailnet, there wouldn&rsquo;t really be much more to do.
Every device would enable Tailscale and go to the <a href="https://tailscale.com/kb/1081/magicdns/">MagicDNS</a> hostname for the server.
That&rsquo;s actually what I did for the two devices of my own (the MacBook and Pixelbook),
both of which had Tailscale installed and set up ahead of time.
Because they were tethered on the same phone, Tailscale connected them directly over local IP addresses.</p>
<p>To provide access for the track manager, I used Tailscale <a href="https://tailscale.com/kb/1223/tailscale-funnel/">Funnel</a> to expose the DerbyNet server to the public internet.
On my laptop, that was as simple as running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ tailscale serve https / http://127.0.0.1:8080
</span></span><span class="line"><span class="cl">$ tailscale funnel <span class="m">443</span> on
</span></span></code></pre></div><p>The track manager (who was tethered on a separate phone) was then able to navigate to my same MagicDNS hostname
(something like <em>https://derby.tailnet.ts.net</em>) which routed through Tailscale&rsquo;s public funnel servers and down to my laptop.
It worked amazingly well, especially considering that Funnel was a <strong>very</strong> new feature at the time.</p>
<p>We ran the whole pinewood derby like this without even the slightest hiccup.
For the parents, I could have simply had them go to the same MagicDNS hostname,
but I wanted try something a little different and easier to remember.
I set up a reverse proxy on <a href="https://derby.pack263.org/">derby.pack263.org</a> to direct traffic to Tailscale Funnel, which in turn routed it to my laptop.
It wasn&rsquo;t really necessary, but the reverse proxy was so simple to do in Caddy
(with automatic SSL cert provisioning and all):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="gh">derby.pack263.org</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">reverse_proxy</span> <span class="s">https://derby.tailnet.ts.net</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">header_up</span> <span class="s">Host</span> <span class="se">{upstream_hostport}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>So the parents were able to access the DerbyNet server running on my laptop from their phones to vote on cars.
I found out later that we even had one scout that was sick at home
and was refreshing the DerbyNet site to see the results as the races were happening.</p>
<p>The final traffic flow was something like this:</p>
<figure>
  <a href="network.png"><img src="network.png" width="600" alt="An update of the previous network diagram.
  The two laptops tethered to the same phone are connected directly. The iPad tethered to the other phone
  is routing through Tailscale Funnel to reach the DerbyNet Server. Phones of parents and volunteers are
  routed through a reverse proxy, then to Tailscale Funnel, and to the DerbyNet Server."></a>
</figure>
<p>I technically did run the reverse proxy on a cloud VM that I had available,
but otherwise everything was just vanilla Tailscale with nothing too exotic.
And even the reverse proxy was just a nice to have, I could have just as easily set up a simple redirect.</p>
<p>To be perfectly honest, it did feel a little risky to be trying something with
so many moving parts for my very first pinewood derby.
But I really couldn&rsquo;t have been happier with how stable it was and how it turned out,
and now I can&rsquo;t imagine doing an event like this any other way.</p>

    ]]></content>
    <link href="https://willnorris.com/2023/tailscale-pinewood-derby/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2022/rain-machine-password/</id>
    <title>RainMachine Default Password</title>
    <published>2022-03-07T21:45:03-08:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2022/rain-machine-password/"><![CDATA[
      
      <p><strong>The default password on a <a href="https://www.rainmachine.com/products/rainmachine-pro.html">RainMachine Pro-16</a> (and presumably all device versions?) is an empty string.</strong>
Not <code>password</code>, not <code>admin</code>, and not <code>hunter2</code>. Just an empty string.
When you get a screen prompting you for a password, don&rsquo;t enter anything, just click &lsquo;OK&rsquo;.
Hopefully that will save future me (and maybe current you) an hour of headache.</p>
<p>(<em>Update:</em> Or at least this was my experience after resetting my RainMachine.
<a href="https://support.rainmachine.com/hc/en-us/articles/360007505074-RainMachine-Pro-Quick-Setup">This article</a> seems to suggest that the default password <strong>is</strong> in
fact <code>admin</code>? So try both.)</p>
<p>This evening, I accidentally reset the password on my RainMachine Pro 16.
I was trying to reboot it through the on-device screen, but the touch sensor misinterpreted my selection.
And then I didn&rsquo;t read the confirmation screen closely, and ended up resetting the password.</p>
<p>The next hour or so was spent trying to reset the password following the <a href="https://www.rainmachine.com/reset/how-to-reset-the-password-for-the-RainMachine-Pro.html">instructions provided by RainMachine</a>.
My RainMachine sits about 5 feet away from my network switch, so it is wired via ethernet.
But the instructions seem to assume wifi, and certainly imply that this is the only way to reset the password.
This is a lie. A bald-faced lie.</p>
<p>I don&rsquo;t know whether to blame Android, iOS, or RainMachine
for the abysmal experience that resulted in trying to re-configure the device over wifi.
Android simply refused to configure the device and iOS insisted on trying to set up HomeKit,
and then still refused to set things up properly.
But it really doesn&rsquo;t matter, because in the end it wasn&rsquo;t necessary.
Once the device is reset, you can simply login with an empty string, whether that&rsquo;s over ethernet or wifi.</p>
<p>It turns out that this is mentioned on the <a href="https://www.rainmachine.com/reset/how-to-reset-the-password-for-the-RainMachine-Mini-8.html">documentation for the RainMachine Mini-8</a>:</p>
<blockquote>
<p>Leave the password field empty since the password for the RainMachine device has been erased.</p>
</blockquote>
<p>But that&rsquo;s not the version I have, and so I didn&rsquo;t initially read that.
Why they didn&rsquo;t include this on the <a href="https://www.rainmachine.com/reset/how-to-reset-the-password-for-the-RainMachine-Pro.html">documentation for the Pro-16</a> is beyond me.
Maybe they&rsquo;ll update that. But in the meantime, hopefully this page will guide folks in the right direction.</p>

    ]]></content>
    <link href="https://willnorris.com/2022/rain-machine-password/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2021/intentionally-positive/</id>
    <title>Intentionally Positive</title>
    <published>2021-07-07T16:25:59-07:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2021/intentionally-positive/"><![CDATA[
      <img src="hand-heart.jpg" alt="Woman holding her hands in the shape of a heart, framing a sunset." />
      <p>Over the last decade or so, many companies (especially in tech) have adopted a
&ldquo;No Jerks&rdquo; policy. The idea is to have policies in place for dealing with
workplace bullies and jerks, and to have safeguards in the recruiting and hiring
pipeline to prevent them from being hired in the first place. Much of this wave
of emphasizing workplace civility seems to have begun with Robert Sutton&rsquo;s 2007
book, <em><a href="https://en.wikipedia.org/wiki/The_No_Asshole_Rule">The No Asshole Rule</a></em> and the numerous articles that followed. When I
was at Google, this topic was emphasized in a 2013 internal memo from a senior
executive that was (and still is) widely circulated around the company.</p>
<p>These kinds of articles and memos can serve as great introductions to
understanding the impact bullies and jerks have on teams and the entire
organization, and the more extreme examples of this behavior are often easy to
spot. However, I found that some of the examples in Sutton&rsquo;s book, while true
reports of actual situations, were <strong>so</strong> extreme that they bordered on caricature.
It became all too easy to dismiss them, feeling that <em>my</em> workplace doesn&rsquo;t have
any of <em>those</em> people.</p>
<p>But what I have found in my experience is that <strong>most</strong> people are not overtly
jerks <strong>most</strong> of the time. People tend to be far more nuanced, and the
behaviors that can lead to discord on a team are far more subtle, and often
unintentional.</p>
<p>The risk of teams and organizations only focusing on avoiding negative behavior
is that they will, at best, trend toward neutral behavior. It&rsquo;s not enough to
simply not be a jerk. We must strive to be <strong>intentionally positive</strong>.</p>
<p>Being <em>intentionally positive</em> is not something that happens by accident. It&rsquo;s
not something you stumble into, and I suspect that it does not come naturally to
many people. By definition, being intentionally positive is a conscious and
deliberate choice to behave in a particular way.</p>
<p>So what does it mean to be intentionally positive in the workplace? It&rsquo;s really
nothing profound or surprising. It&rsquo;s about being respectful to coworkers,
empathetic and thoughtful in communication, quick to correct and apologize when
you make a mistake, and quick to forgive when others do. I&rsquo;ve always worked in
engineering organizations, so I&rsquo;ll give two concrete examples from my experience
there, but the lessons apply to any kind of work.</p>
<h2 id="building-a-better-future">Building a better future</h2>
<p>Any sufficiently complex software project is going to
have bugs. The documentation is never as clear as you would like, and the tests
are never as thorough. Decisions or policies that were made years ago might no
longer make sense, and there may be no one left that even remembers how or why
they were made in the first place. We nearly always work in imperfect
situations, but the attitude we have toward that can have a big impact on a
team.</p>
<p>Off-handed remarks about how poorly a piece of work has been done, or how
short-sighted a decision was, sets the tone for how other&rsquo;s work is evaluated.
When done in a critical rather than constructive manner, it only serves to tear
people down, even if they&rsquo;re no longer around to hear it. In fact, it&rsquo;s
especially important if they&rsquo;re no longer around, since this signals to the
current team how they should expect their own work to be discussed after they&rsquo;re
gone.</p>
<p>There&rsquo;s a concept in improvisational acting and comedy called &ldquo;Yes, and…&rdquo;. It&rsquo;s
a technique of accepting whatever idea or direction your partner gives you (the
&ldquo;yes&rdquo;), and then building on that (the &ldquo;and&rdquo;). Even if the idea seems
preposterous or isn&rsquo;t where you were wanting to go. In business settings, this
is often discussed in connection with brainstorming sessions, but I think it
applies here as well because it&rsquo;s really about attitude. If we&rsquo;re dealing with
legacy code or unclear policies, we accept whatever we have today, and then
build on it. We may still end up changing it, even drastically, but it means we
will do so with an intentionally positive attitude.</p>
<p>Bob Ross was famous for saying that &ldquo;we don&rsquo;t make mistakes, we have happy
accidents&rdquo;. What is that, if not applying &ldquo;yes, and…&rdquo; to his painting? Yes
this thing happened, and we&rsquo;re going to work with it and turn it into something
positive.</p>
<h2 id="communicating-with-care">Communicating with care</h2>
<p>I think one of the hardest skills in life, and one I&rsquo;m
not sure I&rsquo;ll ever truly master, is effective communication. Especially as more
people are working remotely, and conversations are split across a variety of
communication channels, the opportunity for misunderstandings is ever
increasing. Whether it&rsquo;s a simple code or design review for a teammate, or
answering the same customer question for the 100th time by a new-hire that
doesn&rsquo;t know any better, there is a huge opportunity to be intentionally
positive in our communication.</p>
<p>In technical reviews in particular, I have found that short, to-the-point
comments meant to be expedient can also be received as brusque, impatient, or
dismissive. Projects like <a href="https://conventionalcomments.org/">Conventional Comments</a> suggest a structured way to
prefix comments with additional context, but I&rsquo;ve also found that simply
responding in complete sentences often leads to clearer communication. Or
asking questions rather than giving commands: &ldquo;Have you considered X here?&rdquo;
rather than &ldquo;do X here&rdquo;. (I&rsquo;ve also seen this backfire where such a Socratic
method led to frustration with a reviewer that clearly seemed to have an opinion
but just wouldn&rsquo;t outright say it.)</p>
<p>When dealing with customer questions, we can start by being careful to fully
understand their specific situation, since it may actually not be the same as
those before them. And then we can be friendly, helpful, and understanding of
their problem. This isn&rsquo;t about platitudes or fake hospitality (that really
drives me crazy); this is about genuine empathy and kindness.</p>
<p>While being intentionally positive with how we communicate toward others, it&rsquo;s
equally (if not more) important to take the same attitude when we&rsquo;re on the
receiving end. This is often described as &ldquo;assuming good intent&rdquo; or the
principle of charity. If something that someone says could be interpreted
multiple ways, assume that they meant it in the best possible interpretation.
Even if you know that they didn&rsquo;t, it can sometimes be effective to simply
ignore that fact and respond as if they did.</p>
<h2 id="the-cost-of-being-intentionally-positive">The cost of being intentionally positive</h2>
<p>Like any other skill, this will not come naturally for everyone (I know it often
doesn&rsquo;t for me). It may require conscious effort, and it may feel awkward at
times. And that may mean that it takes longer to reply to an email or complete
a code review because you have to spend extra time thinking about how you
respond. That&rsquo;s okay. As a manager, <strong>I&rsquo;m willing to sacrifice velocity in
order to improve team health</strong> because I know that we will only get better at it
and it will come more naturally with practice.</p>
<p>The only way to build the kind of team that I would like to work on is to make
deliberate decisions to be that team each day.</p>
<p>(I picked up the term &ldquo;intentionally positive&rdquo; from the <a href="https://indieweb.org/code-of-conduct">IndieWeb Code of
Conduct</a> which begins, &ldquo;IndieWeb is an intentionally positive community&rdquo;, which
I absolutely love and it&rsquo;s always stuck with me. I did some digging, and it
was added by <a href="https://tantek.com/">Tantek Çelik</a> in <a href="https://indieweb.org/wiki/index.php?title=code-of-conduct&amp;type=revision&amp;diff=1939&amp;oldid=1938">February 2013</a>.)</p>

    ]]></content>
    <link href="https://willnorris.com/2021/intentionally-positive/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2021/mac-and-cheese/</id>
    <title>Boxed Mac and Cheese</title>
    <published>2021-06-15T22:39:49-07:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2021/mac-and-cheese/"><![CDATA[
      <img src="mac-and-cheese.jpg" alt="" />
      <p>We were a Kraft household growing up, certainly eating our fair share of blue
box mac and cheese. The steps to make it are quite simple: cook the pasta and
drain out the water, put the pasta back in the pan, then stir in milk, butter,
and the cheese packet. I must have done this hundreds of times throughout my
childhood, never once questioning these instructions. Why would I? Of course
<em>Kraft</em> of all companies knows how to make a pot of mac &amp; cheese! I mean, they
even tweeted out the instructions, lest you throw away the box and find yourself
stranded:</p>
<figure class="aligncenter">
  <a href="https://twitter.com/kraftmacncheese/status/1196914329695723521">
    <img src="kraft-tweet.jpg" style="width: 500px; max-width: 100%" alt="Tweet from Kraft with instructions: boil water, drain cooked pasta, add milk butter and cheese mix">
  </a>
</figure>
<p>It wasn&rsquo;t until a few years into my marriage that I realized that I had been
making mac and cheese <strong>wrong</strong> my entire life. Surely there are different ways
to make mac and cheese, but is it really fair to say that the Kraft way is
<em>wrong</em>? Yes. Yes, it is. What I saw my wife do was nothing less than
life-altering. Okay, well at least I still think about it every time I make mac
and cheese, some 10 years later.</p>
<p>You don&rsquo;t mix the sauce ingredients into the pasta-filled pan like a monster!
That is the way to clumpy, grainy, cheese-powder disaster. Instead, leave the
pasta in the strainer and make the sauce in the empty pan first! Only once it
is nice and smooth do you add the pasta back in so that you get a nice even
coating. (In essence, you&rsquo;re making a roux. Or at least you would be if the
cheese packet has flour in it. I&rsquo;m not sure if it does.)</p>
<p>It turns out that this is what <a href="https://www.annies.com/">Annie&rsquo;s</a> has instructed
on their boxed mac and cheese all along:</p>
<figure class="aligncenter">
  
  
  <img src="/api/imageproxy/1200x898/2021/mac-and-cheese/annies-way.jpg" alt="Annie&#39;s way in 8 minutes: cook and drain pasta; combine milk, cheese, and cheese packet in saucepan; add cooked pasta to saucepan and stir" width="600" height="449">

</figure>
<p>On the evening that I achieved macaroni enlightenment, I was hesitant to
research whether it was my own mother or Kraft that had led me astray as a
child. I guess it was a small comfort to discover that it was indeed Kraft,
though the betrayal I felt was palpable. Suffice it to say, we&rsquo;re an Annie&rsquo;s
household now.</p>
<p>That said, I can&rsquo;t fully explain what&rsquo;s going on in this picture. I&rsquo;m willing to
accept that this was staged just for the picture, and that someone did not, in
fact, have a serious lapse in macaroni-and-cheese judgement.</p>
<figure class="aligncenter">
  <a href="https://twitter.com/annieshomegrown/status/1234600905066139654">
    <img src="annies-tweet.jpg" style="width: 500px; max-width: 100%" alt="Tweet from Annie's showing milk and butter being added into a pot of cooked pasta">
  </a>
</figure>

    ]]></content>
    <link href="https://willnorris.com/2021/mac-and-cheese/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2020/leaving-google/</id>
    <title>Leaving Google</title>
    <published>2020-09-18T14:38:23-07:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2020/leaving-google/"><![CDATA[
      <img src="google.jpg" alt="" />
      <p>After 10 years, 8 months, and a handful of days, today is my last day at Google.
It&rsquo;s surreal and bittersweet, but I&rsquo;m really excited about what&rsquo;s next. As I&rsquo;m
writing this, I&rsquo;m sitting outside of Charlie&rsquo;s, getting ready to go gather my
personal belongings and turn in my badge to security.</p>
<p>I joined Google a lot younger, not yet married two years, and before we had our
two boys (who just started preschool and Kindergarten!). I spent my first
couple of years working on Google Buzz and then Google+, then starting a 20%
project managing Google&rsquo;s open source releases on GitHub. That turned out to
require a lot more than just 20% time, so now eight years later in Google&rsquo;s
<a href="https://opensource.google/">Open Source Programs Office</a>, I&rsquo;m leaving behind an amazing organization I&rsquo;m
so honored to have gotten to be a part of. And I&rsquo;m so grateful to <a href="http://dibona.com/">Chris
DiBona</a> for taking a bet on a no-name engineer, and giving me so many
opportunities to help get me to where I am today.</p>
<p>I&rsquo;ll be sticking around in open source, and will be starting the next adventure
in a couple of weeks. For now, I&rsquo;m enjoying my final walk around a very empty
Googleplex, recalling some great memories, and trying to fully take in how
amazing this ride has been. I will miss this place and the phenomenal people I
got to work with.</p>

    ]]></content>
    <link href="https://willnorris.com/2020/leaving-google/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

  <entry>
    <id>https://willnorris.com/2016/gophercon-family-track/</id>
    <title>GopherCon Family Track</title>
    <published>2016-06-22T09:42:24-07:00</published>
    <updated>2023-10-27T21:04:04-07:00</updated>
    <content type="html" xml:base="https://willnorris.com/2016/gophercon-family-track/"><![CDATA[
      
      <p>So I just got an email about <a href="https://www.gophercon.com/">GopherCon</a>&rsquo;s Significant Other and Family Track:</p>
<blockquote>
<p>Summer is a great time of year to travel with our loved ones. We&rsquo;re excited to announce that
SO/Family Track tickets are now available for significant others and kids who&rsquo;ll be in Denver July
11-13th! Please register each person regardless of age so we&rsquo;ll know how many people to expect.</p>
<p>This will be a great way to meet new people and explore the area while attendees take part in the
third annual Gophercon. We&rsquo;ve got activities planned Monday, Tuesday, and Wednesday, plus a
rendezvous room at the convention center for afternoon chill time. We&rsquo;ll take a tour of the Denver
Art Museum and have free time for exploring the city on Monday, then make an excursion to the
gorgeous country outside of Denver on Tuesday. Wednesday we&rsquo;ll have a chance to tinker during Hack
Day. We&rsquo;ll also have family-friendly meetups in the evenings.</p>
</blockquote>
<p>This is amazingly cool! I don&rsquo;t think I&rsquo;ve ever seen a conference that specifically planned
something like this for families.</p>

    ]]></content>
    <link href="https://willnorris.com/2016/gophercon-family-track/" rel="alternate" type="text/html"/>
    <author>
      <name>Will Norris</name>
      <uri>https://willnorris.com/</uri>
    </author>
  </entry>

</feed>
