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/>.
27 #include <sys/types.h>
28 #include <sys/socket.h>
31 #include <linux/netlink.h>
32 #include <linux/rtnetlink.h>
34 #include <mosquitto.h>
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"
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 */
64 struct mac_entry macs[MAX_MACS];
68 bool want_shutdown = false;
70 void shutdown_request(int signal)
75 bool mac_compare(uint8_t *a, uint8_t *b)
79 for (i = 0; i < 6; i++)
84 printf("Matched: %02x:%02x:%02x:%02x:%02x:%02x\n",
91 int mqtt_mac_presence(struct ma_config *config, struct mosquitto *mosq,
92 uint8_t *mac, bool present)
102 while (i < MAX_MACS && config->macs[i].valid) {
103 if (mac_compare(mac, config->macs[i].mac))
108 if (i >= MAX_MACS || !config->macs[i].valid)
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)
116 config->macs[i].last_reported = t;
118 snprintf(topic, sizeof(topic),
119 "%s/%02X:%02X:%02X:%02X:%02X:%02X",
121 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
124 printf("Publishing to %s\n", topic);
127 ret = mosquitto_publish(mosq, NULL, topic,
128 strlen(config->location), config->location,
131 ret = mosquitto_publish(mosq, NULL, topic,
132 strlen("unknown"), "unknown", 0, 0);
137 void prune_macs(struct ma_config *config, struct mosquitto *mosq)
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;
158 void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level,
162 printf("%i:%s\n", level, str);
165 void main_loop(struct ma_config *config, struct mosquitto *mosq, int sock)
169 struct nlmsghdr *hdr;
175 hdr = (struct nlmsghdr *) buf;
176 nd = (struct ndmsg *) (hdr + 1);
177 while (!want_shutdown) {
178 received = recv(sock, buf, sizeof(buf), 0);
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,
188 switch (hdr->nlmsg_type) {
191 printf(" Family: %d, interface: %d, "
192 "state: %x, flags: %x, type: %x\n",
193 nd->ndm_family, /* AF_INET etc */
195 nd->ndm_state, /* NUD_REACHABLE etc */
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,
207 attr = (struct nlattr *) (((uint8_t *) attr) +
208 NLA_ALIGN(attr->nla_len));
214 printf("Unknown message type: %d\n", hdr->nlmsg_type);
217 prune_macs(config, mosq);
222 struct mosquitto *mqtt_init(struct ma_config *config)
224 struct mosquitto *mosq;
227 mosquitto_lib_init();
228 mosq = mosquitto_new("mqtt-arp", true, NULL);
230 printf("Couldn't allocate mosquitto structure\n");
234 mosquitto_log_callback_set(mosq, mosq_log_callback);
236 /* DTRT if username is NULL */
237 mosquitto_username_pw_set(mosq,
238 config->mqtt_username,
239 config->mqtt_password);
241 mosquitto_tls_set(mosq, config->capath,
242 NULL, NULL, NULL, NULL);
244 ret = mosquitto_connect(mosq, config->mqtt_host,
245 config->mqtt_port, 60);
247 printf("Unable to connect to MQTT server.\n");
251 ret = mosquitto_loop_start(mosq);
253 printf("Unable to start Mosquitto loop.\n");
260 int netlink_init(void)
263 struct sockaddr_nl group_addr;
265 bzero(&group_addr, sizeof(group_addr));
266 group_addr.nl_family = AF_NETLINK;
267 group_addr.nl_pid = getpid();
268 group_addr.nl_groups = RTMGRP_NEIGH;
270 sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
272 perror("Couldn't open netlink socket");
276 if (bind(sock, (struct sockaddr *) &group_addr,
277 sizeof(group_addr)) < 0) {
278 perror("Failed to bind to netlink socket");
285 struct option long_options[] = {
286 { "capath", required_argument, 0, 'c' },
287 { "host", required_argument, 0, 'h' },
288 { "location", required_argument, 0, 'l' },
289 { "mac", required_argument, 0, 'm' },
290 { "password", required_argument, 0, 'P' },
291 { "port", required_argument, 0, 'p' },
292 { "topic", required_argument, 0, 't' },
293 { "username", required_argument, 0, 'u' },
294 { "verbose", no_argument, 0, 'v' },
298 int main(int argc, char *argv[])
301 struct mosquitto *mosq;
302 struct ma_config config;
303 int option_index = 0;
307 bzero(&config, sizeof(config));
308 config.mqtt_port = MQTT_PORT;
311 c = getopt_long(argc, argv, "c:h:l:m:p:P:t:u:v",
312 long_options, &option_index);
319 config.capath = optarg;
322 config.mqtt_host = optarg;
325 config.location = optarg;
328 if (macs >= MAX_MACS) {
329 printf("Can only accept %d MAC addresses to"
330 " watch for.\n", MAX_MACS);
334 "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
335 &config.macs[macs].mac[0],
336 &config.macs[macs].mac[1],
337 &config.macs[macs].mac[2],
338 &config.macs[macs].mac[3],
339 &config.macs[macs].mac[4],
340 &config.macs[macs].mac[5]);
341 config.macs[macs].valid = true;
345 config.mqtt_port = atoi(optarg);
348 config.mqtt_password = optarg;
351 config.mqtt_topic = optarg;
354 config.mqtt_username = optarg;
360 printf("Unrecognized option: %c\n", c);
365 if (!config.mqtt_host)
366 config.mqtt_host = MQTT_HOST;
367 if (!config.mqtt_topic)
368 config.mqtt_host = MQTT_TOPIC;
369 if (!config.location)
370 config.mqtt_host = LOCATION;
372 signal(SIGTERM, shutdown_request);
374 sock = netlink_init();
375 mosq = mqtt_init(&config);
377 main_loop(&config, mosq, sock);
379 mosquitto_disconnect(mosq);
380 mosquitto_loop_stop(mosq, true);
381 mosquitto_destroy(mosq);
382 mosquitto_lib_cleanup();