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,6 +23,7 @@ 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")
) )
@ -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"
@ -86,6 +87,7 @@ var handler = map[string]LocalAPIHandler{
"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-reverse-path-filtering": (*Handler).serveCheckReversePathFiltering,
"check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding, "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
"component-debug-logging": (*Handler).serveComponentDebugLogging, "component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug, "debug": (*Handler).serveDebug,
@ -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+