Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions net/alfred/Config.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
config ALFRED_NEEDS_lua
bool

config ALFRED_NEEDS_libgps
bool

config PACKAGE_ALFRED_VIS
bool "enable vis server for alfred"
depends on PACKAGE_alfred
default y

config PACKAGE_ALFRED_BATHOSTS
bool "enable autogeneration of /etc/bat-hosts"
depends on PACKAGE_alfred
select ALFRED_NEEDS_lua
default n

config PACKAGE_ALFRED_GPSD
bool "enable gpsd service for alfred"
depends on PACKAGE_alfred
select ALFRED_NEEDS_libgps
default n
89 changes: 89 additions & 0 deletions net/alfred/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SPDX-License-Identifier: GPL-2.0-only

include $(TOPDIR)/rules.mk

PKG_NAME:=alfred
PKG_VERSION:=2026.2
PKG_RELEASE:=1

PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://downloads.open-mesh.org/batman/releases/batman-adv-$(PKG_VERSION)
PKG_HASH:=d995748be5f62a42091525ccc102df5ac642186e9b9014698a955a4469b69b96

PKG_MAINTAINER:=Simon Wunderlich <sw@simonwunderlich.de>
PKG_LICENSE:=GPL-2.0-only MIT
PKG_LICENSE_FILES:=LICENSES/preferred/GPL-2.0 LICENSES/preferred/MIT

PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=gc-sections lto

PKG_CONFIG_DEPENDS += \
CONFIG_ALFRED_NEEDS_lua \
CONFIG_ALFRED_NEEDS_libgps \
CONFIG_PACKAGE_ALFRED_VIS \
CONFIG_PACKAGE_ALFRED_BATHOSTS \
CONFIG_PACKAGE_ALFRED_GPSD

include $(INCLUDE_DIR)/package.mk

define Package/alfred
SECTION:=net
CATEGORY:=Network
SUBMENU:=Wireless
TITLE:=A.L.F.R.E.D. - Almighty Lightweight Fact Remote Exchange Daemon
URL:=https://www.open-mesh.org/
DEPENDS:=@IPV6 +libnl-tiny +librt \
+ALFRED_NEEDS_lua:lua \
+ALFRED_NEEDS_libgps:libgps
endef

define Package/alfred/description
alfred is a user space daemon for distributing arbitrary local information
over the mesh/network in a decentralized fashion. This data can be anything
which appears to be useful - originally designed to replace the batman-adv
visualization (vis), you may distribute hostnames, phone books, administration
information, DNS information, the local weather forecast ...

alfred runs as daemon in the background of the system. A user may insert
information by using the alfred binary on the command line, or use special
programs to communicate with alfred (done via unix sockets). alfred then takes
care of distributing the local information to other alfred servers on other
nodes. This is done via IPv6 link-local multicast, and does not require any
configuration. A user can request data from alfred, and will receive the
information available from all alfred servers in the network.
endef

define Package/alfred/conffiles
/etc/config/alfred
/etc/alfred/bat-hosts.lua

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: this entry is unconditional, but the file is only installed when CONFIG_PACKAGE_ALFRED_BATHOSTS is set (lines 84-85), and it goes in via $(INSTALL_BIN) as an executable facter script rather than as a config file.

Nothing breaks — the packaging step skips conffiles that aren't in the image (package-pack.mk: [ -f $(IDIR)/$file ] || continue) — so this is only worth confirming the entry is intentional rather than a leftover.


Generated by Claude Code

endef

define Package/alfred/config
source "$(SOURCE)/Config.in"
endef

MAKE_FLAGS += \
CONFIG_ALFRED_VIS=$(if $(CONFIG_PACKAGE_ALFRED_VIS),y,n) \
CONFIG_ALFRED_GPSD=$(if $(CONFIG_PACKAGE_ALFRED_GPSD),y,n) \
CONFIG_ALFRED_CAPABILITIES=n \
LIBNL_NAME="libnl-tiny" \
LIBNL_GENL_NAME="libnl-tiny" \
REVISION="$(PKG_VERSION)-openwrt-$(PKG_RELEASE)"

define Package/alfred/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/alfred $(1)/usr/sbin/
$(if $(CONFIG_PACKAGE_ALFRED_VIS), \
$(INSTALL_BIN) $(PKG_BUILD_DIR)/vis/batadv-vis $(1)/usr/sbin/)
$(if $(CONFIG_PACKAGE_ALFRED_GPSD), \
$(INSTALL_BIN) $(PKG_BUILD_DIR)/gpsd/alfred-gpsd $(1)/usr/sbin/)
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/alfred.init $(1)/etc/init.d/alfred
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/alfred.config $(1)/etc/config/alfred
$(INSTALL_DIR) $(1)/etc/alfred
$(if $(CONFIG_PACKAGE_ALFRED_BATHOSTS), \
$(INSTALL_BIN) ./files/bat-hosts.lua $(1)/etc/alfred/bat-hosts.lua)
endef

$(eval $(call BuildPackage,alfred))
8 changes: 8 additions & 0 deletions net/alfred/files/alfred.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
config 'alfred' 'alfred'
list interface 'br-lan'
option mode 'master'
option batmanif 'bat0'
option start_vis '1'
option run_facters '1'
# REMOVE THIS LINE TO ENABLE ALFRED
option disabled '1'
Comment on lines +7 to +8

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: "THIS LINE" reads as the comment itself, but deleting the comment does nothing — it's the option disabled '1' below that has to go.

Suggested change
# REMOVE THIS LINE TO ENABLE ALFRED
option disabled '1'
# REMOVE THE LINE BELOW TO ENABLE ALFRED
option disabled '1'

Generated by Claude Code

97 changes: 97 additions & 0 deletions net/alfred/files/alfred.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/sh /etc/rc.common

#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#

START=99
USE_PROCD=1
alfred_args=""
vis_args=""
facters_dir="/etc/alfred"
enable=0
vis_enable=0

append_interface()
{
append "interfaces" "$1" ","
}

alfred_start() {
local args=""
local section="$1"
local disabled interface mode

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: batmanif and start_vis are used only inside alfred_start (lines 43-52) but are not declared local, unlike their neighbours on this line, so they leak into the rc.common shell and carry over between sections.

Suggested change
local disabled interface mode
local disabled interface mode batmanif start_vis

run_facters has to stay global — start_service reads it at line 78.


Generated by Claude Code

local interfaces

# check if section is disabled
config_get_bool disabled "$section" disabled 0
[ $disabled = 0 ] || return 1

args="-f"

config_list_foreach "$section" "interface" append_interface
if [ -z "$interfaces" ]; then
config_get interface "$section" interface
append_interface "$interface"
fi
append args "-i $interfaces"

config_get mode "$section" mode
[ "$mode" = "master" ] && append args "-m"

config_get batmanif "$section" batmanif
append args "-b $batmanif"

append alfred_args "$args"

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.

config_foreach runs alfred_start for every alfred section, but the results are accumulated into one flat alfred_args that feeds a single procd instance. Two enabled sections therefore produce alfred -f -i eth0 -m -b bat0 -f -i eth1 -b bat1 — one daemon with repeated -i/-b, not two daemons. vis_args at line 52 has the same shape, and run_facters ends up holding whatever the last section set.

Is a single section the only supported configuration? If so, opening one procd instance per section inside alfred_start (instance name derived from $section) would make the multi-section case actually work, or the config_foreach could go away in favour of a single named section so the limitation is visible in the script.


Generated by Claude Code

enable=1

config_get_bool start_vis "$section" start_vis 0
if [ "$start_vis" = 1 ] && [ -x /usr/sbin/batadv-vis ]; then
vis_enable=1
append vis_args "-i $batmanif -s"
fi

config_get_bool run_facters "$section" run_facters 0

return 0
}

start_service() {
config_load "alfred"
config_foreach alfred_start alfred

[ "$enable" = "0" ] && return 0

procd_open_instance "alfred"
procd_set_param command /usr/sbin/alfred
procd_append_param command ${alfred_args}
procd_close_instance

[ "$vis_enable" = "1" ] && {
procd_open_instance "batadv-vis"
procd_set_param command /usr/sbin/batadv-vis
procd_append_param command ${vis_args}
procd_close_instance
}

[ "$run_facters" = "1" ] && {
( for file in $facters_dir/* ; do [ -x $file ] && $file ; done )
if ! ( grep -q "for file in $facters_dir/\* ; do " /etc/crontabs/root 2>/dev/null ) ; then
echo "*/5 * * * * ( for file in $facters_dir/* ; do [ -x \$file ] && \$file ; done )" >> /etc/crontabs/root
/etc/init.d/cron enable
/etc/init.d/cron restart
fi
}
}

service_triggers() {
procd_add_reload_trigger "alfred"
}

stop_service() {
[ -e /etc/crontabs/root ] && {
sed "\|for file in $facters_dir/\* ; do |d" -i /etc/crontabs/root
/etc/init.d/cron restart
}
}
116 changes: 116 additions & 0 deletions net/alfred/files/bat-hosts.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/lua

local type_id = 64 -- bat-hosts

function get_hostname()
local hostfile = io.open("/proc/sys/kernel/hostname", "r")
if not hostfile then return nil end
local ret_string = hostfile:read()
hostfile:close()
return ret_string
end

function get_interfaces_names()
local ret = {}

for name in io.popen("ls -1 /sys/class/net/"):lines() do
table.insert(ret, name)
end

return ret
end

function get_interface_address(name)
-- /sys/class/net also contains plain files, e.g. bonding_masters
-- once the bonding module is loaded, which have no address below them
local addressfile = io.open("/sys/class/net/"..name.."/address", "r")
if not addressfile then return nil end
local ret_string = addressfile:read()
addressfile:close()
return ret_string
end
Comment on lines +23 to +31

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.

io.open is not checked before addressfile:read() is called on it. /sys/class/net/ does not contain only interfaces: with the bonding kmod loaded it also contains the plain file bonding_masters, which the ls -1 at line 15 happily returns and which has no address below it. io.open then yields nil, nil:read() raises "attempt to index a nil value", and the whole facter dies — so no bat-hosts data gets published at all on such a node.

Suggested change
function get_interface_address(name)
local addressfile = io.open("/sys/class/net/"..name.."/address", "r")
local ret_string = addressfile:read()
addressfile:close()
return ret_string
end
function get_interface_address(name)
local addressfile = io.open("/sys/class/net/"..name.."/address", "r")
if not addressfile then return nil end
local ret_string = addressfile:read()
addressfile:close()
return ret_string
end

Returning nil needs a matching guard at the caller: line 40 would otherwise do ifaces[nil] = i, which raises "table index is nil". if address and not ifaces[address] then ifaces[address] = i end covers it.

get_hostname at line 6 has the same unchecked-io.open shape, though /proc/sys/kernel/hostname is always there in practice.


Generated by Claude Code



local function generate_bat_hosts()
-- get hostname and interface macs/names
-- then return a table containing valid bat-hosts lines
local n, i
local ifaces, ret = {}, {}

local hostname = get_hostname()

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 get_interface_address half of this fix is complete — nil is returned and the caller now skips it. The get_hostname half is not: the new if not hostfile then return nil end at line 7 has no matching guard here, so hostname can be nil and mac.." "..hostname.."_"..iname at line 49 raises "attempt to concatenate a nil value". The guard converts one crash into a different crash rather than removing it.

Same applies when the file opens but is empty — hostfile:read() returns nil too, which the if not hostfile check does not cover.

Suggested change
local hostname = get_hostname()
local hostname = get_hostname()
if not hostname then return ret end

Bailing out with the empty ret means publish_bat_hosts writes an empty chunk instead of dying, which matches how the rest of the script degrades.


Generated by Claude Code


for n, i in ipairs(get_interfaces_names()) do
local address = get_interface_address(i)
if address and not ifaces[address] then ifaces[address] = i end
end

for mac, iname in pairs(ifaces) do
if mac:match("^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$") and not mac:match("00:00:00:00:00:00") then
table.insert(ret, mac.." "..hostname.."_"..iname.."\n")
end
end

return ret
end

local function publish_bat_hosts()
-- pass a raw chunk of data to alfred
local fd = io.popen("alfred -s " .. type_id, "w")
if fd then
local ret = generate_bat_hosts()
if ret then
fd:write(table.concat(ret))
end
fd:close()
end
end

local function write_bat_hosts(rows)
local content = { "### /tmp/bat-hosts generated by alfred-bat-hosts\n",
"### /!\\ This file is overwritten every 5 minutes /!\\\n",
"### (To keep manual changes, replace /etc/bat-hosts symlink with a static file)\n" }

-- merge the chunks from all nodes, de-escaping newlines
for _, row in ipairs(rows) do
local node, value = unpack(row)
table.insert(content, "# Node ".. node .. "\n")
table.insert(content, value:gsub("\x0a", "\n") .. "\n")
end

-- write parsed content down to disk
local fd = io.open("/tmp/bat-hosts", "w")
if fd then
fd:write(table.concat(content))
fd:close()
end

-- try to make a symlink in /etc pointing to /tmp,
-- if it exists, ln will do nothing.
os.execute("ln -ns /tmp/bat-hosts /etc/bat-hosts 2>/dev/null")
end

local function receive_bat_hosts()
-- read raw chunks from alfred, convert them to a nested table and call write_bat_hosts
-- "alfred -r" can fail in slave nodes (returns empty stdout), so:
-- check output is not null before writing /tmp/bat-hosts, and retry 3 times before giving up.
for n = 1, 3 do
local fd = io.popen("alfred -r " .. type_id)
--[[ this command returns something like
{ "54:e6:fc:b9:cb:37", "00:11:22:33:44:55 ham_wlan0\x0a00:22:33:22:33:22 ham_eth0\x0a" },
{ "90:f6:52:bb:ec:57", "00:22:33:22:33:23 spam\x0a" },
]]--

if fd then
local output = fd:read("*a")
fd:close()
if output and output ~= "" then
assert(loadstring("rows = {" .. output .. "}"))()

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.

loadstring executes the output of alfred -r 64 as Lua code, and that output is assembled from payloads published by every other node on the mesh. If a remote node can get a " or a \ through alfred's escaping into a type-64 payload, it breaks out of the string literal and runs arbitrary Lua as root on every node with PACKAGE_ALFRED_BATHOSTS enabled — and this script is re-run from cron every 5 minutes, see the crontab line installed at alfred.init:81.

alfred's client does escape quotes/backslashes/non-printables as \xNN when it prints the { "mac", "data" } records, so this is probably safe in practice — but it makes a remote-input trust boundary depend entirely on the exact escaping of another program, with no local check. Parsing the records with a string.gmatch pattern (the MAC is already validated with ^%x%x:... at line 44) instead of evaluating them would remove the question entirely.

Is relying on alfred's escaping here deliberate?


Generated by Claude Code

write_bat_hosts(rows)
break
end
end
end
end

publish_bat_hosts()
receive_bat_hosts()