cmd/containerboot: ensure that subnet routes can be unset. (#10734)

A Tailnet node can be told to stop advertise subnets by passing
an empty string to --advertise-routes flag.
Respect an explicitly passed empty value to TS_ROUTES env var
so that users have a way to stop containerboot acting as a subnet
router without recreating it.
Distinguish between TS_ROUTES being unset and empty.

Updates tailscale/tailscale#10708

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
pull/10725/merge
Irbe Krumina 11 months ago committed by GitHub
parent e32a064659
commit 5a2eb26db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,10 @@
// //
// - TS_AUTHKEY: the authkey to use for login. // - TS_AUTHKEY: the authkey to use for login.
// - TS_HOSTNAME: the hostname to request for the node. // - TS_HOSTNAME: the hostname to request for the node.
// - TS_ROUTES: subnet routes to advertise. To accept routes, use TS_EXTRA_ARGS to pass in --accept-routes. // - TS_ROUTES: subnet routes to advertise. Explicitly setting it to an empty
// value will cause containerboot to stop acting as a subnet router for any
// previously advertised routes. To accept routes, use TS_EXTRA_ARGS to pass
// in --accept-routes.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given // - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination. // destination.
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given // - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
@ -101,7 +104,7 @@ func main() {
cfg := &settings{ cfg := &settings{
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""), AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
Hostname: defaultEnv("TS_HOSTNAME", ""), Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnv("TS_ROUTES", ""), Routes: defaultEnvPointer("TS_ROUTES"),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""), ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
ProxyTo: defaultEnv("TS_DEST_IP", ""), ProxyTo: defaultEnv("TS_DEST_IP", ""),
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""), TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
@ -138,7 +141,7 @@ func main() {
if err := ensureTunFile(cfg.Root); err != nil { if err := ensureTunFile(cfg.Root); err != nil {
log.Fatalf("Unable to create tuntap device file: %v", err) log.Fatalf("Unable to create tuntap device file: %v", err)
} }
if cfg.ProxyTo != "" || cfg.Routes != "" || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" { if cfg.ProxyTo != "" || cfg.Routes != nil || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" {
if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.TailnetTargetIP, cfg.TailnetTargetFQDN, cfg.Routes); err != nil { if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.TailnetTargetIP, cfg.TailnetTargetFQDN, cfg.Routes); err != nil {
log.Printf("Failed to enable IP forwarding: %v", err) log.Printf("Failed to enable IP forwarding: %v", err)
log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.") log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.")
@ -649,8 +652,12 @@ func tailscaleUp(ctx context.Context, cfg *settings) error {
if cfg.AuthKey != "" { if cfg.AuthKey != "" {
args = append(args, "--authkey="+cfg.AuthKey) args = append(args, "--authkey="+cfg.AuthKey)
} }
if cfg.Routes != "" { // --advertise-routes can be passed an empty string to configure a
args = append(args, "--advertise-routes="+cfg.Routes) // device (that might have previously advertised subnet routes) to not
// advertise any routes. Respect an empty string passed by a user and
// use it to explicitly unset the routes.
if cfg.Routes != nil {
args = append(args, "--advertise-routes="+*cfg.Routes)
} }
if cfg.Hostname != "" { if cfg.Hostname != "" {
args = append(args, "--hostname="+cfg.Hostname) args = append(args, "--hostname="+cfg.Hostname)
@ -678,8 +685,12 @@ func tailscaleSet(ctx context.Context, cfg *settings) error {
} else { } else {
args = append(args, "--accept-dns=false") args = append(args, "--accept-dns=false")
} }
if cfg.Routes != "" { // --advertise-routes can be passed an empty string to configure a
args = append(args, "--advertise-routes="+cfg.Routes) // device (that might have previously advertised subnet routes) to not
// advertise any routes. Respect an empty string passed by a user and
// use it to explicitly unset the routes.
if cfg.Routes != nil {
args = append(args, "--advertise-routes="+*cfg.Routes)
} }
if cfg.Hostname != "" { if cfg.Hostname != "" {
args = append(args, "--hostname="+cfg.Hostname) args = append(args, "--hostname="+cfg.Hostname)
@ -714,7 +725,7 @@ func ensureTunFile(root string) error {
} }
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container. // ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTargetFQDN, routes string) error { func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTargetFQDN string, routes *string) error {
var ( var (
v4Forwarding, v6Forwarding bool v4Forwarding, v6Forwarding bool
) )
@ -745,8 +756,8 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTarget
if tailnetTargetFQDN != "" { if tailnetTargetFQDN != "" {
v4Forwarding = true v4Forwarding = true
} }
if routes != "" { if routes != nil {
for _, route := range strings.Split(routes, ",") { for _, route := range strings.Split(*routes, ",") {
cidr, err := netip.ParsePrefix(route) cidr, err := netip.ParsePrefix(route)
if err != nil { if err != nil {
return fmt.Errorf("invalid subnet route: %v", err) return fmt.Errorf("invalid subnet route: %v", err)
@ -850,7 +861,7 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
type settings struct { type settings struct {
AuthKey string AuthKey string
Hostname string Hostname string
Routes string Routes *string
// ProxyTo is the destination IP to which all incoming // ProxyTo is the destination IP to which all incoming
// Tailscale traffic should be proxied. If empty, no proxying // Tailscale traffic should be proxied. If empty, no proxying
// is done. This is typically a locally reachable IP. // is done. This is typically a locally reachable IP.
@ -888,6 +899,16 @@ func defaultEnv(name, defVal string) string {
return defVal return defVal
} }
// defaultEnvPointer returns a pointer to the given envvar value if set, else
// returns nil. This is useful in cases where we need to distinguish between a
// variable being set to empty string vs unset.
func defaultEnvPointer(name string) *string {
if v, ok := os.LookupEnv(name); ok {
return &v
}
return nil
}
func defaultEnvs(names []string, defVal string) string { func defaultEnvs(names []string, defVal string) string {
for _, name := range names { for _, name := range names {
if v, ok := os.LookupEnv(name); ok { if v, ok := os.LookupEnv(name); ok {

@ -218,6 +218,28 @@ func TestContainerBoot(t *testing.T) {
}, },
}, },
}, },
{
Name: "empty routes",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
},
},
},
{ {
Name: "routes_kernel_ipv4", Name: "routes_kernel_ipv4",
Env: map[string]string{ Env: map[string]string{

Loading…
Cancel
Save