]> the.earth.li Git - mqtt-arp.git/blob - mqtt-arp.c
Updated README
[mqtt-arp.git] / mqtt-arp.c
1 /*
2  * mqtt-arp.c - Watch the Linux ARP table to report device presence via MQTT
3  *
4  * Copyright 2018 Jonathan McDowell <noodles@earth.li>
5  *
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.
10  *
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.
15  *
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/>.
18  */
19 #include <ctype.h>
20 #include <errno.h>
21 #include <getopt.h>
22 #include <signal.h>
23 #include <stdbool.h>
24 #include <stdint.h>
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include <string.h>
28 #include <strings.h>
29 #include <sys/types.h>
30 #include <sys/socket.h>
31 #include <time.h>
32 #include <unistd.h>
33 #include <linux/netlink.h>
34 #include <linux/rtnetlink.h>
35
36 #include <mosquitto.h>
37
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"
44
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 */
50 #define MAX_MACS        8
51
52 struct mac_entry {
53         bool valid;
54         uint8_t mac[6];
55         time_t last_seen;
56         time_t last_reported;
57 };
58
59 struct ma_config {
60         char *mqtt_host;
61         int mqtt_port;
62         char *mqtt_username;
63         char *mqtt_password;
64         char *mqtt_topic;
65         char *location;
66         char *capath;
67         struct mac_entry macs[MAX_MACS];
68 };
69
70 bool debug = false;
71 bool want_shutdown = false;
72
73 void shutdown_request(int signal)
74 {
75         want_shutdown = true;
76 }
77
78 bool mac_compare(uint8_t *a, uint8_t *b)
79 {
80         int i;
81
82         for (i = 0; i < 6; i++)
83                 if (a[i] != b[i])
84                         return false;
85
86         if (debug)
87                 printf("Matched: %02x:%02x:%02x:%02x:%02x:%02x\n",
88                                 a[0], a[1], a[2],
89                                 a[3], a[4], a[5]);
90
91         return true;
92 }
93
94 int mqtt_mac_presence(struct ma_config *config, struct mosquitto *mosq,
95                 uint8_t *mac, bool present)
96 {
97         char topic[128];
98         int ret;
99         time_t t;
100         int i;
101
102         t = time(NULL);
103
104         i = 0;
105         while (i < MAX_MACS && config->macs[i].valid) {
106                 if (mac_compare(mac, config->macs[i].mac))
107                         break;
108                 i++;
109         }
110
111         if (i >= MAX_MACS || !config->macs[i].valid)
112                 return 0;
113
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)
117                 return 0;
118
119         config->macs[i].last_reported = t;
120
121         snprintf(topic, sizeof(topic),
122                 "%s/%02X:%02X:%02X:%02X:%02X:%02X",
123                 config->mqtt_topic,
124                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
125
126         if (debug)
127                 printf("Publishing to %s\n", topic);
128
129         if (present)
130                 ret = mosquitto_publish(mosq, NULL, topic,
131                                 strlen(config->location), config->location,
132                                 0, 0);
133         else
134                 ret = mosquitto_publish(mosq, NULL, topic,
135                                 strlen("unknown"), "unknown", 0, 0);
136
137         return ret;
138 }
139
140 void prune_macs(struct ma_config *config, struct mosquitto *mosq)
141 {
142         time_t t;
143         int i;
144
145         t = time(NULL);
146
147         i = 0;
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;
156                 }
157                 i++;
158         }
159 }
160
161 void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level,
162                 const char *str)
163 {
164         if (debug)
165                 printf("%i:%s\n", level, str);
166 }
167
168 void main_loop(struct ma_config *config, struct mosquitto *mosq, int sock)
169 {
170         uint8_t buf[4096];
171         uint8_t *data;
172         struct nlmsghdr *hdr;
173         struct ndmsg *nd;
174         struct nlattr *attr;
175         ssize_t received;
176         time_t t;
177
178         hdr = (struct nlmsghdr *) buf;
179         nd = (struct ndmsg *) (hdr + 1);
180         while (!want_shutdown) {
181                 received = recv(sock, buf, sizeof(buf), 0);
182                 if (debug) {
183                         t = time(NULL);
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,
189                                 hdr->nlmsg_pid);
190                 }
191                 switch (hdr->nlmsg_type) {
192                 case RTM_NEWNEIGH:
193                         if (debug) {
194                                 printf("  Family: %d, interface: %d, "
195                                         "state: %x, flags: %x, type: %x\n",
196                                         nd->ndm_family, /* AF_INET etc */
197                                         nd->ndm_ifindex,
198                                         nd->ndm_state, /* NUD_REACHABLE etc */
199                                         nd->ndm_flags,
200                                         nd->ndm_type);
201                         }
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,
208                                                         data, true);
209                                 }
210                                 attr = (struct nlattr *) (((uint8_t *) attr) +
211                                                 NLA_ALIGN(attr->nla_len));
212                         }
213                         break;
214                 case RTM_DELNEIGH:
215                 case RTM_GETNEIGH:
216                         break;
217                 default:
218                         printf("Unknown message type: %d\n", hdr->nlmsg_type);
219                 }
220
221                 prune_macs(config, mosq);
222         }
223
224 }
225
226 struct mosquitto *mqtt_init(struct ma_config *config)
227 {
228         struct mosquitto *mosq;
229         int ret;
230
231         mosquitto_lib_init();
232         mosq = mosquitto_new("mqtt-arp", true, NULL);
233         if (!mosq) {
234                 printf("Couldn't allocate mosquitto structure\n");
235                 exit(EXIT_FAILURE);
236         }
237
238         mosquitto_log_callback_set(mosq, mosq_log_callback);
239
240         /* DTRT if username is NULL */
241         mosquitto_username_pw_set(mosq,
242                         config->mqtt_username,
243                         config->mqtt_password);
244         if (config->capath)
245                 mosquitto_tls_set(mosq, config->capath,
246                                 NULL, NULL, NULL, NULL);
247
248         ret = mosquitto_connect(mosq, config->mqtt_host,
249                         config->mqtt_port, 60);
250         if (ret) {
251                 printf("Unable to connect to MQTT server.\n");
252                 exit(EXIT_FAILURE);
253         }
254
255         ret = mosquitto_loop_start(mosq);
256         if (ret) {
257                 printf("Unable to start Mosquitto loop.\n");
258                 exit(EXIT_FAILURE);
259         }
260
261         return mosq;
262 }
263
264 int netlink_init(void)
265 {
266         int sock;
267         struct sockaddr_nl group_addr;
268
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;
273
274         sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
275         if (sock < 0) {
276                 perror("Couldn't open netlink socket");
277                 exit(EXIT_FAILURE);
278         }
279
280         if (bind(sock, (struct sockaddr *) &group_addr,
281                         sizeof(group_addr)) < 0) {
282                 perror("Failed to bind to netlink socket");
283                 exit(EXIT_FAILURE);
284         }
285
286         return sock;
287 }
288
289 int read_config(char *file, struct ma_config *config, int *macs)
290 {
291         FILE *f;
292         char line[256];
293         int i;
294
295         f = fopen(file, "r");
296         if (f == NULL) {
297                 fprintf(stderr, "Could not read config file %s\n", file);
298                 return errno;
299         }
300
301 #define INT_OPTION(opt, var) \
302         if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
303                 var = atoi(&line[sizeof(opt)]);          \
304         }
305 #define STRING_OPTION(opt, var) \
306         if (strncmp(line, opt " ", sizeof(opt)) == 0) { \
307                 var = strdup(&line[sizeof(opt)]);       \
308         }
309
310         while (fgets(line, sizeof(line), f) != NULL) {
311                 for (i = strlen(line) - 1; i >= 0 && isspace(line[i]); i--)
312                         line[i] = '\0';
313                 if (line[0] == '\0' || line[0] == '#')
314                         continue;
315
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);
320                                 exit(EXIT_FAILURE);
321                         }
322                         sscanf(&line[4],
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;
331                         (*macs)++;
332                 } else
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)
340         }
341         fclose(f);
342
343         return 0;
344 }
345
346 void override_config(const struct ma_config *source, struct ma_config *target)
347 {
348         int i;
349
350         if (source->mqtt_host != NULL) {
351                 target->mqtt_host = source->mqtt_host;
352         }
353         if (source->mqtt_port != 0) {
354                 target->mqtt_port = source->mqtt_port;
355         }
356         if (source->mqtt_username != NULL) {
357                 target->mqtt_username = source->mqtt_username;
358         }
359         if (source->mqtt_password != NULL) {
360                 target->mqtt_password = source->mqtt_password;
361         }
362         if (source->mqtt_topic != NULL) {
363                 target->mqtt_topic = source->mqtt_topic;
364         }
365         if (source->location != NULL) {
366                 target->location = source->location;
367         }
368         if (source->capath != NULL) {
369                 target->capath = source->capath;
370         }
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));
374                 }
375         }
376 }
377
378 void print_config(const struct ma_config *config)
379 {
380         int i, j;
381
382         printf("Config:\n");
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");
390
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]);
396                                 if (j < 5) {
397                                         printf(":");
398                                 }
399                         }
400                         printf("\n");
401                 } else {
402                         printf("macs[%d]: { valid: false }\n", i);
403                 }
404         }
405 }
406
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' },
418         { 0, 0, 0, 0 }
419 };
420
421 int main(int argc, char *argv[])
422 {
423         int sock;
424         struct mosquitto *mosq;
425         struct ma_config config;
426         struct ma_config cmdline_config;
427         int option_index = 0;
428         int macs = 0;
429         int c;
430         char *config_file = CONFIG_FILE;
431
432         bzero(&config, sizeof(config));
433         bzero(&cmdline_config, sizeof(cmdline_config));
434         config.mqtt_port = MQTT_PORT;
435
436         while (1) {
437                 c = getopt_long(argc, argv, "c:h:l:m:p:P:t:u:f:v",
438                                 long_options, &option_index);
439
440                 if (c == -1)
441                         break;
442                 switch (c) {
443                 case 'f':
444                         config_file = optarg;
445                         break;
446                 case 'c':
447                         cmdline_config.capath = optarg;
448                         break;
449                 case 'h':
450                         cmdline_config.mqtt_host = optarg;
451                         break;
452                 case 'l':
453                         cmdline_config.location = optarg;
454                         break;
455                 case 'm':
456                         if (macs >= MAX_MACS) {
457                                 printf("Can only accept %d MAC addresses to"
458                                         " watch for.\n", MAX_MACS);
459                                 exit(EXIT_FAILURE);
460                         }
461                         sscanf(optarg,
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;
470                         macs++;
471                         break;
472                 case 'p':
473                         cmdline_config.mqtt_port = atoi(optarg);
474                         break;
475                 case 'P':
476                         cmdline_config.mqtt_password = optarg;
477                         break;
478                 case 't':
479                         cmdline_config.mqtt_topic = optarg;
480                         break;
481                 case 'u':
482                         cmdline_config.mqtt_username = optarg;
483                         break;
484                 case 'v':
485                         debug = true;
486                         break;
487                 default:
488                         printf("Unrecognized option: %c\n", c);
489                         exit(EXIT_FAILURE);
490                 }
491         }
492
493         read_config(config_file, &config, &macs);
494
495         override_config(&cmdline_config, &config);
496
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;
503
504         if (debug)
505                 print_config(&config);
506
507         signal(SIGTERM, shutdown_request);
508
509         sock = netlink_init();
510         mosq = mqtt_init(&config);
511
512         main_loop(&config, mosq, sock);
513
514         mosquitto_disconnect(mosq);
515         mosquitto_loop_stop(mosq, true);
516         mosquitto_destroy(mosq);
517         mosquitto_lib_cleanup();
518         close(sock);
519 }