diff --git a/plugins/inputs/radius/README.md b/plugins/inputs/radius/README.md index 6109c1c2e..96fc7d83c 100644 --- a/plugins/inputs/radius/README.md +++ b/plugins/inputs/radius/README.md @@ -23,6 +23,10 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. password = "mypassword" secret = "mysecret" + ## Request source server IP, normally the server running telegraf. + ## This corresponds to Radius' NAS-IP-Address. + # request_ip = "127.0.0.1" + ## Maximum time to receive response. # response_timeout = "5s" ``` diff --git a/plugins/inputs/radius/radius.go b/plugins/inputs/radius/radius.go index ebb148f87..984f31d93 100644 --- a/plugins/inputs/radius/radius.go +++ b/plugins/inputs/radius/radius.go @@ -24,6 +24,7 @@ type Radius struct { Password config.Secret `toml:"password"` Secret config.Secret `toml:"secret"` ResponseTimeout config.Duration `toml:"response_timeout"` + RequestIP string `toml:"request_ip"` Log telegraf.Logger `toml:"-"` client radius.Client } @@ -44,6 +45,13 @@ func (r *Radius) Init() error { Retry: 0, } + if r.RequestIP == "" { + r.RequestIP = "127.0.0.1" + } + if net.ParseIP(r.RequestIP) == nil { + return fmt.Errorf("invalid ip address provided for request_ip: %s", r.RequestIP) + } + return nil } @@ -107,6 +115,12 @@ func (r *Radius) pollServer(acc telegraf.Accumulator, server string) error { return fmt.Errorf("setting password for radius auth failed: %w", err) } + if r.RequestIP != "" { + if err := rfc2865.NASIPAddress_Set(packet, net.ParseIP(r.RequestIP)); err != nil { + return fmt.Errorf("setting NAS IP address for radius auth failed: %w", err) + } + } + // Do the radius request ctx := context.Background() if r.ResponseTimeout > 0 { diff --git a/plugins/inputs/radius/radius_test.go b/plugins/inputs/radius/radius_test.go index 04a20f883..0eb02ea26 100644 --- a/plugins/inputs/radius/radius_test.go +++ b/plugins/inputs/radius/radius_test.go @@ -83,6 +83,87 @@ func TestRadiusLocal(t *testing.T) { } } +func TestRadiusNASIP(t *testing.T) { + handler := func(w radius.ResponseWriter, r *radius.Request) { + username := rfc2865.UserName_GetString(r.Packet) + password := rfc2865.UserPassword_GetString(r.Packet) + ip := rfc2865.NASIPAddress_Get(r.Packet) + + var code radius.Code + if username == "testusername" && password == "testpassword" && + ip.Equal(net.ParseIP("127.0.0.1")) { + code = radius.CodeAccessAccept + } else { + code = radius.CodeAccessReject + } + if err := w.Write(r.Response(code)); err != nil { + require.NoError(t, err, "failed writing radius server response") + } + } + + // Setup a connection to be able to get a random port + conn, err := net.ListenPacket("udp4", "127.0.0.1:0") + require.NoError(t, err) + defer conn.Close() + addr := conn.LocalAddr().String() + host, port, err := net.SplitHostPort(addr) + require.NoError(t, err) + + server := radius.PacketServer{ + Handler: radius.HandlerFunc(handler), + SecretSource: radius.StaticSecretSource([]byte(`testsecret`)), + Addr: addr, + } + + go func() { + if err := server.Serve(conn); err != nil { + if !errors.Is(err, radius.ErrServerShutdown) { + require.NoError(t, err, "local radius server failed") + } + } + }() + + plugin := &Radius{ + Servers: []string{addr}, + Username: config.NewSecret([]byte(`testusername`)), + Password: config.NewSecret([]byte(`testpassword`)), + Secret: config.NewSecret([]byte(`testsecret`)), + Log: testutil.Logger{}, + RequestIP: "127.0.0.1", + } + require.NoError(t, plugin.Init()) + + var acc testutil.Accumulator + require.NoError(t, acc.GatherError(plugin.Gather)) + + if !acc.HasMeasurement("radius") { + t.Errorf("acc.HasMeasurement: expected radius") + } + require.True(t, acc.HasTag("radius", "source")) + require.True(t, acc.HasTag("radius", "source_port")) + require.True(t, acc.HasTag("radius", "response_code")) + require.Equal(t, host, acc.TagValue("radius", "source")) + require.Equal(t, port, acc.TagValue("radius", "source_port")) + require.Equal(t, radius.CodeAccessAccept.String(), acc.TagValue("radius", "response_code")) + require.True(t, acc.HasInt64Field("radius", "responsetime_ms")) + + if err := server.Shutdown(context.Background()); err != nil { + require.NoError(t, err, "failed to properly shutdown local radius server") + } +} + +func TestInvalidRequestIP(t *testing.T) { + plugin := &Radius{ + Servers: []string{"127.0.0.1"}, + Username: config.NewSecret([]byte(`testusername`)), + Password: config.NewSecret([]byte(`testpassword`)), + Secret: config.NewSecret([]byte(`testsecret`)), + Log: testutil.Logger{}, + RequestIP: "foobar", + } + require.Error(t, plugin.Init()) +} + func TestRadiusIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") @@ -202,3 +283,62 @@ func TestRadiusIntegration(t *testing.T) { }) } } + +func TestRadiusIntegrationInvalidSourceIP(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + clients, err := filepath.Abs("testdata/invalidSourceIP/clients.conf") + require.NoError(t, err, "determining absolute path of test-data clients.conf failed") + authorize, err := filepath.Abs("testdata/invalidSourceIP/mods-config/files/authorize") + require.NoError(t, err, "determining absolute path of test-data authorize failed") + radiusd, err := filepath.Abs("testdata/invalidSourceIP/radiusd.conf") + require.NoError(t, err, "determining absolute path of test-data radiusd.conf failed") + + container := testutil.Container{ + Image: "freeradius/freeradius-server", + ExposedPorts: []string{"1812/udp"}, + Files: map[string]string{ + "/etc/raddb/clients.conf": clients, + "/etc/raddb/mods-config/files/authorize": authorize, + "/etc/raddb/radiusd.conf": radiusd, + }, + WaitingFor: wait.ForAll( + wait.ForLog("Ready to process requests"), + ), + } + err = container.Start() + require.NoError(t, err, "failed to start container") + defer container.Terminate() + + port := container.Ports["1812"] + plugin := &Radius{ + ResponseTimeout: config.Duration(time.Second * 1), + Servers: []string{container.Address + ":" + port}, + Username: config.NewSecret([]byte(`testusername`)), + Password: config.NewSecret([]byte(`testpassword`)), + Secret: config.NewSecret([]byte(`testsecret`)), + Log: testutil.Logger{}, + } + + expected := testutil.MustMetric( + "radius", + map[string]string{ + "source": container.Address, + "source_port": port, + "response_code": "timeout", + }, + map[string]interface{}{ + "responsetime_ms": 1000, + }, + time.Time{}, + ) + + var acc testutil.Accumulator + require.NoError(t, plugin.Init()) + require.NoError(t, plugin.Gather(&acc)) + metrics := acc.GetTelegrafMetrics() + require.Len(t, metrics, 1) + testutil.RequireMetricEqual(t, expected, metrics[0], testutil.IgnoreTime()) +} diff --git a/plugins/inputs/radius/sample.conf b/plugins/inputs/radius/sample.conf index 2a8212602..023049085 100644 --- a/plugins/inputs/radius/sample.conf +++ b/plugins/inputs/radius/sample.conf @@ -7,5 +7,9 @@ password = "mypassword" secret = "mysecret" + ## Request source server IP, normally the server running telegraf. + ## This corresponds to Radius' NAS-IP-Address. + # request_ip = "127.0.0.1" + ## Maximum time to receive response. # response_timeout = "5s" diff --git a/plugins/inputs/radius/testdata/invalidSourceIP/clients.conf b/plugins/inputs/radius/testdata/invalidSourceIP/clients.conf new file mode 100644 index 000000000..2ad69e4a2 --- /dev/null +++ b/plugins/inputs/radius/testdata/invalidSourceIP/clients.conf @@ -0,0 +1,4 @@ +client localtest { + ipaddr = 10.123.123.0/24 + secret = testsecret +} diff --git a/plugins/inputs/radius/testdata/invalidSourceIP/mods-config/files/authorize b/plugins/inputs/radius/testdata/invalidSourceIP/mods-config/files/authorize new file mode 100644 index 000000000..384abb308 --- /dev/null +++ b/plugins/inputs/radius/testdata/invalidSourceIP/mods-config/files/authorize @@ -0,0 +1 @@ +testusername Cleartext-Password := "testpassword" diff --git a/plugins/inputs/radius/testdata/invalidSourceIP/radiusd.conf b/plugins/inputs/radius/testdata/invalidSourceIP/radiusd.conf new file mode 100644 index 000000000..f761535ca --- /dev/null +++ b/plugins/inputs/radius/testdata/invalidSourceIP/radiusd.conf @@ -0,0 +1,118 @@ +prefix = /usr +exec_prefix = /usr +sysconfdir = /etc +localstatedir = /var +sbindir = ${exec_prefix}/sbin +logdir = /var/log/freeradius +raddbdir = /etc/freeradius +radacctdir = ${logdir}/radacct + +name = freeradius + +confdir = ${raddbdir} +modconfdir = ${confdir}/mods-config +certdir = ${confdir}/certs +cadir = ${confdir}/certs +run_dir = ${localstatedir}/run/${name} + +db_dir = ${raddbdir} + +libdir = /usr/lib/freeradius + +pidfile = ${run_dir}/${name}.pid + + +max_request_time = 30 + +cleanup_delay = 5 + +max_requests = 16384 + +hostname_lookups = no + + +log { + destination = stdout + + colourise = yes + + file = ${logdir}/radius.log + + syslog_facility = daemon + + stripped_names = no + + auth = yes + + + + auth_badpass = yes + auth_goodpass = yes + + + msg_denied = "You are already logged in - access denied" + +} + +checkrad = ${sbindir}/checkrad + +ENV { + + +} + +security { + + user = freerad + group = freerad + + allow_core_dumps = no + + max_attributes = 200 + + reject_delay = 1 + + status_server = yes + + +} + +proxy_requests = yes +$INCLUDE proxy.conf + + + +$INCLUDE clients.conf + + +thread pool { + start_servers = 5 + + max_servers = 32 + + min_spare_servers = 3 + max_spare_servers = 10 + + + max_requests_per_server = 0 + + + auto_limit_acct = no +} + + +modules { + + + $INCLUDE mods-enabled/ +} + +instantiate { + +} + +policy { + $INCLUDE policy.d/ +} + +$INCLUDE sites-enabled/