diff --git a/tsnet/example/tsnet-funnel/tsnet-funnel.go b/tsnet/example/tsnet-funnel/tsnet-funnel.go new file mode 100644 index 000000000..1d13edbd7 --- /dev/null +++ b/tsnet/example/tsnet-funnel/tsnet-funnel.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The tshello server demonstrates how to use Tailscale as a library. +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "log" + "net/http" + + "tailscale.com/tsnet" +) + +var ( + addr = flag.String("addr", ":443", "address to listen on") +) + +func main() { + flag.Parse() + s := new(tsnet.Server) + defer s.Close() + + publicLis, err := s.ExposeHTTPS() + if err != nil { + log.Fatal(err) + } + + ln, err := s.Listen("tcp", *addr) + if err != nil { + log.Fatal(err) + } + defer ln.Close() + + lc, err := s.LocalClient() + if err != nil { + log.Fatal(err) + } + + ln = tls.NewListener(ln, &tls.Config{ + GetCertificate: lc.GetCertificate, + }) + + go log.Fatal(http.Serve(publicLis, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "
You are connected over the internet!
") + }))) + log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "You are connected over the tailnet!
") + }))) +} diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index c226a5852..27ad69e6d 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -735,6 +735,91 @@ func (s *Server) APIClient() (*tailscale.Client, error) { return c, nil } +// ExposeHTTPS returns a HTTPS listener that is exposed over Funnel. +// It will start the server if it has not been started yet. +func (s *Server) ExposeHTTPS() (net.Listener, error) { + if err := s.Start(); err != nil { + return nil, err + } + + ln, err := net.Listen("tcp", "[::1]:42069") + if err != nil { + return nil, err + } + + st := s.lb.StatusWithoutPeers() + + for st.BackendState != "Running" { + s.logf("waiting for control connection to set up exposed socket") + time.Sleep(time.Second) + st = s.lb.StatusWithoutPeers() + } + + if len(st.CertDomains) == 0 { + return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed") + } + domain := st.CertDomains[0] + + hp := ipn.HostPort(net.JoinHostPort(domain, "443")) + + srvConfig := &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{ + 443: &ipn.TCPPortHandler{ + TCPForward: ln.Addr().String(), + HTTPS: true, + }, + }, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + hp: &ipn.WebServerConfig{ + Handlers: map[string]*ipn.HTTPHandler { + "/": &ipn.HTTPHandler{Proxy: ln.Addr().String()}, + }, + }, + }, + AllowFunnel: map[ipn.HostPort]bool{ + hp: true, + }, + } + + if err := s.lb.SetServeConfig(srvConfig); err != nil { + return nil, err + } + + return &funnelListenerWrapper{ln, s}, nil +} + +type funnelListenerWrapper struct { + net.Listener + s *Server +} + +func (flw *funnelListenerWrapper) Accept() (net.Conn, error) { + conn, err := flw.Listener.Accept() + + flw.s.logf("got connection from %s", conn.RemoteAddr()) + + return conn, err +} + +func (flw *funnelListenerWrapper) Close() error { + defer flw.Listener.Close() + + lc, err := flw.s.LocalClient() + if err != nil { + return err + } + + if err := lc.SetServeConfig(context.Background(), &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{}, + AllowFunnel: map[ipn.HostPort]bool{}, + }); err != nil { + return err + } + + return nil +} + // Listen announces only on the Tailscale network. // It will start the server if it has not been started yet. func (s *Server) Listen(network, addr string) (net.Listener, error) {