libtailscale: Tailscale C library

This commit is contained in:
David Crawshaw
2023-02-20 17:01:13 -08:00
committed by David Crawshaw
parent 0b08c888b9
commit 0dc2f30930
6 changed files with 853 additions and 0 deletions

61
example/echo_server.c Normal file
View File

@@ -0,0 +1,61 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//
// echo_server is a simple Tailscale node that echos any text sent to port 1999.
//
// To build and run it:
//
// cd libtailscale
// go build -buildmode=c-archive .
// cd example
// cc echo_server.c ../libtailscale.a
// TS_AUTHKEY=<your-auth-key> ./a.out
//
// On macOS you may need to add the following flags to your C compiler:
//
// -framework CoreFoundation -framework Security
//
#include "../tailscale.h"
#include <stdio.h>
#include <unistd.h>
int main(void) {
int ret;
tailscale ts = tailscale_new();
if (tailscale_set_ephemeral(ts, 1)) {
return err(ts);
}
if (tailscale_up(ts)) {
return err(ts);
}
tailscale_listener ln;
if (tailscale_listen(ts, "tcp", ":1999", &ln)) {
return err(ts);
}
while (1) {
tailscale_conn conn;
if (tailscale_accept(ln, &conn)) {
return err(ts);
}
char buf[2048];
while ((ret = read(conn, buf, sizeof(buf))) > 0) {
write(1, buf, ret);
}
close(conn);
}
tailscale_listener_close(ln);
tailscale_close(ts);
return 0;
}
char errmsg[256];
int err(tailscale ts) {
tailscale_errmsg(ts, errmsg, sizeof(errmsg));
printf("echo_server: %s\n", errmsg);
return 1;
}

76
tailscale.c Normal file
View File

@@ -0,0 +1,76 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
#include "tailscale.h"
// Functions exported by Go.
extern int TsnetNewServer();
extern int TsnetStart(int sd);
extern int TsnetUp(int sd);
extern int TsnetClose(int sd);
extern int TsnetErrmsg(int sd, char* buf, size_t buflen);
extern int TsnetDial(int sd, char* net, char* addr, int* connOut);
extern int TsnetSetDir(int sd, char* str);
extern int TsnetSetHostname(int sd, char* str);
extern int TsnetSetAuthKey(int sd, char* str);
extern int TsnetSetControlURL(int sd, char* str);
extern int TsnetSetEphemeral(int sd, int ephemeral);
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);
tailscale tailscale_new() {
return TsnetNewServer();
}
int tailscale_start(tailscale sd) {
return TsnetStart(sd);
}
int tailscale_up(tailscale sd) {
return TsnetUp(sd);
}
int tailscale_close(tailscale sd) {
return TsnetClose(sd);
}
int tailscale_dial(tailscale sd, const char* network, const char* addr, tailscale_conn* conn_out) {
return TsnetDial(sd, (char*)network, (char*)addr, (int*)conn_out);
}
int tailscale_listen(tailscale sd, const char* network, const char* addr, tailscale_listener* listener_out) {
return TsnetListen(sd, (char*)network, (char*)addr, (int*)listener_out);
}
int tailscale_listener_close(tailscale_listener ld) {
return TsnetListenerClose(ld);
}
int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) {
return TsnetAccept(ld, (int*)conn_out);
}
int tailscale_set_dir(tailscale sd, const char* dir) {
return TsnetSetDir(sd, (char*)dir);
}
int tailscale_set_hostname(tailscale sd, const char* hostname) {
return TsnetSetHostname(sd, (char*)hostname);
}
int tailscale_set_authkey(tailscale sd, const char* authkey) {
return TsnetSetAuthKey(sd, (char*)authkey);
}
int tailscale_set_control_url(tailscale sd, const char* control_url) {
return TsnetSetControlURL(sd, (char*)control_url);
}
int tailscale_set_ephemeral(tailscale sd, int ephemeral) {
return TsnetSetEphemeral(sd, ephemeral);
}
int tailscale_set_logfd(tailscale sd, int fd) {
return TsnetSetLogFD(sd, fd);
}
int tailscale_errmsg(tailscale sd, char* buf, size_t buflen) {
return TsnetErrmsg(sd, buf, buflen);
}

364
tailscale.go Normal file
View File

@@ -0,0 +1,364 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// A Go c-archive of the tsnet package. See tailscale.h for details.
package main
//#include "errno.h"
import "C"
import (
"context"
"fmt"
"net"
"os"
"sync"
"syscall"
"unsafe"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
)
func main() {}
// servers tracks all the allocated *tsnet.Server objects.
var servers struct {
mu sync.Mutex
next C.int
m map[C.int]*server
}
type server struct {
s *tsnet.Server
lastErr string
}
func getServer(sd C.int) (*server, error) {
servers.mu.Lock()
defer servers.mu.Unlock()
s := servers.m[sd]
if s == nil {
return nil, fmt.Errorf("tsnetc: unknown server descriptors %d (of %d servers)", sd, len(servers.m))
}
return s, nil
}
// listeners tracks all the tsnet_listener objects allocated via tsnet_listen.
var listeners struct {
mu sync.Mutex
next C.int
m map[C.int]*listener
}
type listener struct {
s *server
ln net.Listener
}
// conns tracks all the pipe(2)s allocated via tsnet_dial.
var conns struct {
mu sync.Mutex
m map[C.int]*conn // keyed by the FD given to C (w)
}
type conn struct {
s *tsnet.Server
c net.Conn
r, w *os.File // the r side is held by Go, w is given to C
}
func (s *server) recErr(err error) C.int {
if err == nil {
s.lastErr = ""
return 0
}
s.lastErr = err.Error()
return -1
}
//export TsnetNewServer
func TsnetNewServer() C.int {
servers.mu.Lock()
defer servers.mu.Unlock()
if servers.m == nil {
servers.m = map[C.int]*server{}
}
if servers.next == 0 {
servers.next = 42<<16 + 1
}
sd := servers.next
servers.next++
s := &server{s: &tsnet.Server{}}
servers.m[sd] = s
return (C.int)(sd)
}
//export TsnetStart
func TsnetStart(sd C.int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
return s.recErr(s.s.Start())
}
//export TsnetUp
func TsnetUp(sd C.int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
_, err = s.s.Up(context.Background()) // cancellation is via TsnetClose
return s.recErr(err)
}
//export TsnetClose
func TsnetClose(sd C.int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
servers.mu.Lock()
delete(servers.m, sd)
servers.mu.Unlock()
// TODO: cancel Up
// TODO: close related listeners / conns.
if err := s.s.Close(); err != nil {
return -1
}
return 0
}
//export TsnetErrmsg
func TsnetErrmsg(sd C.int, buf *C.char, buflen C.size_t) C.int {
if buf == nil {
panic("errmsg passed nil buf")
} else if buflen == 0 {
panic("errmsg passed buflen of 0")
}
servers.mu.Lock()
s := servers.m[sd]
servers.mu.Unlock()
out := unsafe.Slice((*byte)(unsafe.Pointer(buf)), buflen)
if s == nil {
out[0] = '\x00'
return C.EBADF
}
n := copy(out, s.lastErr)
if len(out) < len(s.lastErr)-1 {
out[len(out)-1] = '\x00' // always NUL-terminate
return C.ERANGE
}
out[n] = '\x00'
return 0
}
//export TsnetListen
func TsnetListen(sd C.int, network, addr *C.char, listenerOut *C.int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
ln, err := s.s.Listen(C.GoString(network), C.GoString(addr))
if err != nil {
return s.recErr(err)
}
listeners.mu.Lock()
if listeners.next == 0 {
// Arbitrary magic number that will hopefully help someone
// debug some type confusion one day.
listeners.next = 37<<16 + 1
}
if listeners.m == nil {
listeners.m = map[C.int]*listener{}
}
ld := listeners.next
listeners.next++
listeners.m[ld] = &listener{s: s, ln: ln}
listeners.mu.Unlock()
*listenerOut = ld
return 0
}
//export TsnetListenerClose
func TsnetListenerClose(ld C.int) C.int {
listeners.mu.Lock()
defer listeners.mu.Unlock()
l := listeners.m[ld]
err := l.ln.Close()
delete(listeners.m, ld)
if err != nil {
return l.s.recErr(err)
}
return 0
}
func newConn(s *server, netConn net.Conn, connOut *C.int) C.int {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
if err != nil {
return s.recErr(err)
}
w := os.NewFile(uintptr(fds[0]), "socketpair-w")
r := os.NewFile(uintptr(fds[1]), "socketpair-r")
c := &conn{s: s.s, c: netConn, r: r, w: w}
fdC := C.int(fds[0])
conns.mu.Lock()
if conns.m == nil {
conns.m = make(map[C.int]*conn)
}
conns.m[fdC] = c
conns.mu.Unlock()
connCleanup := func() {
r.Close()
w.Close()
conns.mu.Lock()
delete(conns.m, fdC)
conns.mu.Unlock()
}
go func() {
defer connCleanup()
var b [1 << 16]byte
for {
n, err := netConn.Read(b[:])
if err != nil {
return
}
if _, err := r.Write(b[:n]); err != nil {
return
}
}
}()
go func() {
defer connCleanup()
var b [1 << 16]byte
for {
n, err := r.Read(b[:])
if err != nil {
return
}
if _, err := netConn.Write(b[:n]); err != nil {
return
}
}
}()
*connOut = fdC
return 0
}
//export TsnetAccept
func TsnetAccept(ld C.int, connOut *C.int) C.int {
listeners.mu.Lock()
l := listeners.m[ld]
listeners.mu.Unlock()
if l == nil {
return C.EBADF
}
netConn, err := l.ln.Accept()
if err != nil {
return l.s.recErr(err)
}
return newConn(l.s, netConn, connOut)
}
//export TsnetDial
func TsnetDial(sd C.int, network, addr *C.char, connOut *C.int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
netConn, err := s.s.Dial(context.Background(), C.GoString(network), C.GoString(addr))
if err != nil {
return s.recErr(err)
}
return newConn(s, netConn, connOut)
}
//export TsnetSetDir
func TsnetSetDir(sd C.int, str *C.char) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
s.s.Dir = C.GoString(str)
return 0
}
//export TsnetSetHostname
func TsnetSetHostname(sd C.int, str *C.char) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
s.s.Hostname = C.GoString(str)
return 0
}
//export TsnetSetAuthKey
func TsnetSetAuthKey(sd C.int, str *C.char) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
s.s.AuthKey = C.GoString(str)
return 0
}
//export TsnetSetControlURL
func TsnetSetControlURL(sd C.int, str *C.char) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
s.s.ControlURL = C.GoString(str)
return 0
}
//export TsnetSetEphemeral
func TsnetSetEphemeral(sd C.int, e int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
if e == 0 {
s.s.Ephemeral = false
} else {
s.s.Ephemeral = true
}
return 0
}
//export TsnetSetLogFD
func TsnetSetLogFD(sd C.int, fd int) C.int {
s, err := getServer(sd)
if err != nil {
return s.recErr(err)
}
if fd == -1 {
s.s.Logf = logger.Discard
return 0
}
f := os.NewFile(uintptr(fd), "logfd")
s.s.Logf = func(format string, args ...any) {
fmt.Fprintf(f, format, args...)
}
return 0
}

151
tailscale.h Normal file
View File

@@ -0,0 +1,151 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//
// Tailscale C library.
//
// Use this library to compile Tailscale into your program and get
// an entirely userspace IP address on a tailnet.
//
// From here you can listen for other programs on your tailnet dialing
// you, or connect directly to other services.
//
#include <stddef.h>
// tailscale is a handle onto a Tailscale server.
typedef int tailscale;
// tailscale_new creates a tailscale server object.
//
// No network connection is initialized until tailscale_start is called.
extern tailscale tailscale_new();
// tailscale_start connects the server to the tailnet.
//
// Calling this function is optional as it will be called by the first use
// of tailscale_listen or tailscale_dial on a server.
//
// See also: tailscale_up.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_start(tailscale sd);
// tailscale_up connects the server to the tailnet and waits for it to be usable.
//
// To cancel an in-progress call to tailscale_up, use tailscale_close.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_up(tailscale sd);
// tailscale_close shuts down the server.
//
// Returns zero on success or -1 on error. No error details are available.
extern int tailscale_close(tailscale sd);
// The following set tailscale configuration options.
//
// Configure these options before any explicit or implicit call to tailscale_start.
//
// For details of each value see the godoc for the fields of tsnet.Server.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_set_dir(tailscale sd, const char* dir);
extern int tailscale_set_hostname(tailscale sd, const char* hostname);
extern int tailscale_set_authkey(tailscale sd, const char* authkey);
extern int tailscale_set_control_url(tailscale sd, const char* control_url);
extern int tailscale_set_ephemeral(tailscale sd, int ephemeral);
// tailscale_set_logfd instructs the tailscale instance to write logs to fd.
//
// An fd value of -1 means discard all logging.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_set_logfd(tailscale sd, int fd);
// A tailscale_conn is a connection to an address on the tailnet.
//
// It is a pipe(2) on which you can use read(2), write(2), and close(2).
// For extra control over the connection, see the tailscale_conn_* functions.
typedef int tailscale_conn;
// tailscale_dial connects to the address on the tailnet.
//
// The newly allocated connection is written to conn_out.
//
// network is a NUL-terminated string of the form "tcp", "udp", etc.
// addr is a NUL-terminated string of an IP address or domain name.
//
// It will start the server if it has not been started yet.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_dial(tailscale sd, const char* network, const char* addr, tailscale_conn* conn_out);
// tailscale_conn_localaddr writes the local address into buf.
//
// After returning, buf is always NUL-terminated.
//
// Returns:
// 0 - success
// EBADF - conn is not a valid tailscale
// ERANGE - insufficient storage for buf
extern int tailscale_conn_localaddr(tailscale_conn conn, char* buf, char* buflen);
// tailscale_conn_remoteaddr writes the remote address into buf.
//
// After returning, buf is always NUL-terminated.
//
// Returns:
// 0 - success
// EBADF - conn is not a valid tailscale
// ERANGE - insufficient storage for buf
extern int tailscale_conn_remoteaddr(tailscale_conn conn, char* buf, char* buflen);
// A tailscale_listener is a socket on the tailnet listening for connections.
//
// It is much like allocating a system socket(2) and calling listen(2).
// Because it is not a system socket, operate on it using the functions
// tailscale_accept and tailscale_listener_close.
typedef int tailscale_listener;
// tailscale_listen listens for a connection on the tailnet.
//
// It is the spiritual equivalent to listen(2).
// The newly allocated listener is written to listener_out.
//
// network is a NUL-terminated string of the form "tcp", "udp", etc.
// addr is a NUL-terminated string of an IP address or domain name.
//
// It will start the server if it has not been started yet.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_listen(tailscale sd, const char* network, const char* addr, tailscale_listener* listener_out);
// tailscale_listener_close closes the listener.
//
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_listener_close(tailscale_listener listener);
// tailscale_accept accepts a connection on a tailscale_listener.
//
// It is the spiritual equivalent to accept(2).
//
// The newly allocated connection is written to conn_out.
//
// Returns:
// 0 - success
// EBADF - listener is not a valid tailscale
// -1 - call tailscale_errmsg for details
extern int tailscale_accept(tailscale_listener listener, tailscale_conn* conn_out);
// tailscale_errmsg writes the details of the last error to buf.
//
// After returning, buf is always NUL-terminated.
//
// Returns:
// 0 - success
// EBADF - sd is not a valid tailscale
// ERANGE - insufficient storage for buf
extern int tailscale_errmsg(tailscale sd, char* buf, size_t buflen);

37
tailscale_test.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"testing"
"github.com/tailscale/libtailscale/tsnetctest"
)
func TestConn(t *testing.T) {
tsnetctest.RunTestConn(t)
// RunTestConn cleans up after itself, so there shouldn't be
// anything left in the global maps.
conns.mu.Lock()
rem := len(conns.m)
conns.mu.Unlock()
if rem > 0 {
t.Fatalf("want no remaining tsnet_conn objects, got %d", rem)
}
listeners.mu.Lock()
rem = len(listeners.m)
listeners.mu.Unlock()
if rem > 0 {
t.Fatalf("want no remaining tsnet_listener objects, got %d", rem)
}
servers.mu.Lock()
rem = len(servers.m)
servers.mu.Unlock()
if rem > 0 {
t.Fatalf("want no remaining tsnet objects, got %d", rem)
}
}

164
tsnetctest/tsnetctest.go Normal file
View File

@@ -0,0 +1,164 @@
// Package tsnetctest tests the libtailscale C bindings.
//
// It is used by tailscale_test.go, because you are not allowed to
// use the 'import "C"' directive in tests.
package tsnetctest
/*
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "../tailscale.h"
char* tmps1;
char* tmps2;
char* control_url = 0;
int errlen = 512;
char* err = NULL;
int set_err(tailscale sd, char tag) {
err[0] = tag;
err[1] = ':';
err[2] = ' ';
tailscale_errmsg(sd, &err[3], errlen-3);
return 1;
}
int test_conn() {
err = calloc(errlen, 1);
int ret;
tailscale s1 = tailscale_new();
if ((ret = tailscale_set_control_url(s1, control_url)) != 0) {
return set_err(s1, '0');
}
if ((ret = tailscale_set_dir(s1, tmps1)) != 0) {
return set_err(s1, '1');
}
if ((ret = tailscale_set_logfd(s1, -1)) != 0) {
return set_err(s1, '2');
}
if ((ret = tailscale_up(s1)) != 0) {
return set_err(s1, '3');
}
tailscale s2 = tailscale_new();
if ((ret = tailscale_set_control_url(s2, control_url)) != 0) {
return set_err(s2, '4');
}
if ((ret = tailscale_set_dir(s2, tmps2)) != 0) {
return set_err(s2, '5');
}
if ((ret = tailscale_set_logfd(s2, -1)) != 0) {
return set_err(s1, '6');
}
if ((ret = tailscale_up(s2)) != 0) {
return set_err(s2, '7');
}
tailscale_listener ln;
if ((ret = tailscale_listen(s1, "tcp", ":8081", &ln)) != 0) {
return set_err(s1, '8');
}
tailscale_conn w;
if ((ret = tailscale_dial(s2, "tcp", "100.64.0.1:8081", &w)) != 0) {
return set_err(s2, '9');
}
tailscale_conn r;
if ((ret = tailscale_accept(ln, &r)) != 0) {
return set_err(s2, 'a');
}
const char want[] = "hello";
ssize_t wret;
if ((wret = write(w, want, sizeof(want))) != sizeof(want)) {
snprintf(err, errlen, "short write: %zd, errno: %d (%s)", wret, errno, strerror(errno));
return 1;
}
char* got = malloc(sizeof(want));
if ((wret = read(r, got, sizeof(want))) != sizeof("hello")) {
snprintf(err, errlen, "short read: %zd on fd %d, errno: %d (%s)", wret, r, errno, strerror(errno));
return 1;
}
if (strncmp(got, want, sizeof(want)) != 0) {
snprintf(err, errlen, "got '%s' want '%s'", got, want);
return 1;
}
if ((ret = close(w)) != 0) {
snprintf(err, errlen, "failed to close w: %d (%s)", errno, strerror(errno));
return 1;
}
if ((ret = close(r)) != 0) {
snprintf(err, errlen, "failed to close r: %d (%s)", errno, strerror(errno));
return 1;
}
if ((ret = tailscale_listener_close(ln)) != 0) {
return set_err(s1, 'a');
}
if ((ret = tailscale_close(s1)) != 0) {
return set_err(s1, 'b');
}
if ((ret = tailscale_close(s2)) != 0) {
return set_err(s2, 'c');
}
return 0;
}
*/
import "C"
import (
"flag"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"tailscale.com/net/netns"
"tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/logger"
)
var verboseDERP = flag.Bool("verbose-derp", false, "if set, print DERP and STUN logs")
func RunTestConn(t *testing.T) {
// Corp#4520: don't use netns for tests.
netns.SetEnabled(false)
t.Cleanup(func() {
netns.SetEnabled(true)
})
derpLogf := logger.Discard
if *verboseDERP {
derpLogf = t.Logf
}
derpMap := integration.RunDERPAndSTUN(t, derpLogf, "127.0.0.1")
control := &testcontrol.Server{
DERPMap: derpMap,
}
control.HTTPTestServer = httptest.NewUnstartedServer(control)
control.HTTPTestServer.Start()
t.Cleanup(control.HTTPTestServer.Close)
controlURL := control.HTTPTestServer.URL
t.Logf("testcontrol listening on %s", controlURL)
C.control_url = C.CString(controlURL)
tmp := t.TempDir()
tmps1 := filepath.Join(tmp, "s1")
os.MkdirAll(tmps1, 0755)
C.tmps1 = C.CString(tmps1)
tmps2 := filepath.Join(tmp, "s2")
os.MkdirAll(tmps2, 0755)
C.tmps2 = C.CString(tmps2)
if C.test_conn() != 0 {
t.Fatal(C.GoString(C.err))
}
}