Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+98
-17
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
@@ -15,17 +15,34 @@ import (
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// LegacyVersion is the sentinel [ServicesConfigFile.Version] used to mark a
|
||||
// config that was loaded from the legacy raw [ipn.ServeConfig] format (a
|
||||
// version-less file, such as "tailscale serve status --json" output). When
|
||||
// Version is LegacyVersion, [ServicesConfigFile.Legacy] is set and Services is
|
||||
// nil. It is never written to disk; the on-disk format always uses "0.0.1".
|
||||
const LegacyVersion = "0.0.0"
|
||||
|
||||
// ServicesConfigFile is the config file format for services configuration.
|
||||
type ServicesConfigFile struct {
|
||||
// Version is always "0.0.1" and always present.
|
||||
// Version is "0.0.1" for the declarative services configuration file
|
||||
// format, or [LegacyVersion] ("0.0.0") when this value was produced by
|
||||
// [LoadServicesConfig] from a legacy raw ipn.ServeConfig file (in which
|
||||
// case Legacy is set instead of Services).
|
||||
Version string `json:"version"`
|
||||
|
||||
Services map[tailcfg.ServiceName]*ServiceDetailsFile `json:"services,omitzero"`
|
||||
|
||||
// Legacy holds a raw ipn.ServeConfig parsed from a version-less file (e.g.
|
||||
// "tailscale serve status --json" output). It is non-nil only when Version
|
||||
// is [LegacyVersion]. It is an in-memory loading artifact and is never
|
||||
// serialized.
|
||||
Legacy *ipn.ServeConfig `json:"-"`
|
||||
}
|
||||
|
||||
// ServiceDetailsFile is the config syntax for an individual Tailscale Service.
|
||||
@@ -75,11 +92,16 @@ type Target struct {
|
||||
|
||||
// If Protocol is ProtoFile, then Destination is a file path.
|
||||
// If Protocol is ProtoTUN, then Destination is empty.
|
||||
// If Protocol is ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, or
|
||||
// ProtoTLSTerminatedTCP and Destination starts with "unix:", it is a Unix
|
||||
// socket path (e.g. "unix:/var/run/app.sock" or "unix:relative.sock").
|
||||
// Otherwise, it is a host.
|
||||
Destination string
|
||||
|
||||
// If Protocol is not ProtoFile or ProtoTUN, then DestinationPorts is the
|
||||
// set of ports on which to connect to the host referred to by Destination.
|
||||
// For unix socket targets (Destination starting with "unix:"),
|
||||
// DestinationPorts is unused and left at the zero value.
|
||||
DestinationPorts tailcfg.PortRange
|
||||
}
|
||||
|
||||
@@ -116,13 +138,22 @@ func (t *Target) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
t.Destination = target
|
||||
t.DestinationPorts = tailcfg.PortRange{}
|
||||
case ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, ProtoTLSTerminatedTCP:
|
||||
host, portRange, err := tailcfg.ParseHostPortRange(rest)
|
||||
if err != nil {
|
||||
return err
|
||||
if unixPath, ok := strings.CutPrefix(rest, "unix:"); ok {
|
||||
if unixPath == "" {
|
||||
return errors.New("unix socket path cannot be empty")
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = rest
|
||||
t.DestinationPorts = tailcfg.PortRange{}
|
||||
} else {
|
||||
host, portRange, err := tailcfg.ParseHostPortRange(rest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = host
|
||||
t.DestinationPorts = portRange
|
||||
}
|
||||
t.Protocol = ServiceProtocol(proto)
|
||||
t.Destination = host
|
||||
t.DestinationPorts = portRange
|
||||
default:
|
||||
return errors.New("unsupported protocol")
|
||||
}
|
||||
@@ -138,13 +169,27 @@ func (t *Target) MarshalText() ([]byte, error) {
|
||||
case ProtoTUN:
|
||||
out = "TUN"
|
||||
case ProtoHTTP, ProtoHTTPS, ProtoHTTPSInsecure, ProtoTCP, ProtoTLSTerminatedTCP:
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, net.JoinHostPort(t.Destination, t.DestinationPorts.String()))
|
||||
if strings.HasPrefix(t.Destination, "unix:") {
|
||||
// Unix socket: serialize as e.g. "tcp://unix:/path/to/sock"
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, t.Destination)
|
||||
} else {
|
||||
out = fmt.Sprintf("%s://%s", t.Protocol, net.JoinHostPort(t.Destination, t.DestinationPorts.String()))
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
return []byte(out), nil
|
||||
}
|
||||
|
||||
// LoadServicesConfig loads a serve config file as a [ServicesConfigFile].
|
||||
//
|
||||
// If the file has a top-level "version" field it is parsed as that versioned
|
||||
// declarative format. Otherwise it is treated as a legacy raw [ipn.ServeConfig]
|
||||
// (such as "tailscale serve status --json" emits): the returned
|
||||
// ServicesConfigFile has Version [LegacyVersion] and its Legacy field set to the
|
||||
// parsed raw config, with Services left nil.
|
||||
//
|
||||
// forService is used only for the versioned Services configuration file format.
|
||||
func LoadServicesConfig(filename string, forService string) (*ServicesConfigFile, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
@@ -165,13 +210,38 @@ func LoadServicesConfig(filename string, forService string) (*ServicesConfigFile
|
||||
if err = jsonv2.Unmarshal(json, &ver); err != nil {
|
||||
return nil, fmt.Errorf("could not parse config file version: %w", err)
|
||||
}
|
||||
switch ver.Version {
|
||||
case "":
|
||||
return nil, errors.New("config file must have \"version\" field")
|
||||
case "0.0.1":
|
||||
return loadConfigV0(json, forService)
|
||||
if ver.Version == "" {
|
||||
// No "version" field. This is either the legacy raw ipn.ServeConfig
|
||||
// (e.g. "tailscale serve status --json" output, which set-config still
|
||||
// accepts) or a Services configuration file whose required "version"
|
||||
// field was omitted. Distinguish them by the Services config format's
|
||||
// lowercase "services"/"endpoints" keys, which never appear in a raw
|
||||
// ServeConfig: it uses capitalized "Services" and has no "endpoints"
|
||||
// key, and jsonv2 matches case-sensitively. Without this check a
|
||||
// version-less Services config file would parse as an empty
|
||||
// ServeConfig and silently wipe the existing config.
|
||||
var probe struct {
|
||||
Services jsontext.Value `json:"services"`
|
||||
Endpoints jsontext.Value `json:"endpoints"`
|
||||
}
|
||||
if err := jsonv2.Unmarshal(json, &probe); err == nil &&
|
||||
(len(probe.Services) > 0 || len(probe.Endpoints) > 0) {
|
||||
return nil, errors.New(`config file looks like a Services configuration file but is missing the required "version" field`)
|
||||
}
|
||||
// Legacy raw ipn.ServeConfig: parse leniently (like set-raw and
|
||||
// TS_SERVE_CONFIG) so "serve status --json" round-trips keep working.
|
||||
// It is returned wrapped in a ServicesConfigFile with the LegacyVersion
|
||||
// sentinel so the public function signature stays stable.
|
||||
legacy := new(ipn.ServeConfig)
|
||||
if err := jsonv2.Unmarshal(json, legacy); err != nil {
|
||||
return nil, fmt.Errorf("could not parse serve config: %w", err)
|
||||
}
|
||||
return &ServicesConfigFile{Version: LegacyVersion, Legacy: legacy}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported config file version %q", ver.Version)
|
||||
if ver.Version != "0.0.1" {
|
||||
return nil, fmt.Errorf("unsupported config file version %q", ver.Version)
|
||||
}
|
||||
return loadConfigV0(json, forService)
|
||||
}
|
||||
|
||||
func loadConfigV0(json []byte, forService string) (*ServicesConfigFile, error) {
|
||||
@@ -210,8 +280,19 @@ func loadConfigV0(json []byte, forService string) (*ServicesConfigFile, error) {
|
||||
}
|
||||
foundTUN = true
|
||||
} else {
|
||||
if ppr.Ports.Last-ppr.Ports.First != target.DestinationPorts.Last-target.DestinationPorts.First {
|
||||
return nil, fmt.Errorf("service %q: source and destination port ranges must be of equal size", svcName.String())
|
||||
// Unix socket targets (Destination starting with "unix:" on an
|
||||
// HTTP/HTTPS/HTTPSInsecure/TCP/TLSTerminatedTCP inbound protocol)
|
||||
// don't have a destination port range; skip the equality check.
|
||||
isUnixSocket := strings.HasPrefix(target.Destination, "unix:") &&
|
||||
(target.Protocol == ProtoHTTP ||
|
||||
target.Protocol == ProtoHTTPS ||
|
||||
target.Protocol == ProtoHTTPSInsecure ||
|
||||
target.Protocol == ProtoTCP ||
|
||||
target.Protocol == ProtoTLSTerminatedTCP)
|
||||
if !isUnixSocket {
|
||||
if ppr.Ports.Last-ppr.Ports.First != target.DestinationPorts.Last-target.DestinationPorts.First {
|
||||
return nil, fmt.Errorf("service %q: source and destination port ranges must be of equal size", svcName.String())
|
||||
}
|
||||
}
|
||||
foundNonTUN = true
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user