diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 73e93dfbb..7b99fc61e 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -1332,6 +1332,15 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin return decodeJSON[*ipnstate.DebugDERPRegionReport](body) } +// DebugPacketFilterRules returns the packet filter rules for the current device. +func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) { + body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil) + if err != nil { + return nil, fmt.Errorf("error %w: %s", err, body) + } + return decodeJSON[[]tailcfg.FilterRule](body) +} + // DebugSetExpireIn marks the current node key to expire in d. // // This is meant primarily for debug and testing. diff --git a/client/web/src/components/login-toggle.tsx b/client/web/src/components/login-toggle.tsx index f3ed08be2..880437b5b 100644 --- a/client/web/src/components/login-toggle.tsx +++ b/client/web/src/components/login-toggle.tsx @@ -8,6 +8,7 @@ import { ReactComponent as Eye } from "src/assets/icons/eye.svg" import { ReactComponent as User } from "src/assets/icons/user.svg" import { AuthResponse, AuthType } from "src/hooks/auth" import { NodeData } from "src/hooks/node-data" +import Button from "src/ui/button" import Popover from "src/ui/popover" import ProfilePic from "src/ui/profile-pic" @@ -140,44 +141,68 @@ function LoginPopoverContent({ {!auth.canManageNode ? "Viewing" : "Managing"} {auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`} - {!auth.canManageNode && - (!auth.viewerIdentity || auth.authNeeded === AuthType.tailscale ? ( - <> -

- {auth.viewerIdentity ? ( + {!auth.canManageNode && ( + <> + {!auth.viewerIdentity ? ( + // User is not connected over Tailscale. + // These states are only possible on the login client. + <> + {!canConnectOverTS ? ( <> - To make changes, sign in to confirm your identity. This extra - step helps us keep your device secure. +

+ {!node.ACLAllowsAnyIncomingTraffic ? ( + // Tailnet ACLs don't allow access. + <> + The current tailnet policy file does not allow + connecting to this device. + + ) : ( + // ACLs allow access, but user can't connect. + <> + Cannot access this device's Tailscale IP. Make sure you + are connected to your tailnet, and that your policy file + allows access. + + )}{" "} + + Learn more → + +

) : ( + // User can connect to Tailcale IP; sign in when ready. <> - You can see most of this device's details. To make changes, - you need to sign in. +

+ You can see most of this device's details. To make changes, + you need to sign in. +

+ )} + + ) : auth.authNeeded === AuthType.tailscale ? ( + // User is connected over Tailscale, but needs to complete check mode. + <> +

+ To make changes, sign in to confirm your identity. This extra + step helps us keep your device secure. +

+ + + ) : ( + // User is connected over tailscale, but doesn't have permission to manage. +

+ You don’t have permission to make changes to this device, but you + can view most of its details.

- - - ) : ( -

- You don’t have permission to make changes to this device, but you - can view most of its details. -

- ))} + )} + + )} {auth.viewerIdentity && ( <>
@@ -195,3 +220,22 @@ function LoginPopoverContent({ ) } + +function SignInButton({ + auth, + onClick, +}: { + auth: AuthResponse + onClick: () => void +}) { + return ( + + ) +} diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts index 149da7025..50cf82c5d 100644 --- a/client/web/src/hooks/node-data.ts +++ b/client/web/src/hooks/node-data.ts @@ -36,6 +36,7 @@ export type NodeData = { ControlAdminURL: string LicensesURL: string Features: { [key in Feature]: boolean } // value is true if given feature is available on this client + ACLAllowsAnyIncomingTraffic: boolean } type NodeState = diff --git a/client/web/web.go b/client/web/web.go index 8e642fbda..49e4ff38b 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -562,6 +562,9 @@ type nodeData struct { ClientVersion *tailcfg.ClientVersion + // whether tailnet ACLs allow access to port 5252 on this device + ACLAllowsAnyIncomingTraffic bool + ControlAdminURL string LicensesURL string @@ -591,6 +594,11 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + filterRules, err := s.lc.DebugPacketFilterRules(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } data := &nodeData{ ID: st.Self.ID, Status: st.BackendState, @@ -610,6 +618,8 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { ControlAdminURL: prefs.AdminPageURL(), LicensesURL: licenses.LicensesURL(), Features: availableFeatures(), + + ACLAllowsAnyIncomingTraffic: s.aclsAllowAccess(filterRules), } cv, err := s.lc.CheckUpdate(r.Context()) @@ -692,6 +702,20 @@ func availableFeatures() map[string]bool { } } +// aclsAllowAccess returns whether tailnet ACLs (as expressed in the provided filter rules) +// permit any devices to access the local web client. +// This does not currently check whether a specific device can connect, just any device. +func (s *Server) aclsAllowAccess(rules []tailcfg.FilterRule) bool { + for _, rule := range rules { + for _, dp := range rule.DstPorts { + if dp.Ports.Contains(ListenPort) { + return true + } + } + } + return false +} + type exitNode struct { ID tailcfg.StableNodeID Name string