tailscale-manager dynamically manages Tailscale subnet route advertisements based on user-configurable discovery sources. It runs alongside tailscaled on the node(s) where you want to advertise routes.
config keyword | example | description |
---|---|---|
routes |
["192.168.0.0/24"] |
Static routes |
hostRoutes |
["private-app.example.com"] |
DNS hostname lookup |
awsManagedPrefixLists |
["pl-02761f4a40454a3c9"] |
AWS Managed Prefix Lists |
hostRoutes
can be used to emulate Tailscale App Connectors by advertising a
set of individual IP address routes that are kept in sync with DNS lookups of a
set of hostnames. This is most useful when using Headscale, which doesn't
normally support App Connectors.
- DNS SRV records
- Extra JSON files on disk
- Generic HTTP service discovery, similar to Prometheus
http_sd
- NetBox lists
- Google Cloud public-advertised-prefixes
- Other cloud providers?
Here is a sample config file, in JSON format:
{
"routes": [
"172.16.0.0/22",
"192.168.0.0/24"
],
"hostRoutes": [
"github.com",
"private-app.example.com"
],
"awsManagedPrefixLists": [
"pl-02761f4a40454a3c9"
]
}
Run tailscale-manager:
tailscale-manager your-config-file.json --interval 300
The above will result in individual /32 (or /128 for ipv6) route advertisements
on your tailnet for the IP addresses that github.com
and
private-app.example.com
resolve to, plus any routes found in the named AWS
managed prefix lists, and any static routes provided in the routes
list.
Every 300 seconds, tailscale-manager will refresh its route discovery sources
and update tailscale route advertisements accordingly.
Usage: tailscale-manager <configfile.json> [--dryrun] [--tailscale PATH]
[--interval INT] [--max-shrink-ratio RATIO]
Tailscale routes manager
Dynamically resolves a list of hostRoutes to IP addresses, then tells tailscale
to advertise them as /32 routes along with any normal CIDR routes.
Config file example:
{
"routes": [
"172.16.0.0/22",
"192.168.0.0/24"
],
"hostRoutes": [
"special-hostname1.example",
"special-hostname2.example",
],
"awsManagedPrefixLists": [
"pl-02761f4a40454a3c9"
],
"extraArgs": ["--webclient"]
}
Available options:
--dryrun Dryrun mode
--tailscale PATH Path to the tailscale executable
(default: "tailscale")
--interval INT Interval (in seconds) between runs. 0 means exit
after running once. (default: 0)
--max-shrink-ratio RATIO Max allowed route shrinkage between consecutive runs,
as a ratio between 0 and 1. 1 means no limit.
(default: 0.33)
-h,--help Show this help text
A Docker image is built and pushed to GitHub Container Registry on each version, commit, and pull request. The image is built based on Alpine images and contains a statically-linked tailscale-manager
binary.
You can use it to build your own custom Tailscale Docker images using the following Dockerfile and entrypoint.sh
script.
# Dockerfile
FROM ghcr.io/singlestore-labs/tailscale-manager AS tailscale-manager
FROM tailscale/tailscale AS tailscale
COPY --from=tailscale-manager /bin/tailscale-manager /bin/tailscale-manager
COPY config.json <CONFIG_FILE>
COPY entrypoint.sh /usr/local/bin/entrypoint
CMD ["entrypoint"]
# entrypoint.sh
#!/bin/sh
tailscale-manager <CONFIG_FILE> --interval 300 &
containerboot
This will allow you to use Tailscale as a Docker container while having tailscale-manager
running in the background, periodically updating routes.
If you use NixOS, this repository provides a flake with a NixOS module to install and run tailscale-manager as a systemd service. You can incorporate it into your flake.nix like so:
{
description = "my nixos config";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
tailscale-manager = {
url = "github:singlestore-labs/tailscale-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, tailscale-manager }:
{
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
tailscale-manager.nixosModules.x86_64-linux.default
({ config, lib, pkgs, ... }:
{
services.tailscale.enable = true;
services.tailscale-manager = {
enable = true;
routes = [
"172.16.0.0/22"
"192.168.0.0/24"
];
hostRoutes = [
"app1.example.com"
"app2.example.com"
];
interval = 300;
maxShrinkRatio = 0.25;
};
})
];
};
};
}