cmd/tailscale/cli: allow remote target as service destination (#17607)
This commit enables user to set service backend to remote destinations, that can be a partial URL or a full URL. The commit also prevents user to set remote destinations on linux system when socket mark is not working. For user on any version of mac extension they can't serve a service either. The socket mark usability is determined by a new local api. Fixes tailscale/corp#24783 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
@@ -149,6 +149,7 @@ type localServeClient interface {
|
||||
IncrementCounter(ctx context.Context, name string, delta int) error
|
||||
GetPrefs(ctx context.Context) (*ipn.Prefs, error)
|
||||
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
|
||||
CheckSOMarkInUse(ctx context.Context) (bool, error)
|
||||
}
|
||||
|
||||
// serveEnv is the environment the serve command runs within. All I/O should be
|
||||
|
||||
@@ -860,6 +860,7 @@ type fakeLocalServeClient struct {
|
||||
setCount int // counts calls to SetServeConfig
|
||||
queryFeatureResponse *mockQueryFeatureResponse // mock response to QueryFeature calls
|
||||
prefs *ipn.Prefs // fake preferences, used to test GetPrefs and SetPrefs
|
||||
SOMarkInUse bool // fake SO mark in use status
|
||||
statusWithoutPeers *ipnstate.Status // nil for fakeStatus
|
||||
}
|
||||
|
||||
@@ -937,6 +938,10 @@ func (lc *fakeLocalServeClient) IncrementCounter(ctx context.Context, name strin
|
||||
return nil // unused in tests
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) CheckSOMarkInUse(ctx context.Context) (bool, error) {
|
||||
return lc.SOMarkInUse, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/prompt"
|
||||
"tailscale.com/util/set"
|
||||
@@ -516,6 +518,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
}
|
||||
if err := e.shouldWarnRemoteDestCompatibility(ctx, target); err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel, magicDNSSuffix, e.acceptAppCaps, int(e.proxyProtocol))
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
}
|
||||
@@ -999,16 +1004,17 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveTy
|
||||
}
|
||||
|
||||
var (
|
||||
msgFunnelAvailable = "Available on the internet:"
|
||||
msgServeAvailable = "Available within your tailnet:"
|
||||
msgServiceWaitingApproval = "This machine is configured as a service proxy for %s, but approval from an admin is required. Once approved, it will be available in your Tailnet as:"
|
||||
msgRunningInBackground = "%s started and running in the background."
|
||||
msgRunningTunService = "IPv4 and IPv6 traffic to %s is being routed to your operating system."
|
||||
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
|
||||
msgDisableServiceProxy = "To disable the proxy, run: tailscale serve --service=%s --%s=%d off"
|
||||
msgDisableServiceTun = "To disable the service in TUN mode, run: tailscale serve --service=%s --tun off"
|
||||
msgDisableService = "To remove config for the service, run: tailscale serve clear %s"
|
||||
msgToExit = "Press Ctrl+C to exit."
|
||||
msgFunnelAvailable = "Available on the internet:"
|
||||
msgServeAvailable = "Available within your tailnet:"
|
||||
msgServiceWaitingApproval = "This machine is configured as a service proxy for %s, but approval from an admin is required. Once approved, it will be available in your Tailnet as:"
|
||||
msgRunningInBackground = "%s started and running in the background."
|
||||
msgRunningTunService = "IPv4 and IPv6 traffic to %s is being routed to your operating system."
|
||||
msgDisableProxy = "To disable the proxy, run: tailscale %s --%s=%d off"
|
||||
msgDisableServiceProxy = "To disable the proxy, run: tailscale serve --service=%s --%s=%d off"
|
||||
msgDisableServiceTun = "To disable the service in TUN mode, run: tailscale serve --service=%s --tun off"
|
||||
msgDisableService = "To remove config for the service, run: tailscale serve clear %s"
|
||||
msgWarnRemoteDestCompatibility = "Warning: %s doesn't support connecting to remote destinations from non-default route, see tailscale.com/kb/1552/tailscale-services for detail."
|
||||
msgToExit = "Press Ctrl+C to exit."
|
||||
)
|
||||
|
||||
// messageForPort returns a message for the given port based on the
|
||||
@@ -1134,6 +1140,77 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// isRemote reports whether the given destination from serve config
|
||||
// is a remote destination.
|
||||
func isRemote(target string) bool {
|
||||
// target being a port number means it's localhost
|
||||
if _, err := strconv.ParseUint(target, 10, 16); err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// prepend tmp:// if no scheme is present just to help parsing
|
||||
if !strings.Contains(target, "://") {
|
||||
target = "tmp://" + target
|
||||
}
|
||||
|
||||
// make sure we can parse the target, wether it's a full URL or just a host:port
|
||||
u, err := url.ParseRequestURI(target)
|
||||
if err != nil {
|
||||
// If we can't parse the target, it doesn't matter if it's remote or not
|
||||
return false
|
||||
}
|
||||
validHN := dnsname.ValidHostname(u.Hostname()) == nil
|
||||
validIP := net.ParseIP(u.Hostname()) != nil
|
||||
if !validHN && !validIP {
|
||||
return false
|
||||
}
|
||||
if u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// shouldWarnRemoteDestCompatibility reports whether we should warn the user
|
||||
// that their current OS/environment may not be compatible with
|
||||
// service's proxy destination.
|
||||
func (e *serveEnv) shouldWarnRemoteDestCompatibility(ctx context.Context, target string) error {
|
||||
// no target means nothing to check
|
||||
if target == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filepath.IsAbs(target) || strings.HasPrefix(target, "text:") {
|
||||
// local path or text target, nothing to check
|
||||
return nil
|
||||
}
|
||||
|
||||
// only check for remote destinations
|
||||
if !isRemote(target) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if running as Mac extension and warn
|
||||
if version.IsMacAppStore() || version.IsMacSysExt() {
|
||||
return fmt.Errorf(msgWarnRemoteDestCompatibility, "the MacOS extension")
|
||||
}
|
||||
|
||||
// Check for linux, if it's running with TS_FORCE_LINUX_BIND_TO_DEVICE=true
|
||||
// and tailscale bypass mark is not working. If any of these conditions are true, and the dest is
|
||||
// a remote destination, return true.
|
||||
if runtime.GOOS == "linux" {
|
||||
SOMarkInUse, err := e.lc.CheckSOMarkInUse(ctx)
|
||||
if err != nil {
|
||||
log.Printf("error checking SO mark in use: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !SOMarkInUse {
|
||||
return fmt.Errorf(msgWarnRemoteDestCompatibility, "the Linux tailscaled without SO_MARK")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target, mds string, caps []tailcfg.PeerCapability) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
switch {
|
||||
@@ -1193,6 +1270,8 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
||||
return fmt.Errorf("invalid TCP target %q", target)
|
||||
}
|
||||
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
|
||||
targetURL, err := ipn.ExpandProxyTargetValue(target, []string{"tcp"}, "tcp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to expand target: %v", err)
|
||||
@@ -1204,7 +1283,6 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
||||
}
|
||||
|
||||
// TODO: needs to account for multiple configs from foreground mode
|
||||
svcName := tailcfg.AsServiceName(dnsName)
|
||||
if sc.IsServingWeb(srcPort, svcName) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d for %s", srcPort, dnsName)
|
||||
}
|
||||
|
||||
@@ -220,10 +220,20 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "invalid_host",
|
||||
name: "ip_host",
|
||||
initialState: fakeLocalServeClient{
|
||||
SOMarkInUse: true,
|
||||
},
|
||||
steps: []step{{
|
||||
command: cmd("serve --https=443 --bg http://somehost:3000"), // invalid host
|
||||
wantErr: anyErr(),
|
||||
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"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
@@ -233,6 +243,16 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
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{
|
||||
@@ -402,15 +422,11 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "unknown_host_tcp",
|
||||
steps: []step{{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:5432"),
|
||||
wantErr: exactErrMsg(errHelp),
|
||||
}},
|
||||
},
|
||||
{
|
||||
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),
|
||||
@@ -418,6 +434,9 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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),
|
||||
@@ -532,6 +551,9 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "bad_path",
|
||||
initialState: fakeLocalServeClient{
|
||||
SOMarkInUse: true,
|
||||
},
|
||||
steps: []step{{
|
||||
command: cmd("serve --bg --https=443 bad/path"),
|
||||
wantErr: exactErrMsg(errHelp),
|
||||
@@ -832,6 +854,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
},
|
||||
CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"},
|
||||
},
|
||||
SOMarkInUse: true,
|
||||
},
|
||||
steps: []step{{
|
||||
command: cmd("serve --service=svc:foo --http=80 text:foo"),
|
||||
|
||||
Reference in New Issue
Block a user