2023-01-27 13:37:20 -08:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2022-11-12 13:55:43 -08:00
|
|
|
|
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
2022-11-12 15:28:32 -08:00
|
|
|
"bytes"
|
|
|
|
"context"
|
2023-08-09 10:06:58 -04:00
|
|
|
"errors"
|
2022-11-12 15:28:32 -08:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2024-04-07 18:17:25 -07:00
|
|
|
"io"
|
2022-11-17 16:09:43 -05:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2022-11-12 13:55:43 -08:00
|
|
|
"reflect"
|
|
|
|
"runtime"
|
2022-11-12 15:28:32 -08:00
|
|
|
"strings"
|
2022-11-12 13:55:43 -08:00
|
|
|
"testing"
|
|
|
|
|
2023-03-13 21:43:28 -04:00
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
2023-08-09 10:06:58 -04:00
|
|
|
"tailscale.com/client/tailscale"
|
2022-11-12 13:55:43 -08:00
|
|
|
"tailscale.com/ipn"
|
2022-11-17 16:09:43 -05:00
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
|
|
"tailscale.com/tailcfg"
|
2024-03-23 16:23:59 -07:00
|
|
|
"tailscale.com/tstest"
|
2023-08-28 15:01:38 -07:00
|
|
|
"tailscale.com/types/logger"
|
2022-11-12 13:55:43 -08:00
|
|
|
)
|
|
|
|
|
2022-11-17 16:09:43 -05:00
|
|
|
func TestCleanMountPoint(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
mount string
|
|
|
|
want string
|
|
|
|
wantErr bool
|
|
|
|
}{
|
|
|
|
{"foo", "/foo", false}, // missing prefix
|
|
|
|
{"/foo/", "/foo/", false}, // keep trailing slash
|
|
|
|
{"////foo", "", true}, // too many slashes
|
|
|
|
{"/foo//", "", true}, // too many slashes
|
|
|
|
{"", "", true}, // empty
|
|
|
|
{"https://tailscale.com", "", true}, // not a path
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
mp, err := cleanMountPoint(tt.mount)
|
|
|
|
if err != nil && tt.wantErr {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if mp != tt.want {
|
|
|
|
t.Fatalf("got %q, want %q", mp, tt.want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-12 13:55:43 -08:00
|
|
|
func TestServeConfigMutations(t *testing.T) {
|
2024-04-07 18:17:25 -07:00
|
|
|
tstest.Replace(t, &Stderr, io.Discard)
|
|
|
|
tstest.Replace(t, &Stdout, io.Discard)
|
|
|
|
|
2022-11-12 13:55:43 -08:00
|
|
|
// Stateful mutations, starting from an empty config.
|
|
|
|
type step struct {
|
2022-11-12 15:28:32 -08:00
|
|
|
command []string // serve args; nil means no command to run (only reset)
|
|
|
|
reset bool // if true, reset all ServeConfig state
|
|
|
|
want *ipn.ServeConfig // non-nil means we want a save of this value
|
|
|
|
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
|
|
|
line int // line number of addStep call, for error messages
|
2023-03-13 21:43:28 -04:00
|
|
|
|
|
|
|
debugBreak func()
|
2022-11-12 13:55:43 -08:00
|
|
|
}
|
|
|
|
var steps []step
|
|
|
|
add := func(s step) {
|
|
|
|
_, _, s.line, _ = runtime.Caller(1)
|
|
|
|
steps = append(steps, s)
|
|
|
|
}
|
2022-11-12 15:28:32 -08:00
|
|
|
|
2022-11-17 16:09:43 -05:00
|
|
|
// funnel
|
2022-11-12 13:55:43 -08:00
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 on"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
2022-11-12 15:28:32 -08:00
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 on"),
|
2022-11-12 15:28:32 -08:00
|
|
|
want: nil, // nothing to save
|
2022-11-12 13:55:43 -08:00
|
|
|
})
|
2022-11-12 15:28:32 -08:00
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: nil, // nothing to save
|
|
|
|
})
|
|
|
|
add(step{
|
|
|
|
command: cmd("funnel"),
|
|
|
|
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
|
|
|
})
|
|
|
|
|
2023-06-21 12:32:20 -04:00
|
|
|
// https
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{ // allow omitting port (default to 80)
|
|
|
|
command: cmd("http / 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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // support non Funnel port
|
|
|
|
command: cmd("http:9999 /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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
|
|
|
command: cmd("http:9999 /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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
|
|
|
command: cmd("http:8080 /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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2022-11-17 16:09:43 -05:00
|
|
|
// https
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / http://somehost:3000"), // invalid host
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
|
|
|
})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // allow omitting port (default to 443)
|
|
|
|
command: cmd("https / 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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // support non Funnel port
|
|
|
|
command: cmd("https:9999 /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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
2022-11-17 16:09:43 -05:00
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:9999 /abc off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
2022-11-12 15:28:32 -08:00
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:10000 / text:hi"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
|
|
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {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.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/": {Text: "hi"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /foo off"),
|
2022-11-12 15:28:32 -08:00
|
|
|
want: nil, // nothing to save
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
|
|
|
}) // handler doesn't exist, so we get an error
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:10000 / off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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{
|
|
|
|
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:8443 /abc off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // clean mount: "bar" becomes "/bar"
|
|
|
|
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: nil, // nothing to save
|
|
|
|
})
|
2023-05-15 23:34:55 -04:00
|
|
|
add(step{ // try resetting using reset command
|
|
|
|
command: cmd("reset"),
|
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
2022-11-17 16:09:43 -05:00
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /foo localhost:3000"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // test a second handler on the same port
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:8443 /foo localhost:3000"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2023-04-05 12:33:18 -04:00
|
|
|
add(step{reset: true})
|
|
|
|
add(step{ // support path in proxy
|
|
|
|
command: cmd("https / 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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2022-11-17 16:09:43 -05:00
|
|
|
|
|
|
|
// tcp
|
|
|
|
add(step{reset: true})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // must include scheme for tcp
|
|
|
|
command: cmd("tls-terminated-tcp:443 localhost:5432"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2023-03-13 21:43:28 -04:00
|
|
|
})
|
|
|
|
add(step{ // !somehost, must be localhost or 127.0.0.1
|
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2023-03-13 21:43:28 -04:00
|
|
|
})
|
|
|
|
add(step{ // bad target port, too low
|
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2023-03-13 21:43:28 -04:00
|
|
|
})
|
|
|
|
add(step{ // bad target port, too high
|
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2023-03-13 21:43:28 -04:00
|
|
|
})
|
2022-11-17 16:09:43 -05:00
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
2023-03-13 21:43:28 -04:00
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:5432",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
2022-11-17 16:09:43 -05:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:8443",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: nil, // nothing to save
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:8444",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
2023-03-13 21:43:28 -04:00
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:8445",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
2022-11-17 16:09:43 -05:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
2023-03-13 21:43:28 -04:00
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:123",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
2022-11-17 16:09:43 -05:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // handler doesn't exist, so we get an error
|
|
|
|
command: cmd("tls-terminated-tcp:8443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
wantErr: anyErr(),
|
2023-03-13 21:43:28 -04:00
|
|
|
})
|
2022-11-17 16:09:43 -05:00
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
|
|
|
|
|
|
|
// text
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / text:hello"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// path
|
|
|
|
td := t.TempDir()
|
|
|
|
writeFile := func(suffix, contents string) {
|
|
|
|
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
add(step{reset: true})
|
|
|
|
writeFile("foo", "this is foo")
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / " + filepath.Join(td, "foo")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
|
|
|
writeFile("subdir/file-a", "this is A")
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
2022-11-12 15:28:32 -08:00
|
|
|
})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // bad path
|
|
|
|
command: cmd("https:443 / bad/path"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2022-11-17 16:09:43 -05:00
|
|
|
})
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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/")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
|
|
|
|
|
|
|
// combos
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / localhost:3000"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 on"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // serving on secondary port doesn't change funnel
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:8443 /bar localhost:3001"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // turn funnel on for secondary port
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 8443 on"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // turn funnel off for primary port 443
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // remove secondary port
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:8443 /bar off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": 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://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // start a tcp forwarder on 8443
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tcp:8443 tcp://localhost:5432"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
|
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
|
|
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
|
|
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // remove primary port http handler
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // remove tcp forwarder
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:8443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // turn off funnel
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("funnel 8443 off"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{},
|
|
|
|
})
|
|
|
|
|
|
|
|
// tricky steps
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{ // a directory with a trailing slash mount point
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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/")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // this should overwrite the previous one
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{reset: true}) // reset and do the opposite
|
|
|
|
add(step{ // a file without a trailing slash mount point
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // this should overwrite the previous one
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
2022-11-17 16:09:43 -05:00
|
|
|
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/")},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// error states
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{ // tcp forward 5432 on serve port 443
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
2022-11-17 16:09:43 -05:00
|
|
|
want: &ipn.ServeConfig{
|
|
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
2023-03-13 21:43:28 -04:00
|
|
|
443: {
|
|
|
|
TCPForward: "127.0.0.1:5432",
|
|
|
|
TerminateTLS: "foo.test.ts.net",
|
|
|
|
},
|
2022-11-17 16:09:43 -05:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
add(step{ // try to start a web handler on the same port
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / localhost:3000"),
|
2023-08-29 10:55:38 -04:00
|
|
|
wantErr: exactErr(errHelp, "errHelp"),
|
2022-11-17 16:09:43 -05:00
|
|
|
})
|
|
|
|
add(step{reset: true})
|
|
|
|
add(step{ // start a web handler on port 443
|
2023-03-13 21:43:28 -04:00
|
|
|
command: cmd("https:443 / localhost:3000"),
|
2022-11-17 16:09:43 -05:00
|
|
|
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"},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2023-03-13 21:43:28 -04:00
|
|
|
add(step{ // try to start a tcp forwarder on the same serve port
|
|
|
|
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
2022-11-19 09:42:14 -05:00
|
|
|
wantErr: anyErr(),
|
2022-11-12 15:28:32 -08:00
|
|
|
})
|
|
|
|
|
2023-01-20 12:36:25 -05:00
|
|
|
lc := &fakeLocalServeClient{}
|
2022-11-12 15:28:32 -08:00
|
|
|
// And now run the steps above.
|
2022-11-12 13:55:43 -08:00
|
|
|
for i, st := range steps {
|
2023-03-13 21:43:28 -04:00
|
|
|
if st.debugBreak != nil {
|
|
|
|
st.debugBreak()
|
|
|
|
}
|
2022-11-12 13:55:43 -08:00
|
|
|
if st.reset {
|
2022-11-12 15:28:32 -08:00
|
|
|
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
2023-01-20 12:36:25 -05:00
|
|
|
lc.config = nil
|
2022-11-12 13:55:43 -08:00
|
|
|
}
|
2022-11-12 15:28:32 -08:00
|
|
|
if st.command == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
t.Logf("Executing step #%d, line %v: %q ... ", i, st.line, st.command)
|
|
|
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
var flagOut bytes.Buffer
|
|
|
|
e := &serveEnv{
|
2023-01-20 12:36:25 -05:00
|
|
|
lc: lc,
|
2022-11-12 15:28:32 -08:00
|
|
|
testFlagOut: &flagOut,
|
|
|
|
testStdout: &stdout,
|
2024-04-07 18:17:25 -07:00
|
|
|
testStderr: io.Discard,
|
2022-11-12 15:28:32 -08:00
|
|
|
}
|
2023-01-20 12:36:25 -05:00
|
|
|
lastCount := lc.setCount
|
2023-03-13 21:43:28 -04:00
|
|
|
var cmd *ffcli.Command
|
|
|
|
var args []string
|
|
|
|
if st.command[0] == "funnel" {
|
|
|
|
cmd = newFunnelCommand(e)
|
|
|
|
args = st.command[1:]
|
|
|
|
} else {
|
2023-10-17 09:32:17 -07:00
|
|
|
cmd = newServeLegacyCommand(e)
|
2023-03-13 21:43:28 -04:00
|
|
|
args = st.command
|
|
|
|
}
|
2024-04-07 18:17:25 -07:00
|
|
|
if cmd.FlagSet == nil {
|
|
|
|
cmd.FlagSet = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
|
|
|
|
cmd.FlagSet.SetOutput(Stdout)
|
|
|
|
}
|
2023-03-13 21:43:28 -04:00
|
|
|
err := cmd.ParseAndRun(context.Background(), args)
|
2022-11-12 15:28:32 -08:00
|
|
|
if flagOut.Len() > 0 {
|
|
|
|
t.Logf("flag package output: %q", flagOut.Bytes())
|
|
|
|
}
|
2022-11-12 13:55:43 -08:00
|
|
|
if err != nil {
|
2022-11-12 15:28:32 -08:00
|
|
|
if st.wantErr == nil {
|
|
|
|
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, err)
|
|
|
|
}
|
|
|
|
if bad := st.wantErr(err); bad != "" {
|
|
|
|
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, bad)
|
|
|
|
}
|
|
|
|
continue
|
2022-11-12 13:55:43 -08:00
|
|
|
}
|
2022-11-12 15:28:32 -08:00
|
|
|
if st.wantErr != nil {
|
2023-01-20 12:36:25 -05:00
|
|
|
t.Fatalf("step #%d, line %v: got success (saved=%v), but wanted an error", i, st.line, lc.config != nil)
|
2022-11-12 13:55:43 -08:00
|
|
|
}
|
2023-01-20 12:36:25 -05:00
|
|
|
var got *ipn.ServeConfig = nil
|
|
|
|
if lc.setCount > lastCount {
|
|
|
|
got = lc.config
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, st.want) {
|
2023-08-28 15:01:38 -07:00
|
|
|
t.Fatalf("[%d] %v: bad state. got:\n%v\n\nwant:\n%v\n",
|
|
|
|
i, st.command, logger.AsJSON(got), logger.AsJSON(st.want))
|
2022-11-17 16:09:43 -05:00
|
|
|
// NOTE: asJSON will omit empty fields, which might make
|
|
|
|
// result in bad state got/want diffs being the same, even
|
|
|
|
// though the actual state is different. Use below to debug:
|
|
|
|
// t.Fatalf("[%d] %v: bad state. got:\n%+v\n\nwant:\n%+v\n",
|
2023-01-20 12:36:25 -05:00
|
|
|
// i, st.command, got, st.want)
|
2022-11-12 15:28:32 -08:00
|
|
|
}
|
2022-11-12 13:55:43 -08:00
|
|
|
}
|
|
|
|
}
|
2022-11-12 15:28:32 -08:00
|
|
|
|
2023-08-09 10:06:58 -04:00
|
|
|
func TestVerifyFunnelEnabled(t *testing.T) {
|
2024-04-07 18:17:25 -07:00
|
|
|
tstest.Replace(t, &Stderr, io.Discard)
|
|
|
|
tstest.Replace(t, &Stdout, io.Discard)
|
|
|
|
|
2023-08-09 10:06:58 -04:00
|
|
|
lc := &fakeLocalServeClient{}
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
var flagOut bytes.Buffer
|
|
|
|
e := &serveEnv{
|
|
|
|
lc: lc,
|
|
|
|
testFlagOut: &flagOut,
|
|
|
|
testStdout: &stdout,
|
2024-04-07 18:17:25 -07:00
|
|
|
testStderr: io.Discard,
|
2023-08-09 10:06:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
// queryFeatureResponse is the mock response desired from the
|
|
|
|
// call made to lc.QueryFeature by verifyFunnelEnabled.
|
|
|
|
queryFeatureResponse mockQueryFeatureResponse
|
2023-09-06 10:17:25 -07:00
|
|
|
caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities
|
2023-08-09 10:06:58 -04:00
|
|
|
wantErr string
|
|
|
|
wantPanic string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "enabled",
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: &tailcfg.QueryFeatureResponse{Complete: true}, err: nil},
|
|
|
|
wantErr: "", // no error, success
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "fallback-to-non-interactive-flow",
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
|
|
|
wantErr: "Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "fallback-flow-missing-acl-rule",
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
2023-09-06 10:17:25 -07:00
|
|
|
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS},
|
2023-08-09 10:06:58 -04:00
|
|
|
wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "fallback-flow-enabled",
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
|
2023-10-26 10:38:08 -07:00
|
|
|
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"},
|
2023-08-09 10:06:58 -04:00
|
|
|
wantErr: "", // no error, success
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "not-allowed-to-enable",
|
|
|
|
queryFeatureResponse: mockQueryFeatureResponse{resp: &tailcfg.QueryFeatureResponse{
|
|
|
|
Complete: false,
|
|
|
|
Text: "You don't have permission to enable this feature.",
|
|
|
|
ShouldWait: false,
|
|
|
|
}, err: nil},
|
|
|
|
wantErr: "",
|
|
|
|
wantPanic: "unexpected call to os.Exit(0) during test", // os.Exit(0) should be called to end process
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
lc.setQueryFeatureResponse(tt.queryFeatureResponse)
|
|
|
|
|
|
|
|
if tt.caps != nil {
|
2024-03-23 16:23:59 -07:00
|
|
|
cm := make(tailcfg.NodeCapMap)
|
|
|
|
for _, c := range tt.caps {
|
|
|
|
cm[c] = nil
|
|
|
|
}
|
|
|
|
tstest.Replace(t, &fakeStatus.Self.CapMap, cm)
|
2023-08-09 10:06:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
r := recover()
|
|
|
|
var gotPanic string
|
|
|
|
if r != nil {
|
|
|
|
gotPanic = fmt.Sprint(r)
|
|
|
|
}
|
|
|
|
if gotPanic != tt.wantPanic {
|
|
|
|
t.Errorf("wrong panic; got=%s, want=%s", gotPanic, tt.wantPanic)
|
|
|
|
}
|
|
|
|
}()
|
2023-10-26 10:38:08 -07:00
|
|
|
gotErr := e.verifyFunnelEnabled(ctx, 443)
|
2023-08-09 10:06:58 -04:00
|
|
|
var got string
|
|
|
|
if gotErr != nil {
|
|
|
|
got = gotErr.Error()
|
|
|
|
}
|
|
|
|
if got != tt.wantErr {
|
|
|
|
t.Errorf("wrong error; got=%s, want=%s", gotErr, tt.wantErr)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-20 12:36:25 -05:00
|
|
|
// fakeLocalServeClient is a fake tailscale.LocalClient for tests.
|
|
|
|
// It's not a full implementation, just enough to test the serve command.
|
|
|
|
//
|
|
|
|
// The fake client is stateful, and is used to test manipulating
|
|
|
|
// ServeConfig state. This implementation cannot be used concurrently.
|
|
|
|
type fakeLocalServeClient struct {
|
2023-08-09 10:06:58 -04:00
|
|
|
config *ipn.ServeConfig
|
|
|
|
setCount int // counts calls to SetServeConfig
|
|
|
|
queryFeatureResponse *mockQueryFeatureResponse // mock response to QueryFeature calls
|
2023-01-20 12:36:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// fakeStatus is a fake ipnstate.Status value for tests.
|
|
|
|
// It's not a full implementation, just enough to test the serve command.
|
|
|
|
//
|
|
|
|
// It returns a state that's running, with a fake DNSName and the Funnel
|
|
|
|
// node attribute capability.
|
|
|
|
var fakeStatus = &ipnstate.Status{
|
|
|
|
BackendState: ipn.Running.String(),
|
|
|
|
Self: &ipnstate.PeerStatus{
|
2024-03-23 16:23:59 -07:00
|
|
|
DNSName: "foo.test.ts.net",
|
|
|
|
CapMap: tailcfg.NodeCapMap{
|
|
|
|
tailcfg.NodeAttrFunnel: nil,
|
|
|
|
tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil,
|
|
|
|
},
|
2023-01-20 12:36:25 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-08-17 14:09:58 -04:00
|
|
|
func (lc *fakeLocalServeClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
2023-01-20 12:36:25 -05:00
|
|
|
return fakeStatus, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
|
|
|
return lc.config.Clone(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
|
|
|
lc.setCount += 1
|
|
|
|
lc.config = config.Clone()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-09 10:06:58 -04:00
|
|
|
type mockQueryFeatureResponse struct {
|
|
|
|
resp *tailcfg.QueryFeatureResponse
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) setQueryFeatureResponse(resp mockQueryFeatureResponse) {
|
|
|
|
lc.queryFeatureResponse = &resp
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
|
|
|
|
if resp := lc.queryFeatureResponse; resp != nil {
|
|
|
|
// If we're testing QueryFeature, use the response value set for the test.
|
|
|
|
return resp.resp, resp.err
|
|
|
|
}
|
|
|
|
return &tailcfg.QueryFeatureResponse{Complete: true}, nil // fallback to already enabled
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error) {
|
2023-08-15 00:07:51 -04:00
|
|
|
return nil, nil // unused in tests
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc *fakeLocalServeClient) IncrementCounter(ctx context.Context, name string, delta int) error {
|
|
|
|
return nil // unused in tests
|
2023-08-02 10:56:18 -04:00
|
|
|
}
|
|
|
|
|
2022-11-12 15:28:32 -08:00
|
|
|
// exactError returns an error checker that wants exactly the provided want error.
|
|
|
|
// If optName is non-empty, it's used in the error message.
|
|
|
|
func exactErr(want error, optName ...string) func(error) string {
|
|
|
|
return func(got error) string {
|
|
|
|
if got == want {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if len(optName) > 0 {
|
|
|
|
return fmt.Sprintf("got error %v, want %v", got, optName[0])
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("got error %v, want %v", got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 16:09:43 -05:00
|
|
|
// anyErr returns an error checker that wants any error.
|
|
|
|
func anyErr() func(error) string {
|
|
|
|
return func(got error) string {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-12 15:28:32 -08:00
|
|
|
func cmd(s string) []string {
|
2023-03-13 21:43:28 -04:00
|
|
|
return strings.Fields(s)
|
2022-11-12 15:28:32 -08:00
|
|
|
}
|