mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
client/web: show features based on platform support
Hiding/disabling UI features when not available on the running client. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
7d61b827e8
commit
7a4ba609d9
@ -11,7 +11,11 @@ import SSHView from "src/components/views/ssh-view"
|
|||||||
import SubnetRouterView from "src/components/views/subnet-router-view"
|
import SubnetRouterView from "src/components/views/subnet-router-view"
|
||||||
import { UpdatingView } from "src/components/views/updating-view"
|
import { UpdatingView } from "src/components/views/updating-view"
|
||||||
import useAuth, { AuthResponse } from "src/hooks/auth"
|
import useAuth, { AuthResponse } from "src/hooks/auth"
|
||||||
import useNodeData, { NodeData } from "src/hooks/node-data"
|
import useNodeData, {
|
||||||
|
Feature,
|
||||||
|
featureDescription,
|
||||||
|
NodeData,
|
||||||
|
} from "src/hooks/node-data"
|
||||||
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@ -63,27 +67,27 @@ function WebClient({
|
|||||||
<Route path="/details">
|
<Route path="/details">
|
||||||
<DeviceDetailsView readonly={!auth.canManageNode} node={data} />
|
<DeviceDetailsView readonly={!auth.canManageNode} node={data} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/subnets">
|
<FeatureRoute path="/subnets" feature="advertise-routes" node={data}>
|
||||||
<SubnetRouterView
|
<SubnetRouterView
|
||||||
readonly={!auth.canManageNode}
|
readonly={!auth.canManageNode}
|
||||||
node={data}
|
node={data}
|
||||||
nodeUpdaters={nodeUpdaters}
|
nodeUpdaters={nodeUpdaters}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</FeatureRoute>
|
||||||
<Route path="/ssh">
|
<FeatureRoute path="/ssh" feature="ssh" node={data}>
|
||||||
<SSHView
|
<SSHView
|
||||||
readonly={!auth.canManageNode}
|
readonly={!auth.canManageNode}
|
||||||
node={data}
|
node={data}
|
||||||
nodeUpdaters={nodeUpdaters}
|
nodeUpdaters={nodeUpdaters}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</FeatureRoute>
|
||||||
<Route path="/serve">{/* TODO */}Share local content</Route>
|
<Route path="/serve">{/* TODO */}Share local content</Route>
|
||||||
<Route path="/update">
|
<FeatureRoute path="/update" feature="auto-update" node={data}>
|
||||||
<UpdatingView
|
<UpdatingView
|
||||||
versionInfo={data.ClientVersion}
|
versionInfo={data.ClientVersion}
|
||||||
currentVersion={data.IPNVersion}
|
currentVersion={data.IPNVersion}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</FeatureRoute>
|
||||||
<Route>
|
<Route>
|
||||||
<h2 className="mt-8">Page not found</h2>
|
<h2 className="mt-8">Page not found</h2>
|
||||||
</Route>
|
</Route>
|
||||||
@ -93,6 +97,36 @@ function WebClient({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeatureRoute renders a Route component,
|
||||||
|
* but only displays the child view if the specified feature is
|
||||||
|
* available for use on this node's platform. If not available,
|
||||||
|
* a not allowed view is rendered instead.
|
||||||
|
*/
|
||||||
|
function FeatureRoute({
|
||||||
|
path,
|
||||||
|
node,
|
||||||
|
feature,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
path: string
|
||||||
|
node: NodeData // TODO: once we have swr, just call useNodeData within FeatureView
|
||||||
|
feature: Feature
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Route path={path}>
|
||||||
|
{!node.Features[feature] ? (
|
||||||
|
<h2 className="mt-8">
|
||||||
|
{featureDescription(feature)} not available on this device.
|
||||||
|
</h2>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Header({
|
function Header({
|
||||||
node,
|
node,
|
||||||
auth,
|
auth,
|
||||||
|
@ -175,7 +175,7 @@ function ExitNodeSelectorInner({
|
|||||||
onSelect: (node: ExitNode) => void
|
onSelect: (node: ExitNode) => void
|
||||||
}) {
|
}) {
|
||||||
const [filter, setFilter] = useState<string>("")
|
const [filter, setFilter] = useState<string>("")
|
||||||
const { data: exitNodes } = useExitNodes(node.TailnetName, filter)
|
const { data: exitNodes } = useExitNodes(node, filter)
|
||||||
const listRef = useRef<HTMLDivElement>(null)
|
const listRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const hasNodes = useMemo(
|
const hasNodes = useMemo(
|
||||||
|
@ -50,9 +50,10 @@ export default function DeviceDetailsView({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{node.ClientVersion &&
|
{node.Features["auto-update"] &&
|
||||||
!node.ClientVersion.RunningLatest &&
|
!readonly &&
|
||||||
!readonly && (
|
node.ClientVersion &&
|
||||||
|
!node.ClientVersion.RunningLatest && (
|
||||||
<UpdateAvailableNotification details={node.ClientVersion} />
|
<UpdateAvailableNotification details={node.ClientVersion} />
|
||||||
)}
|
)}
|
||||||
<div className="-mx-5 card">
|
<div className="-mx-5 card">
|
||||||
|
@ -33,37 +33,44 @@ export default function HomeView({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-gray-800 text-lg leading-[25.20px]">{node.IP}</p>
|
<p className="text-gray-800 text-lg leading-[25.20px]">{node.IP}</p>
|
||||||
</div>
|
</div>
|
||||||
<ExitNodeSelector
|
{(node.Features["advertise-exit-node"] ||
|
||||||
className="mb-5"
|
node.Features["use-exit-node"]) && (
|
||||||
node={node}
|
<ExitNodeSelector
|
||||||
nodeUpdaters={nodeUpdaters}
|
className="mb-5"
|
||||||
disabled={readonly}
|
node={node}
|
||||||
/>
|
nodeUpdaters={nodeUpdaters}
|
||||||
|
disabled={readonly}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Link className="text-blue-500 font-medium leading-snug" to="/details">
|
<Link className="text-blue-500 font-medium leading-snug" to="/details">
|
||||||
View device details →
|
View device details →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mb-3">Settings</h2>
|
<h2 className="mb-3">Settings</h2>
|
||||||
<SettingsCard
|
{node.Features["advertise-routes"] && (
|
||||||
link="/subnets"
|
<SettingsCard
|
||||||
className="mb-3"
|
link="/subnets"
|
||||||
title="Subnet router"
|
className="mb-3"
|
||||||
body="Add devices to your tailnet without installing Tailscale on them."
|
title="Subnet router"
|
||||||
/>
|
body="Add devices to your tailnet without installing Tailscale on them."
|
||||||
<SettingsCard
|
/>
|
||||||
link="/ssh"
|
)}
|
||||||
className="mb-3"
|
{node.Features["ssh"] && (
|
||||||
title="Tailscale SSH server"
|
<SettingsCard
|
||||||
body="Run a Tailscale SSH server on this device and allow other devices in your tailnet to SSH into it."
|
link="/ssh"
|
||||||
badge={
|
className="mb-3"
|
||||||
node.RunningSSHServer
|
title="Tailscale SSH server"
|
||||||
? {
|
body="Run a Tailscale SSH server on this device and allow other devices in your tailnet to SSH into it."
|
||||||
text: "Running",
|
badge={
|
||||||
icon: <div className="w-2 h-2 bg-emerald-500 rounded-full" />,
|
node.RunningSSHServer
|
||||||
}
|
? {
|
||||||
: undefined
|
text: "Running",
|
||||||
}
|
icon: <div className="w-2 h-2 bg-emerald-500 rounded-full" />,
|
||||||
/>
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* TODO(sonia,will): hiding unimplemented settings pages until implemented */}
|
{/* TODO(sonia,will): hiding unimplemented settings pages until implemented */}
|
||||||
{/* <SettingsCard
|
{/* <SettingsCard
|
||||||
link="/serve"
|
link="/serve"
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { apiFetch } from "src/api"
|
import { apiFetch } from "src/api"
|
||||||
|
import { NodeData } from "src/hooks/node-data"
|
||||||
|
|
||||||
export type ExitNode = {
|
export type ExitNode = {
|
||||||
ID: string
|
ID: string
|
||||||
@ -28,7 +29,7 @@ export type ExitNodeGroup = {
|
|||||||
nodes: ExitNode[]
|
nodes: ExitNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useExitNodes(tailnetName: string, filter?: string) {
|
export default function useExitNodes(node: NodeData, filter?: string) {
|
||||||
const [data, setData] = useState<ExitNode[]>([])
|
const [data, setData] = useState<ExitNode[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -47,6 +48,14 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
let tailnetNodes: ExitNode[] = []
|
let tailnetNodes: ExitNode[] = []
|
||||||
const locationNodes = new Map<CountryCode, Map<CityCode, ExitNode[]>>()
|
const locationNodes = new Map<CountryCode, Map<CityCode, ExitNode[]>>()
|
||||||
|
|
||||||
|
if (!node.Features["use-exit-node"]) {
|
||||||
|
// early-return
|
||||||
|
return {
|
||||||
|
tailnetNodesSorted: tailnetNodes,
|
||||||
|
locationNodesMap: locationNodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data?.forEach((n) => {
|
data?.forEach((n) => {
|
||||||
const loc = n.Location
|
const loc = n.Location
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
@ -55,7 +64,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
// Only Mullvad exit nodes have locations filled.
|
// Only Mullvad exit nodes have locations filled.
|
||||||
tailnetNodes.push({
|
tailnetNodes.push({
|
||||||
...n,
|
...n,
|
||||||
Name: trimDNSSuffix(n.Name, tailnetName),
|
Name: trimDNSSuffix(n.Name, node.TailnetName),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -70,12 +79,15 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
tailnetNodesSorted: tailnetNodes.sort(compareByName),
|
tailnetNodesSorted: tailnetNodes.sort(compareByName),
|
||||||
locationNodesMap: locationNodes,
|
locationNodesMap: locationNodes,
|
||||||
}
|
}
|
||||||
}, [data, tailnetName])
|
}, [data, node.Features, node.TailnetName])
|
||||||
|
|
||||||
const hasFilter = Boolean(filter)
|
const hasFilter = Boolean(filter)
|
||||||
|
|
||||||
const mullvadNodesSorted = useMemo(() => {
|
const mullvadNodesSorted = useMemo(() => {
|
||||||
const nodes: ExitNode[] = []
|
const nodes: ExitNode[] = []
|
||||||
|
if (!node.Features["use-exit-node"]) {
|
||||||
|
return nodes // early-return
|
||||||
|
}
|
||||||
|
|
||||||
// addBestMatchNode adds the node with the "higest priority"
|
// addBestMatchNode adds the node with the "higest priority"
|
||||||
// match from a list of exit node `options` to `nodes`.
|
// match from a list of exit node `options` to `nodes`.
|
||||||
@ -123,14 +135,27 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nodes.sort(compareByName)
|
return nodes.sort(compareByName)
|
||||||
}, [hasFilter, locationNodesMap])
|
}, [hasFilter, locationNodesMap, node.Features])
|
||||||
|
|
||||||
// Ordered and filtered grouping of exit nodes.
|
// Ordered and filtered grouping of exit nodes.
|
||||||
const exitNodeGroups = useMemo(() => {
|
const exitNodeGroups = useMemo(() => {
|
||||||
const filterLower = !filter ? undefined : filter.toLowerCase()
|
const filterLower = !filter ? undefined : filter.toLowerCase()
|
||||||
|
|
||||||
|
const selfGroup = {
|
||||||
|
id: "self",
|
||||||
|
name: undefined,
|
||||||
|
nodes: filter
|
||||||
|
? []
|
||||||
|
: !node.Features["advertise-exit-node"]
|
||||||
|
? [noExitNode] // don't show "runAsExitNode" option
|
||||||
|
: [noExitNode, runAsExitNode],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.Features["use-exit-node"]) {
|
||||||
|
return [selfGroup]
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
{ id: "self", nodes: filter ? [] : [noExitNode, runAsExitNode] },
|
selfGroup,
|
||||||
{
|
{
|
||||||
id: "tailnet",
|
id: "tailnet",
|
||||||
nodes: filterLower
|
nodes: filterLower
|
||||||
@ -149,7 +174,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
: mullvadNodesSorted,
|
: mullvadNodesSorted,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}, [tailnetNodesSorted, mullvadNodesSorted, filter])
|
}, [filter, node.Features, tailnetNodesSorted, mullvadNodesSorted])
|
||||||
|
|
||||||
return { data: exitNodeGroups }
|
return { data: exitNodeGroups }
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"
|
|||||||
import { apiFetch, setUnraidCsrfToken } from "src/api"
|
import { apiFetch, setUnraidCsrfToken } from "src/api"
|
||||||
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
||||||
import { VersionInfo } from "src/hooks/self-update"
|
import { VersionInfo } from "src/hooks/self-update"
|
||||||
|
import { assertNever } from "src/util"
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
Profile: UserProfile
|
Profile: UserProfile
|
||||||
@ -34,6 +35,7 @@ export type NodeData = {
|
|||||||
RunningSSHServer: boolean
|
RunningSSHServer: boolean
|
||||||
ControlAdminURL: string
|
ControlAdminURL: string
|
||||||
LicensesURL: string
|
LicensesURL: string
|
||||||
|
Features: { [key in Feature]: boolean } // value is true if given feature is available on this client
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeState =
|
type NodeState =
|
||||||
@ -55,6 +57,30 @@ export type SubnetRoute = {
|
|||||||
Approved: boolean
|
Approved: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Feature =
|
||||||
|
| "advertise-exit-node"
|
||||||
|
| "advertise-routes"
|
||||||
|
| "use-exit-node"
|
||||||
|
| "ssh"
|
||||||
|
| "auto-update"
|
||||||
|
|
||||||
|
export const featureDescription = (f: Feature) => {
|
||||||
|
switch (f) {
|
||||||
|
case "advertise-exit-node":
|
||||||
|
return "Advertising as an exit node"
|
||||||
|
case "advertise-routes":
|
||||||
|
return "Advertising subnet routes"
|
||||||
|
case "use-exit-node":
|
||||||
|
return "Using an exit node"
|
||||||
|
case "ssh":
|
||||||
|
return "Running a Tailscale SSH server"
|
||||||
|
case "auto-update":
|
||||||
|
return "Auto updating client versions"
|
||||||
|
default:
|
||||||
|
assertNever(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NodeUpdaters provides a set of mutation functions for a node.
|
* NodeUpdaters provides a set of mutation functions for a node.
|
||||||
*
|
*
|
||||||
|
10
client/web/src/util.ts
Normal file
10
client/web/src/util.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
/**
|
||||||
|
* assertNever ensures a branch of code can never be reached,
|
||||||
|
* resulting in a Typescript error if it can.
|
||||||
|
*/
|
||||||
|
export function assertNever(a: never): never {
|
||||||
|
return a
|
||||||
|
}
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/clientupdate"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
@ -563,6 +564,15 @@ type nodeData struct {
|
|||||||
|
|
||||||
ControlAdminURL string
|
ControlAdminURL string
|
||||||
LicensesURL string
|
LicensesURL string
|
||||||
|
|
||||||
|
// Features is the set of available features for use on the
|
||||||
|
// current platform. e.g. "ssh", "advertise-exit-node", etc.
|
||||||
|
// Map value is true if the given feature key is available.
|
||||||
|
//
|
||||||
|
// See web.availableFeatures func for population of this field.
|
||||||
|
// Contents are expected to match values defined in node-data.ts
|
||||||
|
// on the frontend.
|
||||||
|
Features map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type subnetRoute struct {
|
type subnetRoute struct {
|
||||||
@ -599,6 +609,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
|||||||
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
|
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
|
||||||
ControlAdminURL: prefs.AdminPageURL(),
|
ControlAdminURL: prefs.AdminPageURL(),
|
||||||
LicensesURL: licenses.LicensesURL(),
|
LicensesURL: licenses.LicensesURL(),
|
||||||
|
Features: availableFeatures(),
|
||||||
}
|
}
|
||||||
|
|
||||||
cv, err := s.lc.CheckUpdate(r.Context())
|
cv, err := s.lc.CheckUpdate(r.Context())
|
||||||
@ -671,6 +682,16 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, *data)
|
writeJSON(w, *data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func availableFeatures() map[string]bool {
|
||||||
|
return map[string]bool{
|
||||||
|
"advertise-exit-node": true, // available on all platforms
|
||||||
|
"advertise-routes": true, // available on all platforms
|
||||||
|
"use-exit-node": distro.Get() != distro.Synology, // see https://github.com/tailscale/tailscale/issues/1995
|
||||||
|
"ssh": envknob.CanRunTailscaleSSH() == nil,
|
||||||
|
"auto-update": clientupdate.CanAutoUpdate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type exitNode struct {
|
type exitNode struct {
|
||||||
ID tailcfg.StableNodeID
|
ID tailcfg.StableNodeID
|
||||||
Name string
|
Name string
|
||||||
|
@ -222,6 +222,13 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanAutoUpdate reports whether auto-updating via the clientupdate package
|
||||||
|
// is supported for the current os/distro.
|
||||||
|
func CanAutoUpdate() bool {
|
||||||
|
_, canAutoUpdate := (&Updater{}).getUpdateFunction()
|
||||||
|
return canAutoUpdate
|
||||||
|
}
|
||||||
|
|
||||||
// Update runs a single update attempt using the platform-specific mechanism.
|
// Update runs a single update attempt using the platform-specific mechanism.
|
||||||
//
|
//
|
||||||
// On Windows, this copies the calling binary and re-executes it to apply the
|
// On Windows, this copies the calling binary and re-executes it to apply the
|
||||||
|
@ -180,8 +180,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
|||||||
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
if !clientupdate.CanAutoUpdate() {
|
||||||
if errors.Is(err, errors.ErrUnsupported) {
|
|
||||||
return errors.New("automatic updates are not supported on this platform")
|
return errors.New("automatic updates are not supported on this platform")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
|
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
|
||||||
💣 tailscale.com/clientupdate from tailscale.com/cmd/tailscale/cli
|
💣 tailscale.com/clientupdate from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||||
|
39
envknob/features.go
Normal file
39
envknob/features.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package envknob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"tailscale.com/version"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanRunTailscaleSSH reports whether serving a Tailscale SSH server is
|
||||||
|
// supported for the current os/distro.
|
||||||
|
func CanRunTailscaleSSH() error {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
if distro.Get() == distro.Synology && !UseWIPCode() {
|
||||||
|
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||||
|
}
|
||||||
|
if distro.Get() == distro.QNAP && !UseWIPCode() {
|
||||||
|
return errors.New("The Tailscale SSH server does not run on QNAP.")
|
||||||
|
}
|
||||||
|
// otherwise okay
|
||||||
|
case "darwin":
|
||||||
|
// okay only in tailscaled mode for now.
|
||||||
|
if version.IsSandboxedMacOS() {
|
||||||
|
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||||
|
}
|
||||||
|
case "freebsd", "openbsd":
|
||||||
|
default:
|
||||||
|
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||||
|
}
|
||||||
|
if !CanSSHD() {
|
||||||
|
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -348,10 +348,9 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
|||||||
// Note that we create the Updater solely to check for errors; we do not
|
// Note that we create the Updater solely to check for errors; we do not
|
||||||
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
|
||||||
prefs := b.Prefs().AutoUpdate()
|
prefs := b.Prefs().AutoUpdate()
|
||||||
_, err := clientupdate.NewUpdater(clientupdate.Arguments{ForAutoUpdate: true})
|
|
||||||
return tailcfg.C2NUpdateResponse{
|
return tailcfg.C2NUpdateResponse{
|
||||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
|
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
|
||||||
Supported: err == nil,
|
Supported: clientupdate.CanAutoUpdate(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2893,27 +2893,11 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
|||||||
if !p.RunSSH {
|
if !p.RunSSH {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch runtime.GOOS {
|
if err := envknob.CanRunTailscaleSSH(); err != nil {
|
||||||
case "linux":
|
return err
|
||||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
|
||||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
|
||||||
}
|
|
||||||
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
|
|
||||||
return errors.New("The Tailscale SSH server does not run on QNAP.")
|
|
||||||
}
|
|
||||||
b.updateSELinuxHealthWarning()
|
|
||||||
// otherwise okay
|
|
||||||
case "darwin":
|
|
||||||
// okay only in tailscaled mode for now.
|
|
||||||
if version.IsSandboxedMacOS() {
|
|
||||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
|
||||||
}
|
|
||||||
case "freebsd", "openbsd":
|
|
||||||
default:
|
|
||||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
|
||||||
}
|
}
|
||||||
if !envknob.CanSSHD() {
|
if runtime.GOOS == "linux" {
|
||||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
b.updateSELinuxHealthWarning()
|
||||||
}
|
}
|
||||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
x
Reference in New Issue
Block a user