Files
tailscale-custom/cmd/tailscale/cli/serve_v2_test.go
T
Will Norris 3ec5be3f51 all: remove AUTHORS file and references to it
This file was never truly necessary and has never actually been used in
the history of Tailscale's open source releases.

A Brief History of AUTHORS files
---

The AUTHORS file was a pattern developed at Google, originally for
Chromium, then adopted by Go and a bunch of other projects. The problem
was that Chromium originally had a copyright line only recognizing
Google as the copyright holder. Because Google (and most open source
projects) do not require copyright assignemnt for contributions, each
contributor maintains their copyright. Some large corporate contributors
then tried to add their own name to the copyright line in the LICENSE
file or in file headers. This quickly becomes unwieldy, and puts a
tremendous burden on anyone building on top of Chromium, since the
license requires that they keep all copyright lines intact.

The compromise was to create an AUTHORS file that would list all of the
copyright holders. The LICENSE file and source file headers would then
include that list by reference, listing the copyright holder as "The
Chromium Authors".

This also become cumbersome to simply keep the file up to date with a
high rate of new contributors. Plus it's not always obvious who the
copyright holder is. Sometimes it is the individual making the
contribution, but many times it may be their employer. There is no way
for the proejct maintainer to know.

Eventually, Google changed their policy to no longer recommend trying to
keep the AUTHORS file up to date proactively, and instead to only add to
it when requested: https://opensource.google/docs/releasing/authors.
They are also clear that:

> Adding contributors to the AUTHORS file is entirely within the
> project's discretion and has no implications for copyright ownership.

It was primarily added to appease a small number of large contributors
that insisted that they be recognized as copyright holders (which was
entirely their right to do). But it's not truly necessary, and not even
the most accurate way of identifying contributors and/or copyright
holders.

In practice, we've never added anyone to our AUTHORS file. It only lists
Tailscale, so it's not really serving any purpose. It also causes
confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header
in other open source repos which don't actually have an AUTHORS file, so
it's ambiguous what that means.

Instead, we just acknowledge that the contributors to Tailscale (whoever
they are) are copyright holders for their individual contributions. We
also have the benefit of using the DCO (developercertificate.org) which
provides some additional certification of their right to make the
contribution.

The source file changes were purely mechanical with:

    git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g'

Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
2026-01-23 15:49:45 -08:00

2363 lines
65 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/netip"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/views"
)
func TestServeDevConfigMutations(t *testing.T) {
// step is a stateful mutation within a group
type step struct {
command []string // serve args; nil means no command to run (only reset)
want *ipn.ServeConfig // non-nil means we want a save of this value
wantErr func(error) (badErrMsg string) // nil means no error is wanted
}
// group is a group of steps that share the same
// config mutation
type group struct {
name string
steps []step
initialState fakeLocalServeClient // use the zero value for empty config
}
// creaet a temporary directory for path-based destinations
td := t.TempDir()
writeFile := func(suffix, contents string) {
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
t.Fatal(err)
}
}
writeFile("foo", "this is foo")
err := os.MkdirAll(filepath.Join(td, "subdir"), 0700)
if err != nil {
t.Fatal(err)
}
writeFile("subdir/file-a", "this is subdir")
groups := [...]group{
{
name: "using_port_number",
steps: []step{{
command: cmd("funnel --bg 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
},
}},
},
{
name: "funnel_background",
steps: []step{{
command: cmd("funnel --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
},
}},
},
{
name: "serve_background",
steps: []step{{
command: cmd("serve --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
}},
},
{
name: "set_path_bg",
steps: []step{{
command: cmd("serve --set-path=/ --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
}},
},
{
name: "http_listener",
steps: []step{{
command: cmd("serve --bg --http=80 localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
}},
},
{
name: "https_listener_valid_port",
steps: []step{{
command: cmd("serve --bg --https=8443 localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
}},
},
{
name: "multiple_http_with_off",
steps: []step{
{
command: cmd("serve --http=80 --bg http://localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // support non Funnel port
command: cmd("serve --bg --http=9999 --set-path=/abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://localhost:3001"},
}},
},
},
},
{ // turn off one handler
command: cmd("serve --bg --http=9999 --set-path=/abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // add another handler
command: cmd("serve --bg --http=8080 --set-path=/abc http://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
},
},
},
{
name: "invalid_port_too_low",
steps: []step{{
command: cmd("serve --https=443 --bg http://localhost:0"), // invalid port, too low
wantErr: anyErr(),
}},
},
{
name: "invalid_port_too_high",
steps: []step{{
command: cmd("serve --https=443 --bg http://localhost:65536"), // invalid port, too high
wantErr: anyErr(),
}},
},
{
name: "invalid_mount_port_too_high",
steps: []step{{
command: cmd("serve --https=65536 --bg http://localhost:3000"), // invalid port, too high
wantErr: anyErr(),
}},
},
{
name: "ip_host",
initialState: fakeLocalServeClient{
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --https=443 --bg http://192.168.1.1:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://192.168.1.1:3000"},
}},
},
},
}},
},
{
name: "invalid_scheme",
steps: []step{{
command: cmd("serve --https=443 --bg httpz://127.0.0.1"), // invalid scheme
wantErr: anyErr(),
}},
},
{
name: "no_scheme_remote_host_tcp",
initialState: fakeLocalServeClient{
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --https=443 --bg 192.168.1.1:3000"),
wantErr: exactErrMsg(errHelp),
}},
},
{
name: "turn_off_https",
steps: []step{
{
command: cmd("serve --bg --https=443 http://localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{
command: cmd("serve --bg --https=9999 --set-path=/abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://localhost:3001"},
}},
},
},
},
{
command: cmd("serve --bg --https=9999 --set-path=/abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{
command: cmd("serve --bg --https=8443 --set-path=/abc http://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
},
},
},
{
name: "https_text_bg",
steps: []step{{
command: cmd("serve --bg --https=10000 text:hi"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{10000: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Text: "hi"},
}},
},
},
}},
},
{
name: "handler_not_found",
steps: []step{{
command: cmd("serve --https=443 --set-path=/foo off"),
want: nil, // nothing to save
wantErr: anyErr(),
}},
},
{
name: "clean_mount", // "bar" becomes "/bar"
steps: []step{{
command: cmd("serve --bg --https=443 --set-path=bar https://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "https://127.0.0.1:8443"},
}},
},
},
}},
},
{
name: "serve_reset",
steps: []step{
{
command: cmd("serve --bg --https=443 --set-path=bar https://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "https://127.0.0.1:8443"},
}},
},
},
},
{
command: cmd("serve reset"),
want: &ipn.ServeConfig{},
},
},
},
{
name: "https_insecure",
steps: []step{{
command: cmd("serve --bg --https=443 https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "https+insecure://127.0.0.1:3001"},
}},
},
},
}},
},
{
name: "two_ports_same_dest",
steps: []step{
{
command: cmd("serve --bg --https=443 --set-path=/foo localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://localhost:3000"},
}},
},
},
},
{
command: cmd("serve --bg --https=8443 --set-path=/foo localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://localhost:3000"},
}},
},
},
},
},
},
{
name: "path_in_dest",
steps: []step{{
command: cmd("serve --bg --https=443 http://127.0.0.1:3000/foo/bar"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
}},
},
},
}},
},
{
name: "tcp_port_too_low",
initialState: fakeLocalServeClient{
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:0"),
wantErr: exactErrMsg(errHelp),
}},
},
{
name: "tcp_port_too_high",
initialState: fakeLocalServeClient{
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:65536"),
wantErr: exactErrMsg(errHelp),
}},
},
{
name: "tcp_shorthand",
steps: []step{{
command: cmd("serve --tls-terminated-tcp=443 --bg 5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
},
},
}},
},
{
name: "tls_terminated_tcp",
steps: []step{
{
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TerminateTLS: "foo.test.ts.net",
},
},
},
},
{
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:8443",
TerminateTLS: "foo.test.ts.net",
},
},
},
},
},
},
{
name: "tcp_off",
steps: []step{
{
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:123"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:123",
TerminateTLS: "foo.test.ts.net",
},
},
},
},
{ // handler doesn't exist
command: cmd("serve --tls-terminated-tcp=8443 off"),
wantErr: anyErr(),
},
{
command: cmd("serve --tls-terminated-tcp=443 off"),
want: &ipn.ServeConfig{},
},
},
},
{
name: "text",
steps: []step{{
command: cmd("serve --https=443 --bg text:hello"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Text: "hello"},
}},
},
},
}},
},
{
name: "path",
steps: []step{
{
command: cmd("serve --https=443 --bg " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "foo")},
}},
},
},
},
{
command: cmd("serve --bg --https=443 --set-path=/some/where " + filepath.Join(td, "subdir/file-a")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "foo")},
"/some/where": {Path: filepath.Join(td, "subdir/file-a")},
}},
},
},
},
},
},
{
name: "bad_path",
initialState: fakeLocalServeClient{
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --bg --https=443 bad/path"),
wantErr: exactErrMsg(errHelp),
}},
},
{
name: "path_off",
steps: []step{
{
command: cmd("serve --bg --https=443 " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
},
{
command: cmd("serve --bg --https=443 off"),
want: &ipn.ServeConfig{},
},
},
},
{
name: "combos",
steps: []step{
{
command: cmd("serve --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // enable funnel for primary port
command: cmd("funnel --bg localhost:3000"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // serving on secondary port doesn't change funnel on primary port
command: cmd("serve --bg --https=8443 --set-path=/bar localhost:3001"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://localhost:3001"},
}},
},
},
},
{ // turn funnel on for secondary port
command: cmd("funnel --bg --https=8443 --set-path=/bar localhost:3001"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://localhost:3001"},
}},
},
},
},
{ // turn funnel off for primary port 443
command: cmd("serve --bg localhost:3000"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://localhost:3001"},
}},
},
},
},
{ // remove secondary port
command: cmd("serve --bg --https=8443 --set-path=/bar off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // start a tcp forwarder on 8443
command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "localhost:5432"}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // remove primary port http handler
command: cmd("serve off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "localhost:5432"}},
},
},
{ // remove tcp forwarder
command: cmd("serve --tls-terminated-tcp=8443 off"),
want: &ipn.ServeConfig{},
},
},
},
{
name: "tricky_steps",
steps: []step{
{ // a directory with a trailing slash mount point
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
},
{ // this should overwrite the previous one
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir": {Path: filepath.Join(td, "foo")},
}},
},
},
},
{ // reset and do opposite
command: cmd("serve reset"),
want: &ipn.ServeConfig{},
},
{ // a file without a trailing slash mount point
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir": {Path: filepath.Join(td, "foo")},
}},
},
},
},
{ // this should overwrite the previous one
command: cmd("serve --bg --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
},
},
},
{
name: "cannot_override_tcp_with_http",
steps: []step{
{ // tcp forward 5432 on serve port 443
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TerminateTLS: "foo.test.ts.net",
},
},
},
},
{
command: cmd("serve --https=443 --bg localhost:3000"),
wantErr: anyErr(),
},
},
},
{
name: "cannot_override_http_with_tcp",
steps: []step{
{
command: cmd("serve --https=443 --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
}},
},
},
},
{ // try to start a tcp forwarder on the same serve port
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
wantErr: anyErr(),
},
},
},
{
name: "turn_off_multiple_handlers",
steps: []step{
{
command: cmd("serve --https=4545 --set-path=/foo --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://localhost:3000"},
}},
},
},
},
{
command: cmd("serve --https=4545 --set-path=/bar --bg localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://localhost:3000"},
"/bar": {Proxy: "http://localhost:3000"},
}},
},
},
},
{
command: cmd("serve --https=4545 --bg --yes localhost:3000 off"),
want: &ipn.ServeConfig{},
},
},
},
{
name: "no_http_with_funnel",
steps: []step{
{
command: cmd("funnel --http=80 3000"),
// error parsing commandline arguments: flag provided but not defined: -http
wantErr: anyErr(),
},
},
},
{
name: "advertise_service",
initialState: fakeLocalServeClient{
statusWithoutPeers: &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrFunnel: nil,
tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil,
},
Tags: ptrToReadOnlySlice([]string{"some-tag"}),
},
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
SOMarkInUse: true,
},
steps: []step{{
command: cmd("serve --service=svc:foo --http=80 text:foo"),
want: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Text: "foo"},
}},
},
},
},
},
}},
},
{
name: "advertise_service_from_untagged_node",
steps: []step{{
command: cmd("serve --service=svc:foo --http=80 text:foo"),
wantErr: anyErr(),
}},
},
{
name: "forward_grant_header",
steps: []step{
{
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo"},
},
}},
},
},
},
{
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo,example.com/cap/bar 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"},
},
}},
},
},
},
{
command: cmd("serve --bg --accept-app-caps=example.com/cap/bar 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/bar"},
},
}},
},
},
},
},
},
{
name: "invalid_accept_caps_invalid_app_cap",
steps: []step{
{
command: cmd("serve --bg --accept-app-caps=example.com/cap/fine,NOTFINE 3000"), // should be {domain.tld}/{name}
wantErr: func(err error) (badErrMsg string) {
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%q does not match", "NOTFINE")) {
return fmt.Sprintf("wanted validation error that quotes the non-matching capability (and nothing more) but got %q", err.Error())
}
return ""
},
},
},
},
{
name: "tcp_with_proxy_protocol_v1",
steps: []step{{
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {
TCPForward: "localhost:5432",
ProxyProtocol: 1,
},
},
},
}},
},
{
name: "tls_terminated_tcp_with_proxy_protocol_v2",
steps: []step{{
command: cmd("serve --tls-terminated-tcp=443 --proxy-protocol=2 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "localhost:5432",
TerminateTLS: "foo.test.ts.net",
ProxyProtocol: 2,
},
},
},
}},
},
{
name: "tcp_update_to_add_proxy_protocol",
steps: []step{
{
command: cmd("serve --tcp=8000 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {TCPForward: "localhost:5432"},
},
},
},
{
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
8000: {
TCPForward: "localhost:5432",
ProxyProtocol: 1,
},
},
},
},
},
},
{
name: "tcp_proxy_protocol_invalid_version",
steps: []step{{
command: cmd("serve --tcp=8000 --proxy-protocol=3 --bg tcp://localhost:5432"),
wantErr: anyErr(),
}},
},
{
name: "proxy_protocol_without_tcp",
steps: []step{{
command: cmd("serve --https=443 --proxy-protocol=1 --bg http://localhost:3000"),
wantErr: anyErr(),
}},
},
}
for _, group := range groups {
t.Run(group.name, func(t *testing.T) {
lc := group.initialState
for i, st := range group.steps {
var stderr bytes.Buffer
var stdout bytes.Buffer
var flagOut bytes.Buffer
e := &serveEnv{
lc: &lc,
testFlagOut: &flagOut,
testStdout: &stdout,
testStderr: &stderr,
}
lastCount := lc.setCount
var cmd *ffcli.Command
var args []string
mode := serve
if st.command[0] == "funnel" {
mode = funnel
}
cmd = newServeV2Command(e, mode)
args = st.command[1:]
err := cmd.ParseAndRun(context.Background(), args)
if flagOut.Len() > 0 {
t.Logf("flag package output: %q", flagOut.Bytes())
}
if err != nil {
if st.wantErr == nil {
t.Fatalf("step #%d: unexpected error: %v", i, err)
}
if bad := st.wantErr(err); bad != "" {
t.Fatalf("step #%d: unexpected error: %v", i, bad)
}
continue
}
if st.wantErr != nil {
t.Fatalf("step #%d: got success (saved=%v), but wanted an error", i, lc.config != nil)
}
var got *ipn.ServeConfig = nil
if lc.setCount > lastCount {
got = lc.config
}
if !reflect.DeepEqual(got, st.want) {
gotbts, _ := json.MarshalIndent(got, "", "\t")
wantbts, _ := json.MarshalIndent(st.want, "", "\t")
t.Fatalf("step: %d, cmd: %v, diff:\n%s", i, st.command, cmp.Diff(string(gotbts), string(wantbts)))
}
}
})
}
}
func TestSrcTypeFromFlags(t *testing.T) {
tests := []struct {
name string
env *serveEnv
expectedType serveType
expectedPort uint16
expectedErr bool
}{
{
name: "only http set",
env: &serveEnv{http: 80},
expectedType: serveTypeHTTP,
expectedPort: 80,
expectedErr: false,
},
{
name: "only https set",
env: &serveEnv{https: 10000},
expectedType: serveTypeHTTPS,
expectedPort: 10000,
expectedErr: false,
},
{
name: "only tcp set",
env: &serveEnv{tcp: 8000},
expectedType: serveTypeTCP,
expectedPort: 8000,
expectedErr: false,
},
{
name: "only tls-terminated-tcp set",
env: &serveEnv{tlsTerminatedTCP: 8080},
expectedType: serveTypeTLSTerminatedTCP,
expectedPort: 8080,
expectedErr: false,
},
{
name: "defaults to https, port 443",
env: &serveEnv{},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
},
{
name: "defaults to https, port 443 for service",
env: &serveEnv{service: "svc:foo"},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
},
{
name: "multiple types set",
env: &serveEnv{http: 80, https: 443},
expectedPort: 0,
expectedErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srcType, srcPort, err := srvTypeAndPortFromFlags(tt.env)
if (err != nil) != tt.expectedErr {
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
}
if srcType != tt.expectedType {
t.Errorf("Expected srcType: %s, got: %s", tt.expectedType.String(), srcType)
}
if srcPort != tt.expectedPort {
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
}
})
}
}
func TestAcceptSetAppCapsFlag(t *testing.T) {
testCases := []struct {
name string
inputs []string
expectErr bool
expectErrToMatch *regexp.Regexp
expectedValue []tailcfg.PeerCapability
}{
{
name: "valid_simple",
inputs: []string{"example.com/name"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"example.com/name"},
},
{
name: "valid_unicode",
inputs: []string{"bücher.de/something"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"bücher.de/something"},
},
{
name: "more_valid_unicode",
inputs: []string{"example.tw/某某某"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"example.tw/某某某"},
},
{
name: "valid_path_slashes",
inputs: []string{"domain.com/path/to/name"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"domain.com/path/to/name"},
},
{
name: "valid_multiple_sets",
inputs: []string{"one.com/foo,two.com/bar"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"one.com/foo", "two.com/bar"},
},
{
name: "valid_empty_string",
inputs: []string{""},
expectErr: false,
expectedValue: nil, // Empty string should be a no-op and not append anything.
},
{
name: "invalid_path_chars",
inputs: []string{"domain.com/path_with_underscore"},
expectErr: true,
expectErrToMatch: regexp.MustCompile(`"domain.com/path_with_underscore"`),
expectedValue: nil, // Slice should remain empty.
},
{
name: "valid_subdomain",
inputs: []string{"sub.domain.com/name"},
expectErr: false,
expectedValue: []tailcfg.PeerCapability{"sub.domain.com/name"},
},
{
name: "invalid_no_path",
inputs: []string{"domain.com/"},
expectErr: true,
expectErrToMatch: regexp.MustCompile(`"domain.com/"`),
expectedValue: nil,
},
{
name: "invalid_no_domain",
inputs: []string{"/path/only"},
expectErr: true,
expectErrToMatch: regexp.MustCompile(`"/path/only"`),
expectedValue: nil,
},
{
name: "some_invalid_some_valid",
inputs: []string{"one.com/foo,bad/bar,two.com/baz"},
expectErr: true,
expectErrToMatch: regexp.MustCompile(`"bad/bar"`),
expectedValue: []tailcfg.PeerCapability{"one.com/foo"}, // Parsing will stop after first error
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var v []tailcfg.PeerCapability
flag := &acceptAppCapsFlag{Value: &v}
var err error
for _, s := range tc.inputs {
err = flag.Set(s)
if err != nil {
break
}
}
if tc.expectErr && err == nil {
t.Errorf("expected an error, but got none")
}
if tc.expectErrToMatch != nil {
if !tc.expectErrToMatch.MatchString(err.Error()) {
t.Errorf("expected error to match %q, but was %q", tc.expectErrToMatch, err)
}
}
if !tc.expectErr && err != nil {
t.Errorf("did not expect an error, but got: %v", err)
}
if !reflect.DeepEqual(tc.expectedValue, v) {
t.Errorf("unexpected value, got: %q, want: %q", v, tc.expectedValue)
}
})
}
}
func TestCleanURLPath(t *testing.T) {
tests := []struct {
input string
expected string
wantErr bool
}{
{input: "", expected: "/"},
{input: "/", expected: "/"},
{input: "/foo", expected: "/foo"},
{input: "/foo/", expected: "/foo/"},
{input: "/../bar", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
actual, err := cleanURLPath(tt.input)
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 TestAddServiceToPrefs(t *testing.T) {
tests := []struct {
name string
svcName tailcfg.ServiceName
startServices []string
expected []string
}{
{
name: "add service to empty prefs",
svcName: "svc:foo",
expected: []string{"svc:foo"},
},
{
name: "add service to existing prefs",
svcName: "svc:bar",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo", "svc:bar"},
},
{
name: "add existing service to prefs",
svcName: "svc:foo",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lc := &fakeLocalServeClient{}
ctx := t.Context()
lc.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseServicesSet: true,
Prefs: ipn.Prefs{
AdvertiseServices: tt.startServices,
},
})
e := &serveEnv{lc: lc, bg: bgBoolFlag{true, false}}
err := e.addServiceToPrefs(ctx, tt.svcName)
if err != nil {
t.Fatalf("addServiceToPrefs(%q) returned unexpected error: %v", tt.svcName, err)
}
if !slices.Equal(lc.prefs.AdvertiseServices, tt.expected) {
t.Errorf("addServiceToPrefs(%q) = %v, want %v", tt.svcName, lc.prefs.AdvertiseServices, tt.expected)
}
})
}
}
func TestRemoveServiceFromPrefs(t *testing.T) {
tests := []struct {
name string
svcName tailcfg.ServiceName
startServices []string
expected []string
}{
{
name: "remove service from empty prefs",
svcName: "svc:foo",
expected: []string{},
},
{
name: "remove existing service from prefs",
svcName: "svc:foo",
startServices: []string{"svc:foo"},
expected: []string{},
},
{
name: "remove service not in prefs",
svcName: "svc:bar",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lc := &fakeLocalServeClient{}
ctx := t.Context()
lc.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseServicesSet: true,
Prefs: ipn.Prefs{
AdvertiseServices: tt.startServices,
},
})
e := &serveEnv{lc: lc, bg: bgBoolFlag{true, false}}
err := e.removeServiceFromPrefs(ctx, tt.svcName)
if err != nil {
t.Fatalf("removeServiceFromPrefs(%q) returned unexpected error: %v", tt.svcName, err)
}
if !slices.Equal(lc.prefs.AdvertiseServices, tt.expected) {
t.Errorf("removeServiceFromPrefs(%q) = %v, want %v", tt.svcName, lc.prefs.AdvertiseServices, tt.expected)
}
})
}
}
func TestMessageForPort(t *testing.T) {
svcIPMap := tailcfg.ServiceIPMappings{
"svc:foo": []netip.Addr{
netip.MustParseAddr("100.101.101.101"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
},
}
svcIPMapJSON, _ := json.Marshal(svcIPMap)
svcIPMapJSONRawMSG := tailcfg.RawMessage(svcIPMapJSON)
tests := []struct {
name string
subcmd serveMode
serveConfig *ipn.ServeConfig
status *ipnstate.Status
prefs *ipn.Prefs
dnsName string
srvType serveType
srvPort uint16
expected string
}{
{
name: "funnel-https",
subcmd: funnel,
serveConfig: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
},
},
},
AllowFunnel: map[ipn.HostPort]bool{
"foo.test.ts.net:443": true,
},
},
status: &ipnstate.Status{CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTPS,
srvPort: 443,
expected: strings.Join([]string{
msgFunnelAvailable,
"",
"https://foo.test.ts.net/",
"|-- proxy http://127.0.0.1:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Funnel"),
fmt.Sprintf(msgDisableProxy, "funnel", "https", 443),
}, "\n"),
},
{
name: "serve-http",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
},
},
},
},
status: &ipnstate.Status{CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
msgServeAvailable,
"",
"https://foo.test.ts.net:80/",
"|-- proxy http://127.0.0.1:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableProxy, "serve", "http", 80),
}, "\n"),
},
{
name: "serve service http",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{
AdvertiseServices: []string{"svc:foo"},
},
dnsName: "svc:foo",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
msgServeAvailable,
"",
"http://foo.test.ts.net/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "http", 80),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service no capmap",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{
AdvertiseServices: []string{"svc:bar"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
expected: strings.Join([]string{
fmt.Sprintf(msgServiceWaitingApproval, "svc:bar"),
"",
"http://bar.test.ts.net/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:bar", "http", 80),
fmt.Sprintf(msgDisableService, "svc:bar"),
}, "\n"),
},
{
name: "serve service https non-default port",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {HTTPS: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:2200": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeHTTPS,
srvPort: 2200,
expected: strings.Join([]string{
msgServeAvailable,
"",
"https://foo.test.ts.net:2200/",
"|-- proxy http://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "https", 2200),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service TCPForward",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
TCP: map[uint16]*ipn.TCPPortHandler{
2200: {TCPForward: "localhost:3000"},
},
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeTCP,
srvPort: 2200,
expected: strings.Join([]string{
msgServeAvailable,
"",
"|-- tcp://foo.test.ts.net:2200 (TLS over TCP)",
"|-- tcp://100.101.101.101:2200",
"|-- tcp://[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:2200",
"|--> tcp://localhost:3000",
"",
fmt.Sprintf(msgRunningInBackground, "Serve"),
fmt.Sprintf(msgDisableServiceProxy, "svc:foo", "tcp", 2200),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
{
name: "serve service Tun",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:foo": {
Tun: true,
},
},
},
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
Self: &ipnstate.PeerStatus{
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG},
},
},
},
prefs: &ipn.Prefs{AdvertiseServices: []string{"svc:foo"}},
dnsName: "svc:foo",
srvType: serveTypeTUN,
expected: strings.Join([]string{
msgServeAvailable,
"",
fmt.Sprintf(msgRunningTunService, "foo.test.ts.net"),
fmt.Sprintf(msgDisableServiceTun, "svc:foo"),
fmt.Sprintf(msgDisableService, "svc:foo"),
}, "\n"),
},
}
for _, tt := range tests {
e := &serveEnv{bg: bgBoolFlag{true, false}, subcmd: tt.subcmd}
t.Run(tt.name, func(t *testing.T) {
actual := e.messageForPort(tt.serveConfig, tt.status, tt.dnsName, tt.srvType, tt.srvPort)
if actual == "" {
t.Errorf("Got empty message")
}
if actual != tt.expected {
t.Errorf("\nGot: %q\nExpected: %q", actual, tt.expected)
}
})
}
}
func TestIsLegacyInvocation(t *testing.T) {
tests := []struct {
subcmd serveMode
args []string
expected bool
translation string
}{
{
subcmd: serve,
args: []string{"https", "/", "localhost:3000"},
expected: true,
translation: "tailscale serve --bg localhost:3000",
},
{
subcmd: serve,
args: []string{"https", "/", "localhost:3000", "off"},
expected: true,
translation: "tailscale serve --bg localhost:3000 off",
},
{
subcmd: serve,
args: []string{"https", "/", "off"},
expected: true,
translation: "tailscale serve --bg off",
},
{
subcmd: serve,
args: []string{"https:4545", "/foo", "off"},
expected: true,
translation: "tailscale serve --bg --https 4545 --set-path /foo off",
},
{
subcmd: serve,
args: []string{"https:4545", "/foo", "localhost:9090", "off"},
expected: true,
translation: "tailscale serve --bg --https 4545 --set-path /foo localhost:9090 off",
},
{
subcmd: serve,
args: []string{"https:8443", "/", "localhost:3000"},
expected: true,
translation: "tailscale serve --bg --https 8443 localhost:3000",
},
{
subcmd: serve,
args: []string{"http", "/", "localhost:3000"},
expected: true,
translation: "tailscale serve --bg --http 80 localhost:3000",
},
{
subcmd: serve,
args: []string{"http:80", "/", "localhost:3000"},
expected: true,
translation: "tailscale serve --bg --http 80 localhost:3000",
},
{
subcmd: serve,
args: []string{"https:10000", "/motd.txt", `text:Hello, world!`},
expected: true,
translation: `tailscale serve --bg --https 10000 --set-path /motd.txt "text:Hello, world!"`,
},
{
subcmd: serve,
args: []string{"tcp:2222", "tcp://localhost:22"},
expected: true,
translation: "tailscale serve --bg --tcp 2222 tcp://localhost:22",
},
{
subcmd: serve,
args: []string{"tls-terminated-tcp:443", "tcp://localhost:80"},
expected: true,
translation: "tailscale serve --bg --tls-terminated-tcp 443 tcp://localhost:80",
},
{
subcmd: funnel,
args: []string{"443", "on"},
expected: true,
},
{
subcmd: funnel,
args: []string{"443", "off"},
expected: true,
},
{
subcmd: serve,
args: []string{"3000"},
expected: false,
},
{
subcmd: serve,
args: []string{"localhost:3000"},
expected: false,
},
}
for idx, tt := range tests {
t.Run(strconv.Itoa(idx), func(t *testing.T) {
gotTranslation, actual := isLegacyInvocation(tt.subcmd, tt.args)
if actual != tt.expected {
t.Fatalf("got: %v; expected: %v", actual, tt.expected)
}
if gotTranslation != tt.translation {
t.Fatalf("expected translation to be %q but got %q", tt.translation, gotTranslation)
}
})
}
}
func TestSetServe(t *testing.T) {
e := &serveEnv{}
magicDNSSuffix := "test.ts.net"
tests := []struct {
name string
desc string
cfg *ipn.ServeConfig
st *ipnstate.Status
dnsName string
srvType serveType
srvPort uint16
mountPath string
target string
allowFunnel bool
proxyProtocol int
expected *ipn.ServeConfig
expectErr bool
}{
{
name: "add new handler",
desc: "add a new http handler to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3000",
expected: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
{
name: "update http handler",
desc: "update an existing http handler on the same port to same type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expected: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3001"},
},
},
},
},
},
{
name: "update TCP handler",
desc: "update an existing TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "http://localhost:3000"}},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expectErr: true,
},
{
name: "add new service handler",
desc: "add a new service TCP handler to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
target: "3000",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
},
{
name: "update service handler",
desc: "update an existing service TCP handler on the same port to same type",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
target: "3001",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3001"}},
},
},
},
},
{
name: "update service handler",
desc: "update an existing service TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "127.0.0.1:3000"}},
},
},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expectErr: true,
},
{
name: "add new service handler",
desc: "add a new service HTTP handler to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3000",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
},
{
name: "update existing service handler",
desc: "update an existing service HTTP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/",
target: "http://localhost:3001",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3001"},
},
},
},
},
},
},
},
{
name: "add new service handler",
desc: "add a new service HTTP handler to existing service config",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 88,
mountPath: "/",
target: "http://localhost:3001",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
88: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
"bar.test.ts.net:88": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3001"},
},
},
},
},
},
},
},
{
name: "add new service mount",
desc: "add a new service mount to existing service config",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mountPath: "/added",
target: "http://localhost:3001",
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
"/added": {Proxy: "http://localhost:3001"},
},
},
},
},
},
},
},
{
name: "add new service handler",
desc: "add a new service handler in tun mode to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "svc:bar",
srvType: serveTypeTUN,
expected: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
Tun: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := e.setServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mountPath, tt.target, tt.allowFunnel, magicDNSSuffix, nil, tt.proxyProtocol)
if err != nil && !tt.expectErr {
t.Fatalf("got error: %v; did not expect error.", err)
}
if err == nil && tt.expectErr {
t.Fatalf("got no error; expected error.")
}
if !tt.expectErr {
if diff := cmp.Diff(tt.expected, tt.cfg); diff != "" {
// svcName := tailcfg.ServiceName(tt.dnsName)
t.Fatalf("got diff:\n%s", diff)
}
}
})
}
}
func TestUnsetServe(t *testing.T) {
tests := []struct {
name string
desc string
cfg *ipn.ServeConfig
st *ipnstate.Status
dnsName string
srvType serveType
srvPort uint16
mount string
setServeEnv bool
serveEnv *serveEnv // if set, use this instead of the default serveEnv
expected *ipn.ServeConfig
expectErr bool
}{
{
name: "unset http handler",
desc: "remove an existing http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mount: "/",
expected: &ipn.ServeConfig{},
expectErr: false,
},
{
name: "unset service handler",
desc: "remove an existing service TCP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mount: "/",
expected: &ipn.ServeConfig{},
expectErr: false,
},
{
name: "unset service handler tun",
desc: "remove an existing service handler in tun mode",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
Tun: true,
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTUN,
expected: &ipn.ServeConfig{},
expectErr: false,
},
{
name: "unset service handler tcp",
desc: "remove an existing service TCP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {TCPForward: "11.11.11.11:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
expected: &ipn.ServeConfig{},
expectErr: false,
},
{
name: "unset http handler not found",
desc: "try to remove a non-existing http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "bar.test.ts.net",
srvType: serveTypeHTTP,
srvPort: 80,
mount: "/abc",
expected: &ipn.ServeConfig{},
expectErr: true,
},
{
name: "unset service handler not found",
desc: "try to remove a non-existing service TCP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeHTTP,
srvPort: 80,
mount: "/abc",
setServeEnv: true,
serveEnv: &serveEnv{setPath: "/abc"},
expected: &ipn.ServeConfig{},
expectErr: true,
},
{
name: "unset service doesn't exist",
desc: "try to remove a non-existing service's handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {TCPForward: "11.11.11.11:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:foo",
srvType: serveTypeTCP,
srvPort: 80,
expectErr: true,
},
{
name: "unset tcp while port is in use",
desc: "try to remove a TCP handler while the port is used for web",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "foo.test.ts.net",
srvType: serveTypeTCP,
srvPort: 80,
mount: "/",
expectErr: true,
},
{
name: "unset service tcp while port is in use",
desc: "try to remove a service TCP handler while the port is used for web",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
"svc:bar": {
TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTP: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"bar.test.ts.net:80": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://localhost:3000"},
},
},
},
},
},
},
st: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
},
dnsName: "svc:bar",
srvType: serveTypeTCP,
srvPort: 80,
mount: "/",
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &serveEnv{}
if tt.setServeEnv {
e = tt.serveEnv
}
err := e.unsetServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mount, tt.st.CurrentTailnet.MagicDNSSuffix)
if err != nil && !tt.expectErr {
t.Fatalf("got error: %v; did not expect error.", err)
}
if err == nil && tt.expectErr {
t.Fatalf("got no error; expected error.")
}
if !tt.expectErr && !reflect.DeepEqual(tt.cfg, tt.expected) {
t.Fatalf("got: %v; expected: %v", tt.cfg, tt.expected)
}
})
}
}
// exactErrMsg returns an error checker that wants exactly the provided want error.
// If optName is non-empty, it's used in the error message.
func exactErrMsg(want error) func(error) string {
return func(got error) string {
if got.Error() == want.Error() {
return ""
}
return fmt.Sprintf("\ngot: %v\nwant: %v\n", got, want)
}
}
func ptrToReadOnlySlice[T any](s []T) *views.Slice[T] {
vs := views.SliceOf(s)
return &vs
}