diff --git a/tailscale.c b/tailscale.c index 0502fe7..8cbbb22 100644 --- a/tailscale.c +++ b/tailscale.c @@ -23,6 +23,7 @@ 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 TsnetLoopback(int sd, char* addrOut, size_t addrLen, char* proxyOut, char* localOut); +extern int TsnetEnableFunnelToLocalhostPlaintextHttp1(int sd, int localhostPort); tailscale tailscale_new() { return TsnetNewServer(); @@ -106,3 +107,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_enable_funnel_to_localhost_plaintext_http1(tailscale sd, int localhostPort) { + return TsnetEnableFunnelToLocalhostPlaintextHttp1(sd, localhostPort); +} diff --git a/tailscale.go b/tailscale.go index 68dca70..13d8ef9 100644 --- a/tailscale.go +++ b/tailscale.go @@ -14,12 +14,14 @@ import ( "net" "os" "regexp" + "strconv" "strings" "sync" "syscall" "unsafe" "tailscale.com/hostinfo" + "tailscale.com/ipn" "tailscale.com/tsnet" "tailscale.com/types/logger" ) @@ -531,3 +533,44 @@ func TsnetLoopback(sd C.int, addrOut *C.char, addrLen C.size_t, proxyOut *C.char return 0 } + +//export TsnetEnableFunnelToLocalhostPlaintextHttp1 +func TsnetEnableFunnelToLocalhostPlaintextHttp1(sd C.int, localhostPort C.int) C.int { + s, err := getServer(sd) + if err != nil { + return s.recErr(err) + } + + ctx := context.Background() + lc, err := s.s.LocalClient() + if err != nil { + return s.recErr(err) + } + + st, err := lc.StatusWithoutPeers(ctx) + if err != nil { + return s.recErr(err) + } + domain := st.CertDomains[0] + + hp := ipn.HostPort(net.JoinHostPort(domain, strconv.Itoa(443))) + tcpForward := fmt.Sprintf("127.0.0.1:%d", localhostPort) + sc := &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{ + 443: { + TCPForward: tcpForward, + TerminateTLS: domain, + }, + }, + AllowFunnel: map[ipn.HostPort]bool{ + hp: true, + }, + } + + lc.SetServeConfig(ctx, sc) + if !sc.AllowFunnel[hp] { + return s.recErr(fmt.Errorf("libtailscale: failed to enable funnel")) + } + + return 0 +} diff --git a/tailscale.h b/tailscale.h index df08d7e..4531f06 100644 --- a/tailscale.h +++ b/tailscale.h @@ -175,6 +175,22 @@ extern int tailscale_accept(tailscale_listener listener, tailscale_conn* conn_ou // Returns zero on success or -1 on error, call tailscale_errmsg for details. extern int tailscale_loopback(tailscale sd, char* addr_out, size_t addrlen, char* proxy_cred_out, char* local_api_cred_out); +// tailscale_enable_funnel_to_localhost_plaintext_http1 configures sd to have +// Tailscale Funnel enabled, routing requests from the public web +// (without any authentication) down to this Tailscale node, requesting new +// LetsEncrypt TLS certs as needed, terminating TLS, and proxying all incoming +// HTTPS requests to http://127.0.0.1:localhostPort without TLS. +// +// There should be a plaintext HTTP/1 server listening on 127.0.0.1:localhostPort +// or tsnet will serve HTTP 502 errors. +// +// Expect junk traffic from the internet from bots watching the public CT logs. +// +// Returns: +// 0 - success +// -1 - other error, details printed to the tsnet logger +extern int tailscale_enable_funnel_to_localhost_plaintext_http1(tailscale sd, int localhostPort); + // tailscale_errmsg writes the details of the last error to buf. // // After returning, buf is always NUL-terminated.