Learn how to use the recently-added Tailscale, DNS, and Zeroconf endpoint discovery in Wishlist, our SSH host directory.

Wishlist Endpoint Discovery

We just added endpoint discovery to Wishlist, our SSH host directory.

Wishlist can act as a bastion, presenting the user with a list of hosts they can SSH into from that host.

You can also run it locally, in which case it becomes a TUI (text-based user interface) for your ~/.ssh/config (or to a predefined list of hosts in a YAML configuration file).

But there’s more to it: did you know you can discover hosts from several sources?

Endpoint Discovery

Currently, we support the following sources:

  • DNS SRV records
  • Zeroconf with mDNS
  • Tailscale

Let’s see how it works, shall we?


We partnered with our friends at Tailscale to add endpoint discovery to Wishlist. Check out what they have to say about it here.

Tailscale and Charm logos

Tailscale is a VPN service that makes devices and applications you own securely accessible anywhere in the world. It uses the WireGuard protocol, and has apps for pretty much all platforms.

Made with VHS

To discover your Tailscale-connected machines, you’ll need an API Access Token, and the name of your tailnet.

With that information in hand, run wishlist with --tailscale.key (or set $TAILSCALE_KEY) and --tailscale.net, for example:

wishlist --tailscale.net=charm --tailscale.key=ts-key-aaabbb...

And that’s it! Wishlist will discover all your tailnet’s machines on startup.

Note: We can’t get the open ports through the API, so all endpoints discovered will use the default SSH port (22). You can change that with hints.

It’s also worth mentioning that Tailscale’s API keys expire after 90 days (max). To avoid the trouble of having to change the key every couple of months, you can also use their beta OAuth Clients.

Create an app with devices:read scope here, and run with:

wishlist --tailscale.net=charm \
  --tailscale.client.id=aaabbb... \

This also gives Wishlist a more restricted access: only reading the device list, nothing else.

Zeroconf with mDNS

Zeroconf enables service discovery in a network without any operator intervention. It was originally adapted from AppleTalk, circa 1997, and is tracked by the RFC 6762.

Made with VHS

If you run anything Apple in your network, chances are they are all already available in .local domains. This happens because Apple devices employ Bonjour (a Zeroconf implementation) which is installed and configured by default.

On Linux, you can use either Avahi or SystemD for the same purpose.

For Avahi, installing and enabling the daemon (the package is usually named avahi-daemon) will make your host available in the network as hostname.local.

To make the host available in Wishlist, though, you’ll need to expose a _ssh._tcp service. You can do that by creating the file /etc/avahi/services/ssh.service with the following contents:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
  <name replace-wildcards="yes">%h</name>

You can check if you have any available endpoints by running:

# on Linux:
avahi-browse --domain local _ssh._tcp

# on macOS:
dns-sd -B _ssh._tcp

All that being said, you can run wishlist with --zeroconf.enabled, and it’ll operate using sensible defaults:

wishlist --zeroconf.enabled

Run wishlist --help | grep zeroconf to see all options.


Service Records (SRV) are specified in DNS for defining the host name and port number of servers for specific services.

Made with VHS

It is defined as:

_service._proto.name. ttl IN SRV priority weight port target

Wishlist will look for records with _ssh._tcp as service and proto.

You can mimic what it’ll do with dig, e.g. for the caarlos0.dev domain:

dig SRV _ssh._tcp.caarlos0.dev +short

So, for instance, if I want to expose a SRV record on port 2244, I would add an entry like this to my DNS:

_ssh._tcp.caarlos0.dev. 300 IN SRV 10 2 2244

Once you’ve done that, you can run wishlist setting --srv.domain to make it query the your name server and list the SRV records as endpoints. For example:

wishlist --srv.domain=caarlos0.dev


You might be asking yourself: “What if I want to set some more advanced options in these endpoints?” That’s what hints are for.

Hints have a similar structure to the endpoints setting, but they work differently: if a hint doesn’t match a discovered endpoint, it won’t get added to the final endpoint list, whereas regular endpoints would.

It works by using a match field (which can be a glob), and then tries to match all discovered endpoints hostnames with it. If it matches, the options in that hint will be set onto the final endpoint.

You can set port (especially useful with Tailscale), link, user, description, and more. Here’s an example showing all available fields:

  - match: "*.local"
    port: 23234
    description: "A description of this endpoint.\nCan have multiple lines."
    user: notme
    remote_command: uptime -a
    forward_agent: true
    request_tty: true
    connect_timeout: 10s
    proxy_jump: user@host:22
      name: Optional link name
      url: https://github.com/charmbracelet/wishlist
      - ~/.ssh/id_ed25519
      - ~/.ssh/charm_id_ed25519
      - FOO=bar
      - BAR=baz
      - LC_*
      - LANG
      - SOME_ENV

You can see the full, commented out, configuration file here.

Once you have your configuration file, run wishlist passing its path to --config. Wishlist will then discover all the endpoints (through all options available), and then, if there are any hints, iterate through them and apply them to the discovered nodes.

Finally, on server mode, you can specify a --endpoints.refresh.interval, so Wishlist will re-discover the nodes and also reload the configuration file, re-applying the hints too.

We believe that those features will make your Wishlist-powered SSH directories easier to maintain and to use, and can’t wait to see what you’ll do with it.

This post is cross-posted from The Charm Blog.