Skip to content

luci-mod-system: improve Dropbear interface binding selection - #8948

Open
Ser9ei wants to merge 1 commit into
openwrt:masterfrom
Ser9ei:changes/dropbear
Open

luci-mod-system: improve Dropbear interface binding selection#8948
Ser9ei wants to merge 1 commit into
openwrt:masterfrom
Ser9ei:changes/dropbear

Conversation

@Ser9ei

@Ser9ei Ser9ei commented Aug 16, 2026

Copy link
Copy Markdown

Motivation

The current Dropbear LuCI configuration exposes interface binding through a boolean
"Bind to Interface" option and two different Interface fields:

  • when disabled, Interface means: listen on IP addresses assigned to the selected
    network interface;
  • when enabled, DirectInterface means: bind directly to the selected network interface.

This works correctly internally, but the UI is confusing because the same-looking Interface selector represents different Dropbear configuration options depending on the state of the flag.

The current UI can therefore be ambiguous:

Bind to Interface: [x]
Interface: [lan]

versus:

Bind to Interface: [ ]
Interface: [lan]

Although both configurations display an Interface selector, they result in different Dropbear behaviour.

Additionally, the UI flag was persisted as “option _direct” in UCI, although the dropbear service never uses it.

Description

New UI

Replace the boolean "Bind to Interface" option with a three-way "Bind to" selection:

Bind to
    ( ) All interfaces (unspecified)
    ( ) IP addresses of interface
        Interface: [ lan ]
    ( ) Network interface
        Interface: [ lan ]

The three modes map to the existing Dropbear configuration:

  1. All interfaces
    Neither Interface nor DirectInterface is configured.

  2. IP addresses of interface
    The existing Interface option is used.

  3. Network interface
    The existing DirectInterface option is used.

Implementation

The change is limited to the LuCI dropbear configuration.

A virtual "_bind_to" ListValue controls the UI. Its value is derived from the existing Interface/DirectInterface options and is not persisted.

Added a one-time "uci-defaults" migration to remove the legacy "_direct" UI state from existing Dropbear configs.

Screenshot or video of changes

IMG_0156 IMG_0157 IMG_0158

Maintainer (preferred)


Tested on

OpenWrt version: OpenWrt 25.12.5 (r33051-f5dae5ece4)
LuCI version: LuCI openwrt-25.12 branch (26.209.73834~b61f907)
Web browser(s): Firefox 153.0.4


Checklist

  • (Nice to have) Includes what Issue it closes (e.g. openwrt/luci#issue-number).
  • (Nice to have) Includes what it depends on (e.g. openwrt/packages#pr-number in sister repo).

@Ser9ei

Ser9ei commented Aug 16, 2026

Copy link
Copy Markdown
Author

@systemcrash & @jow-, could you please take a look when you have a moment?

This builds on the previous dropbear listen options UI (boolean "Bind to Interface") and replaces it with an explicit three-way selector.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed 1 new commit. The three-way selector approach looks sound — _bind_to is correctly kept out of UCI (the write override is needed, since the default write() would happily persist _bind_to just like the old _direct did), the DirectInterface-before-Interface precedence in cfgvalue() matches the dropbear init script's own precedence, and mode switching clears the unused option through the normal inactive-option removal path. No new UCI options are introduced, so there is no backend-coupling gap.

Three inline notes, mainly about the interaction between the new radio and the interface dropdowns.


Generated by Claude Code

Comment on lines +43 to +45
o.validate = function(section, value) {
return value ? true : _('Please select an interface');
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The dropdown still offers an explicit unspecified choice, which now duplicates the new All interfaces (unspecified) radio and can only ever produce this validation error. widgets.NetworkSelect adds that entry whenever rmempty or optional is set (widgets.js:507-508),`` and rmempty defaults to `true` (form.js:1431) — neither option sets it here.

Setting rmempty = false drops the entry from the choice list and makes the option required, so the custom validate() becomes redundant: parse() already rejects an empty active value with Option "Interface" must not be empty. (form.js:2152-2161). Clearing on mode switch is unaffected — an inactive option is removed via the !this.retain branch (form.js:2167-2169) regardless of rmempty.

Suggested change
o.validate = function(section, value) {
return value ? true : _('Please select an interface');
};
o.rmempty = false;

Same applies to lines 50-52 for Interface.


Generated by Claude Code

this.remove(section);
};

o = s.option(widgets.NetworkSelect, 'DirectInterface', _('Interface'), _('Listen only on the given interface or, if unspecified, on all'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The , if unspecified, on all clause no longer describes reachable behaviour after this change: "unspecified" is now the separate All interfaces (unspecified) radio, and the validate() added below turns an empty selection into an error rather than a bind-to-all fallback. Leaving the old wording keeps the ambiguity the PR is trying to remove.

Suggested change
o = s.option(widgets.NetworkSelect, 'DirectInterface', _('Interface'), _('Listen only on the given interface or, if unspecified, on all'));
o = s.option(widgets.NetworkSelect, 'DirectInterface', _('Interface'), _('Listen only on the given interface'));

Line 47 has the same stale clause — _('Listen on up to 10 IPs on the given interface or, if unspecified, on all interfaces') should become _('Listen on up to 10 IPs on the given interface').


Generated by Claude Code

o.default = o.disabled;
// Virtual option: derives UI mode from Interface/DirectInterface,
// is not stored in UCI and clears the real options on change
o = s.option(form.ListValue, '_bind_to', _('Bind to'), _('Select how the SSH service should be bound to network interfaces or IP addresses'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: the replaced _direct flag was actually persisted to /etc/config/dropbear, unlike the new _bind_to. form.Flag.parse() writes any non-default value (form.js:5153-5156) and uci.set() only filters option names starting with ., not _ (uci.js:607). So every instance where a user had ticked "Bind to Interface" carries a stale option _direct '1' that nothing reads any more (the dropbear init script only consumes Interface/DirectInterface), and this PR never removes it.

Would it be worth clearing it here, e.g. this.map.data.unset('dropbear', section, '_direct') alongside the this.remove(section) in the write handler below, so configs get tidied up on the next save?

Unrelated to that: the comment says the virtual option "clears the real options on change", but the clearing actually comes from the framework removing Interface/DirectInterface once their depends() stop matching — write() here only suppresses _bind_to itself.


Generated by Claude Code

@Ser9ei

Ser9ei commented Aug 17, 2026

Copy link
Copy Markdown
Author

Updated:

  • kept the custom validation for "Interface" and "DirectInterface", since "rmempty = false" would implicitly select the first interface (and possible first in the list will be not a "lan") instead of leaving the selection unspecified;
  • clarified both option descriptions to remove the obsolete "if unspecified, on all";
  • added a one-time "uci-defaults" migration to remove the legacy "_direct" UI state from existing Dropbear configs.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed after the force-push (301bf5eaf34b76). The two earlier notes about the stale or, if unspecified, on all clauses and the misleading code comment are addressed, and the rmempty = false suggestion is fairly answered in your comment — dropping the unspecified entry would make NetworkSelect preselect whatever network happens to come first, so keeping the custom validate() is the better trade. I traced the empty-value path to confirm the custom validator actually fires: Validator.validate() only short-circuits on an empty value when the field is not optional, otherwise it falls through to vfunc (validation.js:208-236),`` so "Please select an interface" does get raised.

The new uci-defaults migration is correct as written. Worth recording for whoever merges: _direct did ship in a release — it is on openwrt-25.12 via a cherry-pick of 37aa280 — so the migration is not dead code. It also reaches users on both paths (sysupgrade first boot sees the new file since it has no overlay whiteout, and an apk/opkg upgrade of luci-mod-system alone gets it through default_postinst). config_load handles the case where /etc/config/dropbear is absent without erroring, _direct survives the uci exportconfig_set round-trip fine despite the leading underscore, and the 90_ prefix orders it after 50-dropbear.

Two nits inline, both on the new script; nothing blocking.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: /etc/uci-defaults/ is a single flat namespace shared by every installed package, and this filename doesn't say which package owns it — it reads as if it belongs to the dropbear package, which already installs its own /etc/uci-defaults/50-dropbear from openwrt.git.

Every other uci-defaults file shipped by a LuCI package names its LuCI package: 50_luci-mod-admin-full, 40_luci-statistics, 95-luci-app-banip-housekeeping, 30_luci-theme-*, etc. Something like 90_luci-mod-system-dropbear would keep that pattern and make the owner obvious when the file shows up in luci-mod-system.list.


Generated by Claude Code

Comment on lines +7 to +22
is_commit=0

dropbear_update() {
local _direct

config_get _direct "$1" _direct
[ -n "$_direct" ] || return 0

if uci -q delete dropbear."$1"._direct; then
is_commit=1
fi
}

config_load "dropbear"
config_foreach dropbear_update dropbear
[ $is_commit -eq 0 ] || uci commit dropbear

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: the is_commit bookkeeping doesn't buy anything — both runners of uci-defaults scripts already uci commit unconditionally right after sourcing them: uci_apply_defaults() in /etc/init.d/boot on the boot path and default_postinst() on the package-install path. That's why the in-tree precedents for exactly this kind of stale-option cleanup just stage the delete and stop — 95-luci-app-attendedsysupgrade-housekeeping`` does uci -q delete with no commit at all, and openwrt's own `dropbear.defaults` relies on the same thing.

An unconditional uci -q commit dropbear is a no-op when the delta is empty, so the flag can go (also drops the unquoted $is_commit in [ ]). Using config_foreach is still right here, since s.addremove = true means there can be several dropbear sections.

Suggested change
is_commit=0
dropbear_update() {
local _direct
config_get _direct "$1" _direct
[ -n "$_direct" ] || return 0
if uci -q delete dropbear."$1"._direct; then
is_commit=1
fi
}
config_load "dropbear"
config_foreach dropbear_update dropbear
[ $is_commit -eq 0 ] || uci commit dropbear
dropbear_update() {
local _direct
config_get _direct "$1" _direct
[ -n "$_direct" ] || return 0
uci -q delete dropbear."$1"._direct
}
config_load "dropbear"
config_foreach dropbear_update dropbear
uci -q commit dropbear

Generated by Claude Code

Replace the boolean "Bind to Interface" option with a three-way
"Bind to" selector to make the distinction between Interface and
DirectInterface explicit.

The virtual _bind_to option is not stored in UCI; existing
Interface/DirectInterface options and the init script are unchanged.
Legacy _direct UI state is removed from existing configurations by
a one-time uci-defaults migration.

Signed-off-by: Sergei S. <it4notice@proton.me>
@Ser9ei

Ser9ei commented Aug 18, 2026

Copy link
Copy Markdown
Author

Update:

Renamed the uci-defaults file to 90_luci-mod-system-dropbear.
Kept the "is_commit + conditional commit" so the script stays self-contained if run manually. No issue simplifying further if a human reviewer prefers that.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed 1 new commit; no new issues found.

The force-push (af34b76c10f903) contains only the rename 90_dropbear_remove_direct90_luci-mod-system-dropbear, which addresses the naming nit; the script body and dropbear.js are byte-identical to what I reviewed before. The commit message still matches the diff. Nothing references the old filename, and luci.mk installs everything under root/ automatically, so no .list/Makefile update is needed. The 0644 mode is fine — both runners source the file rather than exec it (uci_apply_defaults() in /etc/init.d/boot and default_postinst()) — and matches the majority of in-tree LuCI uci-defaults files.

The is_commit nit is answered in your comment and left for a human reviewer to call, so I'm not re-raising it. FormalityCheck is green on c10f903.


Generated by Claude Code

@Ser9ei

Ser9ei commented Aug 24, 2026

Copy link
Copy Markdown
Author

CI is waiting for approval (first-time contributor). @hnyman would you
mind approving the workflows when convenient?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants