2023-03-10 11:44:28 -08:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2025-06-16 12:02:20 -07:00
|
|
|
|
2023-03-10 11:44:28 -08:00
|
|
|
package ipn
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
|
2023-09-27 23:01:09 -07:00
|
|
|
"tailscale.com/ipn/ipnstate"
|
2023-03-10 11:44:28 -08:00
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestCheckFunnelAccess(t *testing.T) {
|
2023-09-06 10:17:25 -07:00
|
|
|
caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c }
|
|
|
|
const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
|
2023-03-10 11:44:28 -08:00
|
|
|
tests := []struct {
|
2023-03-11 08:45:40 -08:00
|
|
|
port uint16
|
2023-09-06 10:17:25 -07:00
|
|
|
caps []tailcfg.NodeCapability
|
2023-03-10 11:44:28 -08:00
|
|
|
wantErr bool
|
|
|
|
}{
|
2023-09-06 10:17:25 -07:00
|
|
|
{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},
|
2023-03-10 11:44:28 -08:00
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
2023-09-27 23:01:09 -07:00
|
|
|
cm := tailcfg.NodeCapMap{}
|
|
|
|
for _, c := range tt.caps {
|
|
|
|
cm[c] = nil
|
|
|
|
}
|
|
|
|
err := CheckFunnelAccess(tt.port, &ipnstate.PeerStatus{CapMap: cm})
|
2023-03-10 11:44:28 -08:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-26 14:40:44 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-03-05 13:54:37 -05:00
|
|
|
|
cmd/tailscale/cli: Add service flag to serve command
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>
2025-06-04 12:09:46 -04:00
|
|
|
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) {
|
2025-06-26 17:07:51 -04:00
|
|
|
got := tt.cfg.IsTCPForwardingOnPort(tt.dns, tt.port)
|
cmd/tailscale/cli: Add service flag to serve command
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>
2025-06-04 12:09:46 -04:00
|
|
|
if tt.want != got {
|
|
|
|
t.Errorf("IsTCPForwardingOnPort() = %v, want %v", got, tt.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-05 13:54:37 -05:00
|
|
|
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"},
|
2024-05-20 17:38:08 -07:00
|
|
|
{name: "hostname+port", input: "localhost:8080", expected: "http://localhost:8080"},
|
2024-03-05 13:54:37 -05:00
|
|
|
{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"},
|
2024-05-20 17:38:08 -07:00
|
|
|
{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"},
|
2024-03-05 13:54:37 -05:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2025-01-19 19:00:21 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|