From 0dc2f30930782a36313b827a4eef0a43a94a404a Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Mon, 20 Feb 2023 17:01:13 -0800 Subject: [PATCH] libtailscale: Tailscale C library --- example/echo_server.c | 61 +++++++ tailscale.c | 76 ++++++++ tailscale.go | 364 +++++++++++++++++++++++++++++++++++++++ tailscale.h | 151 ++++++++++++++++ tailscale_test.go | 37 ++++ tsnetctest/tsnetctest.go | 164 ++++++++++++++++++ 6 files changed, 853 insertions(+) create mode 100644 example/echo_server.c create mode 100644 tailscale.c create mode 100644 tailscale.go create mode 100644 tailscale.h create mode 100644 tailscale_test.go create mode 100644 tsnetctest/tsnetctest.go diff --git a/example/echo_server.c b/example/echo_server.c new file mode 100644 index 0000000..684a74a --- /dev/null +++ b/example/echo_server.c @@ -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= ./a.out +// +// On macOS you may need to add the following flags to your C compiler: +// +// -framework CoreFoundation -framework Security +// + +#include "../tailscale.h" +#include +#include + +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; +} diff --git a/tailscale.c b/tailscale.c new file mode 100644 index 0000000..acdd325 --- /dev/null +++ b/tailscale.c @@ -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); +} diff --git a/tailscale.go b/tailscale.go new file mode 100644 index 0000000..5329b44 --- /dev/null +++ b/tailscale.go @@ -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 +} diff --git a/tailscale.h b/tailscale.h new file mode 100644 index 0000000..f2f81c9 --- /dev/null +++ b/tailscale.h @@ -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 + +// 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); diff --git a/tailscale_test.go b/tailscale_test.go new file mode 100644 index 0000000..33b5a3e --- /dev/null +++ b/tailscale_test.go @@ -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) + } +} diff --git a/tsnetctest/tsnetctest.go b/tsnetctest/tsnetctest.go new file mode 100644 index 0000000..0679085 --- /dev/null +++ b/tsnetctest/tsnetctest.go @@ -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 +#include +#include +#include +#include +#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)) + } +}