Files
tailscale/cmd/testcontrol/testcontrol.go
T
Brad Fitzpatrick a2263542f2 cmd/testcontrol: add --addr and --ssh-policy flags
The test control server always listened on 127.0.0.1:9911, which is
useless for a node in a VM on the same machine. --addr picks the listen
address; the DERP and STUN servers follow it.

--ssh-policy loads a tailcfg.SSHPolicy from a JSON or HuJSON file and
sends it to every node, which also grants them the SSH node capability so
that "tailscale up --ssh" is accepted. That is what testcontrol.Server
already supported for tests; this exposes it for manual testing of the
Tailscale SSH server.

Updates tailscale/corp#47865

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I2b7c4e9f1a3d5c6e8b0f2a4d6c8e1b3f5a7d9c0e
2026-09-15 20:03:21 -07:00

132 lines
2.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Program testcontrol runs a simple test control server.
package main
import (
"encoding/json"
"flag"
"log"
"net"
"net/http"
"os"
"testing"
"github.com/tailscale/hujson"
"tailscale.com/tailcfg"
"tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/logger"
)
var (
flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network")
flagAddr = flag.String("addr", "127.0.0.1:9911", "IP:port to listen on; the DERP and STUN servers also run on that IP")
flagSSHPolicy = flag.String("ssh-policy", "", "optional path to a JSON (or HuJSON) file holding a tailcfg.SSHPolicy to send to all nodes; it also grants them the SSH capability")
)
func main() {
flag.Parse()
host, _, err := net.SplitHostPort(*flagAddr)
if err != nil {
log.Fatalf("invalid --addr %q: %v", *flagAddr, err)
}
var t fakeTB
derpMap := integration.RunDERPAndSTUN(t, logger.Discard, host)
control := &testcontrol.Server{
DERPMap: derpMap,
ExplicitBaseURL: "http://" + *flagAddr,
}
if *flagSSHPolicy != "" {
pol, err := loadSSHPolicy(*flagSSHPolicy)
if err != nil {
log.Fatalf("loading --ssh-policy: %v", err)
}
control.SSHPolicy = pol
}
for range *flagNFake {
control.AddFakeNode()
}
mux := http.NewServeMux()
mux.Handle("/", control)
log.Printf("listening on %s", *flagAddr)
err = http.ListenAndServe(*flagAddr, mux)
log.Fatal(err)
}
func loadSSHPolicy(path string) (*tailcfg.SSHPolicy, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
b, err = hujson.Standardize(b)
if err != nil {
return nil, err
}
pol := new(tailcfg.SSHPolicy)
if err := json.Unmarshal(b, pol); err != nil {
return nil, err
}
return pol, nil
}
type fakeTB struct {
*testing.T
}
func (t fakeTB) Cleanup(_ func()) {}
func (t fakeTB) Error(args ...any) {
t.Fatal(args...)
}
func (t fakeTB) Errorf(format string, args ...any) {
t.Fatalf(format, args...)
}
func (t fakeTB) Fail() {
t.Fatal("failed")
}
func (t fakeTB) FailNow() {
t.Fatal("failed")
}
func (t fakeTB) Failed() bool {
return false
}
func (t fakeTB) Fatal(args ...any) {
log.Fatal(args...)
}
func (t fakeTB) Fatalf(format string, args ...any) {
log.Fatalf(format, args...)
}
func (t fakeTB) Helper() {}
func (t fakeTB) Log(args ...any) {
log.Print(args...)
}
func (t fakeTB) Logf(format string, args ...any) {
log.Printf(format, args...)
}
func (t fakeTB) Name() string {
return "faketest"
}
func (t fakeTB) Setenv(key string, value string) {
panic("not implemented")
}
func (t fakeTB) Skip(args ...any) {
t.Fatal("skipped")
}
func (t fakeTB) SkipNow() {
t.Fatal("skipnow")
}
func (t fakeTB) Skipf(format string, args ...any) {
t.Logf(format, args...)
t.Fatal("skipped")
}
func (t fakeTB) Skipped() bool {
return false
}
func (t fakeTB) TempDir() string {
panic("not implemented")
}