메뉴
HN
Hacker News • 30일 전

테일스케일, 토큰 하나로 초당 보안 터널 여는 '테일캣' 공개

IMP
7/10
핵심 요약

Tailscale이 자사 오픈소스 구성요소를 재조합해 만든 'Tailcat'을 공개했습니다. Tailcat은 Tailscale의 제어 평면 없이 데이터 평면(magicsock)만 활용해 두 대의 머신 사이에 WireGuard로 암호화된 P2P 터널을 몇 초 만에 구축하는 netcat 스타일 도구입니다. 계정이나 루트 권한 없이 사용자 공간(userspace)에서 동작하며, NAT 통과 실패 시 DERP 릴레이를 경유하고, 가능하면 직접 P2P UDP 연결로 업그레이드됩니다.

번역된 본문

"Tailscale이 만든, Tailscale 없는 Tailscale"

Tailcat은 Tailscale 오픈소스 구성요소들을 재조합하여 netcat처럼 동작하지만 Tailscale의 제어 평면 없이 데이터 평면 위에서 작동하는 도구입니다.

Tailscale의 데이터 평면(내부적으로 magicsock)은 두 머신 사이에 point-to-point WireGuard® 암호화 터널을 제공하며, DERP가 NAT 홀펀칭 통신용 사이드 채널 및 NAT 통과 실패 시의 최종 릴레이 역할을 합니다.

Tailscale 제어 평면을 사용하는 대신, 모든 tailcat 연결 메타데이터는 원하는 어떤 방식으로든 대역 외(out of band)로 교환됩니다.

tailcat CLI(cmd/tailcat에 위치)는 tailcat Go 라이브러리(github.com/tailscale/tailcat로 임포트 가능) 위에 구축되었습니다. CLI 도구나 라이브러리 어느 쪽으로 사용하든, 한쪽은 tailcat 서버(리스너)를 실행하고 짧은 연결 토큰을 받습니다. 다른 쪽은 그 토큰을 tailcat 클라이언트에 전달해 연결합니다.

양쪽 간의 모든 트래픽은 WireGuard로 종단 간 암호화됩니다. 초기 연결은 Tailscale의 DERP 릴레이 네트워크를 통해 부트스트랩되며, 이후 magicsock이 NAT 통과를 수행해 가능한 경우(보통 가능합니다!) 직접 P2P UDP 연결로 업그레이드합니다.

Tailscale 계정도, 머신의 root/admin 권한도 필요 없습니다(머신의 라우팅 테이블이나 DNS 등을 변경하지 않습니다). 순수한 사용자 공간 라이브러리이자 CLI 도구이며, 모두 오픈소스입니다. 무료 속도 제한 DERP 릴레이(기본 DERP 맵: https://tailcat.dev/derpmap.json)를 사용하거나 직접 운영할 수도 있습니다.

사용법

두 머신 간 stdin/stdout 파이프

서버가 시작되며 임시 주소를 출력합니다:

$ tailcat

선택된 부트스트랩 릴레이 지역 302, 샌프란시스코

🐈 서버가 새 주소로 리스닝 중: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu

(대기 중...)

클라이언트는 다음과 같이 실행합니다:

$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu $

그러면 서버가 차단 해제되며:

$ tailcat

선택된 부트스트랩 릴레이 지역 302, 샌프란시스코

🐈 서버가 새 주소로 리스닝 중: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu

hello $

터널을 통한 로컬 포트 노출

로컬 TCP 포트를 서빙하도록 설정할 수도 있습니다(localhost로 포워딩):

$ tailcat --serve=8080,8443 # 또는 --serve=all

🐈 서버가 새 주소로 리스닝 중: tcXXXXXXXXX

클라이언트:

$ tailcat tcXXXXXXXXX 8080 GET / HTTP/1.1 Host: foo HTTP/1.1 200 OK ....

인증 없는 SSH 서버

Linux와 macOS에서는 인증 없는 SSH 서버도 실행할 수 있습니다. (인증을 원한다면 tailcat --serve=22로 시스템 SSH 서버에 프록시하면 됩니다)

$ tailcat --serve=no-auth-ssh

🐈 서버가 새 주소로 리스닝 중: tcXXXXXXXXX

클라이언트 쪽:

$ tailcat ssh tcXXXXXXXXX $ tailcat ssh tcXXXXXXXXX ls -la

기타 명령

연결 테스트용 ping: 각 pong은 DERP 릴레이 경유인지 직접 경로인지 보고합니다. --until-direct는 직접 경로가 작동할 때까지(--timeout, 기본 10초) 계속 핑을 보내며, 실패 시 0이 아닌 값으로 종료합니다:

$ tailcat ping --until-direct pong in 42.1ms via DERP(sfo) pong in 1.2ms via 203.0.113.7:41641

터널을 경유하는 SOCKS5 프록시를 통해 명령 실행:

$ tailcat socks curl http://server.tailcat:8081/

토큰은 URL 호스트명으로도 직접 사용할 수 있습니다: SOCKS 프록시가 이를 인식하여 다이얼하므로 토큰 인자는 선택 사항입니다. (토큰은 대소문자를 구분하며, curl과 대부분의 CLI 도구에서 동작하지만 호스트명을 소문자로 바꾸는 브라우저에서는 동작하지 않습니다.)

$ tailcat socks curl http://:8081/

클라이언트가 서버의 네트워크에 접근할 수 있도록 exit node로 동작:

$ tailcat --serve=exit-node

연결 토큰을 파싱하여 내용(서버의 WireGuard 공개 키와 DERP 정보)을 아무것에도 연결하지 않고 JSON으로 출력:

$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu { "ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34", "RegionID": 302 }

짧은 토큰(ID로 DERP 지역을 참조하므로 클라이언트가 DERP 맵을 가져와야 함)을 더 긴 자기 완결형 토큰으로 변환하는 것도 가능합니다.

원문 보기
원문 보기 (영어)
"Tailscale without Tailscale, by Tailscale" Tailcat Tailcat is a remix of Tailscale open source pieces to act like netcat , but over Tailscale's data plane, without Tailscale's control plane. Tailscale's data plane ( magicsock , internally) gives you point-to-point WireGuard®-encrypted tunnels between two machines with DERP as the NAT-hole-punching communication side channel and the ultimate relay-of-last-resort if NAT traversal fails. Instead of using the Tailscale control plane, all tailcat connection metadata is exchanged out of band, however you want. The tailcat CLI (in cmd/tailcat ) is built on the tailcat Go library (importable as github.com/tailscale/tailcat ). Whether you use tailcat as a CLI tool or library, one side runs a tailcat server (listener) and gets back a short connection token. The other side passes that token to tailcat 's client side to connect. All traffic between the two is encrypted end-to-end with WireGuard. The initial connection bootstraps through Tailscale's DERP relay network, and then magicsock performs NAT traversal to upgrade to a direct peer-to-peer UDP connection when possible (usually!). You don't need a Tailscale account, root/admin access on the machine (it doesn't alter your machine's routing tables, DNS, etc.). It's just a userspace library and CLI tool. And it's all open source. You can use our free rate-limited DERP relays (the default DERP map is https://tailcat.dev/derpmap.json ) or you can run your own . Usage Pipe stdin/stdout between two machines Server starts, printing out its ephemeral address: $ tailcat # Selected bootstrap relay region 302, San Francisco # 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu (hangs, waiting...) And then the client can: $ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu $ Then the server unblocks: $ tailcat # Selected bootstrap relay region 302, San Francisco # 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu hello $ Expose local ports through the tunnel Or you can serve a local TCP port, forwarded to localhost: $ tailcat --serve=8080,8443 # or --serve=all # 🐈 Server listening with new address: tcXXXXXXXXX And then the client: $ tailcat tcXXXXXXXXX 8080 GET / HTTP/1.1 Host: foo HTTP/1.1 200 OK .... Auth-free SSH server On Linux and macOS, you can run an SSH server too with no auth. (If you want auth, you can just tailcat --serve=22 and proxy to your system SSH server) $ tailcat --serve=no-auth-ssh # 🐈 Server listening with new address: tcXXXXXXXXX And on the client side: $ tailcat ssh tcXXXXXXXXX $ tailcat ssh tcXXXXXXXXX ls -la Misc commands Ping to test connectivity; each pong reports whether it arrived via a DERP relay or a direct path. --until-direct keeps pinging (up to --timeout , default 10s) until a direct path works, exiting non-zero if one doesn't: $ tailcat ping --until-direct < token > pong in 42.1ms via DERP(sfo) pong in 1.2ms via 203.0.113.7:41641 Run a command through a SOCKS5 proxy routed over the tunnel: $ tailcat socks < token > curl http://server.tailcat:8081/ Tokens also work directly as URL hostnames: the SOCKS proxy recognizes and dials them, so the token argument is optional. (Tokens are case-sensitive; this works with curl and most CLI tools, but not with browsers, which lowercase hostnames.) $ tailcat socks curl http:// < token > :8081/ Act as an exit node so the client can reach the server's network: $ tailcat --serve=exit-node Parse a connection token and print its contents (the server's WireGuard public key and DERP info) as JSON, without connecting to anything: $ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu { " ServerPublic " : " nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34 " , " RegionID " : 302 } Resolve a short token (which references a DERP region by ID, requiring clients to fetch the DERP map) into a longer self-contained one with the DERP server info embedded, letting clients connect more quickly: $ tailcat resolve tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA Parsing that resolved token shows the embedded DERP info: $ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA { " ServerPublic " : " nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34 " , " Region " : [ { " Nodes " : [ { " HostName " : " tc302a.ipn.dev " , " IPv4 " : " 208.111.39.38 " , " IPv6 " : " 2607:f740:0:3f::720 " } ] } ] } A server can print the long self-contained form directly with the --full-address flag. Key Management A server's address (connection token) is derived from its WireGuard key, so the key you use determines who can reach you: Ephemeral keys (the default): each server run generates a fresh key in memory and prints an address nobody has ever seen. When the process exits, the key is discarded and the address is dead forever. This is the safe default: sharing that address only ever refers to that one run. Saved keys: tailcat genkey generates a key saved to disk so the address stays stable across restarts. The flip side: anyone you've ever shared that address with can connect to any future server using that key, unless you restrict clients with --allow (see tailcat genkey --client ). The CLI says at startup which kind it's using, so you know whether you're starting a fresh single-use server or re-listening on an address you may have shared in the past. $ tailcat genkey --region=nyc # prints the token; key saved to ~/.config/tailcat/keys/default.private.json # later; the key named "default" is used automatically once it exists: $ tailcat --serve=8080 # 🐈 Server listening with saved key "default": tcXXXXXXXXX # ... unless you force a one-off ephemeral key: $ tailcat --serve=8080 --key=new # 🐈 Server listening with new address: tcXXXXXXXXX That is, default is a magic key name: once it exists, plain tailcat silently uses it instead of generating an ephemeral key, and the startup line above is what tells you which happened. Use --key=new to get an ephemeral key anyway, --key=<name> to use a different saved key, or tailcat genkey --delete --key=default to remove the saved default key. tailcat genkey --list lists your saved keys. Tokens can also be published as DNS TXT records and looked up by name; a DNS name works anywhere the CLI takes a token: # If example.com has a TXT record "tailcat=tc..." $ tailcat example.com 8080 $ tailcat ssh example.com $ tailcat ping example.com Examples Protected SSH server over DNS Who needs port forwarding or port knocking? This runs an SSH server reachable from anywhere by name, with no open inbound ports on the server, where WireGuard authenticates the client before the SSH server ever sees a packet. On the client machine, generate a client identity keypair. It prints the public key, which is all the server needs to know: client$ tailcat genkey --client # wrote file to ~/.config/tailcat/keys/client-default.private.json nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16 On the server, generate a server keypair pinned to its nearest DERP region (see why below), then serve SSH to only that client: server$ tailcat genkey --fixed-region # wrote file to ~/.config/tailcat/keys/default.private.json tcXXXXXXXXX server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16 # 🐈 Server listening with saved key "default": tcXXXXXXXXX Publish the token in DNS as a TXT record: my-server.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX" And then the client side is just: client$ tailcat ssh my-server.example.com Client modes automatically use the saved client-default key when it exists, so no extra flags are needed to present the allowed identity. Anyone else's handshake is silentl