Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ const (
ServerStop ServerControlValue = iota // 0
ServerStart ServerControlValue = iota // 1
ServerRestart ServerControlValue = iota // 2

linkLocalAddressWaitTimeout = 30 * time.Second
linkLocalAddressRetryDelay = time.Second
)

var (
interfaceByName = net.InterfaceByName
interfaceAddresses = func(iface *net.Interface) ([]net.Addr, error) { return iface.Addrs() }
resolveIPv4LinkLocalAddress = ipv4LinkLocalAddress
)

type TelemetryConfig struct {
Expand All @@ -52,6 +61,7 @@ type TelemetryConfig struct {
ZmqPort *string
Insecure *bool
NoTLS *bool
NoTLSLinkLocalInterface *string
AllowNoClientCert *bool
JwtRefInt *uint64
JwtValInt *uint64
Expand Down Expand Up @@ -86,7 +96,7 @@ type TelemetryConfig struct {
func main() {
err := runTelemetry(os.Args)
if err != nil {
log.Errorf("Unable to setup telemetry config due to err: %v", err)
log.Fatalf("Unable to setup telemetry config due to err: %v", err)
}
}

Expand Down Expand Up @@ -181,6 +191,7 @@ func setupFlags(fs *flag.FlagSet) (*TelemetryConfig, *gnmi.Config, error) {
ZmqPort: fs.String("zmq_port", "", "Orchagent ZMQ port, when not set or empty string telemetry server will switch to Redis based communication channel."),
Insecure: fs.Bool("insecure", false, "Skip providing TLS cert and key, for testing only!"),
NoTLS: fs.Bool("noTLS", false, "disable TLS, for testing only!"),
NoTLSLinkLocalInterface: fs.String("no_tls_link_local_interface", "", "Bind --noTLS to the only IPv4 link-local address on this interface."),
AllowNoClientCert: fs.Bool("allow_no_client_auth", false, "When set, telemetry server will request but not require a client certificate."),
Comment on lines 192 to 195
JwtRefInt: fs.Uint64("jwt_refresh_int", 900, "Seconds before JWT expiry the token can be refreshed."),
JwtValInt: fs.Uint64("jwt_valid_int", 3600, "Seconds that JWT token is valid for."),
Expand Down Expand Up @@ -252,12 +263,30 @@ func setupFlags(fs *flag.FlagSet) (*TelemetryConfig, *gnmi.Config, error) {
log.Infof("Log level must be greater than 0, setting to default value of 2")
}

if *telemetryCfg.NoTLSLinkLocalInterface != "" {
if !*telemetryCfg.NoTLS {
return nil, nil, fmt.Errorf("--no_tls_link_local_interface requires --noTLS")
}
if *telemetryCfg.BindAddress != "" {
return nil, nil, fmt.Errorf("--no_tls_link_local_interface cannot be used with --bind_address")
}
if *telemetryCfg.GnmiVrf != "" && *telemetryCfg.GnmiVrf != "default" {
return nil, nil, fmt.Errorf("--no_tls_link_local_interface cannot be used with a non-default --gnmi_vrf")
}
}

if *telemetryCfg.NoTLS {
ip := net.ParseIP(*telemetryCfg.BindAddress)
if ip == nil || !ip.IsLoopback() {
return nil, nil, fmt.Errorf(
"--noTLS requires --bind_address to be a loopback address (e.g. 127.0.0.1 or ::1) " +
"to prevent cleartext gRPC exposure over the network")
if *telemetryCfg.NoTLSLinkLocalInterface != "" {
bindAddress, err := waitForIPv4LinkLocalAddress(*telemetryCfg.NoTLSLinkLocalInterface, linkLocalAddressWaitTimeout, linkLocalAddressRetryDelay)
if err != nil {
return nil, nil, err
}
*telemetryCfg.BindAddress = bindAddress
} else {
ip := net.ParseIP(*telemetryCfg.BindAddress)
if ip == nil || !ip.IsLoopback() {
return nil, nil, fmt.Errorf("--noTLS requires --bind_address to be a loopback address (e.g. 127.0.0.1 or ::1)")
}
}
}

Expand Down Expand Up @@ -340,6 +369,49 @@ func setupFlags(fs *flag.FlagSet) (*TelemetryConfig, *gnmi.Config, error) {
return telemetryCfg, cfg, nil
}

func ipv4LinkLocalAddress(interfaceName string) (string, error) {
iface, err := interfaceByName(interfaceName)
if err != nil {
return "", fmt.Errorf("cannot find link-local interface %q: %w", interfaceName, err)
}

addresses, err := interfaceAddresses(iface)
if err != nil {
return "", fmt.Errorf("cannot read addresses for link-local interface %q: %w", interfaceName, err)
}

return selectIPv4LinkLocalAddress(interfaceName, addresses)
}

func waitForIPv4LinkLocalAddress(interfaceName string, timeout, retryDelay time.Duration) (string, error) {
deadline := time.Now().Add(timeout)
for {
address, err := resolveIPv4LinkLocalAddress(interfaceName)
if err == nil {
return address, nil
}
if !time.Now().Before(deadline) {
return "", fmt.Errorf("timed out waiting for IPv4 link-local address on interface %q: %w", interfaceName, err)
}
time.Sleep(retryDelay)
}
}

func selectIPv4LinkLocalAddress(interfaceName string, addresses []net.Addr) (string, error) {
var linkLocalAddresses []string
for _, address := range addresses {
ip, _, err := net.ParseCIDR(address.String())
if err == nil && ip.To4() != nil && ip.IsLinkLocalUnicast() {
linkLocalAddresses = append(linkLocalAddresses, ip.String())
}
}

if len(linkLocalAddresses) != 1 {
return "", fmt.Errorf("link-local interface %q must have exactly one IPv4 link-local address; found %d", interfaceName, len(linkLocalAddresses))
}
return linkLocalAddresses[0], nil
}

func isFlagPassed(fs *flag.FlagSet, name string) bool {
found := false
fs.Visit(func(f *flag.Flag) {
Expand Down
127 changes: 125 additions & 2 deletions telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"io/ioutil"
"math/big"
"net"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -1472,8 +1473,7 @@ func TestCertAuthDisabledWhenNoCaCert(t *testing.T) {
}
}

func TestNoTLSRequiresLoopbackAddress(t *testing.T) {
// --noTLS must be rejected unless --bind_address is a loopback address.
func TestNoTLSBindAddress(t *testing.T) {
originalArgs := os.Args
defer func() { os.Args = originalArgs }()

Expand All @@ -1487,6 +1487,11 @@ func TestNoTLSRequiresLoopbackAddress(t *testing.T) {
{"loopback ipv4", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "127.0.0.1"}, false},
{"loopback ipv4 alt", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "127.0.0.2"}, false},
{"loopback ipv6", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "::1"}, false},
{"link-local bind address", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "169.254.200.1"}, true},
{"loopback with non-default vrf", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "127.0.0.1", "-gnmi_vrf", "mgmt"}, false},
{"interface and bind address", []string{"cmd", "-port", "8080", "-noTLS", "-bind_address", "127.0.0.1", "-no_tls_link_local_interface", "lo"}, true},
{"interface without noTLS", []string{"cmd", "-port", "8080", "-insecure", "-no_tls_link_local_interface", "lo"}, true},
{"interface with non-default vrf", []string{"cmd", "-port", "8080", "-noTLS", "-no_tls_link_local_interface", "lo", "-gnmi_vrf", "mgmt"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -1503,6 +1508,124 @@ func TestNoTLSRequiresLoopbackAddress(t *testing.T) {
}
}

func TestNoTLSLinkLocalInterface(t *testing.T) {
originalResolver := resolveIPv4LinkLocalAddress
defer func() { resolveIPv4LinkLocalAddress = originalResolver }()
resolveIPv4LinkLocalAddress = func(interfaceName string) (string, error) {
if interfaceName != "eth0-midplane" {
t.Fatalf("unexpected interface %q", interfaceName)
}
return "169.254.200.1", nil
}

originalArgs := os.Args
defer func() { os.Args = originalArgs }()
os.Args = []string{"cmd", "-port", "8080", "-noTLS", "-no_tls_link_local_interface", "eth0-midplane"}
telemetryCfg, cfg, err := setupFlags(flag.NewFlagSet("test", flag.ContinueOnError))
if err != nil {
t.Fatal(err)
}
if got := *telemetryCfg.BindAddress; got != "169.254.200.1" {
t.Fatalf("telemetry bind address = %q, want 169.254.200.1", got)
}
if got := cfg.BindAddress; got != "169.254.200.1" {
t.Fatalf("gNMI bind address = %q, want 169.254.200.1", got)
}
}

func TestWaitForIPv4LinkLocalAddress(t *testing.T) {
originalResolver := resolveIPv4LinkLocalAddress
defer func() { resolveIPv4LinkLocalAddress = originalResolver }()

calls := 0
resolveIPv4LinkLocalAddress = func(interfaceName string) (string, error) {
calls++
if calls == 1 {
return "", errors.New("address not ready")
}
return "169.254.200.1", nil
}
address, err := waitForIPv4LinkLocalAddress("eth0-midplane", time.Second, 0)
if err != nil {
t.Fatal(err)
}
if address != "169.254.200.1" || calls != 2 {
t.Fatalf("address = %q, calls = %d; want 169.254.200.1 after two calls", address, calls)
}

resolveIPv4LinkLocalAddress = func(interfaceName string) (string, error) {
return "", errors.New("address not ready")
}
if _, err := waitForIPv4LinkLocalAddress("eth0-midplane", 0, 0); err == nil {
t.Fatal("expected timeout error, got nil")
}
}

func TestIPv4LinkLocalAddress(t *testing.T) {
originalInterfaceByName := interfaceByName
originalInterfaceAddresses := interfaceAddresses
defer func() {
interfaceByName = originalInterfaceByName
interfaceAddresses = originalInterfaceAddresses
}()

interfaceByName = func(name string) (*net.Interface, error) {
return nil, errors.New("not found")
}
if _, err := ipv4LinkLocalAddress("eth0-midplane"); err == nil {
t.Fatal("expected interface lookup error, got nil")
}

interfaceByName = func(name string) (*net.Interface, error) {
return &net.Interface{Name: name}, nil
}
interfaceAddresses = func(iface *net.Interface) ([]net.Addr, error) {
return nil, errors.New("addresses unavailable")
}
if _, err := ipv4LinkLocalAddress("eth0-midplane"); err == nil {
t.Fatal("expected address lookup error, got nil")
}

interfaceAddresses = func(iface *net.Interface) ([]net.Addr, error) {
return []net.Addr{&net.IPNet{IP: net.ParseIP("169.254.200.1"), Mask: net.CIDRMask(24, 32)}}, nil
}
address, err := ipv4LinkLocalAddress("eth0-midplane")
if err != nil {
t.Fatal(err)
}
if address != "169.254.200.1" {
t.Fatalf("address = %q, want 169.254.200.1", address)
}
}

func TestSelectIPv4LinkLocalAddress(t *testing.T) {
tests := []struct {
name string
addresses []net.Addr
want string
wantErr bool
}{
{"one link-local", []net.Addr{&net.IPNet{IP: net.ParseIP("10.0.0.1"), Mask: net.CIDRMask(24, 32)}, &net.IPNet{IP: net.ParseIP("169.254.200.1"), Mask: net.CIDRMask(24, 32)}, &net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)}}, "169.254.200.1", false},
{"no link-local", []net.Addr{&net.IPNet{IP: net.ParseIP("10.0.0.1"), Mask: net.CIDRMask(24, 32)}}, "", true},
{"two link-local", []net.Addr{&net.IPNet{IP: net.ParseIP("169.254.200.1"), Mask: net.CIDRMask(24, 32)}, &net.IPNet{IP: net.ParseIP("169.254.200.2"), Mask: net.CIDRMask(24, 32)}}, "", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := selectIPv4LinkLocalAddress("eth0-midplane", tt.addresses)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Fatalf("address = %q, want %q", got, tt.want)
}
})
}
}

func TestMain(m *testing.M) {
defer test_utils.MemLeakCheck()
m.Run()
Expand Down
Loading