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