Skip to content

mini-mwan: add new package#28118

Open
alex-schwartzman wants to merge 2 commits intoopenwrt:masterfrom
alex-schwartzman:add-mini-mwan
Open

mini-mwan: add new package#28118
alex-schwartzman wants to merge 2 commits intoopenwrt:masterfrom
alex-schwartzman:add-mini-mwan

Conversation

@alex-schwartzman
Copy link
Copy Markdown

@alex-schwartzman alex-schwartzman commented Dec 19, 2025

Mini-MWAN is a minimalistic multi-WAN load balancing and failover daemon for OpenWrt. Unlike mwan3, it is orthogonal to nft and iptables, instead using IP routes and metrics for traffic management.

Ideal for use-cases where mwan3 is too heavy or its dependencies cannot be satisfied (e.g., OpenWrt 23.05+).

Features:

  • Failover mode: automatic switching to backup WAN on primary failure
  • Multi-uplink mode: load balancing across multiple WAN connections
  • Configurable ping-based health monitoring
  • UCI configuration interface
  • LuCI web interface coming soon as a separate package
  • Pure Lua implementation with minimal dependencies
  • Status reporting and logging

Tested on OpenWrt 24.10 (ramips/mt7621 ASUS RT-AX53U and mediatek/filogic GL.iNet Flint 2 MT6000) and on OpenWrt 25.12-rc1 (mediatek/filogic GL.iNet Flint 2 MT6000).

Contributions and feedback are welcome at:
https://github.com/alex-schwartzman/mini-mwan

📦 Package Details

Maintainer: @alex-schwartzman
(You can find this by checking the history of the package Makefile.)

Description:
Minimalistic multi-WAN load balancing and failover daemon that uses IP routes and metrics instead of nft/iptables. Lighter alternative to mwan3, but not a full replacement.


🧪 Run Testing Details

  • OpenWrt Version: 24.10

  • OpenWrt Target/Subtarget: all (architecture-independent Lua package)

  • OpenWrt Device: ASUS RT-AX53U (ramips/mt7621)

  • OpenWrt Device: GL.iNet Flint 2 MT6000 (mediatek/filogic)

  • OpenWrt Version: 25.12-rc1

  • OpenWrt Device: GL.iNet Flint 2 MT6000 (mediatek/filogic)


✅ Formalities

  • I have reviewed the CONTRIBUTING.md file for detailed contributing guidelines.

If your PR contains a patch:

  • It can be applied using git am
  • It has been refreshed to avoid offsets, fuzzes, etc., using
    make package/<your-package>/refresh V=s
  • It is structured in a way that it is potentially upstreamable
    (e.g., subject line, commit description, etc.)
    We must try to upstream patches to reduce maintenance burden.

@BKPepe
Copy link
Copy Markdown
Member

BKPepe commented Dec 20, 2025

Previous PR #27704

@GeorgeSapkin
Copy link
Copy Markdown
Member

@alex-schwartzman you need to squash commits, and use you real name and email both for author and sign-off.

@alex-schwartzman
Copy link
Copy Markdown
Author

@alex-schwartzman you need to squash commits, and use you real name and email both for author and sign-off.

Yes, indeed. I should have turned off private emails in GitHub in the first place.

Now I fixed it - should be better in one single squashed commit.

Comment thread net/mini-mwan/Makefile Outdated
@GeorgeSapkin
Copy link
Copy Markdown
Member

If you have a repo with for the project, why aren't you pulling the source from there?

Comment thread net/mini-mwan/Makefile Outdated
@alex-schwartzman
Copy link
Copy Markdown
Author

If you have a repo with for the project, why aren't you pulling the source from there?

As far as I understood the guidelines, the only solid reason to pull from external repo would be if there is already an existing project, which works on other platforms, and I would like to port it to OpenWRT. Good examples would be, vim, tailscale, nebula. Not mini-mwan, though, which is supposed to be OpenWRT-specific.

@alex-schwartzman alex-schwartzman force-pushed the add-mini-mwan branch 3 times, most recently from fa4a704 to 92c4943 Compare December 23, 2025 11:51
@alex-schwartzman
Copy link
Copy Markdown
Author

Addressed review notes.
Rebased to tip of master (unixodbc: re-enable autoreconf to fix host path leak)

@GeorgeSapkin
Copy link
Copy Markdown
Member

I think it makes sense to test this either on snapshot or 25.12.

@alex-schwartzman
Copy link
Copy Markdown
Author

I think it makes sense to test this either on snapshot or 25.12.

What shall I do to achieve that? Perhaps rebase to https://github.com/openwrt/packages/tree/openwrt-25.12 ?

@GeorgeSapkin
Copy link
Copy Markdown
Member

Build this against 25.12 or snapshot and test the functionality either on device or in an emulator if that applies.

@alex-schwartzman
Copy link
Copy Markdown
Author

Build this against 25.12 or snapshot and test the functionality either on device or in an emulator if that applies.

Oh, got it. Actually I can get away even without building it manually - CI/CD has already built it for me: https://github.com/openwrt/packages/actions/runs/20459973155/artifacts/4952664312

So I upgraded my router to 25.12-rc1, and installed that artifact there.

Quick testing has indicated that both failover and multiuplink scenarios behaved correctly.

@alex-schwartzman alex-schwartzman force-pushed the add-mini-mwan branch 2 times, most recently from dd61691 to 08de78a Compare December 23, 2025 16:38
@BKPepe BKPepe requested a review from Copilot December 31, 2025 12:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +336 to +360
local function load_config()
deps.uci_cursor():load("mini-mwan")

local config = {
enabled = deps.uci_cursor():get("mini-mwan", "settings", "enabled") == "1",
mode = deps.uci_cursor():get("mini-mwan", "settings", "mode") or "failover",
check_interval = tonumber(deps.uci_cursor():get("mini-mwan", "settings", "check_interval")) or 30,
log_level = deps.uci_cursor():get("mini-mwan", "settings", "audit") or "emerg",
interfaces = {}
}

-- Load all interface configurations (config only, no state)
-- Section name is the device name (e.g., config interface 'eth0')
deps.uci_cursor():foreach("mini-mwan", "interface", function(section)
local iface_cfg = {
device = section.device,
metric = tonumber(section.metric) or 10,
weight = tonumber(section.weight) or 3,
ping_target = section.ping_target,
ping_count = tonumber(section.ping_count) or 3,
ping_timeout = tonumber(section.ping_timeout) or 2
}

table.insert(config.interfaces, iface_cfg)
end)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deps.uci_cursor() is called repeatedly, and the :load() happens on a different cursor instance than the subsequent :get()/:foreach(). This can lead to reading unloaded/empty config (and also creates multiple cursors per cycle). Create one cursor (local c = deps.uci_cursor()) and reuse it for load/get/foreach.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +208
-- Use libubus directly instead of shelling out to ubus binary
-- conn is either initialized by register_ubus() or we get it from deps
if not deps.ubus_conn then
deps.ubus_conn = deps.ubus_connect()
end

log("Probe: ubus call network.interface dump", "debug")
local data = deps.ubus_conn:call("network.interface", "dump", {})

local gateway_map = {}

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If deps.ubus_connect() fails (returns nil), deps.ubus_conn:call(...) will throw. After attempting to connect, guard against a nil connection (log + return {}) to avoid crashing the daemon when ubus is temporarily unavailable.

Suggested change
-- Use libubus directly instead of shelling out to ubus binary
-- conn is either initialized by register_ubus() or we get it from deps
if not deps.ubus_conn then
deps.ubus_conn = deps.ubus_connect()
end
log("Probe: ubus call network.interface dump", "debug")
local data = deps.ubus_conn:call("network.interface", "dump", {})
local gateway_map = {}
local gateway_map = {}
-- Use libubus directly instead of shelling out to ubus binary
-- conn is either initialized by register_ubus() or we get it from deps
if not deps.ubus_conn then
deps.ubus_conn = deps.ubus_connect()
end
if not deps.ubus_conn then
log("Failed to connect to ubus for network.interface dump", "err")
return gateway_map
end
log("Probe: ubus call network.interface dump", "debug")
local data = deps.ubus_conn:call("network.interface", "dump", {})

Copilot uses AI. Check for mistakes.
Comment thread net/mini-mwan/files/mini-mwan.lua Outdated
Comment on lines +624 to +626
local wan1_configured = config.interfaces[1] and config.interfaces[1].device and config.interfaces[1].device ~= ""
local wan2_configured = config.interfaces[2] and config.interfaces[2].device and config.interfaces[2].device ~= ""
return wan1_configured and wan2_configured
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only checks the first two UCI interface sections. If the first section is empty/disabled but two later sections are configured, the daemon will incorrectly refuse to work. Consider counting configured devices across all config.interfaces and returning true when the count >= 2.

Suggested change
local wan1_configured = config.interfaces[1] and config.interfaces[1].device and config.interfaces[1].device ~= ""
local wan2_configured = config.interfaces[2] and config.interfaces[2].device and config.interfaces[2].device ~= ""
return wan1_configured and wan2_configured
local configured_count = 0
for _, iface in ipairs(config.interfaces or {}) do
if iface and iface.device and iface.device ~= "" then
configured_count = configured_count + 1
if configured_count >= 2 then
return true
end
end
end
return false

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda yes. However, config comes straight from load_config(), which builds config.interfaces via table.insert with no gaps. So a sparse/holey table can't actually happen here.

On the other hand - yes, better to cover that case here AND in the place where that incorrect configuration may originate from. Will change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and I'll add a unit test for that as well.

Comment thread net/mini-mwan/files/mini-mwan.lua Outdated
Comment on lines +117 to +118
local cmd = string.format("ping -I %s -c %d -W %d -w %d %s 2>&1", device, count, timeout, deadline, target)
local output = system_probe(cmd)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

device and target come from configuration and are interpolated into a shell command executed via io.popen. This allows shell metacharacters to change command behavior (command injection) and can also break on unusual interface names. Prefer executing commands without a shell (argv-style exec) or strictly validate/escape device and target to a safe character set before building the command.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is a technical debt. I shall rather use lib-ip and some lua ping libraries instead of io.popen

Comment thread net/mini-mwan/files/mini-mwan.lua
Comment thread net/mini-mwan/files/mini-mwan.init Outdated
Comment on lines +15 to +16
echo "Mini-MWAN is disabled"
return 1
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a non-zero exit code when the service is intentionally disabled can make procd/init treat this as a start failure. Prefer returning success (0) when disabled (and log via logger), so service management doesn’t report an error state for a deliberate configuration.

Suggested change
echo "Mini-MWAN is disabled"
return 1
logger -t mini-mwan "Mini-MWAN is disabled"
return 0

Copilot uses AI. Check for mistakes.
Comment thread net/mini-mwan/files/mini-mwan.lua Outdated
-- Interface is up and has connectivity (ping succeeded)
table.insert(usable, {cfg = iface_cfg, state = iface_state})
else
-- Interface is up but no connectivity (ping failed)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment in the else branch is inaccurate: that branch is reached when iface_state.is_up is false (interface down), not when it is up but has no connectivity. Update the comment to match the actual condition to avoid confusion during maintenance.

Suggested change
-- Interface is up but no connectivity (ping failed)
-- Interface is down

Copilot uses AI. Check for mistakes.
Mini-MWAN is a minimalistic multi-WAN load balancing and failover
daemon for OpenWrt. Unlike mwan3, it does not depend on nft or
iptables, instead using IP routes and metrics for traffic management.

Ideal for use-cases where mwan3 is too heavy or its dependencies
cannot be satisfied (e.g., OpenWrt 23.05+).

Features:
- Failover mode: automatic switching to backup WAN on primary failure
- Multi-uplink mode: load balancing across multiple WAN connections
- Configurable ping-based health monitoring
- UCI configuration interface
- LuCI web interface coming soon as a separate package
- Pure Lua implementation with minimal dependencies
- Status reporting and logging

Tested on OpenWrt 24.10 (ramips/mt7621 ASUS RT-AX53U and mediatek/filogic GL.iNet Flint 2 MT6000) and on OpenWrt 25.12-rc1 (mediatek/filogic GL.iNet Flint 2 MT6000).

Contributions and feedback are welcome at:
https://github.com/alex-schwartzman/mini-mwan

Signed-off-by: Alex Schwartzman <aleks.schwartzman@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants