«Tailscale без Tailscale, от Tailscale»
Tailcat — это переработка открытых компонентов Tailscale, которая ведёт себя как netcat, но работает поверх плоскости данных (data plane) Tailscale без использования его плоскости управления (control plane). Плоскость данных Tailscale (внутри — magicsock) организует зашифрованные по WireGuard® туннели точка-точка между двумя машинами: DERP выступает вспомогательным каналом для пробоя NAT (NAT hole-punching) и последним резервным ретранслятором, если прямое соединение установить не удалось. Вместо плоскости управления Tailscale все метаданные соединения tailcat передаются внеполосно (out of band) любым удобным способом.
CLI-утилита tailcat (в cmd/tailcat) построена на Go-библиотеке tailcat, доступной для импорта как github.com/tailscale/tailcat.
Независимо от того, используете ли вы tailcat как CLI-инструмент или как библиотеку, одна сторона запускает сервер tailcat (слушатель) и получает короткий адрес tailcat. Другая сторона передаёт этот адрес клиентской части tailcat для подключения. Весь трафик между двумя сторонами шифруется сквозным образом с помощью WireGuard. Начальное соединение устанавливается через DERP-сервер (см. ниже), после чего magicsock выполняет пробой NAT и при возможности переключается на прямое UDP-соединение между узлами (что обычно и происходит).
Вам не нужна учётная запись Tailscale или права root/администратора на машине — утилита не изменяет таблицы маршрутизации, DNS и прочие системные настройки. Это просто библиотека для пользовательского пространства и CLI-инструмент.
И всё это открытый исходный код.
Можно использовать наши бесплатные DERP-ретрансляторы с ограничением скорости (карта DERP по умолчанию: https://tailcat.dev/derpmap.json) или развернуть собственные.
Также существует экспериментальная веб-демонстрация прямо в браузере (tailcat, скомпилированный в WebAssembly) по адресу https://tailscale.github.io/tailcat/ — там можно отправлять и получать файлы или текст в связке с CLI. Трафик в браузере передаётся только через DERP, без прямых соединений — до тех пор, пока не появится поддержка WebRTC (#4).
Установка
Подробности по каждому способу, включая заметки для сборщиков пакетов, см. в INSTALL.md:
| Способ | Linux | macOS | Windows | FreeBSD, OpenBSD | Браузер (js/wasm) |
|---|---|---|---|---|---|
✅ |
✅ |
||||
Debian, Ubuntu, … |
|||||
Red Hat, Fedora, … |
|||||
✅ |
|||||
✅ |
|||||
✅ |
|||||
✅ |
✅ |
||||
Arch |
|||||
✅ |
✅ |
✅ |
|||
✅ |
✅ |
✅ |
✅ |
✅ |
Использование
Передача stdin/stdout между двумя машинами
Сервер запускается и выводит свой эфемерный адрес:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
(hangs, waiting...)
После этого клиент может выполнить:
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
$
Тогда сервер разблокируется:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
hello
$
Проброс локальных портов через туннель
Можно также обслуживать локальный TCP-порт, перенаправляя его на localhost:
$ tailcat serve 8080,8443 # или: tailcat serve all
# 🐈 Server listening with new address: tcXXXXXXXXX
И на стороне клиента:
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: foo
HTTP/1.1 200 OK
....
Проброс локальных портов на tailcat-сервер
Чтобы сделать порты tailcat-сервера доступными как обычные локальные TCP-порты (для браузеров, клиентов баз данных и других инструментов, которые не поддерживают SOCKS или stdio), запустите forward с адресом tailcat-сервера:
$ tailcat serve 8080,3306
# 🐈 Server listening with new address: tcXXXXXXXXX
$ tailcat forward tcXXXXXXXXX 18080:8080 3306
Локальный порт 0 просит операционную систему выделить свободный порт; каждый слушатель выводит свой адрес, как только начинает работать.
Чтобы пробросить локальные порты к ресурсам сети, доступным через сервер в режиме выходного узла (exit node), запустите сервер в этом режиме и укажите в таблице соответствий удалённые IP-адреса и порты:
$ tailcat serve exit-node
# 🐈 Server listening with new address: tcXXXXXXXXX
$ tailcat forward tcXXXXXXXXX \
3001:172.23.52.30:3001 \
17170:172.23.52.31:17170
Это перенаправит 127.0.0.1:3001 на 172.23.52.30:3001 и 127.0.0.1:17170 на 172.23.52.31:17170 через сервер в режиме выходного узла.
По умолчанию слушатели привязываются к 127.0.0.1, а диагностические логи скрыты. Передайте --verbose перед подкомандой, чтобы включить подробное логирование сети. Используйте --bind=0.0.0.0 только если к слушателю должны подключаться клиенты с других машин:
$ tailcat forward --bind=0.0.0.0 tcXXXXXXXXX 18080:8080
Нажмите Ctrl-C, чтобы остановить проброс.
Открытие браузера к tailcat-серверу
Чтобы открыть веб-сервер за tailcat-сервером, выполните browse:
$ tailcat serve 80
# 🐈 Server listening with new address: tcXXXXXXXXX
$ tailcat browse tcXXXXXXXXX
Это псевдоним для tailcat forward --open-browser <tc-addr> 0:80: как только локальный слушатель готов, команда открывает http://127.0.0.1:<порт>/ в браузере, затем блокируется, перенаправляя соединения, пока её не прервут. Флаг --open-browser работает с любым одиночным сопоставлением портов в forward.
SSH-сервер с аутентификацией по публичному ключу
Запустите SSH-сервер, принимающий ключи из локальных файлов authorized_keys, явно заданных строк публичных ключей OpenSSH или учётных записей GitHub:
$ tailcat serve --ssh-authorized-keys=~/.ssh/authorized_keys ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
Несколько источников можно перечислить через запятую. Источник вида user@github загружает https://github.com/user.keys один раз, до запуска сервера:
$ tailcat serve --ssh-authorized-keys=bradfitz@github,./contractor.pub ssh
Каждый источник должен существовать, успешно загружаться и содержать допустимые строки публичных ключей — иначе запуск завершится ошибкой. Опции авторизованных ключей, такие как command= и from=, отклоняются, поскольку встроенный сервер их не реализует. Запуск tailcat serve ssh без --ssh-authorized-keys также завершается ошибкой; используйте явный сервис no-auth-ssh, когда достаточно лишь идентичности туннеля.
SSH-сервер без аутентификации
На Linux, macOS и Windows можно явно запустить SSH-сервер без аутентификации клиента. Зашифрованный туннель сам по себе обеспечивает идентификацию клиента.
$ tailcat serve no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
|
Внимание
|
При использовании |
На стороне клиента:
$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
Запуск команды для каждого соединения
Подобно inetd, сервис exec запускает команду для каждого входящего соединения, передавая само соединение как stdin и stdout команды. Команда указывается после --:
$ tailcat serve exec -- /usr/bin/fortune
# 🐈 Server listening with new address: tcXXXXXXXXX
$ tailcat tcXXXXXXXXX 80 < /dev/null
Stderr команды направляется в stderr сервера. Команда получает публичный ключ узла-партнёра в переменной $TAILCAT_PEER_KEY (в формате --allow) и IP:порт партнёра в $TAILCAT_REMOTE_ADDR.
При использовании с сервисом ssh или no-auth-ssh команда заменяет оболочку, как ForceCommand в OpenSSH: каждая SSH-сессия выполняет только эту команду (с PTY, если клиент его запрашивает), а сервер не предоставляет ни оболочки, ни команды по выбору клиента, ни SFTP. Запрошенная клиентом команда, если есть, поступает в $SSH_ORIGINAL_COMMAND.
$ tailcat serve --ssh-authorized-keys=alice@github ssh -- ./deploy.sh
$ tailcat serve no-auth-ssh -- git-upload-pack /srv/repo.git
Отправка и получение файлов
Чтобы принимать файлы, создайте «ящик для приёма» и поделитесь выведенным адресом tailcat:
$ tailcat recv ~/inbox
# 🐈 Server listening with new address: tcXXXXXXXXX
Отправитель выполняет:
$ tailcat cp report.pdf tcXXXXXXXXX:
tailcat cp запускает системный scp, маршрутизируя соединение через tailcat, — вы получаете стандартное отображение прогресса и флаг -r для деревьев каталогов. Ящик для приёма доступен только на запись: отправители не могут просматривать каталог, читать файлы обратно или изменять существующие.
Чтобы предложить файлы, раздайте каталог только для чтения (по умолчанию) или для чтения и записи:
$ tailcat serve files # текущий каталог, только чтение
$ tailcat serve --files=/pub:rw files # указанный каталог, чтение и запись
$ tailcat ls -l tcXXXXXXXXX
$ tailcat cp tcXXXXXXXXX:report.pdf .
tailcat ls работает напрямую по протоколу SFTP, поэтому работает даже без установленного OpenSSH.
Сервер ограничивает все пути обслуживаемым каталогом (через Go-интерфейс os.Root), так что ни .., ни символические ссылки из него не выходят. Файловый сервис использует SFTP, поэтому стандартные клиенты sftp и scp тоже работают с ним при наличии ProxyCommand, пробрасывающего соединение через tailcat (тот же приём, что использует tailcat cp и tailcat ssh). Оба сервера — ssh и no-auth-ssh — тоже раздают SFTP с теми же правами доступа, что и оболочка.
Передача файлов не сжимается: в протоколе SFTP сжатие отсутствует, транспортный уровень SSH здесь его тоже не применяет (стек SSH на Go его не реализует; сжатие на транспортном уровне имеет историю проблем с безопасностью, и TLS от него тоже отказался). Если сжатие важно, архивируйте файлы перед отправкой.
Прочие команды
Ping для проверки связности; каждый «pong» сообщает, пришёл ли он через DERP-ретранслятор или по прямому пути. --until-direct продолжает пинговать (не дольше --timeout, по умолчанию 10 с) до получения прямого пути и завершается с ненулевым кодом, если прямой путь так и не установился:
$ tailcat ping --until-direct <tc-addr>
pong in 42.1ms via DERP(sfo)
pong in 1.2ms via 203.0.113.7:41641
Выполнить команду через SOCKS5-прокси, маршрутизированный через туннель:
$ tailcat socks <tc-addr> curl http://server.tailcat:8081/
Адреса tailcat работают непосредственно как имена хостов в URL: SOCKS-прокси их распознаёт и устанавливает соединение, поэтому аргумент с адресом tailcat необязателен. (Адреса tailcat чувствительны к регистру; это работает с curl и большинством CLI-инструментов, но не с браузерами, которые переводят имена хостов в нижний регистр.)
$ tailcat socks curl http://<tc-addr>:8081/
Работа в режиме выходного узла, чтобы клиент мог достичь сети сервера:
$ tailcat serve exit-node
Разобрать адрес tailcat и вывести его содержимое (публичный ключ WireGuard сервера и информацию о DERP) в формате JSON, не устанавливая соединения:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"RegionID": 302
}
Преобразовать короткий адрес tailcat (который ссылается на DERP-регион по ID, требуя от клиентов загрузки карты DERP) в более длинный самодостаточный адрес со встроенными данными DERP-сервера — это позволяет клиентам подключаться быстрее:
$ tailcat resolve tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
Разбор этого развёрнутого адреса tailcat показывает встроенные данные DERP:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"Region": [
{
"Nodes": [
{
"HostName": "tc302a.ipn.dev",
"IPv4": "208.111.39.38",
"IPv6": "2607:f740:0:3f::720"
}
]
}
]
}
Сервер может вывести длинный самодостаточный адрес напрямую с помощью флага tailcat serve --full-address.
Управление ключами
Адрес tailcat-сервера содержит его публичный ключ WireGuard и независимый предварительно распределённый ключ (pre-shared key) WireGuard, поэтому именно сохранённые ключи определяют, кто может к вам подключиться:
-
Эфемерные ключи (по умолчанию): при каждом запуске сервер генерирует новый ключ в памяти и выводит адрес, который никто прежде не видел. Когда процесс завершается, ключ уничтожается, а адрес перестаёт работать навсегда. Это безопасный вариант по умолчанию: переданный адрес относится лишь к тому конкретному запуску.
-
Сохранённые ключи:
tailcat genkeyгенерирует ключ, сохраняемый на диске, — адрес остаётся стабильным между перезапусками. Обратная сторона: любой, кому вы когда-либо передавали этот адрес, сможет подключиться к любому будущему серверу с этим ключом — если только вы не ограничите клиентов черезtailcat serve --allow(см.tailcat genkey --client).
При запуске CLI сообщает, какой вид ключа используется, — так вы всегда знаете, запускаете ли вы свежий одноразовый сервер или снова прослушиваете адрес, который могли передавать в прошлом.
Предварительно распределённые ключи WireGuard включены по умолчанию и настоятельно рекомендуются. Для совместимости с клиентами tailcat версии v0.5.0 и более ранних флаг --psk=false в serve или genkey создаёт более короткие адреса, однако лишает постквантовой защиты и защиты от публичных DERP-операторов, наблюдающих публичные ключи узлов.
$ tailcat genkey --key=default --region=nyc
# выводит адрес tailcat; ключ сохраняется в ~/.config/tailcat/keys/default.private.json
# позже; ключ с именем "default" используется автоматически, как только существует:
$ tailcat serve 8080
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
# ... если только не форсировать разовый эфемерный ключ:
$ tailcat serve --key=new 8080
# 🐈 Server listening with new address: tcXXXXXXXXX
Иными словами, default — это магическое имя ключа: как только оно существует, обычный tailcat молча использует его вместо генерации эфемерного ключа, и строка запуска выше сообщает вам, что именно произошло. Используйте --key=new, чтобы принудительно получить эфемерный ключ, --key=<имя> — для использования другого сохранённого ключа, или tailcat genkey --delete --key=default — для удаления сохранённого ключа по умолчанию. tailcat genkey --list выводит список ваших сохранённых ключей.
Адреса tailcat также можно публиковать как DNS TXT-записи и обращаться к ним по имени; DNS-имя работает везде, где CLI принимает адрес tailcat:
# Если у example.com есть TXT-запись "tailcat=tc..."
$ tailcat example.com 8080
$ tailcat ssh example.com
$ tailcat ping example.com
|
Внимание
|
Адрес tailcat — это, как правило, секрет: именно знание адреса позволяет клиенту подключиться. DNS TXT-запись не является секретом. Она публична, доступна для чтения всем и активно сканируется. Публикация адреса в DNS открывает его всему интернету, поэтому сервер за ним должен аутентифицировать клиентов иным способом, а не по знанию адреса: ограничивайте туннель известными ключами клиентов через |
Примеры
Защищённый SSH-сервер через DNS
Зачем нужны проброс портов или port knocking? Здесь SSH-сервер доступен из любой точки по имени, без открытых входящих портов на сервере, а WireGuard аутентифицирует клиента прежде, чем SSH-сервер вообще увидит первый пакет.
|
Внимание
|
Флаг |
На клиентской машине сгенерируйте пару ключей клиентской идентичности. Команда выводит публичный ключ — это всё, что нужно знать серверу:
client$ tailcat genkey --client --key=client-default
# wrote file to ~/.config/tailcat/keys/client-default.private.json
nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16
На сервере сгенерируйте серверную пару ключей, привязанную к ближайшему DERP-региону (причина — ниже), затем запустите SSH с доступом только для этого клиента:
server$ tailcat genkey --key=default --fixed-region
# wrote file to ~/.config/tailcat/keys/default.private.json
tcXXXXXXXXX
server$ tailcat serve --allow=nodekey:cfb6bf...ddfd16 22
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
Опубликуйте адрес tailcat в DNS как TXT-запись:
my-server.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"
И тогда на стороне клиента достаточно:
client$ tailcat ssh my-server.example.com
Клиентские режимы автоматически используют сохранённый ключ client-default, когда он существует, — дополнительные флаги для предъявления разрешённой идентичности не нужны. Рукопожатия от посторонних молча игнорируются: они не достигнут SSH-сервера и не узнают, что он вообще работает.
В качестве дополнительной защиты tailcat ssh перед подключением к адресу с DNS-именем выполняет проверку: пробует войти по SSH как посторонний — со свежесгенерированным клиентским ключом и без SSH-учётных данных. Если сервер принимает такой вход, значит, любой, кто прочитает TXT-запись, может сделать то же самое, — поэтому tailcat отказывается подключаться и объясняет причину. Проверка выявляет неправильную конфигурацию при первом же тестировании собственного сервера; флаг --skip-dns-safety-check её пропускает — если вы действительно хотите публичный сервер или просто хотите обойтись без лишних круговых задержек.
Зачем --fixed-region: он один раз определяет ближайший DERP-регион в момент генерации ключа и вписывает его ID как в выведенный адрес tailcat, так и в сохранённый файл ключа, — благодаря этому при перезапусках сервер привязывается к тому же региону (сохраняя опубликованный адрес действительным) без повторного зондирования. В противном случае genkey по умолчанию использует --region=auto, что означает «выбрать при запуске»: подходит для разового использования, но адрес tailcat, опубликованный в DNS, должен указывать на фиксированный регион, чтобы клиенты и будущие перезапуски сервера встречались в одном месте. (--region=<имя> явно привязывает конкретный регион; --region=list показывает варианты.)
TODO: сделать клиент более устойчивым, если карта DERP изменится со временем: https://github.com/tailscale/tailcat/issues/7
Собственный DERP-ретранслятор
Ничто не обязывает использовать ретрансляторы Tailscale: разверните собственный DERP-сервер (ему нужно имя хоста с TLS-сертификатом — derper может получить его сам через Let’s Encrypt), затем сгенерируйте серверный ключ, который будет его использовать, передав имя хоста (или несколько, через запятую) как регион:
server$ tailcat genkey --key=default --region=derp.example.com
tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
server$ tailcat serve 22
Адрес tailcat встраивает имя хоста вашего ретранслятора:
$ tailcat parse tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
{
"ServerPublic": "nodekey:8022c28ea8f52ec7a0a51b644ce00fef3aae150731a01c61a3abd3ac26e14a49",
"Region": [
{
"Nodes": [
{
"HostName": "derp.example.com"
}
]
}
]
}
Клиентам не нужны никакие дополнительные флаги, они никогда не обращаются к серверу карты DERP или ретрансляторам Tailscale, а единственные ограничения скорости — ваши собственные. Если же вы эксплуатируете целый парк ретрансляторов, раздайте собственный JSON карты DERP и укажите его обеим сторонам через --derpmap-url.
Библиотека Go
Минимальный сервер, который отвечает на любой TCP-порт через туннель и выводит свой адрес tailcat. Нулевое значение Server подставляет умолчания для всего незаданного: свежий эфемерный ключ, ближайший регион карты DERP по умолчанию и логирование через log.Printf (задайте Logf в logger.Discard для тишины):
package main
import (
"fmt"
"log"
"net"
"github.com/tailscale/tailcat"
)
func main() {
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
return func(c net.Conn) {
fmt.Fprintf(c, "hello from port %v\n", port)
c.Close()
}
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
fmt.Println(s.TailcatAddr())
select {}
}
И минимальный клиент, который к нему подключается, получив адрес tailcat в качестве аргумента. Как и Server, нулевое значение Client работает с одним лишь заполненным полем Server (адрес tailcat); tailcat.NewClient — это сокращение именно для такого случая. Туннель устанавливается лениво при первом вызове dial:
package main
import (
"context"
"io"
"log"
"os"
"github.com/tailscale/tailcat"
)
func main() {
cl := tailcat.NewClient(tailcat.Addr(os.Args[1]))
defer cl.Close()
c, err := cl.DialTCPPort(context.Background(), 80)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, c)
}
$ ./client tcomFwWCAWf933BLELdzd3RkHiOufJ...
hello from port 80
UDP использует подключённый пакетный коннект (connected packet connection) для каждого клиентского потока, сохраняя границы дейтаграмм и адреса обоих концов:
s.OnUDP = func(port uint16) func(tailcat.ConnPacketConn) {
if port != 53 {
return nil
}
return func(c tailcat.ConnPacketConn) {
defer c.Close()
buf := make([]byte, tailcat.MaxUDPPayload)
for {
n, err := c.Read(buf)
if err != nil {
return
}
c.Write(buf[:n])
}
}
}
pc, err := cl.DialUDPPort(context.Background(), 53)
ConnPacketConn реализует одновременно net.Conn и net.PacketConn. Держите полезную нагрузку на уровне tailcat.MaxUDPPayload (1232 байта) или ниже, чтобы вписаться в MTU IPv6-туннеля без фрагментации. Для трафика через выходной узел используйте OnUDPForward и DialUDP; ProxyPacketConns обеспечивает безопасную двунаправленную пересылку дейтаграмм. Неактивные серверные UDP-потоки закрываются по истечении tailcat.DefaultUDPIdleTimeout (две минуты); чтобы изменить тайм-аут, задайте Server.UDPIdleTimeout.
Как это работает
Адреса tailcat
Сервер tailcat идентифицируется адресом tailcat — Go-типом tailcat.Addr. Адрес выглядит как tcXYZ… и представляет собой префикс "tc", за которым следует base64-кодированный CBOR, содержащий:
-
Публичный ключ WireGuard сервера (Curve25519, 32 байта)
-
Отдельный публичный ключ обнаружения пути (path-discovery key) (Curve25519, 32 байта)
-
По умолчанию — независимый предварительно распределённый ключ WireGuard (256 случайных бит), который не позволяет DERP-оператору, наблюдающему публичные ключи обоих узлов, влиться в туннель, и обеспечивает постквантовую защиту записанного трафика
-
Информацию о DERP — в одном из двух видов:
-
небольшое целое число, ссылающееся на один из управляемых Tailscale tailcat-серверов по умолчанию, или
-
полные метаданные DERP-сервера — для использования собственного DERP-сервера или чтобы избавить клиента от потенциального круговорота за актуальной картой DERP (эту форму производят флаг
tailcat serve --full-addressи подкомандаtailcat resolve)
-
Типичный адрес tailcat с одним лишь целочисленным ID региона занимает около 140 байт. Со встроенными данными DERP-узла он длиннее, но самодостаточен.
Адрес по умолчанию является секретной возможностью доступа (secret bearer capability), поскольку содержит предварительно распределённый ключ. Передавайте его только клиентам, которым следует подключаться. Публикация адреса — в публичной DNS TXT-записи или где-либо ещё — открывает эту возможность всему интернету. Это безопасно лишь тогда, когда сервер также аутентифицирует клиентов: serve --allow ограничивает туннель перечисленными ключами клиентских узлов, а сервис ssh требует --ssh-authorized-keys.
Сетевой стек
Tailcat повторно использует клиентские сетевые компоненты Tailscale, но без плоскости управления.
-
WireGuard — реализация WireGuard в пользовательском пространстве для шифрования всего туннельного трафика. Не использует ядерное устройство TUN/TAP и не настраивает никаких сетевых маршрутов или DNS, поэтому права root не требуются.
-
magicsock — транспортный уровень Tailscale, мультиплексирующий трафик через прямой UDP и DERP-ретрансляторы. Обрабатывает обнаружение конечных точек на основе STUN и пробой UDP-NAT для обхода трансляции адресов.
-
Netstack (gVisor) — стек TCP/IP в пользовательском пространстве, завершающий TCP-соединения внутри процесса. Именно он позволяет Tailcat принимать входящие соединения и устанавливать исходящие без какой-либо настройки сети в ОС.
-
DERP-ретранслятор — зашифрованный протокол ретрансляции Tailscale, используемый как канал встречи (rendezvous) и резервный путь данных при невозможности прямого соединения.
Схема установки соединения
-
Сервер запускается. Генерирует (или загружает) пару ключей WireGuard и по умолчанию предварительно распределённый ключ, подключается к DERP-ретранслятору и выводит свой адрес tailcat в stderr. Затем ждёт клиентов.
-
Клиент разбирает адрес tailcat, чтобы узнать публичный ключ сервера, ключ обнаружения пути, опциональный предварительно распределённый ключ и DERP-регион. Генерирует собственную эфемерную пару ключей и подключается к тому же DERP-ретранслятору. Отдельный ключ обнаружения пути может присутствовать в открытом виде в фреймах прямого пути (disco frames) без раскрытия публичного ключа WireGuard. Предварительно распределённый ключ остаётся секретной возможностью доступа к соединению даже тогда, когда оператор ретранслятора наблюдает публичные ключи обоих узлов.
-
Рукопожатие обнаружения. Клиент отправляет серверу через DERP-ретранслятор сообщение-пинг «Meow». Оно несёт публичный ключ узла клиента. Сервер получает его, добавляет клиента в список узлов WireGuard и сетевую карту, перенастраивает движок WireGuard и отвечает подтверждением «Meowed».
-
Туннель WireGuard. Когда обе стороны настроены как узлы WireGuard с использованием предварительно распределённого ключа из адреса (при его наличии), выполняется рукопожатие WireGuard (первоначально маршрутизируемое через DERP). После его завершения туннель поднят и зашифрованный трафик может течь.
-
Пробой NAT. Параллельно каждая сторона сообщает другой свои UDP-конечные точки (публичный IP:порт, полученный через STUN, плюс локальные адреса интерфейсов) в disco-сообщениях call-me-maybe через DERP, повторно сообщая при их изменении. Затем обе стороны запускают disco-протокол Tailscale и предпринимают попытку пробоя UDP. В случае успеха трафик переключается с DERP-ретранслятора на прямой путь точка-точка. Если пробой не удался, DERP продолжает работать как резервный вариант — соединение сохраняется, хотя пропускная способность на публичных DERP-ретрансляторах будет ограничена.
-
Передача данных. Клиент устанавливает TCP-соединение с портом сервера через туннель. Стек TCP/IP gVisor на обеих сторонах обрабатывает установку соединения. На сервере входящее соединение диспетчеризируется обработчику по порту: проброс на localhost, вывод в stdout, SSH-сессия и т.д.
Адресация
Каждый узел в настоящее время получает детерминированный IPv6-адрес из своего публичного ключа WireGuard, однако это деталь реализации, не раскрываемая конечным пользователям, которая может измениться. (Например, эти байты могут быть полностью удалены из IP-заголовков, возвращая потерянный MTU.)
Безопасность
О том, как сообщать об уязвимостях, а также о текущей модели угроз tailcat — см. SECURITY.md.
Стабильность
Tailcat можно использовать бесплатно, однако без каких-либо гарантий стабильности API или CLI: Go-API, флаги CLI и выводимый текст, а также формат протокола могут изменяться. Публичные DERP-ретрансляторы tailcat с ограничением скорости не имеют SLA по доступности или целевых показателей пропускной способности, и мы можем в любой момент по любой причине закрыть доступ к ним. Всё предоставляется на принципе «наилучших усилий» без договорных обязательств (например, выделенных DERP-ретрансляторов и/или поддержки).
Обратиться к продажам?
Если вы не хотите самостоятельно запускать и поддерживать инфраструктуру или вам нужна помощь — свяжитесь с отделом продаж, и мы обменяем деньги на товары и услуги.
История
Tailcat зародился в сентябре 2023 года под именем «derpcat» — написан на борту длинного рейса за просмотром плохих фильмов: первый набросок — коммит 9e4d925cc («cmd/dc: start of derpcat tool»), первый рабочий вариант — коммит 911915fbb («derpcat: it’s alive!», в сообщении к которому указано «UA 605 PDX-ORD en route to Ireland. yay not buying the wifi.»). Тогда проект жил внутри форка репозитория tailscale.com и несколько раз деградировал по мере развития внутренностей Tailscale. С тех пор мы возродили его и преобразовали в обычный Go-модуль, использующий репозиторий tailscale.com как зависимость, а не форкающий его.
Проект был открыт в августе 2026 года на конференции TailscaleUp.