diff --git a/.gitignore b/.gitignore index d36329c..43d3b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +libtailscale libtailscale.so +libtailscale.dylib libtailscale.a libtailscale.h libtailscale.tar* diff --git a/tailscale.c b/tailscale.c index 0502fe7..d0a4d11 100644 --- a/tailscale.c +++ b/tailscale.c @@ -5,6 +5,7 @@ #include #include #include +#include // Functions exported by Go. extern int TsnetNewServer(); @@ -22,7 +23,9 @@ extern int TsnetSetLogFD(int sd, int fd); extern int TsnetGetIps(int sd, char *buf, size_t buflen); extern int TsnetGetRemoteAddr(int listener, int conn, char *buf, size_t buflen); extern int TsnetListen(int sd, char* net, char* addr, int* listenerOut); +extern int TsnetListenFunnel(int sd, char *net, char *addr, int funnelOnly, int *listenerOut); extern int TsnetLoopback(int sd, char* addrOut, size_t addrLen, char* proxyOut, char* localOut); +extern int TsnetGetCertDomains(int sd, char* buf, size_t buflen); tailscale tailscale_new() { return TsnetNewServer(); @@ -48,7 +51,11 @@ int tailscale_listen(tailscale sd, const char* network, const char* addr, tailsc return TsnetListen(sd, (char*)network, (char*)addr, (int*)listener_out); } -int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) { +int tailscale_listen_funnel(tailscale sd, const char* network, const char* addr, int funnelOnly, tailscale_listener* listener_out) { + return TsnetListenFunnel(sd, (char*) network, (char*) addr, funnelOnly, (int*) listener_out); +} + +int _tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out, int flags) { struct msghdr msg = {0}; char mbuf[256]; @@ -60,8 +67,17 @@ int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) { msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); - if (recvmsg(ld, &msg, 0) == -1) { - return -1; + if (recvmsg(ld, &msg, flags) == -1) + { + switch (errno) + { + case EAGAIN: + return EAGAIN; + case ECONNRESET: + return ECONNRESET; + default: + return -1; + } } struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); @@ -72,6 +88,14 @@ int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) { return 0; } +int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) { + return _tailscale_accept(ld, conn_out, 0); +} + +int tailscale_accept_nonblocking(tailscale_listener ld, tailscale_conn* conn_out) { + return _tailscale_accept(ld, conn_out, MSG_DONTWAIT); +} + int tailscale_getremoteaddr(tailscale_listener l, tailscale_conn conn, char* buf, size_t buflen) { return TsnetGetRemoteAddr(l, conn, buf, buflen); } @@ -106,3 +130,7 @@ int tailscale_loopback(tailscale sd, char* addr_out, size_t addrlen, char* proxy int tailscale_errmsg(tailscale sd, char* buf, size_t buflen) { return TsnetErrmsg(sd, buf, buflen); } + +int tailscale_cert_domains(tailscale sd, char* buf, size_t buflen) { + return TsnetGetCertDomains(sd, buf, buflen); +} \ No newline at end of file diff --git a/tailscale.go b/tailscale.go index 68dca70..e39d0f5 100644 --- a/tailscale.go +++ b/tailscale.go @@ -295,6 +295,99 @@ func TsnetListen(sd C.int, network, addr *C.char, listenerOut *C.int) C.int { return 0 } +//export TsnetListenFunnel +func TsnetListenFunnel(sd C.int, network, addr *C.char, funnelOnly C.int, listenerOut *C.int) C.int { + s, err := getServer(sd) + if err != nil { + return s.recErr(err) + } + + var ln net.Listener + if funnelOnly != 0 { + ln, err = s.s.ListenFunnel(C.GoString(network), C.GoString(addr), tsnet.FunnelOnly()) + } else { + ln, err = s.s.ListenFunnel(C.GoString(network), C.GoString(addr)) + } + + if err != nil { + return s.recErr(err) + } + + // The tailscale_listener we return to C is one side of a socketpair(2). + // We do this so we can proactively call ln.Accept in a goroutine and + // feed an fd for the connection through the listener. This lets C use + // epoll on the tailscale_listener to know if it should call + // tailscale_accept, which avoids a blocking call on the far side. + fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0) + if err != nil { + return s.recErr(err) + } + sp := fds[1] + fdC := C.int(fds[0]) + + listeners.mu.Lock() + if listeners.m == nil { + listeners.m = map[C.int]*listener{} + } + listeners.m[fdC] = &listener{s: s, ln: ln, fd: sp} + listeners.mu.Unlock() + + cleanup := func() { + // If fdC is closed on the C side, then we end up calling + // into cleanup twice. Be careful to avoid syscall.Close + // twice as the FD may have been reallocated. + listeners.mu.Lock() + if tsLn, ok := listeners.m[fdC]; ok && tsLn.ln == ln { + delete(listeners.m, fdC) + syscall.Close(sp) + } + listeners.mu.Unlock() + + ln.Close() + } + go func() { + // fdC is never written to, so trying to read from sp blocks + // until fdC is closed. We use this as a signal that C is + // done with the listener, and we can tear it down. + // + // TODO: would using os.NewFile avoid a locked up thread? + var buf [256]byte + syscall.Read(sp, buf[:]) + cleanup() + }() + go func() { + defer cleanup() + for { + netConn, err := ln.Accept() + if err != nil { + return + } + var connFd C.int + if err := newConn(s, netConn, &connFd); err != nil { + if s.s.Logf != nil { + s.s.Logf("libtailscale.accept: newConn: %v", err) + } + netConn.Close() + continue + } + rights := syscall.UnixRights(int(connFd)) + err = syscall.Sendmsg(sp, nil, rights, nil, 0) + if err != nil { + // We handle sp being closed in the read goroutine above. + if s.s.Logf != nil { + s.s.Logf("libtailscale.accept: sendmsg failed: %v", err) + } + netConn.Close() + // fallthrough to close connFd, then continue Accept()ing + } + syscall.Close(int(connFd)) // now owned by recvmsg + } + }() + + *listenerOut = fdC + return 0 +} + func newConn(s *server, netConn net.Conn, connOut *C.int) error { fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0) if err != nil { @@ -531,3 +624,30 @@ func TsnetLoopback(sd C.int, addrOut *C.char, addrLen C.size_t, proxyOut *C.char return 0 } + +//export TsnetGetCertDomains +func TsnetGetCertDomains(sd C.int, buf *C.char, buflen C.size_t) C.int { + if buf == nil { + panic("TsnetGetCertDomains passed nil buf") + } else if buflen == 0 { + panic("TsnetGetCertDomains passed buflen of 0") + } + + s, err := getServer(sd) + if err != nil { + return s.recErr(err) + } + + domains := s.s.CertDomains() + if len(domains) == 0 { + return s.recErr(fmt.Errorf("no domains available")) + } + firstDomain := domains[0] + if C.size_t(len(firstDomain)+1) > buflen { + return C.ERANGE // buffer too small + } + output := unsafe.Slice((*byte)(unsafe.Pointer(buf)), buflen) + copy(output, firstDomain) + output[len(firstDomain)] = 0 // null-terminate + return 0 +} diff --git a/tailscale.h b/tailscale.h index df08d7e..b084727 100644 --- a/tailscale.h +++ b/tailscale.h @@ -70,7 +70,7 @@ 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. +// A 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); @@ -81,17 +81,16 @@ extern int tailscale_set_logfd(tailscale sd, int fd); // For extra control over the connection, see the tailscale_conn_* functions. typedef int tailscale_conn; -// Returns the IP addresses of the the Tailscale server as -// a comma separated list. +// Returns the IP addresses of the Tailscale server as a comma separated list. // // The provided buffer must be of sufficient size to hold the concatenated -// IPs as strings. This is typically , but maybe empty, or -// contain any number of ips. The caller is responsible for parsing +// IPs as strings. This is typically `,` but maybe empty, or +// contain any number of ips. The caller is responsible for parsing // the output. You may assume the output is a list of well-formed IPs. // // Returns: -// 0 - Success -// EBADF - sd is not a valid tailscale, or l or conn are not valid listeneras or connections +// 0 - Success +// EBADF - sd is not a valid tailscale, or l or conn are not valid listeners or connections // ERANGE - insufficient storage for buf extern int tailscale_getips(tailscale sd, char* buf, size_t buflen); @@ -110,7 +109,7 @@ extern int tailscale_dial(tailscale sd, const char* network, const char* addr, t // 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). -// Accept connections with tailscale_accept and close the listener with close. +// Accept connections with tailscale_accept and close the listener with close. // // Under the hood, a tailscale_listener is one half of a socketpair itself, // used to move the connection fd from Go to C. This means you can use epoll @@ -131,14 +130,33 @@ typedef int tailscale_listener; // 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); -// Returns the remote address for an incoming connection for a particular listener. The address (eitehr ip4 or ip6) -// will ge written to buf on on success. +// tailscale_listen_funnel announces on the public internet using Tailscale Funnel. +// +// It also by default listens on your local tailnet, so connections can +// come from either inside or outside your network. To restrict connections +// to be just from the internet, use the FunnelOnly option. +// +// Currently, (2024-12-13), Funnel only supports TCP on ports 443, 8443, and 10000. +// The supported host name is limited to that configured for the tsnet.Server. +// +// 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_funnel(tailscale sd, const char *network, const char *addr, int funnelOnly, tailscale_listener *listener_out); + +// Returns the remote address for an incoming connection for a particular listener. +// The address (either ip4 or ip6) will ge written to buf on success. // Returns: -// 0 - Success -// EBADF - sd is not a valid tailscale, or l or conn are not valid listeneras or connections +// 0 - Success +// EBADF - sd is not a valid tailscale, or l or conn are not valid listeners or connections // ERANGE - insufficient storage for buf -extern int tailscale_getremoteaddr(tailscale_listener l, tailscale_conn conn, char* buf, size_t buflen); - +extern int tailscale_getremoteaddr(tailscale_listener l, tailscale_conn conn, char *buf, size_t buflen); // tailscale_accept accepts a connection on a tailscale_listener. // @@ -152,6 +170,17 @@ extern int tailscale_getremoteaddr(tailscale_listener l, tailscale_conn conn, ch // -1 - call tailscale_errmsg for details extern int tailscale_accept(tailscale_listener listener, tailscale_conn* conn_out); +// tailscale_accept_nonblocking accepts a connection on a tailscale_listener. +// +// Acts like tailscale_accept but if there is no connection to accept return immediately. +// Uses MSG_DONTWAIT flag to achieve this. +// +// Returns: +// 0 - success +// EBADF - listener is not a valid tailscale +// -1 - call tailscale_errmsg for details +extern int tailscale_accept_nonblocking(tailscale_listener listener, tailscale_conn *conn_out); + // tailscale_loopback starts a loopback address server. // // The server has multiple functions. @@ -185,6 +214,11 @@ extern int tailscale_loopback(tailscale sd, char* addr_out, size_t addrlen, char // ERANGE - insufficient storage for buf extern int tailscale_errmsg(tailscale sd, char* buf, size_t buflen); +// tailscale_cert_domains returns the list of domains for which the server can +// provide TLS certificates. These are also the DNS names for the Server. +// +// If the server is not running, it returns nil. +extern int tailscale_cert_domains(tailscale sd, char *buf, size_t buflen); #ifdef __cplusplus }