tsnet: add FunnelTLSConfig FunnelOption type

And also validate opts for unknown types, before other side effects.

Fixes #15833

Change-Id: I4cabe16c49c5b7566dcafbec59f2cd1e0c8b4b3c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-04-29 20:05:28 -07:00 committed by Brad Fitzpatrick
parent a9b3e09a1f
commit ab2deda4b7

View File

@ -1101,13 +1101,33 @@ type FunnelOption interface {
funnelOption() funnelOption()
} }
type funnelOnly int type funnelOnly struct{}
func (funnelOnly) funnelOption() {} func (funnelOnly) funnelOption() {}
// FunnelOnly configures the listener to only respond to connections from Tailscale Funnel. // FunnelOnly configures the listener to only respond to connections from Tailscale Funnel.
// The local tailnet will not be able to connect to the listener. // The local tailnet will not be able to connect to the listener.
func FunnelOnly() FunnelOption { return funnelOnly(1) } func FunnelOnly() FunnelOption { return funnelOnly{} }
type funnelTLSConfig struct{ conf *tls.Config }
func (f funnelTLSConfig) funnelOption() {}
// FunnelTLSConfig configures the TLS configuration for [Server.ListenFunnel]
//
// This is rarely needed but can permit requiring client certificates, specific
// ciphers suites, etc.
//
// The provided conf should at least be able to get a certificate, setting
// GetCertificate, Certificates or GetConfigForClient appropriately.
// The most common configuration is to set GetCertificate to
// Server.LocalClient's GetCertificate method.
//
// Unless [FunnelOnly] is also used, the configuration is also used for
// in-tailnet connections that don't arrive over Funnel.
func FunnelTLSConfig(conf *tls.Config) FunnelOption {
return funnelTLSConfig{conf: conf}
}
// ListenFunnel announces on the public internet using Tailscale Funnel. // ListenFunnel announces on the public internet using Tailscale Funnel.
// //
@ -1140,6 +1160,26 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
return nil, err return nil, err
} }
// Process, validate opts.
lnOn := listenOnBoth
var tlsConfig *tls.Config
for _, opt := range opts {
switch v := opt.(type) {
case funnelTLSConfig:
if v.conf == nil {
return nil, errors.New("invalid nil FunnelTLSConfig")
}
tlsConfig = v.conf
case funnelOnly:
lnOn = listenOnFunnel
default:
return nil, fmt.Errorf("unknown opts FunnelOption type %T", v)
}
}
if tlsConfig == nil {
tlsConfig = &tls.Config{GetCertificate: s.getCert}
}
ctx := context.Background() ctx := context.Background()
st, err := s.Up(ctx) st, err := s.Up(ctx)
if err != nil { if err != nil {
@ -1177,19 +1217,11 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
} }
// Start a funnel listener. // Start a funnel listener.
lnOn := listenOnBoth
for _, opt := range opts {
if _, ok := opt.(funnelOnly); ok {
lnOn = listenOnFunnel
}
}
ln, err := s.listen(network, addr, lnOn) ln, err := s.listen(network, addr, lnOn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return tls.NewListener(ln, &tls.Config{ return tls.NewListener(ln, tlsConfig), nil
GetCertificate: s.getCert,
}), nil
} }
type listenOn string type listenOn string