- 만우절 발표로 시작된 Plan 9 지원은 실제 PR과 커널·Go 수정으로 이어졌고, 2025년 4월 2일 기준 Tailscale이 Plan 9에서 동작하는 상태까지 도달함
- 단순한
GOOS=plan9 GOARCH=386빌드 문제가 아니라, Go의 Plan 9 포트에서 런타임 크래시와 컴파일러 특수 처리 문제가 먼저 드러남 - Russ Cox의 Plan 9 커널 및 Go 런타임·컴파일러 수정으로 SSE, 부동소수점 컨텍스트, 단조 시간, DNS, 개발 환경 문제가 함께 정리됨
- Tailscale은 Plan 9의
/net파일 인터페이스를 활용해 TUN 유사 구현, 라우팅, Tailscale SSH, MagicDNS, 서비스 수집을 붙였지만 일부는 임시 구현이거나 미완성임 - 현재 검증 범위는 주로 9legacy와
GOARCH=386이며, 9front·amd64·exit node·Gonet/netns지원은 추가 검증이나 재설계가 필요함
만우절 농담이 실제 포팅으로 이어짐
- Tailscale은 2025년 4월 1일 Plan 9 지원을 발표했고, 다음 날 이 발표가 실제 동작하는 포팅 작업을 바탕으로 했다는 배경을 공개함
- 작업은 Tailscale PR과 여러 Plan 9·Go 수정으로 이어짐
- 초기 접근은 Tailscale의 Go 바이너리 두 개를
GOOS=plan9 GOARCH=386 go install ./cmd/tailscale{,d}로 빌드하면 될 것이라는 기대에서 출발함 - 2023년 8월 첫 시도에서는 일부 빌드가 진행됐지만 실행 중 비정상 크래시가 발생함
- Go의 Plan 9 포트가 first-class port가 아니어서 회귀가 방치된 상태였음
- Tailscale이 Plan 9에서 기존보다 Go를 더 강하게 밀어붙였을 가능성도 있음
- 포팅은 2024년 내내 멈췄다가, 2025년 3월 만우절 아이디어와 함께 다시 진행됨
SSE와 Go의 Plan 9 지원 정리
- 1999년 Intel Pentium III에 도입된 SSE 명령어가 이번 작업의 주요 출발점 중 하나였음
- Go 컴파일러는 Plan 9 타깃에서 SSE 사용을 피하려고 했음
- Plan 9 커널이 note handler에서 SSE 레지스터를 저장·복원하지 않았기 때문임
- Go 컴파일러는 어떤 코드가 note handler 안에서 실행되는지 알 수 없어 SSE를 전역 비활성화하려 했음
- 이 특수 처리는 자주 깨졌고, 컴파일러 곳곳에
plan9예외가 많아짐
- Russ Cox는 Plan 9 커널이 note handler에서 부동소수점·SIMD 컨텍스트를 다루도록 수정함
- 386 쪽에는 sys/src/9: allow floating point in note handlers 수정이 들어감
- amd64 9k 커널에서는 fork 후 FP 상태 aliasing, note handler의 SIMD,
noted(NCONT)레지스터 손실 등 추가 문제가 발견됨
- Go 쪽에서는 Plan 9 코드 생성 특수 처리 제거가 진행되면서
tailscaled가 더 오래 실행될 수 있게 됨
IPC와 개발 환경
- 이후
tailscaled는 스택 손상 대신 메모리 부족으로 크래시하기 시작함 - 이전 Plan 9 포팅 시도에서 Tailscale의
safesocketIPC 패키지에 무한히 goroutine을 만드는 버그가 있었음 - 우선 localhost TCP를 쓰도록 바꾸면서 문제가 해결됨
- Plan 9의 “모든 것이 파일” 방식에는 덜 어울리지만, 다른 Plan 9 서비스도 localhost TCP를 사용한다고 확인함
- 향후에는 Russ가 Go로 포팅한 srv9p package를 이용해 LocalAPI를 얹는 방식이 더 나을 수 있음
- 현재 구현은 다른 플랫폼처럼 localhost 인증을 붙이지 못하므로, 공유 Plan 9 머신에서는 쓰지 말라고 명시됨
- 초기 개발은 9legacy CD 이미지 기반 VM에서 진행됐고, 바이너리를 HTTP로 내려받아 실행하는 반복 과정이 느렸음
- Russ Cox가 만든 rsc/plan9은 Plan 9 소스, 사전 컴파일 바이너리,
./boot/qemu스크립트를 포함함- qemu VM은 디스크 없이 부팅하고, localhost 9P 서버가 제공하는 Git 저장소를 루트 파일시스템으로 사용함
- 개발 머신과 Plan 9 파일시스템을 공유해 반복 시간이 분 단위에서 초 단위로 줄어듦
- qemu는 virtio도 사용해 더 빨라짐
네트워크 통합: TUN, 라우팅, MagicDNS
- 처음 동작한 Tailscale은 커널 네트워크 스택을 쓰지 않는 사용자 공간 네트워킹 모드였음
- TCP, UDP, ICMP 등은 gVisor의 netstack을 통해 처리됨
- Plan 9 머신에서 tailnet으로 접근하려면
tailscaled의 HTTP/SOCKS5 프록시를 써야 함 - Plan 9 프로그램 중
HTTP_PROXY나ALL_PROXY환경 변수를 인식하는 경우가 거의 없어 이상적이지 않았음
- Plan 9의 TUN 유사 구현은 매우 단순했음
/net/ipifc/clone을 열고 새 인터페이스 번호를 읽음- control fd에
"bind pkt\n"를 쓰면/net/ipifc/2/*같은 새 인터페이스가 생김 /net/ipifc/2/data를 열어 IP 패킷을 그대로 읽고 씀- 별도 ioctl이나 길이 프레이밍이 필요 없음
- 라우팅 테이블 조작도
/net/iproute파일을 통해 이뤄짐"tag tail\n"을 써서 이후 추가되는 route에tail태그를 붙임"add 100.64.0.0 /106 100.102.103.104"같은 메시지로 route를 추가함- Plan 9 내부가 IPv6 중심이고 IPv4를 IPv4-mapped IPv6 주소로 다루기 때문에 CGNAT
100.64.0.0/10이/106처럼 표현됨
- MagicDNS는 Plan 9에서 peer를
foo또는foo.tailnet-name.ts.net같은 이름으로 접근하게 만드는 작업이었음/net/dns나/net/cs질의를 가로채는 방안도 논의됨- 최종적으로 Russ가 특정 DNS suffix에 대해 대체 DNS 서버를 지정할 수 있도록 Plan 9을 수정함
- DNS 질의가 잘못 negative cache되는 문제도 수정됨
Tailscale SSH와 서비스 수집
- Tailscale SSH는
tailscaled내장 SSH 서버로, 패킷과 연결된 WireGuard 키를 통해 알려진 Tailscale identity로 인증함 - 처음에는 Plan 9 셸인
/bin/rc를os/exec.Command로 실행하고 stdin/stdout을 연결함- 셸은 실행됐지만 echo, 내비게이션, 프로세스 interrupt 등이 제대로 동작하지 않았음
- Russ는 netshell example을 9fans/go에 추가함
- 이 예제는 매우 안전하지 않은 telnet 서버에 가까웠지만, Tailscale SSH 뒤에 붙이기에는 충분했음
- 이후 SSH로 Plan 9의
/dev/snarf내용을 가져오거나, 노트북에서 Go 테스트를 cross-compile한 뒤 SSH로 실행하기 쉬워짐
- Tailscale의 선택 기능인 서비스 수집도 Plan 9에 맞춰 검토됨
/proc/NNN/fd를 순회해/net/tcp/clone을 연 프로세스를 찾음- fd의 QID를
/net/tcp/NNN/{status,local}와 맞춰 listening 여부와 포트를 확인함 - QID에서 TCP 번호를 계산하는 방식은 커널 구현 변화에 취약해 아쉬운 부분으로 남음
시간, 웹 데모, v86
- gVisor netstack이 단조 시간(monotonic time)이 뒤로 갔다고 보고하면서
tailscaled가 크래시하는 경우가 있었음- Go의 Plan 9 시간 구현이 단조 시간으로 wall time을 쓰고 있었음
- ntpd가 시계를 뒤로 조정하면 netstack의 단조 시간 가정이 깨짐
- Russ는 Plan 9의
/dev/bintime에 단조 시간을 추가하고, Go가 이를 사용하도록 수정함 - 웹에서 Plan 9을 실행하는 데는 v86이 사용됨
- v86은 WASM으로 32비트 운영체제를 실행하고 여러 네트워크 방식을 제공함
- 이 점도
GOARCH=386에 집중한 이유 중 하나였음
- 처음에는 이더넷 프레임을 websocket relay로 보내기 위해 Tailscale의 네트워크 시뮬레이션 환경에 wsproxy protocol support를 추가함
- ARP, DHCP, DNS, NAT, control plane, DERP 등을 gVisor netstack으로 흉내 내는 통합 테스트 환경에서 동작함
- 하지만 DHCP 왕복 때문에 relay가 멀면 Plan 9 GUI인
rio시작이 느려짐
- 이후 WISP 서버도 구현했지만, 프로덕션화 전에 시간이 부족해 copy.sh/v86의 기본 네트워크 relay 설정으로 출시함
- Tailscale과 Plan 9을 넣은 디스크 이미지는 16MB였고, Tailscale 바이너리는 압축 해제 후 23MB였음
- 부팅 시 “gunzip…” 단계가 보이는 이유가 여기에 있음
- 예제 이미지는
copy.sh/v86의 9legacy 프로필에 포함됨
남은 작업과 실제 성과
- 현재 Tailscale Plan 9 포팅은 9legacy에서만 테스트됨
- Plan 9의 주요 fork에는 최소한의 9legacy와 더 많이 수정된 9front가 있음
- Russ가 9legacy에 작성한 일부 패치는 9front로 포팅이 필요할 수 있음
GOARCH=amd6464비트 지원도 아직 검증이 필요함- exit node 지원과 Go
net/netns패키지 지원은 구현되지 않음- 이를 위해서는 Tailscale이 Plan 9에서 자신을 별도
/net으로 드러내는 방식 같은 재검토가 필요할 수 있음
- 이를 위해서는 Tailscale이 Plan 9에서 자신을 별도
- 이번 작업으로 Go의 Plan 9 지원도 개선됨
- cmd/compile: use FMA on plan9, and drop UseFMA
- runtime: remove nextSampleNoFP from plan9
- cmd/compile, runtime: remove plan9 special case avoiding SSE
- net: fix parsing of interfaces on plan9 without associated devices
- os: guarantee min buffer size for ReadFile reads on /proc-like files
- net: unblock UDP Reads upon Close on plan9, add test
- runtime: fix plan9 monotonic time, crypto randomness
- 특히 Go 컴파일러에서 Plan 9 특수 처리를 제거하면서 컴파일러가 더 단순해지고 수정하기 쉬워짐
- v86 데모 공개 시점에는 v86 작성자의 만우절 장난으로 VGA 텍스트 출력까지 가짜 네덜란드어처럼 표시되는 문제가 있었지만,
&nojokequery argument로 피할 수 있었음