Skip to content

Add funnel functionality to C library #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
libtailscale
libtailscale.so
libtailscale.dylib
libtailscale.a
libtailscale.h
libtailscale.tar*
Expand Down
34 changes: 31 additions & 3 deletions tailscale.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

// Functions exported by Go.
extern int TsnetNewServer();
Expand All @@ -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();
Expand All @@ -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];
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
120 changes: 120 additions & 0 deletions tailscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
62 changes: 48 additions & 14 deletions tailscale.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 <ipv4>,<ipv6> but maybe empty, or
// contain any number of ips. The caller is responsible for parsing
// IPs as strings. This is typically `<ipv4>,<ipv6>` 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);

Expand All @@ -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
Expand All @@ -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.
//
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down