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/>.
26 #include <sys/types.h>
27 #include <sys/socket.h>
30 #include <linux/netlink.h>
31 #include <linux/rtnetlink.h>
33 #include <mosquitto.h>
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"
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 */
63 struct mac_entry macs[MAX_MACS];
68 bool mac_compare(uint8_t *a, uint8_t *b)
72 for (i = 0; i < 6; i++)
77 printf("Matched: %02x:%02x:%02x:%02x:%02x:%02x\n",
84 int mqtt_mac_presence(struct ma_config *config, struct mosquitto *mosq,
85 uint8_t *mac, bool present)
95 while (i < MAX_MACS && config->macs[i].valid) {
96 if (mac_compare(mac, config->macs[i].mac))
101 if (i >= MAX_MACS || !config->macs[i].valid)
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)
109 config->macs[i].last_reported = t;
111 snprintf(topic, sizeof(topic),
112 "%s/%02X:%02X:%02X:%02X:%02X:%02X",
114 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
117 printf("Publishing to %s\n", topic);
120 ret = mosquitto_publish(mosq, NULL, topic,
121 strlen(config->location), config->location,
124 ret = mosquitto_publish(mosq, NULL, topic,
125 strlen("unknown"), "unknown", 0, 0);
130 void prune_macs(struct ma_config *config, struct mosquitto *mosq)
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;
151 void mosq_log_callback(struct mosquitto *mosq, void *userdata, int level,
155 printf("%i:%s\n", level, str);
158 void main_loop(struct ma_config *config, struct mosquitto *mosq, int sock)
162 struct nlmsghdr *hdr;
168 hdr = (struct nlmsghdr *) buf;
169 nd = (struct ndmsg *) (hdr + 1);
171 received = recv(sock, buf, sizeof(buf), 0);
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,
181 switch (hdr->nlmsg_type) {
184 printf(" Family: %d, interface: %d, "
185 "state: %x, flags: %x, type: %x\n",
186 nd->ndm_family, /* AF_INET etc */
188 nd->ndm_state, /* NUD_REACHABLE etc */
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,
200 attr = (struct nlattr *)
201 (((uint8_t *) attr) + attr->nla_len);
207 printf("Unknown message type: %d\n", hdr->nlmsg_type);
210 prune_macs(config, mosq);
215 struct mosquitto *mqtt_init(struct ma_config *config)
217 struct mosquitto *mosq;
220 mosquitto_lib_init();
221 mosq = mosquitto_new("mqtt-arp", true, NULL);
223 printf("Couldn't allocate mosquitto structure\n");
227 mosquitto_log_callback_set(mosq, mosq_log_callback);
229 /* DTRT if username is NULL */
230 mosquitto_username_pw_set(mosq,
231 config->mqtt_username,
232 config->mqtt_password);
234 mosquitto_tls_set(mosq, config->capath,
235 NULL, NULL, NULL, NULL);
237 ret = mosquitto_connect(mosq, config->mqtt_host,
238 config->mqtt_port, 60);
240 printf("Unable to connect to MQTT server.\n");
244 ret = mosquitto_loop_start(mosq);
246 printf("Unable to start Mosquitto loop.\n");
253 int netlink_init(void)
256 struct sockaddr_nl group_addr;
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;
263 sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
265 perror("Couldn't open netlink socket");
269 if (bind(sock, (struct sockaddr *) &group_addr,
270 sizeof(group_addr)) < 0) {
271 perror("Failed to bind to netlink socket");
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' },
289 int main(int argc, char *argv[])
292 struct mosquitto *mosq;
293 struct ma_config config;
294 int option_index = 0;
298 bzero(&config, sizeof(config));
299 config.mqtt_port = MQTT_PORT;
302 c = getopt_long(argc, argv, "c:h:l:m:p:t:v",
303 long_options, &option_index);
310 config.capath = optarg;
313 config.mqtt_host = optarg;
316 config.location = optarg;
319 if (macs >= MAX_MACS) {
320 printf("Can only accept %d MAC addresses to"
321 " watch for.\n", MAX_MACS);
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]);
334 config.mqtt_port = atoi(optarg);
337 config.mqtt_topic = optarg;
343 printf("Unrecognized option: %c\n", c);
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;
355 sock = netlink_init();
356 mosq = mqtt_init(&config);
358 main_loop(&config, mosq, sock);