From 72c10539e2084d3c0d62e028a9a9f85259b70b68 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Wed, 1 Mar 2023 20:21:18 -0800 Subject: [PATCH] libtailscale: add tailscale_loopback_api function Signed-off-by: David Crawshaw --- tailscale.c | 5 ++++ tailscale.go | 40 +++++++++++++++++++++++++++++++ tailscale.h | 11 +++++++++ tsnetctest/tsnetctest.go | 52 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/tailscale.c b/tailscale.c index acdd325..091a80e 100644 --- a/tailscale.c +++ b/tailscale.c @@ -19,6 +19,7 @@ extern int TsnetSetLogFD(int sd, int fd); extern int TsnetListen(int sd, char* net, char* addr, int* listenerOut); extern int TsnetListenerClose(int ld); extern int TsnetAccept(int ld, int* connOut); +extern int TsnetLoopbackAPI(int sd, char* addrOut, size_t addrLen, char* credOut); tailscale tailscale_new() { return TsnetNewServer(); @@ -71,6 +72,10 @@ int tailscale_set_logfd(tailscale sd, int fd) { return TsnetSetLogFD(sd, fd); } +int tailscale_loopback_api(tailscale sd, char* addr_out, size_t addrlen, char cred_out[static 33]) { + return TsnetLoopbackAPI(sd, addr_out, addrlen, cred_out); +} + int tailscale_errmsg(tailscale sd, char* buf, size_t buflen) { return TsnetErrmsg(sd, buf, buflen); } diff --git a/tailscale.go b/tailscale.go index 5329b44..f80af39 100644 --- a/tailscale.go +++ b/tailscale.go @@ -362,3 +362,43 @@ func TsnetSetLogFD(sd C.int, fd int) C.int { } return 0 } + +//export TsnetLoopbackAPI +func TsnetLoopbackAPI(sd C.int, addrOut *C.char, addrLen C.size_t, credOut *C.char) C.int { + // Panic here to ensure we always leave the out values NUL-terminated. + if addrOut == nil { + panic("loopback_api passed nil addr_out") + } else if addrLen == 0 { + panic("loopback_api passed addrlen of 0") + } + + // Start out NUL-termianted to cover error conditions. + *addrOut = '\x00' + *credOut = '\x00' + + s, err := getServer(sd) + if err != nil { + return s.recErr(err) + } + addr, cred, err := s.s.LoopbackLocalAPI() + if err != nil { + return s.recErr(err) + } + if len(cred) != 32 { + return s.recErr(fmt.Errorf("libtailscale: len(cred)=%d, want 32", len(cred))) + } + if len(addr)+1 > int(addrLen) { + return s.recErr(fmt.Errorf("libtailscale: loopback addr of %d bytes is too long for addrlen %d", len(addr), addrLen)) + } + out := unsafe.Slice((*byte)(unsafe.Pointer(addrOut)), addrLen) + n := copy(out, addr) + out[n] = '\x00' + + // credOut is non-nil and 33 bytes long because + // it's defined in C as char cred_out[static 33]. + out = unsafe.Slice((*byte)(unsafe.Pointer(credOut)), 33) + copy(out, cred) + out[32] = '\x00' + + return 0 +} diff --git a/tailscale.h b/tailscale.h index 9c67fc8..4b54d17 100644 --- a/tailscale.h +++ b/tailscale.h @@ -118,6 +118,17 @@ extern int tailscale_listener_close(tailscale_listener listener); // -1 - call tailscale_errmsg for details extern int tailscale_accept(tailscale_listener listener, tailscale_conn* conn_out); +// tailscale_loopback_api starts a LocalAPI listener on a loopback address. +// +// To make http requests to the LocalAPI: +// 1. cred must be provided as the basic auth password +// 2. the header "Sec-Tailscale: localapi" must be set +// +// The NUL-terminated ip:port address of the LocalAPI is written to addr_out. +// The 32-byte basic auth password + NUL-terminator is written to cred_out. +// +// Returns zero on success or -1 on error, call tailscale_errmsg for details. +extern int tailscale_loopback_api(tailscale sd, char* addr_out, size_t addrlen, char cred_out[static 33]); // tailscale_errmsg writes the details of the last error to buf. // diff --git a/tsnetctest/tsnetctest.go b/tsnetctest/tsnetctest.go index 0679085..e2a3d34 100644 --- a/tsnetctest/tsnetctest.go +++ b/tsnetctest/tsnetctest.go @@ -17,9 +17,15 @@ char* tmps2; char* control_url = 0; +int addrlen = 128; +char* addr = NULL; +char* cred = NULL; + int errlen = 512; char* err = NULL; +tailscale s1, s2; + int set_err(tailscale sd, char tag) { err[0] = tag; err[1] = ':'; @@ -30,9 +36,11 @@ int set_err(tailscale sd, char tag) { int test_conn() { err = calloc(errlen, 1); + addr = calloc(addrlen, 1); + cred = calloc(33, 1); int ret; - tailscale s1 = tailscale_new(); + s1 = tailscale_new(); if ((ret = tailscale_set_control_url(s1, control_url)) != 0) { return set_err(s1, '0'); } @@ -46,7 +54,7 @@ int test_conn() { return set_err(s1, '3'); } - tailscale s2 = tailscale_new(); + s2 = tailscale_new(); if ((ret = tailscale_set_control_url(s2, control_url)) != 0) { return set_err(s2, '4'); } @@ -102,11 +110,20 @@ int test_conn() { if ((ret = tailscale_listener_close(ln)) != 0) { return set_err(s1, 'a'); } - if ((ret = tailscale_close(s1)) != 0) { + + if ((ret = tailscale_loopback_api(s1, addr, addrlen, cred)) != 0) { return set_err(s1, 'b'); } - if ((ret = tailscale_close(s2)) != 0) { - return set_err(s2, 'c'); + + return 0; +} + +int close_conn() { + if (tailscale_close(s1) != 0) { + return set_err(s1, 'd'); + } + if (tailscale_close(s2) != 0) { + return set_err(s2, 'e'); } return 0; } @@ -114,6 +131,8 @@ int test_conn() { import "C" import ( "flag" + "io" + "net/http" "net/http/httptest" "os" "path/filepath" @@ -161,4 +180,27 @@ func RunTestConn(t *testing.T) { if C.test_conn() != 0 { t.Fatal(C.GoString(C.err)) } + + req, err := http.NewRequest("GET", "http://"+C.GoString(C.addr)+"/localapi/v0/status", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Sec-Tailscale", "localapi") + req.SetBasicAuth("", C.GoString(C.cred)) + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + b, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Errorf("/status: %d: %s", res.StatusCode, b) + } + + if C.close_conn() != 0 { + t.Fatal(C.GoString(C.err)) + } }