2024-08-05 19:06:48 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The tta server is the Tailscale Test Agent.
//
// It runs on each Tailscale node being integration tested and permits the test
// harness to control the node. It connects out to the test drver (rather than
// accepting any TCP connections inbound, which might be blocked depending on
// the scenario being tested) and then the test driver turns the TCP connection
// around and sends request back.
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
2024-08-07 19:41:26 +00:00
"net/http/httputil"
"net/url"
2024-08-05 19:06:48 +00:00
"os"
"os/exec"
"strings"
"sync"
"time"
2024-08-07 19:41:26 +00:00
"tailscale.com/client/tailscale"
"tailscale.com/util/must"
2024-08-05 19:06:48 +00:00
"tailscale.com/util/set"
"tailscale.com/version/distro"
)
var (
driverAddr = flag . String ( "driver" , "test-driver.tailscale:8008" , "address of the test driver; by default we use the DNS name test-driver.tailscale which is special cased in the emulated network's DNS server" )
)
2024-08-07 19:41:26 +00:00
func absify ( cmd string ) string {
2024-08-05 19:06:48 +00:00
if distro . Get ( ) == distro . Gokrazy && ! strings . Contains ( cmd , "/" ) {
2024-08-07 19:41:26 +00:00
return "/user/" + cmd
2024-08-05 19:06:48 +00:00
}
2024-08-07 19:41:26 +00:00
return cmd
}
func serveCmd ( w http . ResponseWriter , cmd string , args ... string ) {
out , err := exec . Command ( absify ( cmd ) , args ... ) . CombinedOutput ( )
2024-08-05 19:06:48 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
if err != nil {
w . Header ( ) . Set ( "Exec-Err" , err . Error ( ) )
w . WriteHeader ( 500 )
}
w . Write ( out )
}
2024-08-07 19:41:26 +00:00
type localClientRoundTripper struct {
lc * tailscale . LocalClient
}
func ( rt localClientRoundTripper ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
return rt . lc . DoLocalRequest ( req )
}
2024-08-05 19:06:48 +00:00
func main ( ) {
if distro . Get ( ) == distro . Gokrazy {
cmdLine , _ := os . ReadFile ( "/proc/cmdline" )
if ! bytes . Contains ( cmdLine , [ ] byte ( "tailscale-tta=1" ) ) {
// "Exiting immediately with status code 0 when the
// GOKRAZY_FIRST_START=1 environment variable is set means “don’ t
// start the program on boot”"
return
}
}
flag . Parse ( )
2024-08-07 19:41:26 +00:00
logc , err := net . Dial ( "tcp" , "9.9.9.9:124" )
if err == nil {
log . SetOutput ( logc )
}
2024-08-05 19:06:48 +00:00
log . Printf ( "Tailscale Test Agent running." )
var mux http . ServeMux
var hs http . Server
hs . Handler = & mux
var (
stMu sync . Mutex
newSet = set . Set [ net . Conn ] { } // conns in StateNew
)
needConnCh := make ( chan bool , 1 )
hs . ConnState = func ( c net . Conn , s http . ConnState ) {
stMu . Lock ( )
defer stMu . Unlock ( )
switch s {
case http . StateNew :
newSet . Add ( c )
2024-08-07 00:33:45 +00:00
default :
2024-08-05 19:06:48 +00:00
newSet . Delete ( c )
}
if len ( newSet ) == 0 {
select {
case needConnCh <- true :
default :
}
}
}
conns := make ( chan net . Conn , 1 )
2024-08-07 19:41:26 +00:00
var lc tailscale . LocalClient
rp := httputil . NewSingleHostReverseProxy ( must . Get ( url . Parse ( "http://local-tailscaled.sock" ) ) )
rp . Transport = localClientRoundTripper { & lc }
mux . Handle ( "/localapi/" , rp )
2024-08-05 19:06:48 +00:00
mux . HandleFunc ( "/" , func ( w http . ResponseWriter , r * http . Request ) {
io . WriteString ( w , "TTA\n" )
return
} )
mux . HandleFunc ( "/up" , func ( w http . ResponseWriter , r * http . Request ) {
2024-08-07 00:33:45 +00:00
serveCmd ( w , "tailscale" , "up" , "--login-server=http://control.tailscale" )
2024-08-05 19:06:48 +00:00
} )
mux . HandleFunc ( "/status" , func ( w http . ResponseWriter , r * http . Request ) {
serveCmd ( w , "tailscale" , "status" , "--json" )
} )
mux . HandleFunc ( "/ping" , func ( w http . ResponseWriter , r * http . Request ) {
target := r . FormValue ( "target" )
2024-08-07 19:41:26 +00:00
cmd := exec . Command ( absify ( "tailscale" ) , "ping" , target )
2024-08-05 19:06:48 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
w . ( http . Flusher ) . Flush ( )
cmd . Stdout = w
cmd . Stderr = w
if err := cmd . Run ( ) ; err != nil {
fmt . Fprintf ( w , "error: %v\n" , err )
}
} )
go hs . Serve ( chanListener ( conns ) )
var lastErr string
needConnCh <- true
for {
<- needConnCh
c , err := connect ( )
log . Printf ( "Connect: %v" , err )
if err != nil {
s := err . Error ( )
if s != lastErr {
log . Printf ( "Connect failure: %v" , s )
}
lastErr = s
time . Sleep ( time . Second )
continue
}
conns <- c
time . Sleep ( time . Second )
}
}
func connect ( ) ( net . Conn , error ) {
c , err := net . Dial ( "tcp" , * driverAddr )
if err != nil {
return nil , err
}
return c , nil
}
2024-08-07 00:33:45 +00:00
type chanListener <- chan net . Conn
2024-08-05 19:06:48 +00:00
func ( cl chanListener ) Accept ( ) ( net . Conn , error ) {
c , ok := <- cl
if ! ok {
return nil , errors . New ( "closed" )
}
return c , nil
}
func ( cl chanListener ) Close ( ) error {
return nil
}
func ( cl chanListener ) Addr ( ) net . Addr {
return & net . TCPAddr {
IP : net . ParseIP ( "52.0.0.34" ) , // TS..DR(iver)
Port : 123 ,
}
}