Working prototype

Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
Harry Harpham
2025-12-16 14:25:31 -07:00
parent d0d993f5d6
commit bb642f8aab
2 changed files with 160 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The tsnet-services example demonstrates how to use tsnet with Services.
// TODO: explain that a Service must be defined for the tailent and link to KB
// on defining a Service
//
// To use it, generate an auth key from the Tailscale admin panel and
// run the demo with the key:
//
// TS_AUTHKEY=<yourkey> go run tsnet-services.go
package main
import (
"flag"
"fmt"
"log"
"math"
"net/http"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
)
var (
svcName = flag.String("service", "", "the name of your Service, e.g. svc:demo-service")
port = flag.Uint("port", 0, "the port to listen on")
)
func main() {
flag.Parse()
if *svcName == "" {
log.Fatal("a Service name must be provided")
}
if *port == 0 {
log.Fatal("the listening port must be provided")
}
if *port > math.MaxUint16 {
log.Fatal("invalid port number")
}
s := &tsnet.Server{
Dir: "./services-demo-config",
Hostname: "tsnet-services-demo",
}
defer s.Close()
ln, err := s.ListenService(*svcName, uint16(*port))
if err != nil {
log.Fatal(err)
}
defer ln.Close()
fmt.Printf("Listening on http://%v\n", tailcfg.AsServiceName(*svcName).WithoutPrefix())
// TODO: maybe just respond to TCP connections? (since we don't know the port)
// Actually, let's hard-code port 80 and provide an example Service definition to use
err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "<html><body><h1>Hello, tailnet!</h1>")
}))
log.Fatal(err)
}

View File

@@ -52,6 +52,7 @@ import (
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/bools"
"tailscale.com/types/logger"
@@ -1239,6 +1240,103 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
return tls.NewListener(ln, tlsConfig), nil
}
// TODO: doc
// TODO: name?
// TODO: can this mirror the format accepted by set-config?
// For now, configures a single endpoint
// Maybe this should be an interface, with implementations like ListenTCPService, etc.
type ListenServiceConfig struct {
Port uint16
PortHandler ipn.TCPPortHandler // TODO: what about UDP support in the future?
// TODO: could be HTTP-specific if this config becomes an interface
WebHandlers map[ipn.HostPort]*ipn.WebServerConfig
// TODO: maybe something like this for things like PROXY protocol support?
// L4Options
}
// TODO: do we actually need this?
type ServiceOption interface {
serviceOption()
}
// TODO: doc
// TODO: tailcfg.ServiceName?
func (s *Server) ListenService(name string, port uint16) (net.Listener, error) {
if err := tailcfg.ServiceName(name).Validate(); err != nil {
return nil, err
}
// TODO:
// - get existing serve config
// - make changes and update
// - pipe to local TCP listener
// TODO:
// - try above with simple TCP listener first
// - handle Services with multiple ports defined
// - support web handlers
// - make sure extras like PROXY mode are supported
// - support TUN mode
ctx := context.Background()
_, err := s.Up(ctx)
if err != nil {
return nil, err
}
lc := s.localClient
// TODO: check for ACL tags
prefs, err := lc.GetPrefs(ctx)
if err != nil {
return nil, fmt.Errorf("fetching node preferences: %w", err)
}
if !slices.Contains(prefs.AdvertiseServices, name) {
_, err = lc.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseServicesSet: true,
Prefs: ipn.Prefs{
AdvertiseServices: append(prefs.AdvertiseServices, name),
},
})
if err != nil {
return nil, fmt.Errorf("updating advertised Services: %w", err)
}
}
srvConfig, err := lc.GetServeConfig(ctx)
if err != nil {
return nil, fmt.Errorf("fetching node serve config: %w", err)
}
if srvConfig == nil {
srvConfig = new(ipn.ServeConfig)
}
// Start listening on a TCP socket. We will direct the local client to
// forward connections to this listener.
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, fmt.Errorf("starting local listener: %w", err)
}
// TODO:
// - Handle terminateTLS
// - Handle proxyProtocol
srvConfig.SetTCPForwarding(port, ln.Addr().String(), false, 0, name)
if err := lc.SetServeConfig(ctx, srvConfig); err != nil {
ln.Close()
return nil, err
}
// TODO: wrap returned listener such that Close stops advertising the
// Service (should update prefs, serve config, etc.)
return ln, nil
}
type listenOn string
const (