mirror of
https://github.com/fumiama/terasu-cloudflared.git
synced 2026-06-05 00:50:24 +08:00
During a refresh of the supported features via the DNS TXT record, cloudflared would update the internal feature list, but would not propagate this information to the edge during a new connection. This meant that a situation could occur in which cloudflared would think that the client's connection could support datagram V3, and would setup that muxer locally, but would not propagate that information to the edge during a register connection in the `ClientInfo` of the `ConnectionOptions`. This meant that the edge still thought that the client was setup to support datagram V2 and since the protocols are not backwards compatible, the local muxer for datagram V3 would reject the incoming RPC calls. To address this, the feature list will be fetched only once during client bootstrapping and will persist as-is until the client is restarted. This helps reduce the complexity involved with different connections having possibly different sets of features when connecting to the edge. The features will now be tied to the client and never diverge across connections. Also, retires the use of `support_datagram_v3` in-favor of `support_datagram_v3_1` to reduce the risk of reusing the feature key. The `dv3` TXT feature key is also deprecated. Closes TUN-9291
504 lines
15 KiB
Go
504 lines
15 KiB
Go
package tunnel
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
"github.com/urfave/cli/v2"
|
|
"github.com/urfave/cli/v2/altsrc"
|
|
"golang.org/x/term"
|
|
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
|
"github.com/cloudflare/cloudflared/config"
|
|
"github.com/cloudflare/cloudflared/connection"
|
|
"github.com/cloudflare/cloudflared/edgediscovery"
|
|
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
|
"github.com/cloudflare/cloudflared/features"
|
|
"github.com/cloudflare/cloudflared/ingress"
|
|
"github.com/cloudflare/cloudflared/orchestration"
|
|
"github.com/cloudflare/cloudflared/supervisor"
|
|
"github.com/cloudflare/cloudflared/tlsconfig"
|
|
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
|
)
|
|
|
|
const (
|
|
secretValue = "*****"
|
|
icmpFunnelTimeout = time.Second * 10
|
|
fedRampRegion = "fed" // const string denoting the region used to connect to FEDRamp servers
|
|
)
|
|
|
|
var (
|
|
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
|
|
|
configFlags = []string{
|
|
flags.AutoUpdateFreq,
|
|
flags.NoAutoUpdate,
|
|
flags.Retries,
|
|
flags.Protocol,
|
|
flags.LogLevel,
|
|
flags.TransportLogLevel,
|
|
flags.OriginCert,
|
|
flags.Metrics,
|
|
flags.MetricsUpdateFreq,
|
|
flags.EdgeIpVersion,
|
|
flags.EdgeBindAddress,
|
|
flags.MaxActiveFlows,
|
|
}
|
|
)
|
|
|
|
func logClientOptions(c *cli.Context, log *zerolog.Logger) {
|
|
flags := make(map[string]interface{})
|
|
for _, flag := range c.FlagNames() {
|
|
if isSecretFlag(flag) {
|
|
flags[flag] = secretValue
|
|
} else {
|
|
flags[flag] = c.Generic(flag)
|
|
}
|
|
}
|
|
|
|
if len(flags) > 0 {
|
|
log.Info().Msgf("Settings: %v", flags)
|
|
}
|
|
|
|
envs := make(map[string]string)
|
|
// Find env variables for Argo Tunnel
|
|
for _, env := range os.Environ() {
|
|
// All Argo Tunnel env variables start with TUNNEL_
|
|
if strings.Contains(env, "TUNNEL_") {
|
|
vars := strings.Split(env, "=")
|
|
if len(vars) == 2 {
|
|
if isSecretEnvVar(vars[0]) {
|
|
envs[vars[0]] = secretValue
|
|
} else {
|
|
envs[vars[0]] = vars[1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(envs) > 0 {
|
|
log.Info().Msgf("Environmental variables %v", envs)
|
|
}
|
|
}
|
|
|
|
func isSecretFlag(key string) bool {
|
|
for _, flag := range secretFlags {
|
|
if flag.Name == key {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isSecretEnvVar(key string) bool {
|
|
for _, flag := range secretFlags {
|
|
for _, secretEnvVar := range flag.EnvVars {
|
|
if secretEnvVar == key {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
|
|
return c.IsSet(flags.ProxyDns) &&
|
|
!(c.IsSet(flags.Name) || // adhoc-named tunnel
|
|
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
|
|
namedTunnel != nil) // named tunnel
|
|
}
|
|
|
|
func prepareTunnelConfig(
|
|
ctx context.Context,
|
|
c *cli.Context,
|
|
info *cliutil.BuildInfo,
|
|
log, logTransport *zerolog.Logger,
|
|
observer *connection.Observer,
|
|
namedTunnel *connection.TunnelProperties,
|
|
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
|
|
clientID, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "can't generate connector UUID")
|
|
}
|
|
log.Info().Msgf("Generated Connector ID: %s", clientID)
|
|
tags, err := NewTagSliceFromCLI(c.StringSlice(flags.Tag))
|
|
if err != nil {
|
|
log.Err(err).Msg("Tag parse failure")
|
|
return nil, nil, errors.Wrap(err, "Tag parse failure")
|
|
}
|
|
tags = append(tags, pogs.Tag{Name: "ID", Value: clientID.String()})
|
|
|
|
transportProtocol := c.String(flags.Protocol)
|
|
isPostQuantumEnforced := c.Bool(flags.PostQuantum)
|
|
|
|
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), c.Bool(flags.PostQuantum), log)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
|
|
}
|
|
clientFeatures := featureSelector.ClientFeatures()
|
|
pqMode := featureSelector.PostQuantumMode()
|
|
if pqMode == features.PostQuantumStrict {
|
|
// Error if the user tries to force a non-quic transport protocol
|
|
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
|
|
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
|
|
}
|
|
transportProtocol = connection.QUIC.String()
|
|
}
|
|
|
|
namedTunnel.Client = pogs.ClientInfo{
|
|
ClientID: clientID[:],
|
|
Features: clientFeatures,
|
|
Version: info.Version(),
|
|
Arch: info.OSArch(),
|
|
}
|
|
cfg := config.GetConfiguration()
|
|
ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), isPostQuantumEnforced, edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
|
|
|
|
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
|
for _, p := range connection.ProtocolList {
|
|
tlsSettings := p.TLSSettings()
|
|
if tlsSettings == nil {
|
|
return nil, nil, fmt.Errorf("%s has unknown TLS settings", p)
|
|
}
|
|
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
|
}
|
|
if len(tlsSettings.NextProtos) > 0 {
|
|
edgeTLSConfig.NextProtos = tlsSettings.NextProtos
|
|
}
|
|
edgeTLSConfigs[p] = edgeTLSConfig
|
|
}
|
|
|
|
gracePeriod, err := gracePeriod(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
edgeIPVersion, err := parseConfigIPVersion(c.String(flags.EdgeIpVersion))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
edgeBindAddr, err := parseConfigBindAddress(c.String(flags.EdgeBindAddress))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := testIPBindable(edgeBindAddr); err != nil {
|
|
return nil, nil, fmt.Errorf("invalid edge-bind-address %s: %v", edgeBindAddr, err)
|
|
}
|
|
edgeIPVersion, err = adjustIPVersionByBindAddress(edgeIPVersion, edgeBindAddr)
|
|
if err != nil {
|
|
// This is not a fatal error, we just overrode edgeIPVersion
|
|
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
|
|
}
|
|
|
|
region := c.String(flags.Region)
|
|
endpoint := namedTunnel.Credentials.Endpoint
|
|
var resolvedRegion string
|
|
// set resolvedRegion to either the region passed as argument
|
|
// or to the endpoint in the credentials.
|
|
// Region and endpoint are interchangeable
|
|
if region != "" && endpoint != "" {
|
|
return nil, nil, fmt.Errorf("region provided with a token that has an endpoint")
|
|
} else if region != "" {
|
|
resolvedRegion = region
|
|
} else if endpoint != "" {
|
|
resolvedRegion = endpoint
|
|
}
|
|
|
|
tunnelConfig := &supervisor.TunnelConfig{
|
|
GracePeriod: gracePeriod,
|
|
ReplaceExisting: c.Bool(flags.Force),
|
|
OSArch: info.OSArch(),
|
|
ClientID: clientID.String(),
|
|
EdgeAddrs: c.StringSlice(flags.Edge),
|
|
Region: resolvedRegion,
|
|
EdgeIPVersion: edgeIPVersion,
|
|
EdgeBindAddr: edgeBindAddr,
|
|
HAConnections: c.Int(flags.HaConnections),
|
|
IsAutoupdated: c.Bool(flags.IsAutoUpdated),
|
|
LBPool: c.String(flags.LBPool),
|
|
Tags: tags,
|
|
Log: log,
|
|
LogTransport: logTransport,
|
|
Observer: observer,
|
|
ReportedVersion: info.Version(),
|
|
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
|
Retries: uint(c.Int(flags.Retries)), // nolint: gosec
|
|
RunFromTerminal: isRunningFromTerminal(),
|
|
NamedTunnel: namedTunnel,
|
|
ProtocolSelector: protocolSelector,
|
|
EdgeTLSConfigs: edgeTLSConfigs,
|
|
FeatureSelector: featureSelector,
|
|
MaxEdgeAddrRetries: uint8(c.Int(flags.MaxEdgeAddrRetries)), // nolint: gosec
|
|
RPCTimeout: c.Duration(flags.RpcTimeout),
|
|
WriteStreamTimeout: c.Duration(flags.WriteStreamTimeout),
|
|
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
|
|
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
|
|
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
|
|
}
|
|
icmpRouter, err := newICMPRouter(c, log)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
|
|
} else {
|
|
tunnelConfig.ICMPRouterServer = icmpRouter
|
|
}
|
|
orchestratorConfig := &orchestration.Config{
|
|
Ingress: &ingressRules,
|
|
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
|
ConfigurationFlags: parseConfigFlags(c),
|
|
WriteTimeout: tunnelConfig.WriteStreamTimeout,
|
|
}
|
|
return tunnelConfig, orchestratorConfig, nil
|
|
}
|
|
|
|
func parseConfigFlags(c *cli.Context) map[string]string {
|
|
result := make(map[string]string)
|
|
|
|
for _, flag := range configFlags {
|
|
if v := c.String(flag); c.IsSet(flag) && v != "" {
|
|
result[flag] = v
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func gracePeriod(c *cli.Context) (time.Duration, error) {
|
|
period := c.Duration(flags.GracePeriod)
|
|
if period > connection.MaxGracePeriod {
|
|
return time.Duration(0), fmt.Errorf("%s must be equal or less than %v", flags.GracePeriod, connection.MaxGracePeriod)
|
|
}
|
|
return period, nil
|
|
}
|
|
|
|
func isRunningFromTerminal() bool {
|
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
|
}
|
|
|
|
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
|
func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err error) {
|
|
switch version {
|
|
case "4":
|
|
v = allregions.IPv4Only
|
|
case "6":
|
|
v = allregions.IPv6Only
|
|
case "auto":
|
|
v = allregions.Auto
|
|
default: // unspecified or invalid
|
|
err = fmt.Errorf("invalid value for edge-ip-version: %s", version)
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseConfigBindAddress(ipstr string) (net.IP, error) {
|
|
// Unspecified - it's fine
|
|
if ipstr == "" {
|
|
return nil, nil
|
|
}
|
|
ip := net.ParseIP(ipstr)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("invalid value for edge-bind-address: %s", ipstr)
|
|
}
|
|
return ip, nil
|
|
}
|
|
|
|
func testIPBindable(ip net.IP) error {
|
|
// "Unspecified" = let OS choose, so always bindable
|
|
if ip == nil {
|
|
return nil
|
|
}
|
|
|
|
addr := &net.UDPAddr{IP: ip, Port: 0}
|
|
listener, err := net.ListenUDP("udp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
listener.Close()
|
|
return nil
|
|
}
|
|
|
|
func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.IP) (allregions.ConfigIPVersion, error) {
|
|
if ip == nil {
|
|
return ipVersion, nil
|
|
}
|
|
// https://pkg.go.dev/net#IP.To4: "If ip is not an IPv4 address, To4 returns nil."
|
|
if ip.To4() != nil {
|
|
if ipVersion == allregions.IPv6Only {
|
|
return allregions.IPv4Only, fmt.Errorf("IPv4 bind address is specified, but edge-ip-version is IPv6")
|
|
}
|
|
return allregions.IPv4Only, nil
|
|
} else {
|
|
if ipVersion == allregions.IPv4Only {
|
|
return allregions.IPv6Only, fmt.Errorf("IPv6 bind address is specified, but edge-ip-version is IPv4")
|
|
}
|
|
return allregions.IPv6Only, nil
|
|
}
|
|
}
|
|
|
|
func newICMPRouter(c *cli.Context, logger *zerolog.Logger) (ingress.ICMPRouterServer, error) {
|
|
ipv4Src, ipv6Src, err := determineICMPSources(c, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, logger, icmpFunnelTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return icmpRouter, nil
|
|
}
|
|
|
|
func determineICMPSources(c *cli.Context, logger *zerolog.Logger) (netip.Addr, netip.Addr, error) {
|
|
ipv4Src, err := determineICMPv4Src(c.String(flags.ICMPV4Src), logger)
|
|
if err != nil {
|
|
return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy")
|
|
}
|
|
|
|
logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src)
|
|
|
|
ipv6Src, zone, err := determineICMPv6Src(c.String(flags.ICMPV6Src), logger, ipv4Src)
|
|
if err != nil {
|
|
return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy")
|
|
}
|
|
|
|
if zone != "" {
|
|
logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone)
|
|
} else {
|
|
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
|
|
}
|
|
|
|
return ipv4Src, ipv6Src, nil
|
|
}
|
|
|
|
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
|
|
if userDefinedSrc != "" {
|
|
addr, err := netip.ParseAddr(userDefinedSrc)
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
if addr.Is4() {
|
|
return addr, nil
|
|
}
|
|
return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc)
|
|
}
|
|
|
|
addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53)
|
|
if err != nil {
|
|
addr = netip.IPv4Unspecified()
|
|
logger.Debug().Err(err).Msgf("Failed to determine the IPv4 for this machine. It will use %s to send/listen for ICMPv4 echo", addr)
|
|
}
|
|
return addr, nil
|
|
}
|
|
|
|
type interfaceIP struct {
|
|
name string
|
|
ip net.IP
|
|
}
|
|
|
|
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
|
|
if userDefinedSrc != "" {
|
|
addr, err := netip.ParseAddr(userDefinedSrc)
|
|
if err != nil {
|
|
return netip.Addr{}, "", err
|
|
}
|
|
if addr.Is6() {
|
|
return addr, addr.Zone(), nil
|
|
}
|
|
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
|
|
}
|
|
|
|
// Loop through all the interfaces, the preference is
|
|
// 1. The interface where ipv4Src is in
|
|
// 2. Interface with IPv6 address
|
|
// 3. Unspecified interface
|
|
|
|
interfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return netip.IPv6Unspecified(), "", nil
|
|
}
|
|
|
|
interfacesWithIPv6 := make([]interfaceIP, 0)
|
|
for _, interf := range interfaces {
|
|
interfaceAddrs, err := interf.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
foundIPv4SrcInterface := false
|
|
for _, interfaceAddr := range interfaceAddrs {
|
|
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
|
|
ip := ipnet.IP
|
|
if ip.Equal(ipv4Src.AsSlice()) {
|
|
foundIPv4SrcInterface = true
|
|
}
|
|
if ip.To4() == nil {
|
|
interfacesWithIPv6 = append(interfacesWithIPv6, interfaceIP{
|
|
name: interf.Name,
|
|
ip: ip,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
// Found the interface of ipv4Src. Loop through the addresses to see if there is an IPv6
|
|
if foundIPv4SrcInterface {
|
|
for _, interfaceAddr := range interfaceAddrs {
|
|
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
|
|
ip := ipnet.IP
|
|
if ip.To4() == nil {
|
|
addr, err := netip.ParseAddr(ip.String())
|
|
if err == nil {
|
|
return addr, interf.Name, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, interf := range interfacesWithIPv6 {
|
|
addr, err := netip.ParseAddr(interf.ip.String())
|
|
if err == nil {
|
|
return addr, interf.name, nil
|
|
}
|
|
}
|
|
logger.Debug().Err(err).Msgf("Failed to determine the IPv6 for this machine. It will use %s to send/listen for ICMPv6 echo", netip.IPv6Unspecified())
|
|
|
|
return netip.IPv6Unspecified(), "", nil
|
|
}
|
|
|
|
// FindLocalAddr tries to dial UDP and returns the local address picked by the OS
|
|
func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
|
|
udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
|
|
IP: dst,
|
|
Port: port,
|
|
})
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
defer udpConn.Close()
|
|
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
localAddr := localAddrPort.Addr()
|
|
return localAddr, nil
|
|
}
|