1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,75 @@
# Tacacs Input Plugin
The Tacacs plugin collects successful tacacs authentication response times
from tacacs servers such as Aruba ClearPass, FreeRADIUS or tac_plus (TACACS+).
It is primarily meant to monitor how long it takes for the server to fully
handle an auth request, including all potential dependent calls (for example
to AD servers, or other sources of truth for auth the tacacs server uses).
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Tacacs plugin collects successful tacacs authentication response times.
[[inputs.tacacs]]
## An array of Server IPs (or hostnames) and ports to gather from. If none specified, defaults to localhost.
# servers = ["127.0.0.1:49"]
## Request source server IP, normally the server running telegraf.
# request_ip = "127.0.0.1"
## Credentials for tacacs authentication.
username = "myuser"
password = "mypassword"
secret = "mysecret"
## Maximum time to receive response.
# response_timeout = "5s"
```
## Metrics
- tacacs
- tags:
- source
- fields:
- response_status (string, [see below](#field-response_status)))
- responsetime_ms (int64 [see below](#field-responsetime_ms)))
### field `response_status`
The field "response_status" is either a translated raw code returned
by the tacacs server, or filled by telegraf in case of a timeout.
| Field Value | Raw Code | From | responsetime_ms
| -------------------- | ------------ | ------------- | ---------------
| AuthenStatusPass | 1 (0x1) | tacacs server | real value
| AuthenStatusFail | 2 (0x2) | tacacs server | real value
| AuthenStatusGetData | 3 (0x3) | tacacs server | real value
| AuthenStatusGetUser | 4 (0x4) | tacacs server | real value
| AuthenStatusGetPass | 5 (0x5) | tacacs server | real value
| AuthenStatusRestart | 6 (0x6) | tacacs server | real value
| AuthenStatusError | 7 (0x7) | tacacs server | real value
| AuthenStatusFollow | 33 (0x21) | tacacs server | real value
| Timeout | Timeout | telegraf | eq. to response_timeout
### field `responsetime_ms`
The field responsetime_ms is response time of the tacacs server
in milliseconds of the furthest achieved stage of auth.
In case of timeout, its filled by telegraf to be the value of
the configured response_timeout.
## Example Output
```text
tacacs,source=127.0.0.1:49 responsetime_ms=311i,response_status="AuthenStatusPass" 1677526200000000000
```

View file

@ -0,0 +1,15 @@
# Tacacs plugin collects successful tacacs authentication response times.
[[inputs.tacacs]]
## An array of Server IPs (or hostnames) and ports to gather from. If none specified, defaults to localhost.
# servers = ["127.0.0.1:49"]
## Request source server IP, normally the server running telegraf.
# request_ip = "127.0.0.1"
## Credentials for tacacs authentication.
username = "myuser"
password = "mypassword"
secret = "mysecret"
## Maximum time to receive response.
# response_timeout = "5s"

View file

@ -0,0 +1,209 @@
//go:generate ../../../tools/readme_config_includer/generator
package tacacs
import (
"context"
_ "embed"
"errors"
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
"github.com/nwaples/tacplus"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
type Tacacs struct {
Servers []string `toml:"servers"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Secret config.Secret `toml:"secret"`
RequestAddr string `toml:"request_ip"`
ResponseTimeout config.Duration `toml:"response_timeout"`
Log telegraf.Logger `toml:"-"`
clients []tacplus.Client
authStart tacplus.AuthenStart
}
func (*Tacacs) SampleConfig() string {
return sampleConfig
}
func (t *Tacacs) Init() error {
if len(t.Servers) == 0 {
t.Servers = []string{"127.0.0.1:49"}
}
if t.Username.Empty() || t.Password.Empty() || t.Secret.Empty() {
return errors.New("empty credentials were provided (username, password or secret)")
}
if t.RequestAddr == "" {
t.RequestAddr = "127.0.0.1"
}
if net.ParseIP(t.RequestAddr) == nil {
return fmt.Errorf("invalid ip address provided for request_ip: %s", t.RequestAddr)
}
t.clients = make([]tacplus.Client, 0, len(t.Servers))
for _, server := range t.Servers {
t.clients = append(t.clients, tacplus.Client{
Addr: server,
ConnConfig: tacplus.ConnConfig{},
})
}
t.authStart = tacplus.AuthenStart{
Action: tacplus.AuthenActionLogin,
AuthenType: tacplus.AuthenTypeASCII,
AuthenService: tacplus.AuthenServiceLogin,
PrivLvl: 1,
Port: "heartbeat",
RemAddr: t.RequestAddr,
}
return nil
}
func (t *Tacacs) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
for idx := range t.clients {
wg.Add(1)
go func(client *tacplus.Client) {
defer wg.Done()
acc.AddError(t.pollServer(acc, client))
}(&t.clients[idx])
}
wg.Wait()
return nil
}
func authenReplyToString(code uint8) string {
switch code {
case tacplus.AuthenStatusPass:
return `AuthenStatusPass`
case tacplus.AuthenStatusFail:
return `AuthenStatusFail`
case tacplus.AuthenStatusGetData:
return `AuthenStatusGetData`
case tacplus.AuthenStatusGetUser:
return `AuthenStatusGetUser`
case tacplus.AuthenStatusGetPass:
return `AuthenStatusGetPass`
case tacplus.AuthenStatusRestart:
return `AuthenStatusRestart`
case tacplus.AuthenStatusError:
return `AuthenStatusError`
case tacplus.AuthenStatusFollow:
return `AuthenStatusFollow`
}
return "AuthenStatusUnknown(" + strconv.FormatUint(uint64(code), 10) + ")"
}
func (t *Tacacs) pollServer(acc telegraf.Accumulator, client *tacplus.Client) error {
// Create the fields for this metric
tags := map[string]string{"source": client.Addr}
fields := make(map[string]interface{})
secret, err := t.Secret.Get()
if err != nil {
return fmt.Errorf("getting secret failed: %w", err)
}
defer secret.Destroy()
client.ConnConfig.Secret = secret.Bytes()
username, err := t.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
defer username.Destroy()
password, err := t.Password.Get()
if err != nil {
return fmt.Errorf("getting password failed: %w", err)
}
defer password.Destroy()
ctx := context.Background()
if t.ResponseTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(t.ResponseTimeout))
defer cancel()
}
startTime := time.Now()
reply, session, err := client.SendAuthenStart(ctx, &t.authStart)
if err != nil {
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, os.ErrDeadlineExceeded) {
return fmt.Errorf("error on new tacacs authentication start request to %s : %w", client.Addr, err)
}
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = "Timeout"
acc.AddFields("tacacs", fields, tags)
return nil
}
defer session.Close()
if reply.Status != tacplus.AuthenStatusGetUser {
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = authenReplyToString(reply.Status)
acc.AddFields("tacacs", fields, tags)
return nil
}
reply, err = session.Continue(ctx, username.String())
if err != nil {
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, os.ErrDeadlineExceeded) {
return fmt.Errorf("error on tacacs authentication continue username request to %s : %w", client.Addr, err)
}
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = "Timeout"
acc.AddFields("tacacs", fields, tags)
return nil
}
if reply.Status != tacplus.AuthenStatusGetPass {
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = authenReplyToString(reply.Status)
acc.AddFields("tacacs", fields, tags)
return nil
}
reply, err = session.Continue(ctx, password.String())
if err != nil {
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, os.ErrDeadlineExceeded) {
return fmt.Errorf("error on tacacs authentication continue password request to %s : %w", client.Addr, err)
}
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = "Timeout"
acc.AddFields("tacacs", fields, tags)
return nil
}
if reply.Status != tacplus.AuthenStatusPass {
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = authenReplyToString(reply.Status)
acc.AddFields("tacacs", fields, tags)
return nil
}
fields["responsetime_ms"] = time.Since(startTime).Milliseconds()
fields["response_status"] = authenReplyToString(reply.Status)
acc.AddFields("tacacs", fields, tags)
return nil
}
func init() {
inputs.Add("tacacs", func() telegraf.Input {
return &Tacacs{ResponseTimeout: config.Duration(time.Second * 5)}
})
}

View file

@ -0,0 +1,389 @@
package tacacs
import (
"context"
"net"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/nwaples/tacplus"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
type testRequestHandler map[string]struct {
password string
args []string
}
func (t testRequestHandler) HandleAuthenStart(_ context.Context, a *tacplus.AuthenStart, s *tacplus.ServerSession) *tacplus.AuthenReply {
user := a.User
for user == "" {
c, err := s.GetUser(context.Background(), "Username:")
if err != nil || c.Abort {
return nil
}
user = c.Message
}
pass := ""
for pass == "" {
c, err := s.GetPass(context.Background(), "Password:")
if err != nil || c.Abort {
return nil
}
pass = c.Message
}
if u, ok := t[user]; ok && u.password == pass {
return &tacplus.AuthenReply{Status: tacplus.AuthenStatusPass}
}
return &tacplus.AuthenReply{Status: tacplus.AuthenStatusFail}
}
func (t testRequestHandler) HandleAuthorRequest(_ context.Context, a *tacplus.AuthorRequest, _ *tacplus.ServerSession) *tacplus.AuthorResponse {
if u, ok := t[a.User]; ok {
return &tacplus.AuthorResponse{Status: tacplus.AuthorStatusPassAdd, Arg: u.args}
}
return &tacplus.AuthorResponse{Status: tacplus.AuthorStatusFail}
}
func (testRequestHandler) HandleAcctRequest(context.Context, *tacplus.AcctRequest, *tacplus.ServerSession) *tacplus.AcctReply {
return &tacplus.AcctReply{Status: tacplus.AcctStatusSuccess}
}
func TestTacacsInit(t *testing.T) {
var testset = []struct {
name string
testingTimeout config.Duration
serversToTest []string
usedUsername config.Secret
usedPassword config.Secret
usedSecret config.Secret
requestAddr string
errContains string
}{
{
name: "empty_creds",
testingTimeout: config.Duration(time.Second * 5),
serversToTest: []string{"foo.bar:80"},
usedUsername: config.NewSecret([]byte(``)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
errContains: "empty credentials were provided (username, password or secret)",
},
{
name: "wrong_reqaddress",
testingTimeout: config.Duration(time.Second * 5),
serversToTest: []string{"foo.bar:80"},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "257.257.257.257",
errContains: "invalid ip address provided for request_ip",
},
{
name: "no_reqaddress_no_servers",
testingTimeout: config.Duration(time.Second * 5),
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: tt.serversToTest,
Username: tt.usedUsername,
Password: tt.usedPassword,
Secret: tt.usedSecret,
RequestAddr: tt.requestAddr,
Log: testutil.Logger{},
}
err := plugin.Init()
if tt.errContains == "" {
require.NoError(t, err)
if tt.requestAddr == "" {
require.Equal(t, "127.0.0.1", plugin.RequestAddr)
}
if len(tt.serversToTest) == 0 {
require.Equal(t, []string{"127.0.0.1:49"}, plugin.Servers)
}
} else {
require.ErrorContains(t, err, tt.errContains)
}
})
}
}
func TestTacacsLocal(t *testing.T) {
testHandler := tacplus.ServerConnHandler{
Handler: &testRequestHandler{
"testusername": {
password: "testpassword",
},
},
ConnConfig: tacplus.ConnConfig{
Secret: []byte(`testsecret`),
Mux: true,
},
}
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err, "local net listen failed to start listening")
srvLocal := l.Addr().String()
srv := &tacplus.Server{
ServeConn: func(nc net.Conn) {
testHandler.Serve(nc)
},
}
go func() {
if err := srv.Serve(l); err != nil {
t.Logf("local srv.Serve failed to start serving on %s", srvLocal)
}
}()
var testset = []struct {
name string
testingTimeout config.Duration
serverToTest []string
usedUsername config.Secret
usedPassword config.Secret
usedSecret config.Secret
requestAddr string
errContains string
reqRespStatus string
}{
{
name: "success_timeout_0s",
testingTimeout: config.Duration(0),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "127.0.0.1",
reqRespStatus: "AuthenStatusPass",
},
{
name: "wrongpw",
testingTimeout: config.Duration(time.Second * 5),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`WRONGPASSWORD`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "127.0.0.1",
reqRespStatus: "AuthenStatusFail",
},
{
name: "wrongsecret",
testingTimeout: config.Duration(time.Second * 5),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`WRONGSECRET`)),
requestAddr: "127.0.0.1",
errContains: "error on new tacacs authentication start request to " + srvLocal + " : bad secret or packet",
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: tt.serverToTest,
Username: tt.usedUsername,
Password: tt.usedPassword,
Secret: tt.usedSecret,
RequestAddr: tt.requestAddr,
Log: testutil.Logger{},
}
var acc testutil.Accumulator
require.NoError(t, plugin.Init())
require.NoError(t, plugin.Gather(&acc))
if tt.errContains == "" {
require.Empty(t, acc.Errors)
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": srvLocal},
map[string]interface{}{
"responsetime_ms": int64(0),
"response_status": tt.reqRespStatus,
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), options...)
} else {
require.Len(t, acc.Errors, 1)
require.ErrorContains(t, acc.FirstError(), tt.errContains)
require.Empty(t, acc.GetTelegrafMetrics())
}
})
}
}
func TestTacacsLocalTimeout(t *testing.T) {
testHandler := tacplus.ServerConnHandler{
Handler: &testRequestHandler{
"testusername": {
password: "testpassword",
},
},
ConnConfig: tacplus.ConnConfig{
Secret: []byte(`testsecret`),
Mux: true,
},
}
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err, "local net listen failed to start listening")
srvLocal := l.Addr().String()
srv := &tacplus.Server{
ServeConn: func(nc net.Conn) {
testHandler.Serve(nc)
},
}
go func() {
if err := srv.Serve(l); err != nil {
t.Logf("local srv.Serve failed to start serving on %s", srvLocal)
}
}()
// Initialize the plugin
plugin := &Tacacs{
ResponseTimeout: config.Duration(time.Microsecond),
Servers: []string{"unreachable.test:49"},
Username: config.NewSecret([]byte(`testusername`)),
Password: config.NewSecret([]byte(`testpassword`)),
Secret: config.NewSecret([]byte(`testsecret`)),
RequestAddr: "127.0.0.1",
Log: &testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Try to connect, this will return a metric with the timeout...
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": "unreachable.test:49"},
map[string]interface{}{
"response_status": string("Timeout"),
"responsetime_ms": int64(0),
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
require.Empty(t, acc.Errors)
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), options...)
}
func TestTacacsIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
container := testutil.Container{
Image: "dchidell/docker-tacacs",
ExposedPorts: []string{"49/tcp"},
WaitingFor: wait.ForAll(
wait.ForLog("Starting server..."),
),
}
require.NoError(t, container.Start(), "failed to start container")
defer container.Terminate()
port := container.Ports["49"]
// Define the testset
var testset = []struct {
name string
testingTimeout config.Duration
usedPassword string
reqRespStatus string
}{
{
name: "timeout_3s",
testingTimeout: config.Duration(time.Second * 3),
usedPassword: "cisco",
reqRespStatus: "AuthenStatusPass",
},
{
name: "timeout_0s",
testingTimeout: config.Duration(0),
usedPassword: "cisco",
reqRespStatus: "AuthenStatusPass",
},
{
name: "wrong_pw",
testingTimeout: config.Duration(time.Second * 5),
usedPassword: "wrongpass",
reqRespStatus: "AuthenStatusFail",
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
// Setup the plugin-under-test
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: []string{container.Address + ":" + port},
Username: config.NewSecret([]byte(`iosadmin`)),
Password: config.NewSecret([]byte(tt.usedPassword)),
Secret: config.NewSecret([]byte(`ciscotacacskey`)),
RequestAddr: "127.0.0.1",
Log: testutil.Logger{},
}
var acc testutil.Accumulator
// Startup the plugin & Gather
require.NoError(t, plugin.Init())
require.NoError(t, plugin.Gather(&acc))
require.NoError(t, acc.FirstError())
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": container.Address + ":" + port},
map[string]interface{}{
"responsetime_ms": int64(0),
"response_status": tt.reqRespStatus,
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
testutil.RequireMetricsStructureEqual(t, expected, acc.GetTelegrafMetrics(), options...)
})
}
}