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.
 
 
 
 
 
 

126 lines
4.4 KiB

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()
}