cmd/tailscale, ipn/localapi: move IP forwarding check to tailscaled, API

Instead of having the CLI check whether IP forwarding is enabled, ask
tailscaled. It has a better idea. If it's netstack, for instance, the
sysctl values don't matter. And it's possible that only the daemon has
permission to know.

Fixes #1626

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-03-31 11:55:21 -07:00
parent ea714c6054
commit 1bd14a072c
5 changed files with 111 additions and 36 deletions

View File

@ -8,6 +8,7 @@
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -198,7 +199,34 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
if res.StatusCode != 200 { if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body) body, _ := ioutil.ReadAll(res.Body)
res.Body.Close() res.Body.Close()
return nil, 0, fmt.Errorf("expected 204 No Content; got HTTP %s: %s", res.Status, body) return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
} }
return res.Body, res.ContentLength, nil return res.Body, res.ContentLength, nil
} }
func CheckIPForwarding(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/check-ip-forwarding", nil)
if err != nil {
return err
}
res, err := DoLocalRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("HTTP %s: %s", res.Status, body)
}
var jres struct {
Warning string
}
if err := json.NewDecoder(res.Body).Decode(&jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}

View File

@ -5,17 +5,14 @@
package cli package cli
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec"
"runtime" "runtime"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
@ -97,34 +94,6 @@ func warnf(format string, args ...interface{}) {
fmt.Printf("Warning: "+format+"\n", args...) fmt.Printf("Warning: "+format+"\n", args...)
} }
// checkIPForwarding prints warnings on linux if IP forwarding is not
// enabled, or if we were unable to verify the state of IP forwarding.
func checkIPForwarding() {
var key string
if runtime.GOOS == "linux" {
key = "net.ipv4.ip_forward"
} else if isBSD(runtime.GOOS) {
key = "net.inet.ip.forwarding"
} else {
return
}
bs, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
warnf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
return
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil {
warnf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
return
}
if !on {
warnf("%s is disabled. Subnet routes won't work.", key)
}
}
var ( var (
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0") ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6default = netaddr.MustParseIPPrefix("::/0") ipv6default = netaddr.MustParseIPPrefix("::/0")
@ -181,9 +150,8 @@ func runUp(ctx context.Context, args []string) error {
routeMap[netaddr.MustParseIPPrefix("::/0")] = true routeMap[netaddr.MustParseIPPrefix("::/0")] = true
} }
if len(routeMap) > 0 { if len(routeMap) > 0 {
checkIPForwarding() if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
if isBSD(runtime.GOOS) { warnf("%v", err)
warnf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
} }
} }
routes := make([]netaddr.IPPrefix, 0, len(routeMap)) routes := make([]netaddr.IPPrefix, 0, len(routeMap))

View File

@ -66,6 +66,7 @@ type Direct struct {
getMachinePrivKey func() (wgkey.Private, error) getMachinePrivKey func() (wgkey.Private, error)
debugFlags []string debugFlags []string
keepSharerAndUserSplit bool keepSharerAndUserSplit bool
skipIPForwardingCheck bool
mu sync.Mutex // mutex guards the following fields mu sync.Mutex // mutex guards the following fields
serverKey wgkey.Key serverKey wgkey.Key
@ -98,6 +99,11 @@ type Options struct {
// KeepSharerAndUserSplit controls whether the client // KeepSharerAndUserSplit controls whether the client
// understands Node.Sharer. If false, the Sharer is mapped to the User. // understands Node.Sharer. If false, the Sharer is mapped to the User.
KeepSharerAndUserSplit bool KeepSharerAndUserSplit bool
// SkipIPForwardingCheck declares that the host's IP
// forwarding works and should not be double-checked by the
// controlclient package.
SkipIPForwardingCheck bool
} }
type Decompressor interface { type Decompressor interface {
@ -159,6 +165,7 @@ func NewDirect(opts Options) (*Direct, error) {
debugFlags: opts.DebugFlags, debugFlags: opts.DebugFlags,
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit, keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
linkMon: opts.LinkMonitor, linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
} }
if opts.Hostinfo == nil { if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo()) c.SetHostinfo(NewHostinfo())
@ -577,7 +584,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
OmitPeers: cb == nil, OmitPeers: cb == nil,
} }
var extraDebugFlags []string var extraDebugFlags []string
if hostinfo != nil && c.linkMon != nil && ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) { if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off") extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
} }
if health.RouterHealth() != nil { if health.RouterHealth() != nil {
@ -1181,6 +1189,11 @@ func TrimWGConfig() opt.Bool {
// and will definitely not work for the routes provided. // and will definitely not work for the routes provided.
// //
// It should not return false positives. // It should not return false positives.
//
// TODO(bradfitz): merge this code into LocalBackend.CheckIPForwarding
// and change controlclient.Options.SkipIPForwardingCheck into a
// func([]netaddr.IPPrefix) error signature instead. Then we only have
// one copy of this code.
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool { func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
if len(routes) == 0 { if len(routes) == 0 {
// Nothing to route, so no need to warn. // Nothing to route, so no need to warn.

View File

@ -12,6 +12,7 @@
"io" "io"
"net" "net"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
@ -640,6 +641,10 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
DiscoPublicKey: discoPublic, DiscoPublicKey: discoPublic,
DebugFlags: controlDebugFlags, DebugFlags: controlDebugFlags,
LinkMonitor: b.e.GetLinkMonitor(), LinkMonitor: b.e.GetLinkMonitor(),
// Don't warn about broken Linux IP forwading when
// netstack is being used.
SkipIPForwardingCheck: wgengine.IsNetstack(b.e),
}) })
if err != nil { if err != nil {
return err return err
@ -2018,3 +2023,45 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
} }
return apiSrv.OpenFile(name) return apiSrv.OpenFile(name)
} }
func isBSD(s string) bool {
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
}
func (b *LocalBackend) CheckIPForwarding() error {
if wgengine.IsNetstack(b.e) {
return nil
}
if isBSD(runtime.GOOS) {
//lint:ignore ST1005 output to users as is
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
}
var keys []string
if runtime.GOOS == "linux" {
keys = append(keys, "net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
} else if isBSD(runtime.GOOS) {
keys = append(keys, "net.inet.ip.forwarding")
} else {
return nil
}
for _, key := range keys {
bs, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
//lint:ignore ST1005 output to users as is
return fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil {
//lint:ignore ST1005 output to users as is
return fmt.Errorf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
}
if !on {
//lint:ignore ST1005 output to users as is
return fmt.Errorf("%s is disabled. Subnet routes won't work.", key)
}
}
return nil
}

View File

@ -67,6 +67,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveGoroutines(w, r) h.serveGoroutines(w, r)
case "/localapi/v0/status": case "/localapi/v0/status":
h.serveStatus(w, r) h.serveStatus(w, r)
case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r)
default: default:
io.WriteString(w, "tailscaled\n") io.WriteString(w, "tailscaled\n")
} }
@ -121,6 +123,23 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
w.Write(buf) w.Write(buf)
} }
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
return
}
var warning string
if err := h.b.CheckIPForwarding(); 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)