ipn/{ipnlocal,localapi},net/netkernelconf,client/tailscale,cmd/containerboot: optionally enable UDP GRO forwarding for containers (#12410)

Add a new TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS env var
that can be set for tailscale/tailscale container running as
a subnet router or exit node to enable UDP GRO forwarding
for improved performance.
See https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
This is currently considered an experimental approach;
the configuration support is partially to allow further experimentation
with containerized environments to evaluate the performance
improvements.

Updates tailscale/tailscale#12295

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
Irbe Krumina 2024-06-10 19:19:03 +01:00 committed by GitHub
parent 6f2bae019f
commit bc53ebd4a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 140 additions and 19 deletions

View File

@ -699,6 +699,27 @@ func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
return nil return nil
} }
// 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
// nodes or subnet routers.
// See https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
func (lc *LocalClient) SetUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/set-udp-gro-forwarding")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from set-udp-gro-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
// CheckPrefs validates the provided preferences, without making any changes. // CheckPrefs validates the provided preferences, without making any changes.
// //
// The CLI uses this before a Start call to fail fast if the preferences won't // The CLI uses this before a Start call to fail fast if the preferences won't

View File

@ -61,6 +61,11 @@
// and not `tailscale up` or `tailscale set`. // and not `tailscale up` or `tailscale set`.
// The config file contents are currently read once on container start. // The config file contents are currently read once on container start.
// NB: This env var is currently experimental and the logic will likely change! // NB: This env var is currently experimental and the logic will likely change!
// TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS: set to true to
// autoconfigure the default network interface for optimal performance for
// Tailscale subnet router/exit node.
// https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
// NB: This env var is currently experimental and the logic will likely change!
// - EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS: if set to true // - EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS: if set to true
// and if this containerboot instance is an L7 ingress proxy (created by // and if this containerboot instance is an L7 ingress proxy (created by
// the Kubernetes operator), set up rules to allow proxying cluster traffic, // the Kubernetes operator), set up rules to allow proxying cluster traffic,
@ -152,6 +157,7 @@ func main() {
TailscaledConfigFilePath: tailscaledConfigFilePath(), TailscaledConfigFilePath: tailscaledConfigFilePath(),
AllowProxyingClusterTrafficViaIngress: defaultBool("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS", false), AllowProxyingClusterTrafficViaIngress: defaultBool("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS", false),
PodIP: defaultEnv("POD_IP", ""), PodIP: defaultEnv("POD_IP", ""),
EnableForwardingOptimizations: defaultBool("TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS", false),
} }
if err := cfg.validate(); err != nil { if err := cfg.validate(); err != nil {
@ -199,6 +205,12 @@ func main() {
} }
defer killTailscaled() defer killTailscaled()
if cfg.EnableForwardingOptimizations {
if err := client.SetUDPGROForwarding(bootCtx); err != nil {
log.Printf("[unexpected] error enabling UDP GRO forwarding: %v", err)
}
}
w, err := client.WatchIPNBus(bootCtx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState) w, err := client.WatchIPNBus(bootCtx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState)
if err != nil { if err != nil {
log.Fatalf("failed to watch tailscaled for updates: %v", err) log.Fatalf("failed to watch tailscaled for updates: %v", err)
@ -1080,22 +1092,23 @@ type settings struct {
// TailnetTargetFQDN is an MagicDNS name to which all incoming // TailnetTargetFQDN is an MagicDNS name to which all incoming
// non-Tailscale traffic should be proxied. This must be a full Tailnet // non-Tailscale traffic should be proxied. This must be a full Tailnet
// node FQDN. // node FQDN.
TailnetTargetFQDN string TailnetTargetFQDN string
ServeConfigPath string ServeConfigPath string
DaemonExtraArgs string DaemonExtraArgs string
ExtraArgs string ExtraArgs string
InKubernetes bool InKubernetes bool
UserspaceMode bool UserspaceMode bool
StateDir string StateDir string
AcceptDNS *bool AcceptDNS *bool
KubeSecret string KubeSecret string
SOCKSProxyAddr string SOCKSProxyAddr string
HTTPProxyAddr string HTTPProxyAddr string
Socket string Socket string
AuthOnce bool AuthOnce bool
Root string Root string
KubernetesCanPatch bool KubernetesCanPatch bool
TailscaledConfigFilePath string TailscaledConfigFilePath string
EnableForwardingOptimizations bool
// If set to true and, if this containerboot instance is a Kubernetes // If set to true and, if this containerboot instance is a Kubernetes
// ingress proxy, set up rules to forward incoming cluster traffic to be // ingress proxy, set up rules to forward incoming cluster traffic to be
// forwarded to the ingress target in cluster. // forwarded to the ingress target in cluster.
@ -1149,6 +1162,9 @@ func (s *settings) validate() error {
if s.AllowProxyingClusterTrafficViaIngress && s.PodIP == "" { if s.AllowProxyingClusterTrafficViaIngress && s.PodIP == "" {
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but POD_IP is not set") return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but POD_IP is not set")
} }
if s.EnableForwardingOptimizations && s.UserspaceMode {
return errors.New("TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS is not supported in userspace mode")
}
return nil return nil
} }

View File

@ -5537,6 +5537,38 @@ func (b *LocalBackend) CheckUDPGROForwarding() error {
return warn return warn
} }
// SetUDPGROForwarding enables UDP GRO forwarding for the default network
// interface of this machine. It can be done to improve performance for nodes
// acting as Tailscale subnet routers or exit nodes. Currently (9/5/2024) this
// functionality is considered experimental and only safe to use via explicit
// user opt-in for ephemeral devices, such as containers.
// https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
func (b *LocalBackend) SetUDPGROForwarding() error {
if b.sys.IsNetstackRouter() {
return errors.New("UDP GRO forwarding cannot be enabled in userspace mode")
}
tunSys, ok := b.sys.Tun.GetOK()
if !ok {
return errors.New("[unexpected] unable to retrieve tun device configuration")
}
tunInterface, err := tunSys.Name()
if err != nil {
return errors.New("[unexpected] unable to determine name of the tun device")
}
netmonSys, ok := b.sys.NetMon.GetOK()
if !ok {
return errors.New("[unexpected] unable to retrieve tailscale netmon configuration")
}
state := netmonSys.InterfaceState()
if state == nil {
return errors.New("[unexpected] unable to retrieve machine's network interface state")
}
if err := netkernelconf.SetUDPGROForwarding(tunInterface, state.DefaultRouteInterface); err != nil {
return fmt.Errorf("error enabling UDP GRO forwarding: %w", err)
}
return nil
}
// DERPMap returns the current DERPMap in use, or nil if not connected. // DERPMap returns the current DERPMap in use, or nil if not connected.
func (b *LocalBackend) DERPMap() *tailcfg.DERPMap { func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
b.mu.Lock() b.mu.Lock()

View File

@ -119,6 +119,7 @@
"set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-expiry-sooner": (*Handler).serveSetExpirySooner,
"set-gui-visible": (*Handler).serveSetGUIVisible, "set-gui-visible": (*Handler).serveSetGUIVisible,
"set-push-device-token": (*Handler).serveSetPushDeviceToken, "set-push-device-token": (*Handler).serveSetPushDeviceToken,
"set-udp-gro-forwarding": (*Handler).serveSetUDPGROForwarding,
"set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled, "set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled,
"start": (*Handler).serveStart, "start": (*Handler).serveStart,
"status": (*Handler).serveStatus, "status": (*Handler).serveStatus,
@ -1182,6 +1183,23 @@ func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Requ
}) })
} }
func (h *Handler) serveSetUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "UDP GRO forwarding set access denied", http.StatusForbidden)
return
}
var warning string
if err := h.b.SetUDPGROForwarding(); err != nil {
warning = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct {
Warning string
}{
Warning: warning,
})
}
func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "status access denied", http.StatusForbidden) http.Error(w, "status access denied", http.StatusForbidden)

View File

@ -10,3 +10,9 @@
func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, err error) { func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, err error) {
return nil, nil return nil, nil
} }
// SetUDPGROForwarding is unimplemented for non-Linux platforms. Refer to the
// docstring in _linux.go.
func SetUDPGROForwarding(tunInterface, defaultRouteInterface string) error {
return nil
}

View File

@ -9,15 +9,18 @@
"github.com/safchain/ethtool" "github.com/safchain/ethtool"
) )
const (
rxWantFeature = "rx-udp-gro-forwarding"
rxDoNotWantFeature = "rx-gro-list"
txFeature = "tx-udp-segmentation"
)
// CheckUDPGROForwarding checks if the machine is optimally configured to // CheckUDPGROForwarding checks if the machine is optimally configured to
// forward UDP packets between the default route and Tailscale TUN interfaces. // forward UDP packets between the default route and Tailscale TUN interfaces.
// It returns a non-nil warn in the case that the configuration is suboptimal. // It returns a non-nil warn in the case that the configuration is suboptimal.
// It returns a non-nil err in the case that an error is encountered while // It returns a non-nil err in the case that an error is encountered while
// performing the check. // performing the check.
func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, err error) { func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, err error) {
const txFeature = "tx-udp-segmentation"
const rxWantFeature = "rx-udp-gro-forwarding"
const rxDoNotWantFeature = "rx-gro-list"
const kbLink = "\nSee https://tailscale.com/s/ethtool-config-udp-gro" const kbLink = "\nSee https://tailscale.com/s/ethtool-config-udp-gro"
errWithPrefix := func(format string, a ...any) error { errWithPrefix := func(format string, a ...any) error {
const errPrefix = "couldn't check system's UDP GRO forwarding configuration, " const errPrefix = "couldn't check system's UDP GRO forwarding configuration, "
@ -52,3 +55,28 @@ func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, er
} }
return nil, nil return nil, nil
} }
// SetUDPGROForwarding enables UDP GRO forwarding for the provided default
// interface. It validates if the provided tun interface has UDP segmentation
// enabled and, if not, returns an error. See
// https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
func SetUDPGROForwarding(tunInterface, defaultInterface string) error {
e, err := ethtool.NewEthtool()
if err != nil {
return fmt.Errorf("failed to init ethtool: %w", err)
}
defer e.Close()
tunFeatures, err := e.Features(tunInterface)
if err != nil {
return fmt.Errorf("failed to retrieve TUN device features: %w", err)
}
if !tunFeatures[txFeature] {
// if txFeature is disabled/nonexistent on the TUN then UDP GRO
// forwarding doesn't matter, we won't be taking advantage of it.
return fmt.Errorf("Not enabling UDP GRO forwarding as UDP segmentation is disabled for Tailscale interface")
}
if err := e.Change(defaultInterface, map[string]bool{rxWantFeature: true, rxDoNotWantFeature: false}); err != nil {
return fmt.Errorf("error enabling UDP GRO forwarding: %w", err)
}
return nil
}