mirror of
https://github.com/tailscale/tailscale.git
synced 2026-01-09 18:42:10 +01:00
tsnet: change format of configuration to ListenService
Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
parent
0d16a0bf32
commit
b4ebe5ae70
@ -44,7 +44,8 @@ func main() {
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ln, err := s.ListenService(*svcName, port, tsnet.ServiceOptionTerminateTLS())
|
||||
// TODO: use HTTPS instead
|
||||
ln, err := s.ListenService(*svcName, port, tsnet.ServiceTCPOptions{TerminateTLS: true})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
144
tsnet/tsnet.go
144
tsnet/tsnet.go
@ -1236,54 +1236,37 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
|
||||
}
|
||||
|
||||
// TODO: doc
|
||||
type ServiceOption interface {
|
||||
serviceOption()
|
||||
type ServiceTransportOptions interface {
|
||||
serviceTransportOptions()
|
||||
}
|
||||
|
||||
type serviceOptionTerminateTLS struct{}
|
||||
|
||||
func (serviceOptionTerminateTLS) serviceOption() {}
|
||||
|
||||
// TODO: doc
|
||||
func ServiceOptionTerminateTLS() ServiceOption {
|
||||
return serviceOptionTerminateTLS{}
|
||||
type ServiceTCPOptions struct {
|
||||
TerminateTLS bool
|
||||
PROXYProtocol int
|
||||
}
|
||||
|
||||
type serviceOptionPROXYProtocol struct {
|
||||
version int
|
||||
}
|
||||
|
||||
func (serviceOptionPROXYProtocol) serviceOption() {}
|
||||
func (ServiceTCPOptions) serviceTransportOptions() {}
|
||||
|
||||
// TODO: doc
|
||||
func ServiceOptionPROXYProtocol(version int) ServiceOption {
|
||||
return serviceOptionPROXYProtocol{version}
|
||||
// TODO: it's not super intutive that an empty struct here sends identity headers
|
||||
type ServiceHTTPOptions struct {
|
||||
HTTPS bool
|
||||
AcceptAppCaps map[string][]string
|
||||
}
|
||||
|
||||
type serviceOptionAppCapabilities struct {
|
||||
path string
|
||||
caps []string
|
||||
}
|
||||
func (ServiceHTTPOptions) serviceTransportOptions() {}
|
||||
|
||||
func (serviceOptionAppCapabilities) serviceOption() {}
|
||||
|
||||
// TODO: doc
|
||||
func ServiceOptionAppCapabilities(capabilities ...string) ServiceOption {
|
||||
return ServiceOptionAppCapabilitiesForPath("/", capabilities...)
|
||||
}
|
||||
|
||||
// TODO: doc; include info on this overriding handlers at earlier paths
|
||||
func ServiceOptionAppCapabilitiesForPath(path string, capabilities ...string) ServiceOption {
|
||||
return serviceOptionAppCapabilities{path, capabilities}
|
||||
}
|
||||
|
||||
type serviceOptionWithHeaders struct{}
|
||||
|
||||
func (serviceOptionWithHeaders) serviceOption() {}
|
||||
|
||||
// TODO: doc
|
||||
func ServiceOptionWithHeaders() ServiceOption {
|
||||
return serviceOptionWithHeaders{}
|
||||
func (opts ServiceHTTPOptions) capsMap() map[string][]tailcfg.PeerCapability {
|
||||
capsMap := map[string][]tailcfg.PeerCapability{}
|
||||
for path, capNames := range opts.AcceptAppCaps {
|
||||
caps := make([]tailcfg.PeerCapability, 0, len(capNames))
|
||||
for _, c := range capNames {
|
||||
caps = append(caps, tailcfg.PeerCapability(c))
|
||||
}
|
||||
capsMap[path] = caps
|
||||
}
|
||||
return capsMap
|
||||
}
|
||||
|
||||
// ErrUntaggedServiceHost is returned by ListenService when run on a node
|
||||
@ -1294,7 +1277,8 @@ var ErrUntaggedServiceHost = errors.New("service hosts must be tagged nodes")
|
||||
|
||||
// TODO: doc
|
||||
// TODO: tailcfg.ServiceName?
|
||||
func (s *Server) ListenService(name string, port uint16, opts ...ServiceOption) (net.Listener, error) {
|
||||
// TODO: does this API allow room for growth? Can everything fit into opts?
|
||||
func (s *Server) ListenService(name string, port uint16, opts ServiceTransportOptions) (net.Listener, error) {
|
||||
if err := tailcfg.ServiceName(name).Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1308,31 +1292,6 @@ func (s *Server) ListenService(name string, port uint16, opts ...ServiceOption)
|
||||
// - maybe worth an http.ReverseProxy example?
|
||||
// - support TUN mode
|
||||
|
||||
// Process options.
|
||||
terminateTLS := false
|
||||
proxyProtocol := 0
|
||||
capsMap := map[string][]tailcfg.PeerCapability{} // mount point => caps
|
||||
isHTTP := false
|
||||
for _, o := range opts {
|
||||
switch opt := o.(type) {
|
||||
case serviceOptionTerminateTLS:
|
||||
terminateTLS = true
|
||||
case serviceOptionPROXYProtocol:
|
||||
proxyProtocol = opt.version
|
||||
case serviceOptionWithHeaders:
|
||||
isHTTP = true
|
||||
case serviceOptionAppCapabilities:
|
||||
isHTTP = true
|
||||
caps := make([]tailcfg.PeerCapability, 0, len(opt.caps))
|
||||
for _, c := range opt.caps {
|
||||
caps = append(caps, tailcfg.PeerCapability(c))
|
||||
}
|
||||
capsMap[opt.path] = append(capsMap[opt.path], caps...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown opts FunnelOption type %T", o)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
@ -1380,33 +1339,42 @@ func (s *Server) ListenService(name string, port uint16, opts ...ServiceOption)
|
||||
return nil, fmt.Errorf("starting local listener: %w", err)
|
||||
}
|
||||
|
||||
if isHTTP {
|
||||
useTLS := false // TODO: set correctly
|
||||
mds := st.CurrentTailnet.MagicDNSSuffix
|
||||
setHandler := func(h ipn.HTTPHandler, path string) {
|
||||
h.Proxy = ln.Addr().String()
|
||||
if path != "/" {
|
||||
h.Proxy += path
|
||||
}
|
||||
srvConfig.SetWebHandler(&h, svcName, port, path, useTLS, mds)
|
||||
}
|
||||
// Set a web handler for every mount point in the caps map. If we don't
|
||||
// end up with a root handler after that, we need to set one.
|
||||
haveRootHandler := false
|
||||
for path, caps := range capsMap {
|
||||
if path == "/" {
|
||||
haveRootHandler = true
|
||||
}
|
||||
setHandler(ipn.HTTPHandler{AcceptAppCaps: caps}, path)
|
||||
}
|
||||
if !haveRootHandler {
|
||||
setHandler(ipn.HTTPHandler{}, "/")
|
||||
}
|
||||
} else {
|
||||
if opts == nil {
|
||||
opts = ServiceTCPOptions{}
|
||||
}
|
||||
switch o := opts.(type) {
|
||||
case ServiceTCPOptions:
|
||||
// Forward all connections from service-hostname:port to our socket.
|
||||
srvConfig.SetTCPForwardingForService(
|
||||
port, ln.Addr().String(), tailcfg.ServiceName(svcName),
|
||||
terminateTLS, proxyProtocol, st.CurrentTailnet.MagicDNSSuffix)
|
||||
o.TerminateTLS, o.PROXYProtocol, st.CurrentTailnet.MagicDNSSuffix)
|
||||
|
||||
case ServiceHTTPOptions:
|
||||
mds := st.CurrentTailnet.MagicDNSSuffix
|
||||
haveRootHandler := false
|
||||
for path, caps := range o.capsMap() {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
h := ipn.HTTPHandler{
|
||||
AcceptAppCaps: caps,
|
||||
Proxy: ln.Addr().String(),
|
||||
}
|
||||
if path == "/" {
|
||||
haveRootHandler = true
|
||||
} else {
|
||||
h.Proxy += path
|
||||
}
|
||||
srvConfig.SetWebHandler(&h, svcName, port, path, o.HTTPS, mds)
|
||||
}
|
||||
if !haveRootHandler {
|
||||
h := ipn.HTTPHandler{Proxy: ln.Addr().String()}
|
||||
srvConfig.SetWebHandler(&h, svcName, port, "/", o.HTTPS, mds)
|
||||
}
|
||||
|
||||
default:
|
||||
ln.Close()
|
||||
return nil, fmt.Errorf("unknown ServiceTransportOptions type %T", opts)
|
||||
}
|
||||
|
||||
if err := lc.SetServeConfig(ctx, srvConfig); err != nil {
|
||||
|
||||
@ -828,7 +828,7 @@ func TestListenService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port uint16
|
||||
opts []ServiceOption
|
||||
opts ServiceTransportOptions
|
||||
|
||||
extraSetup func(t *testing.T, serviceHost, peer *Server, control *testcontrol.Server)
|
||||
|
||||
@ -851,7 +851,7 @@ func TestListenService(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "TLS_terminated_TCP",
|
||||
opts: []ServiceOption{ServiceOptionTerminateTLS()},
|
||||
opts: ServiceTCPOptions{TerminateTLS: true},
|
||||
port: 443,
|
||||
run: func(t *testing.T, serviceListener net.Listener, peer *Server, serviceFQDN string) {
|
||||
go acceptAndEcho(t, serviceListener)
|
||||
@ -868,7 +868,7 @@ func TestListenService(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "identity_headers",
|
||||
opts: []ServiceOption{ServiceOptionWithHeaders()},
|
||||
opts: ServiceHTTPOptions{},
|
||||
port: 80,
|
||||
run: func(t *testing.T, serviceListener net.Listener, peer *Server, serviceFQDN string) {
|
||||
expectHeader := "Tailscale-User-Name"
|
||||
@ -880,11 +880,39 @@ func TestListenService(t *testing.T) {
|
||||
assertEchoHTTP(t, serviceFQDN, "", peer.Dial)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identity_headers_TLS",
|
||||
opts: ServiceHTTPOptions{HTTPS: true},
|
||||
port: 80,
|
||||
run: func(t *testing.T, serviceListener net.Listener, peer *Server, serviceFQDN string) {
|
||||
expectHeader := "Tailscale-User-Name"
|
||||
go checkAndEcho(t, serviceListener, func(r *http.Request) {
|
||||
if _, ok := r.Header[expectHeader]; !ok {
|
||||
t.Error("did not see expected header:", expectHeader)
|
||||
}
|
||||
})
|
||||
|
||||
dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
tcpConn, err := peer.Dial(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.Client(tcpConn, &tls.Config{
|
||||
ServerName: serviceFQDN,
|
||||
RootCAs: testCertRoot.Pool(),
|
||||
}), nil
|
||||
}
|
||||
|
||||
assertEchoHTTP(t, serviceFQDN, "", dial)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app_capabilities",
|
||||
opts: []ServiceOption{
|
||||
ServiceOptionAppCapabilities("example.com/cap/all-paths"),
|
||||
ServiceOptionAppCapabilitiesForPath("/foo", "example.com/cap/all-paths", "example.com/cap/foo"),
|
||||
opts: ServiceHTTPOptions{
|
||||
AcceptAppCaps: map[string][]string{
|
||||
"/": {"example.com/cap/all-paths"},
|
||||
"/foo": {"example.com/cap/all-paths", "example.com/cap/foo"},
|
||||
},
|
||||
},
|
||||
port: 80,
|
||||
extraSetup: func(t *testing.T, serviceHost, peer *Server, control *testcontrol.Server) {
|
||||
@ -936,7 +964,6 @@ func TestListenService(t *testing.T) {
|
||||
// - TLS-terminated-TCP
|
||||
// - Service with multiple ports
|
||||
// - TUN Service
|
||||
// - web handlers
|
||||
// Error cases:
|
||||
// - Untagged node
|
||||
}
|
||||
@ -1013,7 +1040,7 @@ func TestListenService(t *testing.T) {
|
||||
// == Done setting up mock state ==
|
||||
|
||||
// Start a Service listener.
|
||||
ln := must.Get(serviceHost.ListenService(serviceName.String(), tt.port, tt.opts...))
|
||||
ln := must.Get(serviceHost.ListenService(serviceName.String(), tt.port, tt.opts))
|
||||
defer ln.Close()
|
||||
|
||||
tt.run(t, ln, serviceClient, serviceFQDN)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user