mirror of
https://github.com/Drop-OSS/libtailscale.git
synced 2026-01-30 20:55:18 +01:00
libtailscale: Tailscale C library
This commit is contained in:
committed by
David Crawshaw
parent
0b08c888b9
commit
0dc2f30930
61
example/echo_server.c
Normal file
61
example/echo_server.c
Normal 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
76
tailscale.c
Normal 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
364
tailscale.go
Normal 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
151
tailscale.h
Normal 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
37
tailscale_test.go
Normal 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
164
tsnetctest/tsnetctest.go
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user