mirror of
https://github.com/tailscale/tailscale.git
synced 2025-06-30 20:19:04 +00:00

This commit adds the service flag to serve command which allows serving a service and add the service to the advertisedServices field in prefs (What advertise command does that will be removed later). When adding proxies, TCP proxies and WEB proxies work the same way as normal serve, just under a different DNSname. There is a services specific L3 serving mode called Tun, can be set via --tun flag. Serving a service is always in --bg mode. If --bg is explicitly set t o false, an error message will be sent out. The restriction on proxy target being localhost or 127.0.0.1 also applies to services. When removing proxies, TCP proxies can be removed with type and port flag and off argument. Web proxies can be removed with type, port, setPath flag and off argument. To align with normal serve, when setPath is not set, all handler under the hostport will be removed. When flags are not set but off argument was passed by user, it will be a noop. Removing all config for a service will be available later with a new subcommand clear. Updates tailscale/corp#22954 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
386 lines
9.4 KiB
Go
386 lines
9.4 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipn
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
func TestCheckFunnelAccess(t *testing.T) {
|
|
caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c }
|
|
const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
|
|
tests := []struct {
|
|
port uint16
|
|
caps []tailcfg.NodeCapability
|
|
wantErr bool
|
|
}{
|
|
{443, caps(portAttr), true}, // No "funnel" attribute
|
|
{443, caps(portAttr, tailcfg.NodeAttrFunnel), true},
|
|
{443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
|
|
{8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
|
|
{8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
|
|
{8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
|
|
{8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
|
|
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
|
|
}
|
|
for _, tt := range tests {
|
|
cm := tailcfg.NodeCapMap{}
|
|
for _, c := range tt.caps {
|
|
cm[c] = nil
|
|
}
|
|
err := CheckFunnelAccess(tt.port, &ipnstate.PeerStatus{CapMap: cm})
|
|
switch {
|
|
case err != nil && tt.wantErr,
|
|
err == nil && !tt.wantErr:
|
|
continue
|
|
case tt.wantErr:
|
|
t.Fatalf("got no error, want error")
|
|
case !tt.wantErr:
|
|
t.Fatalf("got error %v, want no error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHasPathHandler(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg ServeConfig
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty-config",
|
|
cfg: ServeConfig{},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "with-bg-path-handler",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
|
"/": {Path: "/tmp"},
|
|
}},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "with-fg-path-handler",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{
|
|
443: {HTTPS: true},
|
|
},
|
|
Foreground: map[string]*ServeConfig{
|
|
"abc123": {
|
|
TCP: map[uint16]*TCPPortHandler{80: {HTTP: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:80": {Handlers: map[string]*HTTPHandler{
|
|
"/": {Path: "/tmp"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "with-no-bg-path-handler",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
}},
|
|
},
|
|
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "with-no-fg-path-handler",
|
|
cfg: ServeConfig{
|
|
Foreground: map[string]*ServeConfig{
|
|
"abc123": {
|
|
TCP: map[uint16]*TCPPortHandler{443: {HTTPS: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:443": {Handlers: map[string]*HTTPHandler{
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
}},
|
|
},
|
|
AllowFunnel: map[HostPort]bool{"foo.test.ts.net:443": true},
|
|
},
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.cfg.HasPathHandler()
|
|
if tt.want != got {
|
|
t.Errorf("HasPathHandler() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsTCPForwardingOnPort(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg ServeConfig
|
|
dns string
|
|
port uint16
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty-config",
|
|
cfg: ServeConfig{},
|
|
dns: "foo.test.ts.net",
|
|
port: 80,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "node-tcp-config-match",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
|
},
|
|
dns: "foo.test.ts.net",
|
|
port: 80,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "node-tcp-config-no-match",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
|
},
|
|
dns: "foo.test.ts.net",
|
|
port: 443,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "node-tcp-config-no-match-with-service",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
|
},
|
|
dns: "svc:bar",
|
|
port: 80,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "node-web-config-no-match",
|
|
cfg: ServeConfig{
|
|
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*HTTPHandler{
|
|
"/": {Text: "Hello, world!"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dns: "foo.test.ts.net",
|
|
port: 80,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "service-tcp-config-match",
|
|
cfg: ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
|
},
|
|
},
|
|
},
|
|
dns: "svc:foo",
|
|
port: 80,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "service-tcp-config-no-match",
|
|
cfg: ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*TCPPortHandler{80: {TCPForward: "10.0.0.123:3000"}},
|
|
},
|
|
},
|
|
},
|
|
dns: "svc:bar",
|
|
port: 80,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "service-web-config-no-match",
|
|
cfg: ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ServiceConfig{
|
|
"svc:foo": {
|
|
TCP: map[uint16]*TCPPortHandler{80: {HTTPS: true}},
|
|
Web: map[HostPort]*WebServerConfig{
|
|
"foo.test.ts.net:80": {
|
|
Handlers: map[string]*HTTPHandler{
|
|
"/": {Text: "Hello, world!"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dns: "svc:foo",
|
|
port: 80,
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.cfg.IsTCPForwardingOnPort(tt.port, tt.dns)
|
|
if tt.want != got {
|
|
t.Errorf("IsTCPForwardingOnPort() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpandProxyTargetDev(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
defaultScheme string
|
|
supportedSchemes []string
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
{name: "port-only", input: "8080", expected: "http://127.0.0.1:8080"},
|
|
{name: "hostname+port", input: "localhost:8080", expected: "http://localhost:8080"},
|
|
{name: "no-change", input: "http://127.0.0.1:8080", expected: "http://127.0.0.1:8080"},
|
|
{name: "include-path", input: "http://127.0.0.1:8080/foo", expected: "http://127.0.0.1:8080/foo"},
|
|
{name: "https-scheme", input: "https://localhost:8080", expected: "https://localhost:8080"},
|
|
{name: "https+insecure-scheme", input: "https+insecure://localhost:8080", expected: "https+insecure://localhost:8080"},
|
|
{name: "change-default-scheme", input: "localhost:8080", defaultScheme: "https", expected: "https://localhost:8080"},
|
|
{name: "change-supported-schemes", input: "localhost:8080", defaultScheme: "tcp", supportedSchemes: []string{"tcp"}, expected: "tcp://localhost:8080"},
|
|
|
|
// errors
|
|
{name: "invalid-port", input: "localhost:9999999", wantErr: true},
|
|
{name: "unsupported-scheme", input: "ftp://localhost:8080", expected: "", wantErr: true},
|
|
{name: "not-localhost", input: "https://tailscale.com:8080", expected: "", wantErr: true},
|
|
{name: "empty-input", input: "", expected: "", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
defaultScheme := "http"
|
|
supportedSchemes := []string{"http", "https", "https+insecure"}
|
|
|
|
if tt.supportedSchemes != nil {
|
|
supportedSchemes = tt.supportedSchemes
|
|
}
|
|
if tt.defaultScheme != "" {
|
|
defaultScheme = tt.defaultScheme
|
|
}
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual, err := ExpandProxyTargetValue(tt.input, supportedSchemes, defaultScheme)
|
|
|
|
if tt.wantErr == true && err == nil {
|
|
t.Errorf("Expected an error but got none")
|
|
return
|
|
}
|
|
|
|
if tt.wantErr == false && err != nil {
|
|
t.Errorf("Got an error, but didn't expect one: %v", err)
|
|
return
|
|
}
|
|
|
|
if actual != tt.expected {
|
|
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsFunnelOn(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sc *ServeConfig
|
|
want bool
|
|
}{
|
|
{
|
|
name: "nil_config",
|
|
},
|
|
{
|
|
name: "empty_config",
|
|
sc: &ServeConfig{},
|
|
},
|
|
{
|
|
name: "funnel_enabled_in_background",
|
|
sc: &ServeConfig{
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:443": true,
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "funnel_disabled_in_background",
|
|
sc: &ServeConfig{
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:443": false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "funnel_enabled_in_foreground",
|
|
sc: &ServeConfig{
|
|
Foreground: map[string]*ServeConfig{
|
|
"abc123": {
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:443": true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "funnel_disabled_in_both",
|
|
sc: &ServeConfig{
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:443": false,
|
|
},
|
|
Foreground: map[string]*ServeConfig{
|
|
"abc123": {
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:8443": false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "funnel_enabled_in_both",
|
|
sc: &ServeConfig{
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:443": true,
|
|
},
|
|
Foreground: map[string]*ServeConfig{
|
|
"abc123": {
|
|
AllowFunnel: map[HostPort]bool{
|
|
"tailnet.xyz:8443": true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.sc.IsFunnelOn(); got != tt.want {
|
|
t.Errorf("ServeConfig.IsFunnelOn() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|