You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.4 KiB
127 lines
4.4 KiB
|
2 months ago
|
package snmpagent
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"crypto/rand"
|
||
|
|
"encoding/hex"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"syscall"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Opt-in target test. It binds a private loopback port, never the production
|
||
|
|
// SNMP port, and keeps generated users/configuration in the test directory.
|
||
|
|
func TestDeviceNetSNMPVersions(t *testing.T) {
|
||
|
|
if os.Getenv("TERA_DEVICE_SNMP_TEST") != "1" {
|
||
|
|
t.Skip("requires target Net-SNMP executables")
|
||
|
|
}
|
||
|
|
root := t.TempDir()
|
||
|
|
mainPath, userPath := filepath.Join(root, "etc", "snmpd.conf"), filepath.Join(root, "state", "snmpd.conf")
|
||
|
|
secretBytes := make([]byte, 16)
|
||
|
|
if _, err := rand.Read(secretBytes); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
secret := hex.EncodeToString(secretBytes)
|
||
|
|
config := Config{Enabled: true, V1Enabled: true, V2CEnabled: true, V3Enabled: true, ReadOnlyCommunity: secret,
|
||
|
|
V3Users: []User{{Username: "codex_probe", Access: "read", AuthProtocol: "sha256", PrivacyProtocol: "aes", AuthKey: secret, PrivacyKey: secret, Enabled: true}}}
|
||
|
|
runner := &deviceSNMPRunner{root: root, mainPath: mainPath, userPath: userPath}
|
||
|
|
controller, err := NewOSController(OSOptions{ConfigPath: mainPath, PersistentPath: userPath, InitScriptPath: filepath.Join(root, "init"), Runner: runner})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
t.Cleanup(func() { _, _ = runner.Run(context.Background(), "", "stop") })
|
||
|
|
if err := controller.Apply(context.Background(), config); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
probe := func(args ...string) ([]byte, error) {
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
args = append(args, "-t", "1", "-r", "0", "127.0.0.1:1161", "1.3.6.1.2.1.1.1.0")
|
||
|
|
c := exec.CommandContext(ctx, "snmpget", args...)
|
||
|
|
c.Env = append(os.Environ(), "MIBS=")
|
||
|
|
return c.CombinedOutput()
|
||
|
|
}
|
||
|
|
for i := 0; i < 10; i++ {
|
||
|
|
if _, err := probe("-v2c", "-c", secret); err == nil {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
time.Sleep(100 * time.Millisecond)
|
||
|
|
}
|
||
|
|
for _, version := range []string{"1", "2c", "3"} {
|
||
|
|
t.Run(version, func(t *testing.T) {
|
||
|
|
args := []string{"-v" + version, "-c", secret}
|
||
|
|
if version == "3" {
|
||
|
|
args = []string{"-v3", "-u", "codex_probe", "-l", "authPriv", "-a", "SHA-256", "-A", secret, "-x", "AES", "-X", secret}
|
||
|
|
}
|
||
|
|
out, err := probe(args...)
|
||
|
|
if err != nil || !strings.Contains(string(out), "STRING:") {
|
||
|
|
t.Fatalf("query failed: %v %s", err, strings.ReplaceAll(string(out), secret, "[redacted]"))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
// Reconfigure a live daemon: graceful stop must not overwrite new USM keys.
|
||
|
|
config.V3Users[0].AuthKey = secret + "a"
|
||
|
|
config.V3Users[0].PrivacyKey = secret + "p"
|
||
|
|
if err := controller.Apply(context.Background(), config); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
time.Sleep(200 * time.Millisecond)
|
||
|
|
out, err := probe("-v3", "-u", "codex_probe", "-l", "authPriv", "-a", "SHA-256", "-A", secret+"a", "-x", "AES", "-X", secret+"p")
|
||
|
|
if err != nil || !strings.Contains(string(out), "STRING:") {
|
||
|
|
t.Fatalf("SNMPv3 query after credential update failed: %v", err)
|
||
|
|
}
|
||
|
|
t.Log("SNMPv3 credential update and restart: PASS")
|
||
|
|
if _, err := probe("-v2c", "-c", "incorrect-test-community"); err == nil {
|
||
|
|
t.Error("wrong community unexpectedly accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Emulates the synchronous init-script contract with an isolated real daemon.
|
||
|
|
type deviceSNMPRunner struct {
|
||
|
|
root, mainPath, userPath string
|
||
|
|
cmd *exec.Cmd
|
||
|
|
log *os.File
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *deviceSNMPRunner) Run(ctx context.Context, _ string, args ...string) ([]byte, error) {
|
||
|
|
if args[0] == "stop" {
|
||
|
|
if r.cmd == nil {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
_ = r.cmd.Process.Signal(syscall.SIGTERM)
|
||
|
|
done := make(chan error, 1)
|
||
|
|
go func() { done <- r.cmd.Wait() }()
|
||
|
|
select {
|
||
|
|
case <-done:
|
||
|
|
case <-time.After(5 * time.Second):
|
||
|
|
_ = r.cmd.Process.Kill()
|
||
|
|
<-done
|
||
|
|
return nil, fmt.Errorf("daemon stop timed out")
|
||
|
|
}
|
||
|
|
r.cmd = nil
|
||
|
|
_ = r.log.Close()
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
raw, err := os.ReadFile(r.mainPath)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
raw = []byte(strings.Replace(string(raw), "agentAddress udp:161", "agentAddress udp:127.0.0.1:1161", 1))
|
||
|
|
if err = os.WriteFile(r.mainPath, raw, 0600); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
r.log, err = os.OpenFile(filepath.Join(r.root, "daemon.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
r.cmd = exec.Command("/usr/sbin/snmpd", "-f", "-Lo", "-C", "-c", r.mainPath+","+r.userPath)
|
||
|
|
r.cmd.Env = append(os.Environ(), "SNMP_PERSISTENT_DIR="+filepath.Dir(r.userPath), "MIBS=")
|
||
|
|
r.cmd.Stdout, r.cmd.Stderr = r.log, r.log
|
||
|
|
return nil, r.cmd.Start()
|
||
|
|
}
|