refactor: remove direct DNS mode
This commit is contained in:
+2
-20
@@ -1,8 +1,7 @@
|
||||
# OpenWrt RMM Agent
|
||||
|
||||
Current stable Go agent: `0.6.0`. It reports runtime health, pending command results,
|
||||
the last heartbeat transport error after connectivity is restored, and optionally keeps
|
||||
the router's direct DNS addresses current.
|
||||
Current stable Go agent: `0.6.1`. It reports runtime health, pending command results,
|
||||
and the last heartbeat transport error after connectivity is restored.
|
||||
|
||||
Production Go agent for OpenWrt, with the shell implementation retained as a fallback runtime.
|
||||
|
||||
@@ -29,10 +28,6 @@ SERVER_URL="https://rmm.example.com"
|
||||
ENROLLMENT_TOKEN="paste-a-one-time-grant-from-your-account"
|
||||
INTERVAL_SECONDS="30"
|
||||
TUNNEL_IDENTITY_FILE="/etc/rmm-agent/tunnel_key"
|
||||
DIRECT_DNS_ENABLED="false"
|
||||
DNS_UPDATE_INTERVAL_SECONDS="300"
|
||||
PUBLIC_IPV4_URL="https://api.ipify.org"
|
||||
PUBLIC_IPV6_URL="https://api6.ipify.org"
|
||||
```
|
||||
|
||||
After enrollment the agent writes:
|
||||
@@ -84,19 +79,6 @@ The Go agent is available as the production OpenWrt package and supports the mig
|
||||
|
||||
Other queued commands are reported as failed with a clear message.
|
||||
|
||||
### Direct DNS updates
|
||||
|
||||
Direct DNS reporting is opt-in. When enabled, the agent queries the configured HTTPS
|
||||
endpoints in parallel, validates that their responses are public IPv4/IPv6 addresses, and
|
||||
sends the complete address state through `/api/agent/dns/update` using its existing device
|
||||
token. A successful update is refreshed every five minutes by default; failures retry after
|
||||
one minute and do not make the main heartbeat unhealthy.
|
||||
|
||||
The default discovery endpoints are operated by ipify. Clear the IPv6 URL on routers without
|
||||
public IPv6, or replace either endpoint with trusted HTTPS services under your control. The
|
||||
agent refuses HTTP URLs, embedded URL credentials, redirects, oversized responses, and
|
||||
private or reserved addresses.
|
||||
|
||||
Build locally:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var nonPublicAddressPrefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/10"),
|
||||
netip.MustParsePrefix("192.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.0.2.0/24"),
|
||||
netip.MustParsePrefix("198.18.0.0/15"),
|
||||
netip.MustParsePrefix("198.51.100.0/24"),
|
||||
netip.MustParsePrefix("203.0.113.0/24"),
|
||||
netip.MustParsePrefix("240.0.0.0/4"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
}
|
||||
|
||||
type directDNSUpdateState struct {
|
||||
nextAttempt time.Time
|
||||
}
|
||||
|
||||
func (s directDNSUpdateState) due(now time.Time) bool {
|
||||
return s.nextAttempt.IsZero() || !now.Before(s.nextAttempt)
|
||||
}
|
||||
|
||||
func (s *directDNSUpdateState) schedule(now time.Time, interval time.Duration, updateErr error) {
|
||||
if interval < time.Minute {
|
||||
interval = 5 * time.Minute
|
||||
}
|
||||
if updateErr != nil && interval > time.Minute {
|
||||
interval = time.Minute
|
||||
}
|
||||
s.nextAttempt = now.Add(interval)
|
||||
}
|
||||
|
||||
func updateDirectDNS(ctx context.Context, client *http.Client, cfg config) error {
|
||||
discoveryContext, cancel := context.WithTimeout(ctx, 12*time.Second)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
address string
|
||||
ipv4 bool
|
||||
err error
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
requests := 0
|
||||
for _, request := range []struct {
|
||||
endpoint string
|
||||
ipv4 bool
|
||||
}{{cfg.PublicIPv4URL, true}, {cfg.PublicIPv6URL, false}} {
|
||||
if strings.TrimSpace(request.endpoint) == "" {
|
||||
continue
|
||||
}
|
||||
requests++
|
||||
go func(endpoint string, ipv4 bool) {
|
||||
address, err := fetchPublicIPAddress(discoveryContext, client, endpoint, ipv4)
|
||||
results <- result{address: address, ipv4: ipv4, err: err}
|
||||
}(request.endpoint, request.ipv4)
|
||||
}
|
||||
|
||||
var ipv4, ipv6 string
|
||||
var discoveryErrors []string
|
||||
for range requests {
|
||||
result := <-results
|
||||
if result.err != nil {
|
||||
discoveryErrors = append(discoveryErrors, result.err.Error())
|
||||
continue
|
||||
}
|
||||
if result.ipv4 {
|
||||
ipv4 = result.address
|
||||
} else {
|
||||
ipv6 = result.address
|
||||
}
|
||||
}
|
||||
if ipv4 == "" && ipv6 == "" {
|
||||
if len(discoveryErrors) == 0 {
|
||||
return errors.New("no public IP discovery service is configured")
|
||||
}
|
||||
return fmt.Errorf("public IP discovery failed: %s", strings.Join(discoveryErrors, "; "))
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"device_id": cfg.DeviceID,
|
||||
"ipv4": ipv4,
|
||||
"ipv6": ipv6,
|
||||
}
|
||||
return postJSON(ctx, client, cfg.ServerURL+"/api/agent/dns/update", cfg.DeviceToken, body, nil)
|
||||
}
|
||||
|
||||
func fetchPublicIPAddress(ctx context.Context, client *http.Client, endpoint string, wantIPv4 bool) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(endpoint))
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil {
|
||||
return "", errors.New("public IP discovery URL must use HTTPS without credentials")
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Accept", "text/plain")
|
||||
discoveryClient := *client
|
||||
discoveryClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
response, err := discoveryClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
|
||||
return "", fmt.Errorf("public IP discovery returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(response.Body, 129))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(data) > 128 {
|
||||
return "", errors.New("public IP discovery response is too large")
|
||||
}
|
||||
address, err := normalizePublicAddress(string(data), wantIPv4)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func normalizePublicAddress(raw string, wantIPv4 bool) (string, error) {
|
||||
address, err := netip.ParseAddr(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return "", errors.New("public IP discovery returned an invalid address")
|
||||
}
|
||||
address = address.Unmap()
|
||||
if address.Is4() != wantIPv4 || !address.IsGlobalUnicast() || address.IsPrivate() || address.IsLoopback() || address.IsLinkLocalUnicast() || address.IsMulticast() || address.IsUnspecified() {
|
||||
return "", errors.New("public IP discovery returned a non-public address")
|
||||
}
|
||||
for _, prefix := range nonPublicAddressPrefixes {
|
||||
if prefix.Contains(address) {
|
||||
return "", errors.New("public IP discovery returned a reserved address")
|
||||
}
|
||||
}
|
||||
return address.String(), nil
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const agentVersion = "0.6.0"
|
||||
const agentVersion = "0.6.1"
|
||||
|
||||
type agentRuntimeHealth struct {
|
||||
StartedAt time.Time
|
||||
@@ -31,9 +31,6 @@ type agentRuntimeHealth struct {
|
||||
LastHeartbeatError string
|
||||
LastHeartbeatErrorAt time.Time
|
||||
LastHeartbeatSuccess time.Time
|
||||
LastDNSUpdateError string
|
||||
LastDNSUpdateErrorAt time.Time
|
||||
LastDNSUpdateSuccess time.Time
|
||||
}
|
||||
|
||||
func (h *agentRuntimeHealth) recordFailure(err error) {
|
||||
@@ -50,20 +47,6 @@ func (h *agentRuntimeHealth) recordSuccess() {
|
||||
h.LastHeartbeatSuccess = time.Now().UTC()
|
||||
}
|
||||
|
||||
func (h *agentRuntimeHealth) recordDirectDNSUpdate(err error) {
|
||||
if err == nil {
|
||||
h.LastDNSUpdateSuccess = time.Now().UTC()
|
||||
h.LastDNSUpdateError = ""
|
||||
h.LastDNSUpdateErrorAt = time.Time{}
|
||||
return
|
||||
}
|
||||
h.LastDNSUpdateError = strings.TrimSpace(err.Error())
|
||||
if len(h.LastDNSUpdateError) > 512 {
|
||||
h.LastDNSUpdateError = h.LastDNSUpdateError[:512]
|
||||
}
|
||||
h.LastDNSUpdateErrorAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
func (h *agentRuntimeHealth) snapshot(spoolDir string) map[string]any {
|
||||
result := map[string]any{
|
||||
"started_at": h.StartedAt.UTC().Format(time.RFC3339),
|
||||
@@ -77,13 +60,6 @@ func (h *agentRuntimeHealth) snapshot(spoolDir string) map[string]any {
|
||||
result["last_heartbeat_error"] = h.LastHeartbeatError
|
||||
result["last_heartbeat_error_at"] = h.LastHeartbeatErrorAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if !h.LastDNSUpdateSuccess.IsZero() {
|
||||
result["last_dns_update_success_at"] = h.LastDNSUpdateSuccess.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if h.LastDNSUpdateError != "" {
|
||||
result["last_dns_update_error"] = h.LastDNSUpdateError
|
||||
result["last_dns_update_error_at"] = h.LastDNSUpdateErrorAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -101,10 +77,6 @@ type config struct {
|
||||
CheckTargets []string
|
||||
HostnameOverride string
|
||||
HostnameSuffix string
|
||||
DirectDNSEnabled bool
|
||||
DNSUpdateSeconds int
|
||||
PublicIPv4URL string
|
||||
PublicIPv6URL string
|
||||
ConfigFile string
|
||||
}
|
||||
|
||||
@@ -166,7 +138,6 @@ func main() {
|
||||
|
||||
backoff := time.Duration(cfg.IntervalSeconds) * time.Second
|
||||
health := &agentRuntimeHealth{StartedAt: time.Now().UTC()}
|
||||
dnsState := directDNSUpdateState{}
|
||||
for {
|
||||
if err := heartbeatOnce(ctx, client, cfg, health.snapshot(cfg.SpoolDir)); err != nil {
|
||||
health.recordFailure(err)
|
||||
@@ -178,14 +149,6 @@ func main() {
|
||||
} else {
|
||||
health.recordSuccess()
|
||||
backoff = time.Duration(cfg.IntervalSeconds) * time.Second
|
||||
if cfg.DirectDNSEnabled && dnsState.due(time.Now()) {
|
||||
err := updateDirectDNS(ctx, client, cfg)
|
||||
health.recordDirectDNSUpdate(err)
|
||||
dnsState.schedule(time.Now(), time.Duration(cfg.DNSUpdateSeconds)*time.Second, err)
|
||||
if err != nil {
|
||||
logf("direct DNS update warning: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if *once {
|
||||
return
|
||||
@@ -213,10 +176,6 @@ func loadConfig(path string) (config, error) {
|
||||
CheckTargets: splitWords(envDefault("CHECK_TARGETS", "1.1.1.1 8.8.8.8")),
|
||||
HostnameOverride: os.Getenv("HOSTNAME_OVERRIDE"),
|
||||
HostnameSuffix: os.Getenv("HOSTNAME_SUFFIX"),
|
||||
DirectDNSEnabled: boolDefault(os.Getenv("DIRECT_DNS_ENABLED"), false),
|
||||
DNSUpdateSeconds: intDefault(os.Getenv("DNS_UPDATE_INTERVAL_SECONDS"), 300),
|
||||
PublicIPv4URL: envDefault("PUBLIC_IPV4_URL", "https://api.ipify.org"),
|
||||
PublicIPv6URL: envDefault("PUBLIC_IPV6_URL", "https://api6.ipify.org"),
|
||||
ConfigFile: path,
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
@@ -266,28 +225,10 @@ func loadConfig(path string) (config, error) {
|
||||
if value := values["HOSTNAME_SUFFIX"]; value != "" {
|
||||
cfg.HostnameSuffix = value
|
||||
}
|
||||
if value, ok := values["DIRECT_DNS_ENABLED"]; ok {
|
||||
cfg.DirectDNSEnabled = boolDefault(value, cfg.DirectDNSEnabled)
|
||||
}
|
||||
if value := values["DNS_UPDATE_INTERVAL_SECONDS"]; value != "" {
|
||||
cfg.DNSUpdateSeconds = intDefault(value, cfg.DNSUpdateSeconds)
|
||||
}
|
||||
if value, ok := values["PUBLIC_IPV4_URL"]; ok {
|
||||
cfg.PublicIPv4URL = strings.TrimSpace(value)
|
||||
}
|
||||
if value, ok := values["PUBLIC_IPV6_URL"]; ok {
|
||||
cfg.PublicIPv6URL = strings.TrimSpace(value)
|
||||
}
|
||||
cfg.ServerURL = strings.TrimRight(cfg.ServerURL, "/")
|
||||
if cfg.IntervalSeconds <= 0 {
|
||||
cfg.IntervalSeconds = 30
|
||||
}
|
||||
if cfg.DNSUpdateSeconds < 60 || cfg.DNSUpdateSeconds > 86400 {
|
||||
cfg.DNSUpdateSeconds = 300
|
||||
}
|
||||
if cfg.DirectDNSEnabled && cfg.PublicIPv4URL == "" && cfg.PublicIPv6URL == "" {
|
||||
return cfg, errors.New("direct DNS requires at least one public IP discovery URL")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -318,10 +259,6 @@ func saveConfig(cfg config) error {
|
||||
writeConfigLine(&b, "BACKUP_DIR", cfg.BackupDir)
|
||||
writeConfigLine(&b, "TUNNEL_IDENTITY_FILE", cfg.TunnelIdentity)
|
||||
writeConfigLine(&b, "TUNNEL_STATE_DIR", cfg.TunnelStateDir)
|
||||
writeConfigLine(&b, "DIRECT_DNS_ENABLED", strconv.FormatBool(cfg.DirectDNSEnabled))
|
||||
writeConfigLine(&b, "DNS_UPDATE_INTERVAL_SECONDS", strconv.Itoa(cfg.DNSUpdateSeconds))
|
||||
writeConfigLine(&b, "PUBLIC_IPV4_URL", cfg.PublicIPv4URL)
|
||||
writeConfigLine(&b, "PUBLIC_IPV6_URL", cfg.PublicIPv6URL)
|
||||
if cfg.HostnameOverride != "" {
|
||||
writeConfigLine(&b, "HOSTNAME_OVERRIDE", cfg.HostnameOverride)
|
||||
}
|
||||
@@ -1801,17 +1738,6 @@ func intDefault(value string, fallback int) int {
|
||||
return parsed
|
||||
}
|
||||
|
||||
func boolDefault(value string, fallback bool) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func splitWords(value string) []string {
|
||||
fields := strings.Fields(value)
|
||||
if len(fields) == 0 {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -13,102 +9,11 @@ import (
|
||||
)
|
||||
|
||||
func TestAgentVersionIsStable(t *testing.T) {
|
||||
if agentVersion != "0.6.0" {
|
||||
if agentVersion != "0.6.1" {
|
||||
t.Fatalf("unexpected agent version %q", agentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigDirectDNS(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "rmm-agent.conf")
|
||||
data := `SERVER_URL="https://rmm.example.test"
|
||||
DIRECT_DNS_ENABLED="1"
|
||||
DNS_UPDATE_INTERVAL_SECONDS="600"
|
||||
PUBLIC_IPV4_URL="https://ipv4.example.test"
|
||||
PUBLIC_IPV6_URL=""
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := loadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cfg.DirectDNSEnabled || cfg.DNSUpdateSeconds != 600 || cfg.PublicIPv4URL != "https://ipv4.example.test" || cfg.PublicIPv6URL != "" {
|
||||
t.Fatalf("unexpected direct DNS config: %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDirectDNS(t *testing.T) {
|
||||
updates := make(chan map[string]string, 1)
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v4":
|
||||
_, _ = w.Write([]byte("8.8.8.8\n"))
|
||||
case "/v6":
|
||||
_, _ = w.Write([]byte("2606:4700:4700::1111\n"))
|
||||
case "/api/agent/dns/update":
|
||||
if r.Header.Get("Authorization") != "Bearer device-secret" {
|
||||
t.Errorf("unexpected authorization header: %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
var request map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Errorf("decode update: %v", err)
|
||||
}
|
||||
updates <- request
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"changed":true}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
cfg := config{
|
||||
ServerURL: server.URL,
|
||||
DeviceID: "dev_test",
|
||||
DeviceToken: "device-secret",
|
||||
PublicIPv4URL: server.URL + "/v4",
|
||||
PublicIPv6URL: server.URL + "/v6",
|
||||
}
|
||||
if err := updateDirectDNS(context.Background(), server.Client(), cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := <-updates
|
||||
if request["device_id"] != "dev_test" || request["ipv4"] != "8.8.8.8" || request["ipv6"] != "2606:4700:4700::1111" {
|
||||
t.Fatalf("unexpected DNS update: %#v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicIPAddressValidation(t *testing.T) {
|
||||
if _, err := normalizePublicAddress("192.168.1.1", true); err == nil {
|
||||
t.Fatal("private IPv4 address was accepted")
|
||||
}
|
||||
if _, err := normalizePublicAddress("2001:db8::1", false); err == nil {
|
||||
t.Fatal("documentation IPv6 address was accepted")
|
||||
}
|
||||
if got, err := normalizePublicAddress("8.8.8.8", true); err != nil || got != "8.8.8.8" {
|
||||
t.Fatalf("public IPv4 address rejected: got=%q err=%v", got, err)
|
||||
}
|
||||
if _, err := fetchPublicIPAddress(context.Background(), &http.Client{}, "http://example.test", true); err == nil {
|
||||
t.Fatal("insecure discovery URL was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectDNSUpdateSchedule(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
state := directDNSUpdateState{}
|
||||
if !state.due(now) {
|
||||
t.Fatal("initial direct DNS update is not due")
|
||||
}
|
||||
state.schedule(now, 5*time.Minute, nil)
|
||||
if state.due(now.Add(4*time.Minute)) || !state.due(now.Add(5*time.Minute)) {
|
||||
t.Fatal("successful direct DNS interval was not respected")
|
||||
}
|
||||
state.schedule(now, 5*time.Minute, errors.New("temporary failure"))
|
||||
if state.due(now.Add(59*time.Second)) || !state.due(now.Add(time.Minute)) {
|
||||
t.Fatal("failed direct DNS update retry interval was not respected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRuntimeHealthSnapshot(t *testing.T) {
|
||||
spoolDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(spoolDir, "pending.json"), []byte(`{}`), 0o600); err != nil {
|
||||
@@ -127,14 +32,6 @@ func TestAgentRuntimeHealthSnapshot(t *testing.T) {
|
||||
if health.ConsecutiveFailures != 0 || health.LastHeartbeatSuccess.IsZero() {
|
||||
t.Fatalf("recordSuccess did not reset runtime state: %#v", health)
|
||||
}
|
||||
health.recordDirectDNSUpdate(errors.New("discovery unavailable"))
|
||||
if health.snapshot(spoolDir)["last_dns_update_error"] != "discovery unavailable" {
|
||||
t.Fatalf("unexpected direct DNS health: %#v", health.snapshot(spoolDir))
|
||||
}
|
||||
health.recordDirectDNSUpdate(nil)
|
||||
if health.LastDNSUpdateSuccess.IsZero() || health.LastDNSUpdateError != "" {
|
||||
t.Fatalf("direct DNS recovery was not recorded: %#v", health)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireLockRemovesDirectoryOnUnlock(t *testing.T) {
|
||||
|
||||
@@ -2,10 +2,6 @@ SERVER_URL="https://rmm.example.com"
|
||||
ENROLLMENT_TOKEN=""
|
||||
INTERVAL_SECONDS="30"
|
||||
CHECK_TARGETS="1.1.1.1 8.8.8.8"
|
||||
DIRECT_DNS_ENABLED="false"
|
||||
DNS_UPDATE_INTERVAL_SECONDS="300"
|
||||
PUBLIC_IPV4_URL="https://api.ipify.org"
|
||||
PUBLIC_IPV6_URL="https://api6.ipify.org"
|
||||
TUNNEL_IDENTITY_FILE="/etc/rmm-agent/tunnel_key"
|
||||
LOCK_FILE="/tmp/rmm-agent-go.lock"
|
||||
SPOOL_DIR="/tmp/rmm-agent-go-results"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-rmm-agent
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_VERSION:=0.2.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=RMM OpenWrt
|
||||
|
||||
@@ -8,7 +8,6 @@ It provides:
|
||||
- HTTPS server URL and polling interval;
|
||||
- one-time enrollment grant input;
|
||||
- connectivity targets and tunnel identity path;
|
||||
- opt-in public IPv4/IPv6 discovery and direct DNS update interval;
|
||||
- explicit lab-only HTTP override;
|
||||
- controlled device identity reset for re-enrollment.
|
||||
|
||||
@@ -68,8 +67,8 @@ application to the router. Do not copy or install the shell runtime at the same
|
||||
cd dist/rmm-openwrt-25.12.4-ramips-mt7621
|
||||
sha256sum -c SHA256SUMS
|
||||
scp \
|
||||
rmm-agent-go-production-0.6.0-r1.apk \
|
||||
luci-app-rmm-agent-0.2.0-r1.apk \
|
||||
rmm-agent-go-production-0.6.1-r1.apk \
|
||||
luci-app-rmm-agent-0.2.1-r1.apk \
|
||||
root@ROUTER_IP:/tmp/
|
||||
```
|
||||
|
||||
@@ -77,8 +76,8 @@ Then install the locally built, unsigned packages over SSH:
|
||||
|
||||
```sh
|
||||
apk add --allow-untrusted \
|
||||
/tmp/rmm-agent-go-production-0.6.0-r1.apk \
|
||||
/tmp/luci-app-rmm-agent-0.2.0-r1.apk
|
||||
/tmp/rmm-agent-go-production-0.6.1-r1.apk \
|
||||
/tmp/luci-app-rmm-agent-0.2.1-r1.apk
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
|
||||
-36
@@ -18,12 +18,6 @@ var callRcInit = rpc.declare({
|
||||
params: [ 'name', 'action' ]
|
||||
});
|
||||
|
||||
function validateDiscoveryURL(sectionId, value) {
|
||||
if (!value || /^https:\/\/[A-Za-z0-9.-]+(?::[0-9]+)?(?:\/.*)?$/.test(value))
|
||||
return true;
|
||||
return _('Public IP discovery must use an HTTPS URL without embedded credentials.');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([ uci.load('rmm-agent'), callServiceList('rmm-agent') ]);
|
||||
@@ -76,36 +70,6 @@ return view.extend({
|
||||
option.placeholder = '1.1.1.1 8.8.8.8';
|
||||
option.rmempty = false;
|
||||
|
||||
section = map.section(form.NamedSection, 'main', 'agent', _('Direct DNS'));
|
||||
section.anonymous = true;
|
||||
section.addremove = false;
|
||||
|
||||
option = section.option(form.Flag, 'direct_dns_enabled', _('Update public DNS addresses'));
|
||||
option.default = '0';
|
||||
option.rmempty = false;
|
||||
option.description = _('Opt-in feature for Go agent 0.6.0 or newer. The router periodically reports its public addresses to RMM.');
|
||||
|
||||
option = section.option(form.Value, 'dns_update_interval_seconds', _('Update interval'));
|
||||
option.datatype = 'range(60,86400)';
|
||||
option.default = '300';
|
||||
option.rmempty = false;
|
||||
option.depends('direct_dns_enabled', '1');
|
||||
|
||||
option = section.option(form.Value, 'public_ipv4_url', _('Public IPv4 discovery URL'));
|
||||
option.placeholder = 'https://api.ipify.org';
|
||||
option.default = 'https://api.ipify.org';
|
||||
option.rmempty = false;
|
||||
option.depends('direct_dns_enabled', '1');
|
||||
option.validate = validateDiscoveryURL;
|
||||
|
||||
option = section.option(form.Value, 'public_ipv6_url', _('Public IPv6 discovery URL'));
|
||||
option.placeholder = 'https://api6.ipify.org';
|
||||
option.default = 'https://api6.ipify.org';
|
||||
option.rmempty = true;
|
||||
option.description = _('Clear this field on routers without public IPv6.');
|
||||
option.depends('direct_dns_enabled', '1');
|
||||
option.validate = validateDiscoveryURL;
|
||||
|
||||
section = map.section(form.NamedSection, 'main', 'agent', _('Advanced settings'));
|
||||
section.anonymous = true;
|
||||
section.addremove = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=rmm-agent-go-production
|
||||
PKG_VERSION:=0.6.0
|
||||
PKG_VERSION:=0.6.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=RMM OpenWrt
|
||||
|
||||
@@ -34,5 +34,5 @@ opkg install /tmp/rmm-agent-go-production_*.ipk
|
||||
|
||||
This package is intended for the final shell-to-Go migration when the router should keep the same RMM object identity.
|
||||
|
||||
Version `0.6.0` supports opt-in direct DNS updates through the UCI/LuCI settings supplied
|
||||
by `luci-app-rmm-agent` `0.2.0`.
|
||||
Version `0.6.1` uses the cloud tunnel exclusively and no longer discovers or publishes the
|
||||
router's public WAN addresses.
|
||||
|
||||
@@ -3,10 +3,6 @@ config agent 'main'
|
||||
option server_url 'https://rmm.example.com'
|
||||
option interval_seconds '30'
|
||||
option check_targets '1.1.1.1 8.8.8.8'
|
||||
option direct_dns_enabled '0'
|
||||
option dns_update_interval_seconds '300'
|
||||
option public_ipv4_url 'https://api.ipify.org'
|
||||
option public_ipv6_url 'https://api6.ipify.org'
|
||||
option tunnel_identity_file '/etc/rmm-agent/tunnel_key'
|
||||
option allow_insecure_http '0'
|
||||
option reset_identity '0'
|
||||
|
||||
@@ -2,10 +2,6 @@ SERVER_URL="https://rmm.example.com"
|
||||
ENROLLMENT_TOKEN=""
|
||||
INTERVAL_SECONDS="30"
|
||||
CHECK_TARGETS="1.1.1.1 8.8.8.8"
|
||||
DIRECT_DNS_ENABLED="false"
|
||||
DNS_UPDATE_INTERVAL_SECONDS="300"
|
||||
PUBLIC_IPV4_URL="https://api.ipify.org"
|
||||
PUBLIC_IPV6_URL="https://api6.ipify.org"
|
||||
TUNNEL_IDENTITY_FILE="/etc/rmm-agent/tunnel_key"
|
||||
DEVICE_ID=""
|
||||
DEVICE_TOKEN=""
|
||||
|
||||
@@ -60,10 +60,6 @@ temporary="${SHELL_CONFIG}.tmp"
|
||||
printf 'ENROLLMENT_TOKEN="%s"\n' "$(escape_value "$enrollment_token")"
|
||||
printf 'INTERVAL_SECONDS="%s"\n' "$(escape_value "$(uci_get interval_seconds)")"
|
||||
printf 'CHECK_TARGETS="%s"\n' "$(escape_value "$(uci_get check_targets)")"
|
||||
printf 'DIRECT_DNS_ENABLED="%s"\n' "$(escape_value "$(uci_get direct_dns_enabled)")"
|
||||
printf 'DNS_UPDATE_INTERVAL_SECONDS="%s"\n' "$(escape_value "$(uci_get dns_update_interval_seconds)")"
|
||||
printf 'PUBLIC_IPV4_URL="%s"\n' "$(escape_value "$(uci_get public_ipv4_url)")"
|
||||
printf 'PUBLIC_IPV6_URL="%s"\n' "$(escape_value "$(uci_get public_ipv6_url)")"
|
||||
printf 'TUNNEL_IDENTITY_FILE="%s"\n' "$(escape_value "$(uci_get tunnel_identity_file)")"
|
||||
printf 'DEVICE_ID="%s"\n' "$(escape_value "$device_id")"
|
||||
printf 'DEVICE_TOKEN="%s"\n' "$(escape_value "$device_token")"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=rmm-agent-go
|
||||
PKG_VERSION:=0.6.0
|
||||
PKG_VERSION:=0.6.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=RMM OpenWrt
|
||||
|
||||
@@ -2,10 +2,6 @@ SERVER_URL="https://rmm.example.com"
|
||||
ENROLLMENT_TOKEN=""
|
||||
INTERVAL_SECONDS="30"
|
||||
CHECK_TARGETS="1.1.1.1 8.8.8.8"
|
||||
DIRECT_DNS_ENABLED="false"
|
||||
DNS_UPDATE_INTERVAL_SECONDS="300"
|
||||
PUBLIC_IPV4_URL="https://api.ipify.org"
|
||||
PUBLIC_IPV6_URL="https://api6.ipify.org"
|
||||
TUNNEL_IDENTITY_FILE="/etc/rmm-agent/tunnel_key"
|
||||
LOCK_FILE="/tmp/rmm-agent-go.lock"
|
||||
SPOOL_DIR="/tmp/rmm-agent-go-results"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=rmm-agent
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_MAINTAINER:=RMM OpenWrt
|
||||
|
||||
@@ -3,10 +3,6 @@ config agent 'main'
|
||||
option server_url 'https://rmm.example.com'
|
||||
option interval_seconds '30'
|
||||
option check_targets '1.1.1.1 8.8.8.8'
|
||||
option direct_dns_enabled '0'
|
||||
option dns_update_interval_seconds '300'
|
||||
option public_ipv4_url 'https://api.ipify.org'
|
||||
option public_ipv6_url 'https://api6.ipify.org'
|
||||
option tunnel_identity_file '/etc/rmm-agent/tunnel_key'
|
||||
option allow_insecure_http '0'
|
||||
option reset_identity '0'
|
||||
|
||||
@@ -60,10 +60,6 @@ temporary="${SHELL_CONFIG}.tmp"
|
||||
printf 'ENROLLMENT_TOKEN="%s"\n' "$(escape_value "$enrollment_token")"
|
||||
printf 'INTERVAL_SECONDS="%s"\n' "$(escape_value "$(uci_get interval_seconds)")"
|
||||
printf 'CHECK_TARGETS="%s"\n' "$(escape_value "$(uci_get check_targets)")"
|
||||
printf 'DIRECT_DNS_ENABLED="%s"\n' "$(escape_value "$(uci_get direct_dns_enabled)")"
|
||||
printf 'DNS_UPDATE_INTERVAL_SECONDS="%s"\n' "$(escape_value "$(uci_get dns_update_interval_seconds)")"
|
||||
printf 'PUBLIC_IPV4_URL="%s"\n' "$(escape_value "$(uci_get public_ipv4_url)")"
|
||||
printf 'PUBLIC_IPV6_URL="%s"\n' "$(escape_value "$(uci_get public_ipv6_url)")"
|
||||
printf 'TUNNEL_IDENTITY_FILE="%s"\n' "$(escape_value "$(uci_get tunnel_identity_file)")"
|
||||
printf 'DEVICE_ID="%s"\n' "$(escape_value "$device_id")"
|
||||
printf 'DEVICE_TOKEN="%s"\n' "$(escape_value "$device_token")"
|
||||
|
||||
Reference in New Issue
Block a user