mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-22 16:46:29 +00:00
62
tsnet/example/tsnet-services/tsnet-services.go
Normal file
62
tsnet/example/tsnet-services/tsnet-services.go
Normal 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)
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user