I'm stuck on OpenWRT 18.06, and these info may soon be obsoleted by newer versions, and/or the more detailed ACL scheme in LuCI2.
The motivation So i have this server at home, that serves http://hq.strfry.org , which i sometimes switch off at night. Several when a wanted to access it from remote, i wished for a remote interface to do that. Since my router is running OpenWRT, i thought it might be a good idea to base this functionality of the OpenWRT Lua configuration interface (LuCI).
opkg install luci-app-wol
brings us a WoL menu that is already very close to what we want.
To find out how this is made, we change directory to the luci basedir, in this case /usr/lib/lua/luci
Let's have a look how luci-app-wol installs it's routes (/admin/services/wol)
module("luci.controller.wol", package.seeall)
function index()
entry({"admin", "services", "wol"}, form("wol"), _("Wake on LAN"), 90)
entry({"mini", "services", "wol"}, form("wol"), _("Wake on LAN"), 90)
end
The existing entries place themselves under a parent node that will enforce authentication (/admin, /mini, ...)
If we add our page directly at the root, we get anonymous access to the WoL Interface:
entry({"wol"}, form("wol"), _("Wake on LAN"), 90)
When trying to submit a WoL request as an unauthenticated web user, we get an error related to a missing token.
This is the Cross-Site Request Forgery migitation, and we can't get around it in the form("wol")
, without doing a proper login.
Also the unauthenticated form leaks some internal network info that isn't necessary for the job, like ames and MAC addresses of other hosts. Nonetheless we shall explore some options to realize a password-based authentication for a lesser user:
https://github.com/openwrt/luci/wiki/JsonRpcHowTo
opkg install luci-mod-rpc
/etc/init.d/uhttpd restart
Now we have a new HTTP Endpoint behind http://172.16.42.1/cgi-bin/luci/rpc
An authentification token can be acquired like this:
curl http://172.16.42.1/cgi-bin/luci/rpc/auth -d '
{
"id": 1,
"method": "login",
"params": [
"username",
"password"
]
}'
Unfortunately i couldn't figure out how to do something useful from there, and if i could call the existing WoL code.
To protect the page properly, we can so we better add proper authentication. Also that seems to be a condition to get a CSRF Token and be authorized to send POST requests. To inform LuCI that we desire authentication, we configure the accepted users on the page:
local page = entry({"wol"}, form("wol"), _("Wake on LAN"), 90)
page.sysauth = {"root", "wol"}
page.sysauth_authenticator = "htmlauth"
This will present us with a login form that accepts other usernames than root, unlike the normal login screen. The "htmlauth" authenticators role is to actually show us the login form, and give a web browser user the chance to login. Maybe it can be left out, since we're looking for a way to call this in a automated way.
The htmlauth authenticator authenticates against system users.
Creating a user is not so straightforward in OpenWRT, because it doesn't ship tools like adduser
/useradd
.
Those are hidden in the package shadow-useradd
:
opkg update
opkg install shadow-useradd
useradd wol
passwd wol
The documentation to the previous login method mentions that we can use a cookie store, instead of the explicit token authentication. Unfortunately the RPC API seems to be unrelated to what is going on with LuCI otherwise, so we can't use that.
So just using the Web API, we do it in 2 calls with curl
, and a ./cookiejar to store the auth cookie:
curl -X POST -c ./cookiejar \
http://172.16.42.1/cgi-bin/luci/wol \
-F luci_username=wol -F luci_password=wol
curl -X POST -b ./cookiejar http://172.16.42.1/cgi-bin/luci/wol -F luci_username=wol -F luci_password=wol -F cbid.wol.1.iface=br-lan -F cbid.wol.1.mac=00:E0:61:13:0C:39 -F token=a6da536ce841ddf2a0100c07b44c1b10 -F cbid.wol.1.broadcast=0 -F cbi.submit=1
The -F field of curl comes really handy to auto-format form-queries, but unfortunately, the token=
field must be filled
with a magic value scraped from the previous HTML response.
Unfortunately, this behaviour is an important migitation against Cross Site Request Forgery.
Searching for the "wol"
implementation behind the form, we find model/cbi/wol.lua.
The idea is to find the routine to call directly with the right parameters from the controller.
The main logic happens in host.write()
, which wraps the WoL operation in a write access to a virtual host variable,
as represented by the CBI Form, which transports various parameters like the MAC address.
The function has some logic to select between available binaries, but i just went with etherwake
, which i manually installed on the system. In the end, it's a simple system call through io.popen()
. So we can get rid of the whole dependency on the luci-app-wol:
io.popen("/usr/bin/etherwake 14:02:EC:31:3D:20")
Also I realized that the call()
would trigger our function on GET requests, which violates HTTP conventions.
post()
is perfect to filter the method.
The new controller/hq.lua
now implements this independently, with space for other HQ APIs to be added in the future.
I also created a different API endpoint group /hq
, which can wake the guardian by POST to /hq/wake
.
module("luci.controller.hq", package.seeall)
function index()
entry({"hq"}, nil, _{"HQ API"}, 90)
entry({"hq", "wake"}, post("wake"), _("Wake HQ Guardian"), 90)
end
function wake()
--print("HELLO, WORLD")
local cmd = "/usr/bin/etherwake 14:02:EC:31:3D:20"
io.popen(cmd)
end
In the end,
Unfortunately, public traffic to the Webinterface is still firewalled on my OpenWRT router. Heck, I didn't even configure HTTPS for LuCI yet! Though it is available through a wireguarded backdoor, reachable from my VPN.
So, if you're in the friendly zone and have IPv6, you might be able to annoy me at night with:
curl -v -X POST "http://[2001:470:5082::]"/cgi-bin/luci/hq/wake
This can be executed on my webserver, and start building a remote home control panel :D A satisfying result.