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");
299 #define INT_OPTION(opt, var) \
300 if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
301 var = atoi(&line[sizeof(opt)]); \
303 #define STRING_OPTION(opt, var) \
304 if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
305 var = strdup(&line[sizeof(opt)]); \
308 while (fgets(line, sizeof(line), f) != NULL) {
309 for (i = strlen(line) - 1; i >= 0 && isspace(line[i]); i--)
311 if (line[0] == '\0' || line[0] == '#')
314 if (strncmp(line, "mac ", 4) == 0) {
315 if (*macs >= MAX_MACS) {
316 printf("Can only accept %d MAC addresses to"
317 " watch for.\n", MAX_MACS);
321 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
322 &config->macs[*macs].mac[0],
323 &config->macs[*macs].mac[1],
324 &config->macs[*macs].mac[2],
325 &config->macs[*macs].mac[3],
326 &config->macs[*macs].mac[4],
327 &config->macs[*macs].mac[5]);
328 config->macs[*macs].valid = true;
331 STRING_OPTION("mqtt_host", config->mqtt_host) else
332 INT_OPTION("mqtt_port", config->mqtt_port) else
333 STRING_OPTION("mqtt_user", config->mqtt_username) else
334 STRING_OPTION("mqtt_pass", config->mqtt_password) else
335 STRING_OPTION("mqtt_topic", config->mqtt_topic) else
336 STRING_OPTION("location", config->location) else
337 STRING_OPTION("capath", config->capath)
344 struct option long_options[] = {
345 { "capath", required_argument, 0, 'c' },
346 { "host", required_argument, 0, 'h' },
347 { "location", required_argument, 0, 'l' },
348 { "mac", required_argument, 0, 'm' },
349 { "password", required_argument, 0, 'P' },
350 { "port", required_argument, 0, 'p' },
351 { "topic", required_argument, 0, 't' },
352 { "username", required_argument, 0, 'u' },
353 { "verbose", no_argument, 0, 'v' },
357 int main(int argc, char *argv[])
360 struct mosquitto *mosq;
361 struct ma_config config;
362 int option_index = 0;
366 bzero(&config, sizeof(config));
367 config.mqtt_port = MQTT_PORT;
369 /* Read config before parsing command line */
370 read_config(CONFIG_FILE, &config, &macs);
373 c = getopt_long(argc, argv, "c:h:l:m:p:P:t:u:v",
374 long_options, &option_index);
381 config.capath = optarg;
384 config.mqtt_host = optarg;
387 config.location = optarg;
390 if (macs >= MAX_MACS) {
391 printf("Can only accept %d MAC addresses to"
392 " watch for.\n", MAX_MACS);
396 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
397 &config.macs[macs].mac[0],
398 &config.macs[macs].mac[1],
399 &config.macs[macs].mac[2],
400 &config.macs[macs].mac[3],
401 &config.macs[macs].mac[4],
402 &config.macs[macs].mac[5]);
403 config.macs[macs].valid = true;
407 config.mqtt_port = atoi(optarg);
410 config.mqtt_password = optarg;
413 config.mqtt_topic = optarg;
416 config.mqtt_username = optarg;
422 printf("Unrecognized option: %c\n", c);
427 if (!config.mqtt_host)
428 config.mqtt_host = MQTT_HOST;
429 if (!config.mqtt_topic)
430 config.mqtt_topic = MQTT_TOPIC;
431 if (!config.location)
432 config.location = LOCATION;
434 signal(SIGTERM, shutdown_request);
436 sock = netlink_init();
437 mosq = mqtt_init(&config);
439 main_loop(&config, mosq, sock);
441 mosquitto_disconnect(mosq);
442 mosquitto_loop_stop(mosq, true);
443 mosquitto_destroy(mosq);
444 mosquitto_lib_cleanup();