From 4c6aa115a375b9ce2223ecf1c6d3aeb91fa4df93 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 3 Jul 2026 11:09:06 +0200 Subject: [PATCH] webui: fix more wrappers decoding keyed GETs as bare nodes Three more RESTCONF wrappers assume the response to a keyed GET is the bare node, when the server nests it under the full parent path: the container resource-usage stats always read zero, the WireGuard listen port never populated, and resetting advertised link modes silently re-enabled auto-negotiation on links where it was forced off. Same class of bug as the firewall zone reset fix: the decode matches nothing and the zero value is used as if valid. Model the full nesting in all three wrappers, reusing the existing container list wrapper. Signed-off-by: Joachim Wiberg --- .../internal/handlers/configure_interfaces.go | 17 ++++++++++++----- src/webui/internal/handlers/containers.go | 12 +++++------- src/webui/internal/handlers/vpn.go | 12 +++++++++--- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/webui/internal/handlers/configure_interfaces.go b/src/webui/internal/handlers/configure_interfaces.go index f2073427..0f8da645 100644 --- a/src/webui/internal/handlers/configure_interfaces.go +++ b/src/webui/internal/handlers/configure_interfaces.go @@ -1369,15 +1369,22 @@ func (h *ConfigureInterfacesHandler) ResetEthernetAdvertised(w http.ResponseWrit name := r.PathValue("name") base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet" + // Keyed GETs nest the requested node under its full parent path. var resp struct { - AN struct { - Enable *bool `json:"enable"` - } `json:"ieee802-ethernet-interface:auto-negotiation"` + Interfaces struct { + Interface []struct { + Ethernet struct { + AN struct { + Enable *bool `json:"enable"` + } `json:"auto-negotiation"` + } `json:"ieee802-ethernet-interface:ethernet"` + } `json:"interface"` + } `json:"ietf-interfaces:interfaces"` } enable := true // YANG default if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil { - if resp.AN.Enable != nil { - enable = *resp.AN.Enable + if ifs := resp.Interfaces.Interface; len(ifs) > 0 && ifs[0].Ethernet.AN.Enable != nil { + enable = *ifs[0].Ethernet.AN.Enable } } else if !restconf.IsNotFound(err) { log.Printf("configure interfaces %s reset advertised get: %v", name, err) diff --git a/src/webui/internal/handlers/containers.go b/src/webui/internal/handlers/containers.go index 14d55216..e46cc980 100644 --- a/src/webui/internal/handlers/containers.go +++ b/src/webui/internal/handlers/containers.go @@ -46,11 +46,6 @@ type containerListWrapper struct { } `json:"infix-containers:containers"` } -// containerResourceUsageWrapper wraps the RESTCONF resource-usage response. -type containerResourceUsageWrapper struct { - ResourceUsage containerResourceUsageJSON `json:"infix-containers:resource-usage"` -} - // ContainerEntry holds display-ready data for a single container row. type ContainerEntry struct { Name string @@ -106,13 +101,16 @@ func (h *ContainersHandler) Overview(w http.ResponseWriter, r *http.Request) { defer wg.Done() path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage", url.PathEscape(name)) - var w containerResourceUsageWrapper + var w containerListWrapper // keyed GETs nest under the full parent path if err := h.RC.Get(ctx, path, &w); err != nil { log.Printf("restconf resource-usage %s: %v", name, err) return } + if len(w.Containers.Container) == 0 { + return + } mu.Lock() - usages[idx] = w.ResourceUsage + usages[idx] = w.Containers.Container[0].ResourceUsage mu.Unlock() }(i, c.Name) } diff --git a/src/webui/internal/handlers/vpn.go b/src/webui/internal/handlers/vpn.go index a00cac4e..e0ecc24b 100644 --- a/src/webui/internal/handlers/vpn.go +++ b/src/webui/internal/handlers/vpn.go @@ -21,8 +21,13 @@ type wgConfigJSON struct { } // wgIfaceConfigWrapper is used to fetch per-interface WireGuard config. +// Keyed GETs nest the requested node under its full parent path. type wgIfaceConfigWrapper struct { - WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"` + Interfaces struct { + Interface []struct { + WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"` + } `json:"interface"` + } `json:"ietf-interfaces:interfaces"` } // WGPeer holds display-ready data for a single WireGuard peer. @@ -128,8 +133,9 @@ func buildWGTunnel(ctx context.Context, rc *restconf.Client, iface ifaceJSON) WG // Fetch ListenPort from config endpoint (separate from oper-state). var cfgWrap wgIfaceConfigWrapper path := fmt.Sprintf("/data/ietf-interfaces:interfaces/interface=%s/infix-interfaces:wireguard", iface.Name) - if err := rc.Get(ctx, path, &cfgWrap); err == nil && cfgWrap.WireGuard != nil { - tunnel.ListenPort = cfgWrap.WireGuard.ListenPort + if err := rc.Get(ctx, path, &cfgWrap); err == nil && + len(cfgWrap.Interfaces.Interface) > 0 && cfgWrap.Interfaces.Interface[0].WireGuard != nil { + tunnel.ListenPort = cfgWrap.Interfaces.Interface[0].WireGuard.ListenPort } // Build peers from embedded peer-status.