cmd/tailscale/cli: add a risk message about rp_filter

We already present a health warning about this, but it is easy to miss
on a server when blackholing traffic makes it unreachable.

In addition to a health warning, present a risk message when exit node
is enabled.

Example:

```
$ tailscale up --exit-node=lizard
The following issues on your machine will likely make usage of exit nodes impossible:
- interface "ens4" has strict reverse-path filtering enabled
- interface "tailscale0" has strict reverse-path filtering enabled
Please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310
To skip this warning, use --accept-risk=linux-strict-rp-filter
$
```

Updates #3310

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov 2025-05-22 20:12:59 +01:00 committed by Anton Tolchanov
parent cc8dc9e4dc
commit db34cdcfe7
10 changed files with 143 additions and 71 deletions

View File

@ -788,6 +788,25 @@ func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
return nil return nil
} }
// CheckReversePathFiltering asks the local Tailscale daemon whether strict
// reverse path filtering is enabled, which would break exit node usage on Linux.
func (lc *Client) CheckReversePathFiltering(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-reverse-path-filtering")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-reverse-path-filtering: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
// SetUDPGROForwarding enables UDP GRO forwarding for the main interface of this // SetUDPGROForwarding enables UDP GRO forwarding for the main interface of this
// node. This can be done to improve performance of tailnet nodes acting as exit // node. This can be done to improve performance of tailnet nodes acting as exit
// nodes or subnet routers. // nodes or subnet routers.

View File

@ -796,7 +796,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/envknob/featureknob from tailscale.com/client/web+ tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/ipn/ipnext+ tailscale.com/feature from tailscale.com/ipn/ipnext+
tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
tailscale.com/hostinfo from tailscale.com/client/web+ tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient tailscale.com/internal/noiseconn from tailscale.com/control/controlclient

View File

@ -4,15 +4,18 @@
package cli package cli
import ( import (
"context"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"runtime"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"tailscale.com/ipn"
"tailscale.com/util/testenv" "tailscale.com/util/testenv"
) )
@ -20,11 +23,12 @@ var (
riskTypes []string riskTypes []string
riskLoseSSH = registerRiskType("lose-ssh") riskLoseSSH = registerRiskType("lose-ssh")
riskMacAppConnector = registerRiskType("mac-app-connector") riskMacAppConnector = registerRiskType("mac-app-connector")
riskStrictRPFilter = registerRiskType("linux-strict-rp-filter")
riskAll = registerRiskType("all") riskAll = registerRiskType("all")
) )
const riskMacAppConnectorMessage = ` const riskMacAppConnectorMessage = `
You are trying to configure an app connector on macOS, which is not officially supported due to system limitations. This may result in performance and reliability issues. You are trying to configure an app connector on macOS, which is not officially supported due to system limitations. This may result in performance and reliability issues.
Do not use a macOS app connector for any mission-critical purposes. For the best experience, Linux is the only recommended platform for app connectors. Do not use a macOS app connector for any mission-critical purposes. For the best experience, Linux is the only recommended platform for app connectors.
` `
@ -89,3 +93,18 @@ func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
printf("\r%s\r", strings.Repeat(" ", msgLen)) printf("\r%s\r", strings.Repeat(" ", msgLen))
return errAborted return errAborted
} }
// checkExitNodeRisk checks if the user is using an exit node on Linux and
// whether reverse path filtering is enabled. If so, it presents a risk message.
func checkExitNodeRisk(ctx context.Context, prefs *ipn.Prefs, acceptedRisks string) error {
if runtime.GOOS != "linux" {
return nil
}
if !prefs.ExitNodeIP.IsValid() && prefs.ExitNodeID == "" {
return nil
}
if err := localClient.CheckReversePathFiltering(ctx); err != nil {
return presentRiskToUser(riskStrictRPFilter, err.Error(), acceptedRisks)
}
return nil
}

View File

@ -183,6 +183,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
} }
warnOnAdvertiseRouts(ctx, &maskedPrefs.Prefs) warnOnAdvertiseRouts(ctx, &maskedPrefs.Prefs)
if err := checkExitNodeRisk(ctx, &maskedPrefs.Prefs, setArgs.acceptedRisks); err != nil {
return err
}
var advertiseExitNodeSet, advertiseRoutesSet bool var advertiseExitNodeSet, advertiseRoutesSet bool
setFlagSet.Visit(func(f *flag.Flag) { setFlagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name) updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name)

View File

@ -481,6 +481,9 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
} }
warnOnAdvertiseRouts(ctx, prefs) warnOnAdvertiseRouts(ctx, prefs)
if err := checkExitNodeRisk(ctx, prefs, upArgs.acceptedRisks); err != nil {
return err
}
curPrefs, err := localClient.GetPrefs(ctx) curPrefs, err := localClient.GetPrefs(ctx)
if err != nil { if err != nil {

View File

@ -281,7 +281,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/feature/tpm from tailscale.com/feature/condregister tailscale.com/feature/tpm from tailscale.com/feature/condregister
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
tailscale.com/hostinfo from tailscale.com/client/web+ tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/local+ tailscale.com/ipn from tailscale.com/client/local+

View File

@ -12,4 +12,5 @@ const (
TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller TailscaleSSHOnBut = "Tailscale SSH enabled, but " // + ... something from caller
LockedOut = "this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out" LockedOut = "this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"
WarnExitNodeUsage = "The following issues on your machine will likely make usage of exit nodes impossible" WarnExitNodeUsage = "The following issues on your machine will likely make usage of exit nodes impossible"
DisableRPFilter = "Please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310"
) )

View File

@ -4112,9 +4112,8 @@ func updateExitNodeUsageWarning(p ipn.PrefsView, state *netmon.State, healthTrac
var msg string var msg string
if p.ExitNodeIP().IsValid() || p.ExitNodeID() != "" { if p.ExitNodeIP().IsValid() || p.ExitNodeID() != "" {
warn, _ := netutil.CheckReversePathFiltering(state) warn, _ := netutil.CheckReversePathFiltering(state)
const comment = "please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310"
if len(warn) > 0 { if len(warn) > 0 {
msg = fmt.Sprintf("%s: %v, %s", healthmsg.WarnExitNodeUsage, warn, comment) msg = fmt.Sprintf("%s: %v, %s", healthmsg.WarnExitNodeUsage, warn, healthmsg.DisableRPFilter)
} }
} }
if len(msg) > 0 { if len(msg) > 0 {

View File

@ -32,6 +32,7 @@ import (
"tailscale.com/clientupdate" "tailscale.com/clientupdate"
"tailscale.com/drive" "tailscale.com/drive"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo" "tailscale.com/hostinfo"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnauth"
@ -82,71 +83,72 @@ var handler = map[string]LocalAPIHandler{
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME // The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash: // without a trailing slash:
"alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690 "alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
"bugreport": (*Handler).serveBugReport, "bugreport": (*Handler).serveBugReport,
"check-ip-forwarding": (*Handler).serveCheckIPForwarding, "check-ip-forwarding": (*Handler).serveCheckIPForwarding,
"check-prefs": (*Handler).serveCheckPrefs, "check-prefs": (*Handler).serveCheckPrefs,
"check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding, "check-reverse-path-filtering": (*Handler).serveCheckReversePathFiltering,
"component-debug-logging": (*Handler).serveComponentDebugLogging, "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
"debug": (*Handler).serveDebug, "component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug-derp-region": (*Handler).serveDebugDERPRegion, "debug": (*Handler).serveDebug,
"debug-dial-types": (*Handler).serveDebugDialTypes, "debug-derp-region": (*Handler).serveDebugDERPRegion,
"debug-log": (*Handler).serveDebugLog, "debug-dial-types": (*Handler).serveDebugDialTypes,
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches, "debug-log": (*Handler).serveDebugLog,
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules, "debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges, "debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
"debug-portmap": (*Handler).serveDebugPortmap, "debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
"derpmap": (*Handler).serveDERPMap, "debug-portmap": (*Handler).serveDebugPortmap,
"dev-set-state-store": (*Handler).serveDevSetStateStore, "derpmap": (*Handler).serveDERPMap,
"dial": (*Handler).serveDial, "dev-set-state-store": (*Handler).serveDevSetStateStore,
"disconnect-control": (*Handler).disconnectControl, "dial": (*Handler).serveDial,
"dns-osconfig": (*Handler).serveDNSOSConfig, "disconnect-control": (*Handler).disconnectControl,
"dns-query": (*Handler).serveDNSQuery, "dns-osconfig": (*Handler).serveDNSOSConfig,
"drive/fileserver-address": (*Handler).serveDriveServerAddr, "dns-query": (*Handler).serveDNSQuery,
"drive/shares": (*Handler).serveShares, "drive/fileserver-address": (*Handler).serveDriveServerAddr,
"goroutines": (*Handler).serveGoroutines, "drive/shares": (*Handler).serveShares,
"handle-push-message": (*Handler).serveHandlePushMessage, "goroutines": (*Handler).serveGoroutines,
"id-token": (*Handler).serveIDToken, "handle-push-message": (*Handler).serveHandlePushMessage,
"login-interactive": (*Handler).serveLoginInteractive, "id-token": (*Handler).serveIDToken,
"logout": (*Handler).serveLogout, "login-interactive": (*Handler).serveLoginInteractive,
"logtap": (*Handler).serveLogTap, "logout": (*Handler).serveLogout,
"metrics": (*Handler).serveMetrics, "logtap": (*Handler).serveLogTap,
"ping": (*Handler).servePing, "metrics": (*Handler).serveMetrics,
"pprof": (*Handler).servePprof, "ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs, "pprof": (*Handler).servePprof,
"query-feature": (*Handler).serveQueryFeature, "prefs": (*Handler).servePrefs,
"reload-config": (*Handler).reloadConfig, "query-feature": (*Handler).serveQueryFeature,
"reset-auth": (*Handler).serveResetAuth, "reload-config": (*Handler).reloadConfig,
"serve-config": (*Handler).serveServeConfig, "reset-auth": (*Handler).serveResetAuth,
"set-dns": (*Handler).serveSetDNS, "serve-config": (*Handler).serveServeConfig,
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-dns": (*Handler).serveSetDNS,
"set-gui-visible": (*Handler).serveSetGUIVisible, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
"set-push-device-token": (*Handler).serveSetPushDeviceToken, "set-gui-visible": (*Handler).serveSetGUIVisible,
"set-udp-gro-forwarding": (*Handler).serveSetUDPGROForwarding, "set-push-device-token": (*Handler).serveSetPushDeviceToken,
"set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled, "set-udp-gro-forwarding": (*Handler).serveSetUDPGROForwarding,
"start": (*Handler).serveStart, "set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled,
"status": (*Handler).serveStatus, "start": (*Handler).serveStart,
"suggest-exit-node": (*Handler).serveSuggestExitNode, "status": (*Handler).serveStatus,
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs, "suggest-exit-node": (*Handler).serveSuggestExitNode,
"tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM, "tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
"tka/disable": (*Handler).serveTKADisable, "tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM,
"tka/force-local-disable": (*Handler).serveTKALocalDisable, "tka/disable": (*Handler).serveTKADisable,
"tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM, "tka/force-local-disable": (*Handler).serveTKALocalDisable,
"tka/init": (*Handler).serveTKAInit, "tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM,
"tka/log": (*Handler).serveTKALog, "tka/init": (*Handler).serveTKAInit,
"tka/modify": (*Handler).serveTKAModify, "tka/log": (*Handler).serveTKALog,
"tka/sign": (*Handler).serveTKASign, "tka/modify": (*Handler).serveTKAModify,
"tka/status": (*Handler).serveTKAStatus, "tka/sign": (*Handler).serveTKASign,
"tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM, "tka/status": (*Handler).serveTKAStatus,
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink, "tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM,
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey, "tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
"update/check": (*Handler).serveUpdateCheck, "tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
"update/install": (*Handler).serveUpdateInstall, "update/check": (*Handler).serveUpdateCheck,
"update/progress": (*Handler).serveUpdateProgress, "update/install": (*Handler).serveUpdateInstall,
"upload-client-metrics": (*Handler).serveUploadClientMetrics, "update/progress": (*Handler).serveUpdateProgress,
"usermetrics": (*Handler).serveUserMetrics, "upload-client-metrics": (*Handler).serveUploadClientMetrics,
"watch-ipn-bus": (*Handler).serveWatchIPNBus, "usermetrics": (*Handler).serveUserMetrics,
"whois": (*Handler).serveWhoIs, "watch-ipn-bus": (*Handler).serveWatchIPNBus,
"whois": (*Handler).serveWhoIs,
} }
// Register registers a new LocalAPI handler for the given name. // Register registers a new LocalAPI handler for the given name.
@ -1175,6 +1177,32 @@ func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request)
}) })
} }
func (h *Handler) serveCheckReversePathFiltering(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "reverse path filtering check access denied", http.StatusForbidden)
return
}
var warning string
state := h.b.Sys().NetMon.Get().InterfaceState()
warn, err := netutil.CheckReversePathFiltering(state)
if err == nil && len(warn) > 0 {
var msg strings.Builder
msg.WriteString(healthmsg.WarnExitNodeUsage + ":\n")
for _, w := range warn {
msg.WriteString("- " + w + "\n")
}
msg.WriteString(healthmsg.DisableRPFilter)
warning = msg.String()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct {
Warning string
}{
Warning: warning,
})
}
func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "UDP GRO forwarding check access denied", http.StatusForbidden) http.Error(w, "UDP GRO forwarding check access denied", http.StatusForbidden)

View File

@ -237,7 +237,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/envknob/featureknob from tailscale.com/client/web+ tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/ipn/ipnext+ tailscale.com/feature from tailscale.com/ipn/ipnext+
tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
tailscale.com/hostinfo from tailscale.com/client/web+ tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/local+ tailscale.com/ipn from tailscale.com/client/local+