This provides a mechanism to block, waiting for Tailscale's IP to be ready for a bind/listen, to gate the starting of other services. It also adds a new --assert=[IP] option to "tailscale ip", for services that want extra paranoia about what IP is in use, if they're worried about having switched to the wrong tailnet prior to reboot or something. Updates #3340 Updates #11504 ... and many more, IIRC Change-Id: I88ab19ac5fae58fd8c516065bab685e292395565 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
abdbca47af
commit
8736fbb754
@ -0,0 +1,157 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli |
||||
|
||||
import ( |
||||
"context" |
||||
"flag" |
||||
"fmt" |
||||
"net" |
||||
"net/netip" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli" |
||||
"tailscale.com/ipn" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/util/backoff" |
||||
) |
||||
|
||||
var waitCmd = &ffcli.Command{ |
||||
Name: "wait", |
||||
ShortHelp: "Wait for Tailscale interface/IPs to be ready for binding", |
||||
LongHelp: strings.TrimSpace(` |
||||
Wait for Tailscale resources to be available. As of 2026-01-02, the only |
||||
resource that's available to wait for by is the Tailscale interface and its |
||||
IPs. |
||||
|
||||
With no arguments, this command will block until tailscaled is up, its backend is running, |
||||
and the Tailscale interface is up and has a Tailscale IP address assigned to it. |
||||
|
||||
If running in userspace-networking mode, this command only waits for tailscaled and |
||||
the Running state, as no physical network interface exists. |
||||
|
||||
A future version of this command may support waiting for other types of resources. |
||||
|
||||
The command returns exit code 0 on success, and non-zero on failure or timeout. |
||||
|
||||
To wait on a specific type of IP address, use 'tailscale ip' in combination with |
||||
the 'tailscale wait' command. For example, to wait for an IPv4 address: |
||||
|
||||
tailscale wait && tailscale ip --assert=<specific-IP-address> |
||||
|
||||
Linux systemd users can wait for the "tailscale-online.target" target, which runs |
||||
this command. |
||||
|
||||
More generally, a service that wants to bind to (listen on) a Tailscale interface or IP address |
||||
can run it like 'tailscale wait && /path/to/service [...]' to ensure that Tailscale is ready |
||||
before the program starts. |
||||
`), |
||||
|
||||
ShortUsage: "tailscale wait", |
||||
Exec: runWait, |
||||
FlagSet: (func() *flag.FlagSet { |
||||
fs := newFlagSet("wait") |
||||
fs.DurationVar(&waitArgs.timeout, "timeout", 0, "how long to wait before giving up (0 means wait indefinitely)") |
||||
return fs |
||||
})(), |
||||
} |
||||
|
||||
var waitArgs struct { |
||||
timeout time.Duration |
||||
} |
||||
|
||||
func runWait(ctx context.Context, args []string) error { |
||||
if len(args) > 0 { |
||||
return fmt.Errorf("unexpected arguments: %q", args) |
||||
} |
||||
if waitArgs.timeout > 0 { |
||||
var cancel context.CancelFunc |
||||
ctx, cancel = context.WithTimeout(ctx, waitArgs.timeout) |
||||
defer cancel() |
||||
} |
||||
|
||||
bo := backoff.NewBackoff("wait", logger.Discard, 2*time.Second) |
||||
for { |
||||
_, err := localClient.StatusWithoutPeers(ctx) |
||||
bo.BackOff(ctx, err) |
||||
if err == nil { |
||||
break |
||||
} |
||||
if ctx.Err() != nil { |
||||
return ctx.Err() |
||||
} |
||||
} |
||||
|
||||
watcher, err := localClient.WatchIPNBus(ctx, ipn.NotifyInitialState) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer watcher.Close() |
||||
var firstIP netip.Addr |
||||
for { |
||||
not, err := watcher.Next() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if not.State != nil && *not.State == ipn.Running { |
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if len(st.TailscaleIPs) > 0 { |
||||
firstIP = st.TailscaleIPs[0] |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !st.TUN { |
||||
// No TUN; nothing more to wait for.
|
||||
return nil |
||||
} |
||||
|
||||
// Verify we have an interface using that IP.
|
||||
for { |
||||
err := checkForInterfaceIP(firstIP) |
||||
if err == nil { |
||||
return nil |
||||
} |
||||
bo.BackOff(ctx, err) |
||||
if ctx.Err() != nil { |
||||
return ctx.Err() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func checkForInterfaceIP(ip netip.Addr) error { |
||||
ifs, err := net.Interfaces() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, ifi := range ifs { |
||||
addrs, err := ifi.Addrs() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, addr := range addrs { |
||||
var aip netip.Addr |
||||
switch v := addr.(type) { |
||||
case *net.IPNet: |
||||
aip, _ = netip.AddrFromSlice(v.IP) |
||||
case *net.IPAddr: |
||||
aip, _ = netip.AddrFromSlice(v.IP) |
||||
} |
||||
if aip.Unmap() == ip { |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
return fmt.Errorf("no interface has IP %v", ip) |
||||
} |
||||
Loading…
Reference in new issue