mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
cmd/tailscale,ipn,tailcfg: add tailscale advertise
subcommand behind envknob (#13734)
Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
parent
d32d742af0
commit
22c89fcb19
78
cmd/tailscale/cli/advertise.go
Normal file
78
cmd/tailscale/cli/advertise.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var advertiseArgs struct {
|
||||||
|
services string // comma-separated list of services to advertise
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(naman): This flag may move to set.go or serve_v2.go after the WIPCode
|
||||||
|
// envknob is not needed.
|
||||||
|
var advertiseCmd = &ffcli.Command{
|
||||||
|
Name: "advertise",
|
||||||
|
ShortUsage: "tailscale advertise --services=<services>",
|
||||||
|
ShortHelp: "Advertise this node as a destination for a service",
|
||||||
|
Exec: runAdvertise,
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("advertise")
|
||||||
|
fs.StringVar(&advertiseArgs.services, "services", "", "comma-separated services to advertise; each must start with \"svc:\" (e.g. \"svc:idp,svc:nas,svc:database\")")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeAdvertiseCmd() []*ffcli.Command {
|
||||||
|
if !envknob.UseWIPCode() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []*ffcli.Command{advertiseCmd}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdvertise(ctx context.Context, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := parseServiceNames(advertiseArgs.services)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||||
|
AdvertiseServicesSet: true,
|
||||||
|
Prefs: ipn.Prefs{
|
||||||
|
AdvertiseServices: services,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseServiceNames takes a comma-separated list of service names
|
||||||
|
// (eg. "svc:hello,svc:webserver,svc:catphotos"), splits them into
|
||||||
|
// a list and validates each service name. If valid, it returns
|
||||||
|
// the service names in a slice of strings.
|
||||||
|
func parseServiceNames(servicesArg string) ([]string, error) {
|
||||||
|
var services []string
|
||||||
|
if servicesArg != "" {
|
||||||
|
services = strings.Split(servicesArg, ",")
|
||||||
|
for _, svc := range services {
|
||||||
|
err := tailcfg.CheckServiceName(svc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("service %q: %s", svc, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
@ -177,7 +177,7 @@ func newRootCmd() *ffcli.Command {
|
|||||||
This CLI is still under active development. Commands and flags will
|
This CLI is still under active development. Commands and flags will
|
||||||
change in the future.
|
change in the future.
|
||||||
`),
|
`),
|
||||||
Subcommands: []*ffcli.Command{
|
Subcommands: append([]*ffcli.Command{
|
||||||
upCmd,
|
upCmd,
|
||||||
downCmd,
|
downCmd,
|
||||||
setCmd,
|
setCmd,
|
||||||
@ -207,7 +207,7 @@ func newRootCmd() *ffcli.Command {
|
|||||||
debugCmd,
|
debugCmd,
|
||||||
driveCmd,
|
driveCmd,
|
||||||
idTokenCmd,
|
idTokenCmd,
|
||||||
},
|
}, maybeAdvertiseCmd()...),
|
||||||
FlagSet: rootfs,
|
FlagSet: rootfs,
|
||||||
Exec: func(ctx context.Context, args []string) error {
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
|
@ -946,6 +946,10 @@ func TestPrefFlagMapping(t *testing.T) {
|
|||||||
// Handled by the tailscale share subcommand, we don't want a CLI
|
// Handled by the tailscale share subcommand, we don't want a CLI
|
||||||
// flag for this.
|
// flag for this.
|
||||||
continue
|
continue
|
||||||
|
case "AdvertiseServices":
|
||||||
|
// Handled by the tailscale advertise subcommand, we don't want a
|
||||||
|
// CLI flag for this.
|
||||||
|
continue
|
||||||
case "InternalExitNodePrior":
|
case "InternalExitNodePrior":
|
||||||
// Used internally by LocalBackend as part of exit node usage toggling.
|
// Used internally by LocalBackend as part of exit node usage toggling.
|
||||||
// No CLI flag for this.
|
// No CLI flag for this.
|
||||||
|
@ -164,6 +164,9 @@ func defaultNetfilterMode() string {
|
|||||||
return "on"
|
return "on"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upArgsT is the type of upArgs, the argument struct for `tailscale up`.
|
||||||
|
// As of 2024-10-08, upArgsT is frozen and no new arguments should be
|
||||||
|
// added to it. Add new arguments to setArgsT instead.
|
||||||
type upArgsT struct {
|
type upArgsT struct {
|
||||||
qr bool
|
qr bool
|
||||||
reset bool
|
reset bool
|
||||||
|
@ -27,6 +27,7 @@ func (src *Prefs) Clone() *Prefs {
|
|||||||
*dst = *src
|
*dst = *src
|
||||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||||
|
dst.AdvertiseServices = append(src.AdvertiseServices[:0:0], src.AdvertiseServices...)
|
||||||
if src.DriveShares != nil {
|
if src.DriveShares != nil {
|
||||||
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
||||||
for i := range dst.DriveShares {
|
for i := range dst.DriveShares {
|
||||||
@ -61,6 +62,7 @@ func (src *Prefs) Clone() *Prefs {
|
|||||||
ForceDaemon bool
|
ForceDaemon bool
|
||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
|
AdvertiseServices []string
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
NoStatefulFiltering opt.Bool
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
@ -85,6 +85,9 @@ func (v PrefsView) Egg() bool { return v.ж.Eg
|
|||||||
func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
|
func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
|
||||||
return views.SliceOf(v.ж.AdvertiseRoutes)
|
return views.SliceOf(v.ж.AdvertiseRoutes)
|
||||||
}
|
}
|
||||||
|
func (v PrefsView) AdvertiseServices() views.Slice[string] {
|
||||||
|
return views.SliceOf(v.ж.AdvertiseServices)
|
||||||
|
}
|
||||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||||
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
|
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
|
||||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||||
@ -120,6 +123,7 @@ func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.
|
|||||||
ForceDaemon bool
|
ForceDaemon bool
|
||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
|
AdvertiseServices []string
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
NoStatefulFiltering opt.Bool
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
11
ipn/prefs.go
11
ipn/prefs.go
@ -179,6 +179,12 @@ type Prefs struct {
|
|||||||
// node.
|
// node.
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
|
|
||||||
|
// AdvertiseServices specifies the list of services that this
|
||||||
|
// node can serve as a destination for. Note that an advertised
|
||||||
|
// service must still go through the approval process from the
|
||||||
|
// control server.
|
||||||
|
AdvertiseServices []string
|
||||||
|
|
||||||
// NoSNAT specifies whether to source NAT traffic going to
|
// NoSNAT specifies whether to source NAT traffic going to
|
||||||
// destinations in AdvertiseRoutes. The default is to apply source
|
// destinations in AdvertiseRoutes. The default is to apply source
|
||||||
// NAT, which makes the traffic appear to come from the router
|
// NAT, which makes the traffic appear to come from the router
|
||||||
@ -319,6 +325,7 @@ type MaskedPrefs struct {
|
|||||||
ForceDaemonSet bool `json:",omitempty"`
|
ForceDaemonSet bool `json:",omitempty"`
|
||||||
EggSet bool `json:",omitempty"`
|
EggSet bool `json:",omitempty"`
|
||||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||||
|
AdvertiseServicesSet bool `json:",omitempty"`
|
||||||
NoSNATSet bool `json:",omitempty"`
|
NoSNATSet bool `json:",omitempty"`
|
||||||
NoStatefulFilteringSet bool `json:",omitempty"`
|
NoStatefulFilteringSet bool `json:",omitempty"`
|
||||||
NetfilterModeSet bool `json:",omitempty"`
|
NetfilterModeSet bool `json:",omitempty"`
|
||||||
@ -527,6 +534,9 @@ func (p *Prefs) pretty(goos string) string {
|
|||||||
if len(p.AdvertiseTags) > 0 {
|
if len(p.AdvertiseTags) > 0 {
|
||||||
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
||||||
}
|
}
|
||||||
|
if len(p.AdvertiseServices) > 0 {
|
||||||
|
fmt.Fprintf(&sb, "services=%s ", strings.Join(p.AdvertiseServices, ","))
|
||||||
|
}
|
||||||
if goos == "linux" {
|
if goos == "linux" {
|
||||||
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
||||||
}
|
}
|
||||||
@ -598,6 +608,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
|||||||
p.ForceDaemon == p2.ForceDaemon &&
|
p.ForceDaemon == p2.ForceDaemon &&
|
||||||
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
|
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
|
||||||
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
||||||
|
compareStrings(p.AdvertiseServices, p2.AdvertiseServices) &&
|
||||||
p.Persist.Equals(p2.Persist) &&
|
p.Persist.Equals(p2.Persist) &&
|
||||||
p.ProfileName == p2.ProfileName &&
|
p.ProfileName == p2.ProfileName &&
|
||||||
p.AutoUpdate.Equals(p2.AutoUpdate) &&
|
p.AutoUpdate.Equals(p2.AutoUpdate) &&
|
||||||
|
@ -54,6 +54,7 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
"ForceDaemon",
|
"ForceDaemon",
|
||||||
"Egg",
|
"Egg",
|
||||||
"AdvertiseRoutes",
|
"AdvertiseRoutes",
|
||||||
|
"AdvertiseServices",
|
||||||
"NoSNAT",
|
"NoSNAT",
|
||||||
"NoStatefulFiltering",
|
"NoStatefulFiltering",
|
||||||
"NetfilterMode",
|
"NetfilterMode",
|
||||||
@ -330,6 +331,16 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
&Prefs{NetfilterKind: ""},
|
&Prefs{NetfilterKind: ""},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
||||||
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:xenia"}},
|
||||||
|
&Prefs{AdvertiseServices: []string{"svc:tux", "svc:amelie"}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
got := tt.a.Equals(tt.b)
|
got := tt.a.Equals(tt.b)
|
||||||
|
@ -651,6 +651,21 @@ func CheckTag(tag string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckServiceName validates svc for use as a service name.
|
||||||
|
// We only allow valid DNS labels, since the expectation is that these will be
|
||||||
|
// used as parts of domain names.
|
||||||
|
func CheckServiceName(svc string) error {
|
||||||
|
var ok bool
|
||||||
|
svc, ok = strings.CutPrefix(svc, "svc:")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("services must start with 'svc:'")
|
||||||
|
}
|
||||||
|
if svc == "" {
|
||||||
|
return errors.New("service names must not be empty")
|
||||||
|
}
|
||||||
|
return dnsname.ValidLabel(svc)
|
||||||
|
}
|
||||||
|
|
||||||
// CheckRequestTags checks that all of h.RequestTags are valid.
|
// CheckRequestTags checks that all of h.RequestTags are valid.
|
||||||
func (h *Hostinfo) CheckRequestTags() error {
|
func (h *Hostinfo) CheckRequestTags() error {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user