From f9550e0bed0074644e5b722f9dacd5d9ca5f8497 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Wed, 29 Nov 2023 16:40:41 -0800 Subject: [PATCH] client/web: indicate if ACLs prevent access Use the packet filter rules to determine if any device is allowed to connect on port 5252. This does not check whether a specific device can connect (since we typically don't know the source device when this is used). Nor does it specifically check for wide-open ACLs, which is something we may provide a warning about in the future. Update the login popover content to display information when the src device is unable to connect to the dst device over its Tailscale IP. If we know it's an ACL issue, mention that, otherwise list a couple of things to check. In both cases, link to a placeholder URL to get more information about web client connection issues. Updates #10261 Signed-off-by: Will Norris --- client/tailscale/localclient.go | 9 ++ client/web/src/components/login-toggle.tsx | 106 +++++++++++++++------ client/web/src/hooks/node-data.ts | 1 + client/web/web.go | 24 +++++ 4 files changed, 109 insertions(+), 31 deletions(-) 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