Files
tailscale-custom/cmd/tailscale/cli/ip.go
T
Brad Fitzpatrick 8736fbb754 cmd/tailscale/cli: add 'wait' listening subcommand and ip --assert=<ip>
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>
2026-02-02 15:19:06 -08:00

132 lines
2.8 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"errors"
"flag"
"fmt"
"net/netip"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn/ipnstate"
)
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "tailscale ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ip")
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
fs.StringVar(&ipArgs.assert, "assert", "", "assert that one of the node's IP(s) matches this IP address")
return fs
})(),
}
var ipArgs struct {
want1 bool
want4 bool
want6 bool
assert string
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 1 {
return errors.New("too many arguments, expected at most one peer")
}
var of string
if len(args) == 1 {
of = args[0]
}
v4, v6 := ipArgs.want4, ipArgs.want6
nflags := 0
for _, b := range []bool{ipArgs.want1, v4, v6} {
if b {
nflags++
}
}
if nflags > 1 {
return errors.New("tailscale ip -1, -4, and -6 are mutually exclusive")
}
if !v4 && !v6 {
v4, v6 = true, true
}
st, err := localClient.Status(ctx)
if err != nil {
return err
}
ips := st.TailscaleIPs
if ipArgs.assert != "" {
for _, ip := range ips {
if ip.String() == ipArgs.assert {
return nil
}
}
return fmt.Errorf("assertion failed: IP %q not found among %v", ipArgs.assert, ips)
}
if of != "" {
ip, _, err := tailscaleIPFromArg(ctx, of)
if err != nil {
return err
}
peer, ok := peerMatchingIP(st, ip)
if !ok {
return fmt.Errorf("no peer found with IP %v", ip)
}
ips = peer.TailscaleIPs
}
if len(ips) == 0 {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
if ipArgs.want1 {
ips = ips[:1]
}
match := false
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {
match = true
outln(ip)
}
}
if !match {
if ipArgs.want4 {
return errors.New("no Tailscale IPv4 address")
}
if ipArgs.want6 {
return errors.New("no Tailscale IPv6 address")
}
}
return nil
}
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return
}
for _, ps = range st.Peer {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
if ps := st.Self; ps != nil {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
return nil, false
}