A Short Message from our Server#
Sat, 17 Sep 2022 13:50:04 +0000
Still on the monitoring-things kick, I decided it would be useful to set up alerting. As this network is on the wrong end of a domestic internet connection, I want the alerts to continue to operate even when the internet is unreachable (that might even be one of the things I want to be alerted about!) so I reached for a "wireless broadband" USB dongle with the intent of making it send SMSes.
The Grafana Alerting system out of the box supports a bunch of proprietary systems (Teams, Discord, Hangouts, Slack, Telegram, etc) plus Email, and also "Webhook" - which amounts to "give us a URL and we'll POST a payload to it when things break".
TL;DR I needed to create a web service that sends text messages when people hit it.
Here it is: grafana-sms-alert
I chose to write this in Fennel to see how it holds up to the kind of half-assed sysadminny scripty programming for which I'd usually default to Ruby (and once upon a time, Perl) for. Pretty well, is the answer.
HTTP
Heavy lifting in the HTTP end is performed by lua-http which is way overfeatured for this simple task (even has TLS!) but it's packaged in Nixpkgs so it was an easy choice. The "getting started" section of the docs has an example of using it as a client but not as a server, but happily there's a whole directory of examples in the source distribution that cover that.
JSON parsing is via dkjson which again was chosen mostly just because Nix has already packaged it.
SMS
SMS sending using AT commands is a bit more involved than I expected based on reading a bunch of half-assed examples on the internet - look for AT+CMGS
using your favourite web search engine. The problem is that if your message contains any of a number of popular punctuation characters including {
, }
, [
, ]
, they come out as accented characters. This is because the GSM 7 bit character set only resembles ASCII in some areas, and these symbols aren't in those areas. To encode {
(ASCII 0x7b, LEFT CURLY BRACKET) in an SMS message you have to send the bytes 0x1b 0x28, which poses a problem because 0x1b is ESC, which causes the sending to abort.
The fix is to use "SMS PDU Mode" instead of Text Mode. This allows us to encode the message - and the destination phone number, and optionally the SMS service centre - as a long string of hex digits which means we can send any character in the GSM character set including the "extended" ones with the 0x1B prefix. It also requires us to pack the 7 bit characters into octets as a continuous stream (we don't send the high bit, meaning that 160 characters fit into 140 bytes) which required an exciting foray into Lua's bitwise operators. I'm sure this could be done more elegantly and/or more efficiently but I haven't yet figured out how.
Packaging
We use the Nixpkgs Lua Infrastructure to declare the dependencies (luaposix, dkjson, lua-http), then override the Nixpkgs fennel
derivation to make it use our lua-with-packages.
Inside a nix-shell we can run fennel main.fnl config.json
. But outside of that context we'll need an executable of some form: we use makeWrapper
to create a script that runs fennel with the additional flags that tell it where to find the fennel modules that comprise the app.
Starting it as a service is then fairly straightforward: create a module that looks something like the example and update the configuration attributes for your own device path, SMS service centre and phone number.
A note about Huawei devices
Huawei USB stick modems can be configured to operate in a number of modes, and the probability is that when you plug it in you'll get the wrong one - it can manifest as a USB storage device with Windows drivers, or as a network device, or (what we need) as a modem to which we can send AT commands.
After reading far too many web sites on the subject I'm slightly fuzzy about what the "normal" behaviour is, so don't take this as gospel, but what I think happens is
- you insert the device, and it is recognised as USB storage - which is probably quite helpful if you're running Windows and otherwise not so much.
- if you have usb-modeswitch installed with its default rules, udev notices the new hardware and switches it to network mode.
- you try to switch it to TTY mode by running usb_modeswitch manually, which might work if step 2 didn't happen, but otherwise fails because it's already been switched once.
The workaround is to disable usb-modeswitch from running automatically (I leave you to find out how to do this yourself) and then to run it by hand. For me this is
usb_modeswitch -v 0x12d1 -p 0x1f01 -V 0x12d1 -P 0x14dc \
-M "55534243000000000000000000000011060000000000000000000000000000"
which works on the E3131 (3G dongle) and according to my notes also on the E3372 (LTE). But if it doesn't - well, as I think I already implied, at that point I was in a place of swearing a lot and trying things randomly, so this advice is worth what you paid for it.