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