2 * mqtt-arp.c - Watch the Linux ARP table to report device presence via MQTT
4 * Copyright 2018 Jonathan McDowell <noodles@earth.li>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
29 #include <sys/types.h>
30 #include <sys/socket.h>
33 #include <linux/netlink.h>
34 #include <linux/rtnetlink.h>
36 #include <mosquitto.h>
38 /* Defaults. All overridable from command line. */
39 #define MQTT_HOST "mqtt-host"
40 #define MQTT_PORT 8883
41 #define MQTT_TOPIC "location/by-mac"
42 #define LOCATION "home"
43 #define CONFIG_FILE "/etc/mqtt-arp.conf"
45 /* How often (in seconds) to report that we see a device */
46 #define REPORT_INTERVAL (2 * 60)
47 /* How long to wait without seeing a device before reporting it's gone */
48 #define EXPIRY_TIME (10 * 60)
49 /* Maximum number of MAC addresses to watch for */
67 struct mac_entry macs[MAX_MACS];
71 bool want_shutdown = false;
73 void shutdown_request(int signal)
78 bool mac_compare(uint8_t *a, uint8_t *b)
82 for (i = 0; i < 6; i++)
87 printf("Matched: %02x:%02x:%02x:%02x:%02x:%02x\n",
94 int mqtt_mac_presence(struct ma_config *config, struct mosquitto *mosq,
95 uint8_t *mac, bool present)
105 while (i < MAX_MACS && config->macs[i].valid) {
106 if (mac_compare(mac, config->macs[i].mac))
111 if (i >= MAX_MACS || !config->macs[i].valid)
114 config->macs[i].last_seen = t;
115 /* Report no more often than every 2 minutes */
116 if (present && config->macs[i].last_reported + REPORT_INTERVAL > t)
119 config->macs[i].last_reported = t;
121 snprintf(topic, sizeof(topic),
122 "%s/%02X:%02X:%02X:%02X:%02X:%02X",
124 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
127 printf("Publishing to %s\n", topic);
130 ret = mosquitto_publish(mosq, NULL, topic,
131 strlen(config->location), config->location,
134 ret = mosquitto_publish(mosq, NULL, topic,
135 strlen("unknown"), "unknown", 0, 0);
140 void prune_macs(struct ma_config *config, struct mosquitto *mosq)
148 while (i < MAX_MACS && config->macs[i].valid) {
149 /* Expire if we haven't seen MAC in EXPIRY_TIME */
150 if (config->macs[i].last_seen &&
151 config->macs[i].last_seen + EXPIRY_TIME < t) {
152 mqtt_mac_presence(config, mosq,
153 config->macs[i].mac, false);
154 config->macs[i].last_seen = 0;
155 config->macs[i].last_reported = 0;
161 void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level,
165 printf("%i:%s\n", level, str);
168 void main_loop(struct ma_config *config, struct mosquitto *mosq, int sock)
172 struct nlmsghdr *hdr;
178 hdr = (struct nlmsghdr *) buf;
179 nd = (struct ndmsg *) (hdr + 1);
180 while (!want_shutdown) {
181 received = recv(sock, buf, sizeof(buf), 0);
184 printf("%sReceived %zd bytes:\n", ctime(&t), received);
185 printf(" Len: %d, type: %d, flags: %x, "
186 "seq: %d, pid: %d\n",
187 hdr->nlmsg_len, hdr->nlmsg_type,
188 hdr->nlmsg_flags, hdr->nlmsg_seq,
191 switch (hdr->nlmsg_type) {
194 printf(" Family: %d, interface: %d, "
195 "state: %x, flags: %x, type: %x\n",
196 nd->ndm_family, /* AF_INET etc */
198 nd->ndm_state, /* NUD_REACHABLE etc */
202 attr = (struct nlattr *) (nd + 1);
203 while (((uint8_t *) attr - buf) < hdr->nlmsg_len) {
204 data = (((uint8_t *) attr) + NLA_HDRLEN);
205 if (attr->nla_type == NDA_LLADDR &&
206 nd->ndm_state == NUD_REACHABLE) {
207 mqtt_mac_presence(config, mosq,
210 attr = (struct nlattr *) (((uint8_t *) attr) +
211 NLA_ALIGN(attr->nla_len));
218 printf("Unknown message type: %d\n", hdr->nlmsg_type);
221 prune_macs(config, mosq);
226 struct mosquitto *mqtt_init(struct ma_config *config)
228 struct mosquitto *mosq;
231 mosquitto_lib_init();
232 mosq = mosquitto_new("mqtt-arp", true, NULL);
234 printf("Couldn't allocate mosquitto structure\n");
238 mosquitto_log_callback_set(mosq, mosq_log_callback);
240 /* DTRT if username is NULL */
241 mosquitto_username_pw_set(mosq,
242 config->mqtt_username,
243 config->mqtt_password);
245 mosquitto_tls_set(mosq, config->capath,
246 NULL, NULL, NULL, NULL);
248 ret = mosquitto_connect(mosq, config->mqtt_host,
249 config->mqtt_port, 60);
251 printf("Unable to connect to MQTT server.\n");
255 ret = mosquitto_loop_start(mosq);
257 printf("Unable to start Mosquitto loop.\n");
264 int netlink_init(void)
267 struct sockaddr_nl group_addr;
269 bzero(&group_addr, sizeof(group_addr));
270 group_addr.nl_family = AF_NETLINK;
271 group_addr.nl_pid = getpid();
272 group_addr.nl_groups = RTMGRP_NEIGH;
274 sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
276 perror("Couldn't open netlink socket");
280 if (bind(sock, (struct sockaddr *) &group_addr,
281 sizeof(group_addr)) < 0) {
282 perror("Failed to bind to netlink socket");
289 int read_config(char *file, struct ma_config *config, int *macs)
295 f = fopen(file, "r");
297 fprintf(stderr, "Could not read config file %s\n", file);
301 #define INT_OPTION(opt, var) \
302 if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
303 var = atoi(&line[sizeof(opt)]); \
305 #define STRING_OPTION(opt, var) \
306 if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
307 var = strdup(&line[sizeof(opt)]); \
310 while (fgets(line, sizeof(line), f) != NULL) {
311 for (i = strlen(line) - 1; i >= 0 && isspace(line[i]); i--)
313 if (line[0] == '\0' || line[0] == '#')
316 if (strncmp(line, "mac ", 4) == 0) {
317 if (*macs >= MAX_MACS) {
318 printf("Can only accept %d MAC addresses to"
319 " watch for.\n", MAX_MACS);
323 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
324 &config->macs[*macs].mac[0],
325 &config->macs[*macs].mac[1],
326 &config->macs[*macs].mac[2],
327 &config->macs[*macs].mac[3],
328 &config->macs[*macs].mac[4],
329 &config->macs[*macs].mac[5]);
330 config->macs[*macs].valid = true;
333 STRING_OPTION("mqtt_host", config->mqtt_host) else
334 INT_OPTION("mqtt_port", config->mqtt_port) else
335 STRING_OPTION("mqtt_user", config->mqtt_username) else
336 STRING_OPTION("mqtt_pass", config->mqtt_password) else
337 STRING_OPTION("mqtt_topic", config->mqtt_topic) else
338 STRING_OPTION("location", config->location) else
339 STRING_OPTION("capath", config->capath)
346 void override_config(const struct ma_config *source, struct ma_config *target)
350 if (source->mqtt_host != NULL) {
351 target->mqtt_host = source->mqtt_host;
353 if (source->mqtt_port != 0) {
354 target->mqtt_port = source->mqtt_port;
356 if (source->mqtt_username != NULL) {
357 target->mqtt_username = source->mqtt_username;
359 if (source->mqtt_password != NULL) {
360 target->mqtt_password = source->mqtt_password;
362 if (source->mqtt_topic != NULL) {
363 target->mqtt_topic = source->mqtt_topic;
365 if (source->location != NULL) {
366 target->location = source->location;
368 if (source->capath != NULL) {
369 target->capath = source->capath;
371 for (i = 0; i < MAX_MACS; ++i) {
372 if (source->macs[i].valid) {
373 memcpy(&target->macs[i], &source->macs[i], sizeof(struct mac_entry));
378 void print_config(const struct ma_config *config)
383 printf("mqtt_host: %s\n", config->mqtt_host ? config->mqtt_host : "NULL");
384 printf("mqtt_port: %d\n", config->mqtt_port);
385 printf("mqtt_username: %s\n", config->mqtt_username ? config->mqtt_username : "NULL");
386 printf("mqtt_password: %s\n", config->mqtt_password ? config->mqtt_password : "NULL");
387 printf("mqtt_topic: %s\n", config->mqtt_topic ? config->mqtt_topic : "NULL");
388 printf("location: %s\n", config->location ? config->location : "NULL");
389 printf("capath: %s\n", config->capath ? config->capath : "NULL");
391 for (i = 0; i < MAX_MACS; ++i) {
392 if (config->macs[i].valid) {
393 printf("macs[%d]: { valid: true, mac: ", i);
394 for (j = 0; j < 6; ++j) {
395 printf("%02x", config->macs[i].mac[j]);
402 printf("macs[%d]: { valid: false }\n", i);
407 struct option long_options[] = {
408 { "capath", required_argument, 0, 'c' },
409 { "host", required_argument, 0, 'h' },
410 { "location", required_argument, 0, 'l' },
411 { "mac", required_argument, 0, 'm' },
412 { "password", required_argument, 0, 'P' },
413 { "port", required_argument, 0, 'p' },
414 { "topic", required_argument, 0, 't' },
415 { "username", required_argument, 0, 'u' },
416 { "verbose", no_argument, 0, 'v' },
417 { "configfile", required_argument, 0, 'f' },
421 int main(int argc, char *argv[])
424 struct mosquitto *mosq;
425 struct ma_config config;
426 struct ma_config cmdline_config;
427 int option_index = 0;
430 char *config_file = CONFIG_FILE;
432 bzero(&config, sizeof(config));
433 bzero(&cmdline_config, sizeof(cmdline_config));
434 config.mqtt_port = MQTT_PORT;
437 c = getopt_long(argc, argv, "c:h:l:m:p:P:t:u:f:v",
438 long_options, &option_index);
444 config_file = optarg;
447 cmdline_config.capath = optarg;
450 cmdline_config.mqtt_host = optarg;
453 cmdline_config.location = optarg;
456 if (macs >= MAX_MACS) {
457 printf("Can only accept %d MAC addresses to"
458 " watch for.\n", MAX_MACS);
462 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
463 &cmdline_config.macs[macs].mac[0],
464 &cmdline_config.macs[macs].mac[1],
465 &cmdline_config.macs[macs].mac[2],
466 &cmdline_config.macs[macs].mac[3],
467 &cmdline_config.macs[macs].mac[4],
468 &cmdline_config.macs[macs].mac[5]);
469 cmdline_config.macs[macs].valid = true;
473 cmdline_config.mqtt_port = atoi(optarg);
476 cmdline_config.mqtt_password = optarg;
479 cmdline_config.mqtt_topic = optarg;
482 cmdline_config.mqtt_username = optarg;
488 printf("Unrecognized option: %c\n", c);
493 read_config(config_file, &config, &macs);
495 override_config(&cmdline_config, &config);
497 if (!config.mqtt_host)
498 config.mqtt_host = MQTT_HOST;
499 if (!config.mqtt_topic)
500 config.mqtt_topic = MQTT_TOPIC;
501 if (!config.location)
502 config.location = LOCATION;
505 print_config(&config);
507 signal(SIGTERM, shutdown_request);
509 sock = netlink_init();
510 mosq = mqtt_init(&config);
512 main_loop(&config, mosq, sock);
514 mosquitto_disconnect(mosq);
515 mosquitto_loop_stop(mosq, true);
516 mosquitto_destroy(mosq);
517 mosquitto_lib_cleanup();