Add support for manual devices in device configuration and DHCP manager#186
Add support for manual devices in device configuration and DHCP manager#186DanielLavrushin wants to merge 4 commits intomainfrom
Conversation
DanielLavrushin
commented
Apr 7, 2026
- Introduced ManualDevice struct and updated DevicesConfig to include manual_devices.
- Enhanced DHCP manager to handle manual devices, including methods for setting and retrieving them.
- Updated UI components to display and manage manual devices, including adding and removing functionality.
- Added translations for manual device features in English and Russian.
- Introduced ManualDevice struct and updated DevicesConfig to include manual_devices. - Enhanced DHCP manager to handle manual devices, including methods for setting and retrieving them. - Updated UI components to display and manage manual devices, including adding and removing functionality. - Added translations for manual device features in English and Russian.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 2 medium |
| Security | 1 high |
🟢 Metrics 58 complexity
Metric Results Complexity 58
TIP This summary will be updated as you push new changes. Give us feedback
…ve unified device model, remove separate alias file, and fix traffic routing issue on Keenetic routers.
There was a problem hiding this comment.
Pull request overview
This PR adds “manual devices” support by unifying device configuration into a single device model across backend config, DHCP mapping, and the UI, so devices that aren’t discoverable via ARP can still appear in device pickers and source-device filters.
Changes:
- Replaced the legacy
mac/mss_clampsdevice config with a unifieddevices: Device[]model (incl.selected,name,mss_clamp,is_manual,ip). - Extended the DHCP manager to merge ARP-derived mappings with configured manual devices (and operate in manual-only mode).
- Updated UI to manage manual devices and reflect them in device lists (plus i18n updates).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tables/routing.go | Adjusts auto routing table ID allocation range. |
| src/tables/nftables.go | Switches device filtering to use SelectedMACs() from the new device model. |
| src/tables/monitor.go | Updates monitoring checks to rely on SelectedMACs(). |
| src/tables/iptables.go | Updates iptables manifest generation to rely on SelectedMACs(). |
| src/nfq/pool.go | Plumbs manual devices into DHCP manager on startup and config update. |
| src/http/ui/src/models/devices.ts | Updates UI typings to use unified Device model and adds is_manual. |
| src/http/ui/src/models/config.ts | Introduces unified Device type and updates DevicesConfig. |
| src/http/ui/src/i18n/ru.json | Adds RU strings for manual device UI. |
| src/http/ui/src/i18n/en.json | Adds EN strings for manual device UI. |
| src/http/ui/src/hooks/useDevices.ts | Removes alias mutation helpers; keeps list/refresh only. |
| src/http/ui/src/components/settings/Devices.tsx | Adds manual device add/remove UI and migrates selection/MSS/name to unified model. |
| src/http/ui/src/components/sets/Target.tsx | Displays manual devices in source-device selection (chip + MAC placeholder). |
| src/http/ui/src/barrels/settings.ts | Re-exports the new Device type for consumers. |
| src/http/ui/src/api/devices.ts | Removes alias endpoints usage from the UI API client. |
| src/http/handler/types.go | Removes deviceAliases from API handler state. |
| src/http/handler/devices.go | Populates alias/is_manual from config device entries instead of alias store + adds is_manual field. |
| src/http/handler/common.go | Stops initializing the removed device-alias store. |
| src/dhcp/manager.go | Adds manual device merging, manual-only availability, and source reporting. |
| src/config/types.go | Replaces legacy device config fields with unified DevicesConfig.Devices []Device. |
| src/config/migration.go | Adds v33→v34 migration to unify device model + migrate aliases/manual devices. |
| src/config/methods.go | Updates MSS clamp collection + adds SelectedMACs/FindByMAC/ManualEntries helpers. |
| src/config/methods_test.go | Updates MSS clamp tests to use unified device model. |
| src/config/devices.go | Removes the old on-disk MAC alias store implementation. |
| src/config/config.go | Updates defaults to initialize Devices: []Device{}. |
Comments suppressed due to low confidence (1)
src/dhcp/manager.go:158
- The refresh() log message says entries were loaded "from ARP table" even when ARP is unavailable and mappings come only from manual devices. Adjust the message to reflect the actual source (e.g., manual-only vs arp+manual) to avoid misleading operational logs.
count := len(m.ipToMAC)
m.mu.Unlock()
log.Infof("DHCP: loaded %d entries from ARP table", count)
m.notifyCallbacks()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for attempt := uint32(0); attempt < 4096; attempt++ { | ||
| table := 100 + int((base+attempt)%2000) | ||
| table := 100 + int((base+attempt)%150) | ||
| mark := uint32(0x100 + (base+attempt)%0x7E00) | ||
| if _, ok := usedMarks[mark]; ok { | ||
| continue |
There was a problem hiding this comment.
routeResolveIDs now constrains the primary table selection to 100..249, but the fallback path (after the 4096-attempt loop) still increments table++ without any upper bound. If the safe-range tables are exhausted, this fallback can still produce table IDs >255, which defeats the stated BusyBox/keenetic compatibility goal.
Consider constraining the fallback table allocation to the same safe range (or returning an explicit error when no safe table is available) so table IDs never exceed 255.
| const handleSelectAll = (selectAll: boolean) => { | ||
| const current = [...configDevices]; | ||
| const allMacs = new Set(devices.map((d) => d.mac.toUpperCase())); | ||
| const updated = current.map((d) => | ||
| allMacs.has(d.mac.toUpperCase()) ? { ...d, selected: selectAll } : d | ||
| ); | ||
| if (size === null || size === 0) { | ||
| if (idx !== -1) current.splice(idx, 1); | ||
| } else if (idx === -1) { | ||
| current.push({ mac: mac.toUpperCase(), size }); | ||
| } else { | ||
| current[idx] = { ...current[idx], size }; | ||
| if (selectAll) { | ||
| for (const d of devices) { | ||
| if (!updated.some((u) => u.mac.toUpperCase() === d.mac.toUpperCase())) { | ||
| updated.push({ mac: d.mac.toUpperCase(), selected: true }); | ||
| } | ||
| } | ||
| } | ||
| onChange("queue.devices.mss_clamps", current); | ||
| const cleaned = updated.filter( | ||
| (d) => d.selected || d.is_manual || (d.mss_clamp && d.mss_clamp > 0) || d.name | ||
| ); | ||
| onChange("queue.devices.devices", cleaned); |
There was a problem hiding this comment.
handleSelectAll(false) only clears selected for config entries whose MACs are present in the currently-discovered devices list. Any previously-selected MAC that is no longer in devices (e.g., an offline device) will stay selected in config, but it won’t be visible in the table to let the user deselect it.
Consider making deselect-all clear selection for all non-manual config devices (or otherwise ensure hidden/stale selections can be cleared).
| const generateSyntheticMAC = (ip: string): string => { | ||
| const parts = ip.trim().split("."); | ||
| if (parts.length !== 4) return ""; | ||
| const octets = parts.map((p) => { | ||
| if (!/^\d+$/.test(p)) return -1; | ||
| const n = Number(p); | ||
| return n >= 0 && n <= 255 ? n : -1; | ||
| }); | ||
| if (octets.some((o) => o < 0)) return ""; | ||
| return `02:B4:${octets.map((o) => o.toString(16).toUpperCase().padStart(2, "0")).join(":")}`; | ||
| }; |
There was a problem hiding this comment.
The UI-side generateSyntheticMAC only accepts IPv4 dotted-quad input. The backend migration helper (config/migration.go:generateSyntheticMAC) supports both IPv4 and IPv6, so users could end up with configs containing IPv6 manual devices that the UI cannot add/recreate.
Consider either adding IPv6 support here (e.g., via a real IP parser) or explicitly validating/rejecting IPv6 with a clear UI error message so behavior is consistent across UI and backend.
| m.mu.Lock() | ||
| m.ipToMAC = map[string]string{"192.168.1.1": "AA:BB:CC:DD:EE:FF"} | ||
| m.macToIP = map[string]string{"AA:BB:CC:DD:EE:FF": "192.168.1.1"} | ||
| m.hostnames = make(map[string]string) | ||
| for _, d := range m.manualDevices { | ||
| mac := normalizeMAC(d.MAC) | ||
| m.ipToMAC[d.IP] = mac | ||
| m.macToIP[mac] = d.IP | ||
| if d.Name != "" { | ||
| m.hostnames[mac] = d.Name | ||
| } | ||
| } | ||
| m.mu.Unlock() |
There was a problem hiding this comment.
TestManualDevicesMerge reimplements the manual-device merge logic inline instead of exercising (*Manager).refresh(). This means the test can pass even if refresh() is broken (e.g., lock ordering, overwrite behavior, hostname handling).
Since this test is in package dhcp, it can call the unexported m.refresh() directly after setting m.manualDevices / m.available as needed, which would better validate the production code path.
| m.mu.Lock() | |
| m.ipToMAC = map[string]string{"192.168.1.1": "AA:BB:CC:DD:EE:FF"} | |
| m.macToIP = map[string]string{"AA:BB:CC:DD:EE:FF": "192.168.1.1"} | |
| m.hostnames = make(map[string]string) | |
| for _, d := range m.manualDevices { | |
| mac := normalizeMAC(d.MAC) | |
| m.ipToMAC[d.IP] = mac | |
| m.macToIP[mac] = d.IP | |
| if d.Name != "" { | |
| m.hostnames[mac] = d.Name | |
| } | |
| } | |
| m.mu.Unlock() | |
| m.refresh() |

