]> the.earth.li Git - mqtt-arp.git/blob - mqtt-arp.c
de0fcccea27cce24f0f2b4ffb3aeb6fcda8cc2b0
[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 <getopt.h>
20 #include <signal.h>
21 #include <stdbool.h>
22 #include <stdint.h>
23 #include <stdlib.h>
24 #include <stdio.h>
25 #include <string.h>
26 #include <strings.h>
27 #include <sys/types.h>
28 #include <sys/socket.h>
29 #include <time.h>
30 #include <unistd.h>
31 #include <linux/netlink.h>
32 #include <linux/rtnetlink.h>
33
34 #include <mosquitto.h>
35
36 /* Defaults. All overridable from command line. */
37 #define MQTT_HOST       "mqtt-host"
38 #define MQTT_PORT       8883
39 #define MQTT_TOPIC      "location/by-mac"
40 #define LOCATION        "home"
41
42 /* How often (in seconds) to report that we see a device */
43 #define REPORT_INTERVAL (2 * 60)
44 /* How long to wait without seeing a device before reporting it's gone */
45 #define EXPIRY_TIME     (10 * 60)
46 /* Maximum number of MAC addresses to watch for */
47 #define MAX_MACS        8
48
49 struct mac_entry {
50         bool valid;
51         uint8_t mac[6];
52         time_t last_seen;
53         time_t last_reported;
54 };
55
56 struct ma_config {
57         char *mqtt_host;
58         int mqtt_port;
59         char *mqtt_username;
60         char *mqtt_password;
61         char *mqtt_topic;
62         char *location;
63         char *capath;
64         struct mac_entry macs[MAX_MACS];
65 };
66
67 bool debug = false;
68 bool want_shutdown = false;
69
70 void shutdown_request(int signal)
71 {
72         want_shutdown = true;
73 }
74
75 bool mac_compare(uint8_t *a, uint8_t *b)
76 {
77         int i;
78
79         for (i = 0; i < 6; i++)
80                 if (a[i] != b[i])
81                         return false;
82
83         if (debug)
84                 printf("Matched: %02x:%02x:%02x:%02x:%02x:%02x\n",
85                                 a[0], a[1], a[2],
86                                 a[3], a[4], a[5]);
87
88         return true;
89 }
90
91 int mqtt_mac_presence(struct ma_config *config, struct mosquitto *mosq,
92                 uint8_t *mac, bool present)
93 {
94         char topic[128];
95         int ret;
96         time_t t;
97         int i;
98
99         t = time(NULL);
100
101         i = 0;
102         while (i < MAX_MACS && config->macs[i].valid) {
103                 if (mac_compare(mac, config->macs[i].mac))
104                         break;
105                 i++;
106         }
107
108         if (i >= MAX_MACS || !config->macs[i].valid)
109                 return 0;
110
111         config->macs[i].last_seen = t;
112         /* Report no more often than every 2 minutes */
113         if (present && config->macs[i].last_reported + REPORT_INTERVAL > t)
114                 return 0;
115
116         config->macs[i].last_reported = t;
117
118         snprintf(topic, sizeof(topic),
119                 "%s/%02X:%02X:%02X:%02X:%02X:%02X",
120                 config->mqtt_topic,
121                 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
122
123         if (debug)
124                 printf("Publishing to %s\n", topic);
125
126         if (present)
127                 ret = mosquitto_publish(mosq, NULL, topic,
128                                 strlen(config->location), config->location,
129                                 0, 0);
130         else
131                 ret = mosquitto_publish(mosq, NULL, topic,
132                                 strlen("unknown"), "unknown", 0, 0);
133
134         return ret;
135 }
136
137 void prune_macs(struct ma_config *config, struct mosquitto *mosq)
138 {
139         time_t t;
140         int i;
141
142         t = time(NULL);
143
144         i = 0;
145         while (i < MAX_MACS && config->macs[i].valid) {
146                 /* Expire if we haven't seen MAC in EXPIRY_TIME */
147                 if (config->macs[i].last_seen &&
148                                 config->macs[i].last_seen + EXPIRY_TIME < t) {
149                         mqtt_mac_presence(config, mosq,
150                                         config->macs[i].mac, false);
151                         config->macs[i].last_seen = 0;
152                         config->macs[i].last_reported = 0;
153                 }
154                 i++;
155         }
156 }
157
158 void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level,
159                 const char *str)
160 {
161         if (debug)
162                 printf("%i:%s\n", level, str);
163 }
164
165 void main_loop(struct ma_config *config, struct mosquitto *mosq, int sock)
166 {
167         uint8_t buf[4096];
168         uint8_t *data;
169         struct nlmsghdr *hdr;
170         struct ndmsg *nd;
171         struct nlattr *attr;
172         ssize_t received;
173         time_t t;
174
175         hdr = (struct nlmsghdr *) buf;
176         nd = (struct ndmsg *) (hdr + 1);
177         while (!want_shutdown) {
178                 received = recv(sock, buf, sizeof(buf), 0);
179                 if (debug) {
180                         t = time(NULL);
181                         printf("%sReceived %zd bytes:\n", ctime(&t), received);
182                         printf("  Len: %d, type: %d, flags: %x, "
183                                 "seq: %d, pid: %d\n",
184                                 hdr->nlmsg_len, hdr->nlmsg_type,
185                                 hdr->nlmsg_flags, hdr->nlmsg_seq,
186                                 hdr->nlmsg_pid);
187                 }
188                 switch (hdr->nlmsg_type) {
189                 case RTM_NEWNEIGH:
190                         if (debug) {
191                                 printf("  Family: %d, interface: %d, "
192                                         "state: %x, flags: %x, type: %x\n",
193                                         nd->ndm_family, /* AF_INET etc */
194                                         nd->ndm_ifindex,
195                                         nd->ndm_state, /* NUD_REACHABLE etc */
196                                         nd->ndm_flags,
197                                         nd->ndm_type);
198                         }
199                         attr = (struct nlattr *) (nd + 1);
200                         while (((uint8_t *) attr - buf) < hdr->nlmsg_len) {
201                                 data = (((uint8_t *) attr) + NLA_HDRLEN);
202                                 if (attr->nla_type == NDA_LLADDR &&
203                                         nd->ndm_state == NUD_REACHABLE) {
204                                         mqtt_mac_presence(config, mosq,
205                                                         data, true);
206                                 }
207                                 attr = (struct nlattr *) (((uint8_t *) attr) +
208                                                 NLA_ALIGN(attr->nla_len));
209                         }
210                         break;
211                 case RTM_DELNEIGH:
212                 case RTM_GETNEIGH:
213                         break;
214                 default:
215                         printf("Unknown message type: %d\n", hdr->nlmsg_type);
216                 }
217
218                 prune_macs(config, mosq);
219         }
220
221 }
222
223 struct mosquitto *mqtt_init(struct ma_config *config)
224 {
225         struct mosquitto *mosq;
226         int ret;
227
228         mosquitto_lib_init();
229         mosq = mosquitto_new("mqtt-arp", true, NULL);
230         if (!mosq) {
231                 printf("Couldn't allocate mosquitto structure\n");
232                 exit(EXIT_FAILURE);
233         }
234
235         mosquitto_log_callback_set(mosq, mosq_log_callback);
236
237         /* DTRT if username is NULL */
238         mosquitto_username_pw_set(mosq,
239                         config->mqtt_username,
240                         config->mqtt_password);
241         if (config->capath)
242                 mosquitto_tls_set(mosq, config->capath,
243                                 NULL, NULL, NULL, NULL);
244
245         ret = mosquitto_connect(mosq, config->mqtt_host,
246                         config->mqtt_port, 60);
247         if (ret) {
248                 printf("Unable to connect to MQTT server.\n");
249                 exit(EXIT_FAILURE);
250         }
251
252         ret = mosquitto_loop_start(mosq);
253         if (ret) {
254                 printf("Unable to start Mosquitto loop.\n");
255                 exit(EXIT_FAILURE);
256         }
257
258         return mosq;
259 }
260
261 int netlink_init(void)
262 {
263         int sock;
264         struct sockaddr_nl group_addr;
265
266         bzero(&group_addr, sizeof(group_addr));
267         group_addr.nl_family = AF_NETLINK;
268         group_addr.nl_pid = getpid();
269         group_addr.nl_groups = RTMGRP_NEIGH;
270
271         sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
272         if (sock < 0) {
273                 perror("Couldn't open netlink socket");
274                 exit(EXIT_FAILURE);
275         }
276
277         if (bind(sock, (struct sockaddr *) &group_addr,
278                         sizeof(group_addr)) < 0) {
279                 perror("Failed to bind to netlink socket");
280                 exit(EXIT_FAILURE);
281         }
282
283         return sock;
284 }
285
286 struct option long_options[] = {
287         { "capath", required_argument, 0, 'c' },
288         { "host", required_argument, 0, 'h' },
289         { "location", required_argument, 0, 'l' },
290         { "mac", required_argument, 0, 'm' },
291         { "password", required_argument, 0, 'P' },
292         { "port", required_argument, 0, 'p' },
293         { "topic", required_argument, 0, 't' },
294         { "username", required_argument, 0, 'u' },
295         { "verbose", no_argument, 0, 'v' },
296         { 0, 0, 0, 0 }
297 };
298
299 int main(int argc, char *argv[])
300 {
301         int sock;
302         struct mosquitto *mosq;
303         struct ma_config config;
304         int option_index = 0;
305         int macs = 0;
306         char c;
307
308         bzero(&config, sizeof(config));
309         config.mqtt_port = MQTT_PORT;
310
311         while (1) {
312                 c = getopt_long(argc, argv, "c:h:l:m:p:P:t:u:v",
313                                 long_options, &option_index);
314
315                 if (c == -1)
316                         break;
317
318                 switch (c) {
319                 case 'c':
320                         config.capath = optarg;
321                         break;
322                 case 'h':
323                         config.mqtt_host = optarg;
324                         break;
325                 case 'l':
326                         config.location = optarg;
327                         break;
328                 case 'm':
329                         if (macs >= MAX_MACS) {
330                                 printf("Can only accept %d MAC addresses to"
331                                         " watch for.\n", MAX_MACS);
332                                 exit(EXIT_FAILURE);
333                         }
334                         sscanf(optarg,
335                                 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
336                                 &config.macs[macs].mac[0],
337                                 &config.macs[macs].mac[1],
338                                 &config.macs[macs].mac[2],
339                                 &config.macs[macs].mac[3],
340                                 &config.macs[macs].mac[4],
341                                 &config.macs[macs].mac[5]);
342                         config.macs[macs].valid = true;
343                         macs++;
344                         break;
345                 case 'p':
346                         config.mqtt_port = atoi(optarg);
347                         break;
348                 case 'P':
349                         config.mqtt_password = optarg;
350                         break;
351                 case 't':
352                         config.mqtt_topic = optarg;
353                         break;
354                 case 'u':
355                         config.mqtt_username = optarg;
356                         break;
357                 case 'v':
358                         debug = true;
359                         break;
360                 default:
361                         printf("Unrecognized option: %c\n", c);
362                         exit(EXIT_FAILURE);
363                 }
364         }
365
366         if (!config.mqtt_host)
367                 config.mqtt_host = MQTT_HOST;
368         if (!config.mqtt_topic)
369                 config.mqtt_topic = MQTT_TOPIC;
370         if (!config.location)
371                 config.location = LOCATION;
372
373         signal(SIGTERM, shutdown_request);
374
375         sock = netlink_init();
376         mosq = mqtt_init(&config);
377
378         main_loop(&config, mosq, sock);
379
380         mosquitto_disconnect(mosq);
381         mosquitto_loop_stop(mosq, true);
382         mosquitto_destroy(mosq);
383         mosquitto_lib_cleanup();
384         close(sock);
385 }