Add Go agent read-only commands
This commit is contained in:
@@ -81,7 +81,12 @@
|
||||
- [x] enrollment/heartbeat;
|
||||
- [x] inventory/metrics MVP;
|
||||
- [x] command polling/result/spool;
|
||||
- [ ] перенос allowlist команд из shell-agent;
|
||||
- [~] перенос allowlist команд из shell-agent;
|
||||
- [x] `ping`;
|
||||
- [x] `traceroute`;
|
||||
- [x] `route_show`;
|
||||
- [x] `interfaces_show`;
|
||||
- [x] `pkg_list_installed` / `opkg_list_installed`;
|
||||
- [ ] OpenWrt cross-build/package integration.
|
||||
|
||||
## Этап 2: MVP Server API
|
||||
|
||||
@@ -50,7 +50,15 @@ agent/go/cmd/rmm-agent
|
||||
|
||||
It is protocol-compatible with the server for enrollment, heartbeat, inventory, metrics, command polling, command result reporting, lock handling, backoff, graceful shutdown, and result spooling.
|
||||
|
||||
The Go agent is not the production command runner yet. It intentionally reports queued commands as failed with a clear message until the shell allowlist is migrated command by command.
|
||||
The Go agent is not the production command runner yet. It currently supports the first read-only command set:
|
||||
|
||||
- `ping`
|
||||
- `traceroute`
|
||||
- `route_show`
|
||||
- `interfaces_show`
|
||||
- `pkg_list_installed` / `opkg_list_installed`
|
||||
|
||||
Other queued commands are reported as failed with a clear message until the shell allowlist is migrated command by command.
|
||||
|
||||
Build locally:
|
||||
|
||||
|
||||
@@ -272,11 +272,16 @@ func buildMetrics(targets []string) map[string]any {
|
||||
}
|
||||
|
||||
func processCommand(ctx context.Context, client *http.Client, cfg config, cmd command) {
|
||||
output := fmt.Sprintf("go agent preview does not implement command %q yet; shell agent remains the production command runner", cmd.Type)
|
||||
output, exitCode := runCommand(ctx, cmd)
|
||||
status := "completed"
|
||||
if exitCode != 0 {
|
||||
status = "failed"
|
||||
}
|
||||
output = redactSensitiveOutput(output)
|
||||
result := map[string]any{
|
||||
"device_id": cfg.DeviceID,
|
||||
"status": "failed",
|
||||
"exit_code": 2,
|
||||
"status": status,
|
||||
"exit_code": exitCode,
|
||||
"output": output,
|
||||
"result": map[string]any{"agent_version": agentVersion, "agent_runtime": "go"},
|
||||
}
|
||||
@@ -288,6 +293,127 @@ func processCommand(ctx context.Context, client *http.Client, cfg config, cmd co
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(ctx context.Context, cmd command) (string, int) {
|
||||
args := map[string]string{}
|
||||
if len(cmd.Args) > 0 {
|
||||
_ = json.Unmarshal(cmd.Args, &args)
|
||||
}
|
||||
switch cmd.Type {
|
||||
case "ping":
|
||||
target := commandTarget(args, "1.1.1.1")
|
||||
if !safeHostName(target) {
|
||||
return "target is invalid\n", 2
|
||||
}
|
||||
return execCommand(ctx, 20*time.Second, "ping", "-c", "4", target)
|
||||
case "traceroute":
|
||||
target := commandTarget(args, "1.1.1.1")
|
||||
if !safeHostName(target) {
|
||||
return "target is invalid\n", 2
|
||||
}
|
||||
return execCommand(ctx, 45*time.Second, "traceroute", target)
|
||||
case "route_show":
|
||||
return execCommand(ctx, 10*time.Second, "ip", "route", "show")
|
||||
case "interfaces_show":
|
||||
return execCommand(ctx, 10*time.Second, "ip", "-o", "addr", "show")
|
||||
case "pkg_list_installed", "opkg_list_installed":
|
||||
return runPackageCommand(ctx, "list_installed")
|
||||
default:
|
||||
return fmt.Sprintf("go agent preview does not implement command %q yet; shell agent remains the production command runner\n", cmd.Type), 2
|
||||
}
|
||||
}
|
||||
|
||||
func commandTarget(args map[string]string, fallback string) string {
|
||||
target := strings.TrimSpace(args["target"])
|
||||
if target == "" {
|
||||
return fallback
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func safeHostName(value string) bool {
|
||||
if value == "" || len(value) > 255 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '.', '-', '_', ':', '[', ']':
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func runPackageCommand(ctx context.Context, action string) (string, int) {
|
||||
pm := packageManager()
|
||||
switch pm + ":" + action {
|
||||
case "apk:list_installed":
|
||||
return execCommand(ctx, 30*time.Second, "apk", "list", "-I")
|
||||
case "opkg:list_installed":
|
||||
return execCommand(ctx, 30*time.Second, "opkg", "list-installed")
|
||||
default:
|
||||
return "no supported package manager found\n", 2
|
||||
}
|
||||
}
|
||||
|
||||
func packageManager() string {
|
||||
if _, err := exec.LookPath("apk"); err == nil {
|
||||
return "apk"
|
||||
}
|
||||
if _, err := exec.LookPath("opkg"); err == nil {
|
||||
return "opkg"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func execCommand(ctx context.Context, timeout time.Duration, name string, args ...string) (string, int) {
|
||||
cmdCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(cmdCtx, name, args...)
|
||||
data, err := cmd.CombinedOutput()
|
||||
output := string(data)
|
||||
if cmdCtx.Err() == context.DeadlineExceeded {
|
||||
return output + "\ncommand timed out\n", 124
|
||||
}
|
||||
if err == nil {
|
||||
return output, 0
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return output, exitErr.ExitCode()
|
||||
}
|
||||
if output != "" {
|
||||
return output, 1
|
||||
}
|
||||
return err.Error() + "\n", 1
|
||||
}
|
||||
|
||||
func redactSensitiveOutput(output string) string {
|
||||
words := []string{"private_key", "password", "passwd", "secret", "psk", "token"}
|
||||
lines := strings.Split(output, "\n")
|
||||
for i, line := range lines {
|
||||
lower := strings.ToLower(line)
|
||||
for _, word := range words {
|
||||
if strings.Contains(lower, word) {
|
||||
lines[i] = redactLine(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func redactLine(line string) string {
|
||||
if idx := strings.Index(line, "="); idx >= 0 {
|
||||
return line[:idx+1] + "[redacted]"
|
||||
}
|
||||
return "[redacted]"
|
||||
}
|
||||
|
||||
func sendCommandResult(ctx context.Context, client *http.Client, cfg config, commandID string, body any) error {
|
||||
status, err := postJSONStatus(ctx, client, cfg.ServerURL+"/api/agent/commands/"+commandID+"/result", cfg.DeviceToken, body)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user