aboutsummaryrefslogtreecommitdiffstats
/*
 * (C) 2011-2024 by Christian Hesse <mail@eworm.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "netlink-notify.h"

const static char optstring[] = "ht:vV";
const static struct option options_long[] = {
	/* name		has_arg			flag	val */
	{ "help",	no_argument,		NULL,	'h' },
	{ "timeout",	required_argument,	NULL,	't' },
	{ "verbose",	no_argument,		NULL,	'v' },
	{ "version",	no_argument,		NULL,	'V' },
	{ 0, 0, 0, 0 }
};

char *program;
unsigned int maxinterface = 0;
struct ifs * ifs = NULL;
uint8_t verbose = 0;
uint8_t doexit = 0;
unsigned int notification_timeout = NOTIFICATION_TIMEOUT;

/*** free_addresses ***/
void free_addresses(struct addresses_seen *addresses_seen) {
	struct addresses_seen *next = NULL;

	/* free everything else */
	while (addresses_seen != NULL) {
		next = addresses_seen->next;
		free(addresses_seen);
		addresses_seen = next;
	}
}

/*** add_address ***/
struct addresses_seen * add_address(struct addresses_seen *addresses_seen, const char *address, const unsigned char prefix) {
	struct addresses_seen *first = addresses_seen;

	if (addresses_seen == NULL) {
		addresses_seen = malloc(sizeof(struct addresses_seen));
		strcpy(addresses_seen->address, address);
		addresses_seen->prefix = prefix;
		addresses_seen->next = NULL;
		return addresses_seen;
	}

	while (addresses_seen->next != NULL) {
		/* just find the last one */
		addresses_seen = addresses_seen->next;
	}

	addresses_seen->next = malloc(sizeof(struct addresses_seen));
	addresses_seen = addresses_seen->next;
	strcpy(addresses_seen->address, address);
	addresses_seen->prefix = prefix;
	addresses_seen->next = NULL;

	return first;
}

/*** remove_address ***/
struct addresses_seen * remove_address(struct addresses_seen *addresses_seen, const char *address, const unsigned char prefix) {
	struct addresses_seen *first = addresses_seen, *last = NULL;

	/* no addresses, just return NULL */
	if (addresses_seen == NULL)
		return NULL;

	/* first address matches, return new start */
	if (strcmp(addresses_seen->address, address) == 0 && addresses_seen->prefix == prefix) {
		first = addresses_seen->next;
		free(addresses_seen);
		return first;
	}

	/* find the address and remove it */
	while (addresses_seen != NULL) {
		if (strcmp(addresses_seen->address, address) == 0 && addresses_seen->prefix == prefix) {
			last->next = addresses_seen->next;
			free(addresses_seen);
			break;
		}
		last = addresses_seen;
		addresses_seen = addresses_seen->next;
	}
	return first;
}

/*** match_address ***/
int match_address(struct addresses_seen *addresses_seen, const char *address, const unsigned char prefix) {
	while (addresses_seen != NULL) {
		if (strcmp(addresses_seen->address, address) == 0 && addresses_seen->prefix == prefix) {
			return 1;
		}
		addresses_seen = addresses_seen->next;
	}
	return 0;
}

/*** list_addresses ***/
void list_addresses(struct addresses_seen *addresses_seen, const char *interface) {
	printf("%s: Addresses seen for interface %s:", program, interface);
	while (addresses_seen != NULL) {
		printf(" %s/%d", addresses_seen->address, addresses_seen->prefix);
		addresses_seen = addresses_seen->next;
	}
	putchar('\n');
}

/*** get_ssid ***/
void get_ssid(const char *interface, char *essid) {
	int sockfd;
	struct iwreq wreq;

	memset(&wreq, 0, sizeof(struct iwreq));
	snprintf(wreq.ifr_name, IFNAMSIZ, "%s", interface);
	wreq.u.essid.pointer = essid;
	wreq.u.essid.length = IW_ESSID_MAX_SIZE + 1;

	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
		return;

	ioctl(sockfd, SIOCGIWESSID, &wreq);

	close(sockfd);
}

/*** newstr_link ***/
char * newstr_link(const char *interface, const unsigned int flags) {
	char *notifystr, *e_interface = NULL, *e_essid = NULL;
	char essid[IW_ESSID_MAX_SIZE + 1];

	memset(&essid, 0, IW_ESSID_MAX_SIZE + 1);
	get_ssid(interface, essid);

	e_interface = g_markup_escape_text(interface, -1);

	if (strlen(essid) == 0) {
		notifystr = malloc(sizeof(TEXT_NEWLINK) + strlen(e_interface) + 4);
		sprintf(notifystr, TEXT_NEWLINK, e_interface, (flags & CHECK_CONNECTED) ? "up" : "down");
	} else {
		e_essid = g_markup_escape_text(essid, -1);

		notifystr = malloc(sizeof(TEXT_WIRELESS) + strlen(e_interface) + 4 + strlen(e_essid));
		sprintf(notifystr, TEXT_WIRELESS, e_interface, (flags & CHECK_CONNECTED) ? "up" : "down", e_essid);

		free(e_essid);
	}

	free(e_interface);

	return notifystr;
}

/*** newstr_addr ***/
char * newstr_addr(const char *interface, const unsigned char family, const char *ipaddr, const unsigned char prefix) {
	char *notifystr, *e_interface = NULL;

	e_interface = g_markup_escape_text(interface, -1);

	notifystr = malloc(sizeof(TEXT_NEWADDR)+ strlen(e_interface) + strlen(ipaddr));
	sprintf(notifystr, TEXT_NEWADDR, e_interface, family == AF_INET6 ? "IPv6" : "IP", ipaddr, prefix);

	free(e_interface);

	return notifystr;
}

/*** newstr_away ***/
char * newstr_away(const char *interface) {
	char *notifystr, *e_interface = NULL;

	e_interface = g_markup_escape_text(interface, -1);

	notifystr = malloc(sizeof(TEXT_DELLINK) + strlen(e_interface));
	sprintf(notifystr, TEXT_DELLINK, e_interface);

	free(e_interface);

	return notifystr;
}

/*** open_netlink ***/
int open_netlink (void) {
	int sock;
	struct sockaddr_nl addr;

	memset ((void *) &addr, 0, sizeof(addr));

	if ((sock = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0)
		return sock;

	addr.nl_family = AF_NETLINK;
	addr.nl_pid = getpid();
	addr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;

	if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0)
		return -1;

	return sock;
}

/*** read_event ***/
int read_event (int sockint) {
	int status, rc = EXIT_FAILURE;
	char buf[4096];
	struct iovec iov = { buf, sizeof buf };
	struct sockaddr_nl snl;
	struct msghdr msg = { (void *) &snl, sizeof snl, &iov, 1, NULL, 0, 0 };
	struct nlmsghdr *h;

	if ((status = recvmsg (sockint, &msg, 0)) < 0) {
		/* Socket non-blocking so bail out once we have read everything */
		if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR) {
			rc = EXIT_SUCCESS;
			goto out;
		}

		/* Anything else is an error */
		fprintf (stderr, "read_netlink: Error recvmsg: %d\n", status);
		goto out;
	}

	if (status == 0)
		fprintf (stderr, "read_netlink: EOF\n");

	/* We need to handle more than one message per 'recvmsg' */
	for (h = (struct nlmsghdr *) buf; NLMSG_OK (h, (unsigned int) status); h = NLMSG_NEXT (h, status)) {
		/* Finish reading */
		if (h->nlmsg_type == NLMSG_DONE) {
			rc = EXIT_SUCCESS;
			goto out;
		}

		/* Message is some kind of error */
		if (h->nlmsg_type == NLMSG_ERROR) {
			fprintf (stderr, "read_netlink: Message is an error - decode TBD\n");
			goto out;
		}

		/* Call message handler */
		if (msg_handler(&snl, h) != EXIT_SUCCESS) {
			fprintf (stderr, "read_event: Message hander returned error.\n");
			goto out;
		}
	}

	rc = EXIT_SUCCESS;

out:
	return rc;
}

/*** msg_handler ***/
int msg_handler (struct sockaddr_nl *nl, struct nlmsghdr *msg) {
	int rc = EXIT_FAILURE;
	char *notifystr = NULL;
	GError *error = NULL;
	struct ifaddrmsg *ifa;
	struct ifinfomsg *ifi;
	struct rtattr *rth;
	int rtl;
	char buf[INET6_ADDRSTRLEN];
	NotifyNotification *tmp_notification = NULL, *notification = NULL;
	char *icon = NULL;

	ifa = (struct ifaddrmsg *) NLMSG_DATA (msg);
	ifi = (struct ifinfomsg *) NLMSG_DATA (msg);

	/* make sure we have alloced memory for NotifyNotification and addresses_seen struct array */
	if (maxinterface < ifi->ifi_index) {
		ifs = realloc(ifs, (ifi->ifi_index + 1) * sizeof(struct ifs));
		while(maxinterface < ifi->ifi_index) {
			maxinterface++;

			if (verbose > 0)
				printf("%s: Initializing interface %d: ", program, maxinterface);

			/* get interface name and store it
			 * in case the interface does no longer exist this may fail,
			 * use static string '(unknown)' instead */
			if (if_indextoname(maxinterface, ifs[maxinterface].name) == NULL)
				strcpy(ifs[maxinterface].name, "(unknown)");

			if (verbose > 0)
				printf("%s\n", ifs[maxinterface].name);

			ifs[maxinterface].state = -1;
			ifs[maxinterface].deleted = 0;

			ifs[maxinterface].notification =
#				if NOTIFY_CHECK_VERSION(0, 7, 0)
				notify_notification_new(TEXT_TOPIC, NULL, NULL);
#				else
				notify_notification_new(TEXT_TOPIC, NULL, NULL, NULL);
#				endif
			notify_notification_set_category(ifs[maxinterface].notification, PROGNAME);
			notify_notification_set_urgency(ifs[maxinterface].notification, NOTIFY_URGENCY_NORMAL);
			notify_notification_set_timeout(ifs[maxinterface].notification, notification_timeout);

			ifs[maxinterface].addresses_seen = NULL;
		}
	} else if (ifs[ifi->ifi_index].deleted == 1) {
		if (verbose > 0)
			printf("%s: Ignoring event for deleted interface %d.\n", program, ifi->ifi_index);
		rc = EXIT_SUCCESS;
		goto out;
	}

	/* make notification point to the array element, will be overwritten
	 * later when needed for address notification */
	notification = ifs[ifi->ifi_index].notification;

	/* get interface name and store it
	 * in case the interface does no longer exist this may fail, but it does not overwrite */
	if_indextoname(ifi->ifi_index, ifs[ifi->ifi_index].name);

	if (verbose > 1)
		printf("%s: Event for interface %s (%d): flags = %x, msg type = %d\n",
			program, ifs[ifi->ifi_index].name, ifi->ifi_index, ifa->ifa_flags, msg->nlmsg_type);

	switch (msg->nlmsg_type) {
		/* just return for cases we want to ignore
		 * use break if a notification has to be displayed */
		case RTM_NEWADDR:
			rth = IFA_RTA (ifa);
			rtl = IFA_PAYLOAD (msg);

			while (rtl && RTA_OK (rth, rtl)) {
				if ((rth->rta_type == IFA_LOCAL /* IPv4 */
						|| rth->rta_type == IFA_ADDRESS /* IPv6 */)
						&& ifa->ifa_scope == RT_SCOPE_UNIVERSE /* no IPv6 scope link */) {
					inet_ntop(ifa->ifa_family, RTA_DATA (rth), buf, sizeof(buf));

					/* check if we already notified about this address */
					if (match_address(ifs[ifi->ifi_index].addresses_seen, buf, ifa->ifa_prefixlen)) {
						if (verbose > 0)
							printf("%s: Address %s/%d already known for %s, ignoring.\n",
									program, buf, ifa->ifa_prefixlen, ifs[ifi->ifi_index].name);
						break;
					}

					/* add address to struct */
					ifs[ifi->ifi_index].addresses_seen =
						add_address(ifs[ifi->ifi_index].addresses_seen, buf, ifa->ifa_prefixlen);
					if (verbose > 1)
						list_addresses(ifs[ifi->ifi_index].addresses_seen, ifs[ifi->ifi_index].name);

					/* display notification */
					notifystr = newstr_addr(ifs[ifi->ifi_index].name,
						ifa->ifa_family, buf, ifa->ifa_prefixlen);

					/* we are done, no need to run more loops */
					break;
				}
				rth = RTA_NEXT (rth, rtl);
			}
			/* we did not find anything to notify */
			if (notifystr == NULL) {
				rc = EXIT_SUCCESS;
				goto out;
			}

			/* do we want new notification, not update the notification about link status */
			tmp_notification =
#				if NOTIFY_CHECK_VERSION(0, 7, 0)
				notify_notification_new(TEXT_TOPIC, NULL, NULL);
#				else
				notify_notification_new(TEXT_TOPIC, NULL, NULL, NULL);
#				endif
			notify_notification_set_category(tmp_notification, PROGNAME);
			notify_notification_set_urgency(tmp_notification, NOTIFY_URGENCY_NORMAL);
			notify_notification_set_timeout(tmp_notification, notification_timeout);

			notification = tmp_notification;

			icon = ICON_NETWORK_ADDRESS;

			break;
		case RTM_DELADDR:
			rth = IFA_RTA (ifa);
			rtl = IFA_PAYLOAD (msg);

			while (rtl && RTA_OK (rth, rtl)) {
				if ((rth->rta_type == IFA_LOCAL /* IPv4 */
						|| rth->rta_type == IFA_ADDRESS /* IPv6 */)
						&& ifa->ifa_scope == RT_SCOPE_UNIVERSE /* no IPv6 scope link */) {
					inet_ntop(ifa->ifa_family, RTA_DATA (rth), buf, sizeof(buf));
					ifs[ifi->ifi_index].addresses_seen =
						remove_address(ifs[ifi->ifi_index].addresses_seen, buf, ifa->ifa_prefixlen);
					if (verbose > 1)
						list_addresses(ifs[ifi->ifi_index].addresses_seen, ifs[ifi->ifi_index].name);

					/* we are done, no need to run more loops */
					break;
				}
				rth = RTA_NEXT (rth, rtl);
			}

			rc = EXIT_SUCCESS;
			goto out;

		case RTM_NEWROUTE:
			rc = EXIT_SUCCESS;
			goto out;

		case RTM_DELROUTE:
			rc = EXIT_SUCCESS;
			goto out;

		case RTM_NEWLINK:
			/* ignore if state did not change */
			if ((ifi->ifi_flags & CHECK_CONNECTED) == ifs[ifi->ifi_index].state) {
				rc = EXIT_SUCCESS;
				goto out;
			}

			ifs[ifi->ifi_index].state = ifi->ifi_flags & CHECK_CONNECTED;

			notifystr = newstr_link(ifs[ifi->ifi_index].name, ifi->ifi_flags);

			icon = ifi->ifi_flags & CHECK_CONNECTED ? ICON_NETWORK_UP : ICON_NETWORK_DOWN;

			/* free only if interface goes down */
			if (!(ifi->ifi_flags & CHECK_CONNECTED)) {
				free_addresses(ifs[ifi->ifi_index].addresses_seen);
				ifs[ifi->ifi_index].addresses_seen = NULL;
			}

			break;
		case RTM_DELLINK:
			notifystr = newstr_away(ifs[ifi->ifi_index].name);

			icon = ICON_NETWORK_AWAY;

			free_addresses(ifs[ifi->ifi_index].addresses_seen);
			/* marking interface deleted makes events for this interface to be ignored */
			ifs[ifi->ifi_index].deleted = 1;

			break;
		default:
			/* we should not get here... */
			fprintf(stderr, "msg_handler: Unknown netlink nlmsg_type %d.\n", msg->nlmsg_type);

			goto out;
	}

	if (verbose > 0)
		printf("%s: %s\n", program, notifystr);

	notify_notification_update(notification, TEXT_TOPIC, notifystr, icon);

	if (notify_notification_show(notification, &error) == FALSE) {
		g_printerr("%s: Error showing notification: %s\n", program, error->message);
		g_error_free(error);

		goto out;
	}

	rc = EXIT_SUCCESS;

out:
	if (tmp_notification)
		g_object_unref(G_OBJECT(tmp_notification));
	free(notifystr);

	return rc;
}

/*** received_signal ***/
void received_signal(int signal) {
	if (verbose > 0)
		printf("%s: Received signal: %s\n", program, strsignal(signal));

	doexit++;
}

/*** main ***/
int main (int argc, char **argv) {
	int rc = EXIT_FAILURE;
	int i, nls;
	unsigned int version = 0, help = 0;

	program = argv[0];

	/* get the verbose status */
	while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) {
		switch (i) {
			case 'h':
				help++;
				break;
			case 't':
				notification_timeout = atof(optarg) * 1000;
				break;
			case 'v':
				verbose++;
				break;
			case 'V':
				verbose++;
				version++;
				break;
		}
	}

	if (verbose > 0)
		printf ("%s: %s v%s"
#ifdef HAVE_SYSTEMD
			" +systemd"
#endif
			" (compiled: " __DATE__ ", " __TIME__ ")\n", program, PROGNAME, VERSION);

	if (help > 0)
		printf("usage: %s [-h] [-t TIMEOUT] [-v[v]] [-V]\n", program);

	if (version > 0 || help > 0)
		return EXIT_SUCCESS;

	if ((nls = open_netlink()) < 0) {
		fprintf (stderr, "%s: Error opening netlink socket!\n", program);
		goto out40;
	}

	if (notify_init(PROGNAME) == FALSE) {
		fprintf (stderr, "%s: Can't create notify.\n", program);
		goto out30;
	}

	struct sigaction act = { 0 };
	act.sa_handler = received_signal;

	sigaction(SIGINT, &act, NULL);
	sigaction(SIGTERM, &act, NULL);

#ifdef HAVE_SYSTEMD
	sd_notify(0, "READY=1\nSTATUS=Waiting for netlink events...");
#endif

	while (doexit == 0) {
		if (read_event(nls) != EXIT_SUCCESS) {
			fprintf(stderr, "%s: read_event returned error.\n", program);
			goto out10;
		}
	}

	if (verbose > 0)
		printf("%s: Exiting...\n", program);

	/* report stopping to systemd */
#ifdef HAVE_SYSTEMD
	sd_notify(0, "STOPPING=1\nSTATUS=Stopping...");
#endif

	for(; maxinterface > 0; maxinterface--) {
		if (verbose > 0)
			printf("%s: Freeing interface %d: %s\n", program,
					maxinterface, ifs[maxinterface].name);

		free_addresses(ifs[maxinterface].addresses_seen);
		if (ifs[maxinterface].notification != NULL)
			g_object_unref(G_OBJECT(ifs[maxinterface].notification));
	}

	rc = EXIT_SUCCESS;

out10:
	if (ifs != NULL)
		free(ifs);

/* out20: */
	notify_uninit();

out30:
	if (close(nls) < 0)
		fprintf(stderr, "%s: Failed to close socket.\n", program);

out40:
#ifdef HAVE_SYSTEMD
	sd_notify(0, "STATUS=Stopped. Bye!");
#endif

	return rc;
}