network: Rework interface auto detection, handle route change events

Last part of the rework of handleEvents(), this time we take the
getExternalInterface() function and add it to the handleEvents()
function. That way, waybar can react immediately when a new "external
interface" is available and doesn't need to probe. Also that avoid to
have two different functions consuming from the same socket and we
don't need to recode some of the functions that are already available
via libnl (to send and receive messages).
This commit is contained in:
Anthony PERARD 2021-05-15 16:38:00 +01:00
parent 0fc7ef6685
commit 0bb436f949
2 changed files with 146 additions and 219 deletions

View File

@ -3,7 +3,6 @@
#include <arpa/inet.h>
#include <fmt/format.h>
#include <linux/nl80211.h>
#include <net/if.h>
#include <netlink/genl/ctrl.h>
#include <netlink/genl/genl.h>
#include <netlink/netlink.h>
@ -35,17 +34,12 @@ class Network : public ALabel {
void worker();
void createInfoSocket();
void createEventSocket();
int getExternalInterface(int skip_idx = -1) const;
int netlinkRequest(void*, uint32_t, uint32_t groups = 0) const;
int netlinkResponse(void*, uint32_t, uint32_t groups = 0) const;
void parseEssid(struct nlattr**);
void parseSignal(struct nlattr**);
void parseFreq(struct nlattr**);
bool associatedOrJoined(struct nlattr**);
bool checkInterface(struct ifinfomsg* rtif, std::string name);
int getPreferredIface(int skip_idx = -1, bool wait = true) const;
bool checkInterface(std::string name);
auto getInfo() -> void;
void checkNewInterface(struct ifinfomsg* rtif);
const std::string getNetworkState() const;
void clearIface();
bool wildcardMatch(const std::string& pattern, const std::string& text) const;
@ -60,6 +54,7 @@ class Network : public ALabel {
int nl80211_id_;
std::mutex mutex_;
bool want_route_dump_;
bool want_link_dump_;
bool want_addr_dump_;
bool dump_in_progress_;

View File

@ -86,6 +86,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
family_(config["family"] == "ipv6" ? AF_INET6 : AF_INET),
efd_(-1),
ev_fd_(-1),
want_route_dump_(false),
want_link_dump_(false),
want_addr_dump_(false),
dump_in_progress_(false),
@ -117,7 +118,11 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
bandwidth_up_total_ = 0;
}
if (config_["interface"].isString()) {
if (!config_["interface"].isString()) {
// "interface" isn't configure, then try to guess the external
// interface currently used for internet.
want_route_dump_ = true;
} else {
// Look for an interface that match "interface"
// and then find the address associated with it.
want_link_dump_ = true;
@ -126,14 +131,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
createEventSocket();
createInfoSocket();
auto default_iface = getPreferredIface(-1, false);
if (default_iface != -1) {
ifid_ = default_iface;
char ifname[IF_NAMESIZE];
if_indextoname(default_iface, ifname);
ifname_ = ifname;
want_addr_dump_ = true;
}
dp.emit();
// Ask for a dump of interfaces and then addresses to populate our
// information. First the interface dump, and once done, the callback
@ -184,6 +182,14 @@ void waybar::modules::Network::createEventSocket() {
} else {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_IFADDR);
}
if (!config_["interface"].isString()) {
if (family_ == AF_INET) {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_ROUTE);
} else {
nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_ROUTE);
}
}
efd_ = epoll_create1(EPOLL_CLOEXEC);
if (efd_ < 0) {
throw std::runtime_error("Can't create epoll");
@ -383,196 +389,16 @@ auto waybar::modules::Network::update() -> void {
ALabel::update();
}
// Based on https://gist.github.com/Yawning/c70d804d4b8ae78cc698
int waybar::modules::Network::getExternalInterface(int skip_idx) const {
static const uint32_t route_buffer_size = 8192;
struct nlmsghdr * hdr = nullptr;
struct rtmsg * rt = nullptr;
char resp[route_buffer_size] = {0};
int ifidx = -1;
/* Prepare request. */
constexpr uint32_t reqlen = NLMSG_SPACE(sizeof(*rt));
char req[reqlen] = {0};
/* Build the RTM_GETROUTE request. */
hdr = reinterpret_cast<struct nlmsghdr *>(req);
hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*rt));
hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
hdr->nlmsg_type = RTM_GETROUTE;
rt = static_cast<struct rtmsg *>(NLMSG_DATA(hdr));
rt->rtm_family = family_;
rt->rtm_table = RT_TABLE_MAIN;
/* Issue the query. */
if (netlinkRequest(req, reqlen) < 0) {
goto out;
}
/* Read the response(s).
*
* WARNING: All the packets generated by the request must be consumed (as in,
* consume responses till NLMSG_DONE/NLMSG_ERROR is encountered).
*/
do {
auto len = netlinkResponse(resp, route_buffer_size);
if (len < 0) {
goto out;
}
/* Parse the response payload into netlink messages. */
for (hdr = reinterpret_cast<struct nlmsghdr *>(resp); NLMSG_OK(hdr, len);
hdr = NLMSG_NEXT(hdr, len)) {
if (hdr->nlmsg_type == NLMSG_DONE) {
goto out;
}
if (hdr->nlmsg_type == NLMSG_ERROR) {
/* Even if we found the interface index, something is broken with the
* netlink socket, so return an error.
*/
ifidx = -1;
goto out;
}
/* If we found the correct answer, skip parsing the attributes. */
if (ifidx != -1) {
continue;
}
/* Find the message(s) concerting the main routing table, each message
* corresponds to a single routing table entry.
*/
rt = static_cast<struct rtmsg *>(NLMSG_DATA(hdr));
if (rt->rtm_table != RT_TABLE_MAIN) {
continue;
}
/* Parse all the attributes for a single routing table entry. */
struct rtattr *attr = RTM_RTA(rt);
uint64_t attrlen = RTM_PAYLOAD(hdr);
bool has_gateway = false;
bool has_destination = false;
int temp_idx = -1;
for (; RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) {
/* Determine if this routing table entry corresponds to the default
* route by seeing if it has a gateway, and if a destination addr is
* set, that it is all 0s.
*/
switch (attr->rta_type) {
case RTA_GATEWAY:
/* The gateway of the route.
*
* If someone every needs to figure out the gateway address as well,
* it's here as the attribute payload.
*/
has_gateway = true;
break;
case RTA_DST: {
/* The destination address.
* Should be either missing, or maybe all 0s. Accept both.
*/
const uint32_t nr_zeroes = (family_ == AF_INET) ? 4 : 16;
unsigned char c = 0;
size_t dstlen = RTA_PAYLOAD(attr);
if (dstlen != nr_zeroes) {
break;
}
for (uint32_t i = 0; i < dstlen; i += 1) {
c |= *((unsigned char *)RTA_DATA(attr) + i);
}
has_destination = (c == 0);
break;
}
case RTA_OIF:
/* The output interface index. */
temp_idx = *static_cast<int *>(RTA_DATA(attr));
break;
default:
break;
}
}
/* If this is the default route, and we know the interface index,
* we can stop parsing this message.
*/
if (has_gateway && !has_destination && temp_idx != -1 && temp_idx != skip_idx) {
ifidx = temp_idx;
break;
}
}
} while (true);
out:
return ifidx;
}
int waybar::modules::Network::netlinkRequest(void *req, uint32_t reqlen, uint32_t groups) const {
struct sockaddr_nl sa = {};
sa.nl_family = AF_NETLINK;
sa.nl_groups = groups;
struct iovec iov = {req, reqlen};
struct msghdr msg = {
.msg_name = &sa,
.msg_namelen = sizeof(sa),
.msg_iov = &iov,
.msg_iovlen = 1,
};
return sendmsg(nl_socket_get_fd(ev_sock_), &msg, 0);
}
int waybar::modules::Network::netlinkResponse(void *resp, uint32_t resplen, uint32_t groups) const {
struct sockaddr_nl sa = {};
sa.nl_family = AF_NETLINK;
sa.nl_groups = groups;
struct iovec iov = {resp, resplen};
struct msghdr msg = {
.msg_name = &sa,
.msg_namelen = sizeof(sa),
.msg_iov = &iov,
.msg_iovlen = 1,
};
auto ret = recvmsg(nl_socket_get_fd(ev_sock_), &msg, 0);
if (msg.msg_flags & MSG_TRUNC) {
return -1;
}
return ret;
}
bool waybar::modules::Network::checkInterface(struct ifinfomsg *rtif, std::string name) {
bool waybar::modules::Network::checkInterface(std::string name) {
if (config_["interface"].isString()) {
return config_["interface"].asString() == name ||
wildcardMatch(config_["interface"].asString(), name);
}
// getExternalInterface may need some delay to detect external interface
for (uint8_t tries = 0; tries < MAX_RETRY; tries += 1) {
auto external_iface = getExternalInterface();
if (external_iface > 0) {
return external_iface == rtif->ifi_index;
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return false;
}
int waybar::modules::Network::getPreferredIface(int skip_idx, bool wait) const {
int ifid = -1;
if (config_["interface"].isString()) {
// Using link dump instead
return -1;
}
// getExternalInterface may need some delay to detect external interface
for (uint8_t tries = 0; tries < MAX_RETRY; tries += 1) {
ifid = getExternalInterface(skip_idx);
if (ifid > 0) {
return ifid;
}
if (wait) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
return -1;
}
void waybar::modules::Network::clearIface() {
ifid_ = -1;
essid_.clear();
ipaddr_.clear();
netmask_.clear();
@ -582,22 +408,6 @@ void waybar::modules::Network::clearIface() {
frequency_ = 0;
}
void waybar::modules::Network::checkNewInterface(struct ifinfomsg *rtif) {
auto new_iface = getPreferredIface(rtif->ifi_index);
if (new_iface != -1) {
ifid_ = new_iface;
char ifname[IF_NAMESIZE];
if_indextoname(new_iface, ifname);
ifname_ = ifname;
want_addr_dump_ = true;
askForStateDump();
thread_timer_.wake_up();
} else {
ifid_ = -1;
dp.emit();
}
}
int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
auto net = static_cast<waybar::modules::Network *>(data);
std::lock_guard<std::mutex> lock(net->mutex_);
@ -627,10 +437,16 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
}
}
if (!is_del_event && net->ifid_ == -1) {
if (!is_del_event && ifi->ifi_index == net->ifid_) {
// Update inferface information
if (net->ifname_.empty()) {
std::string new_ifname (ifname, ifname_len);
net->ifname_ = new_ifname;
}
} else if (!is_del_event && net->ifid_ == -1) {
// Checking if it's an interface we care about.
std::string new_ifname (ifname, ifname_len);
if (net->checkInterface(ifi, new_ifname)) {
if (net->checkInterface(new_ifname)) {
spdlog::debug("network: selecting new interface {}/{}", new_ifname, ifi->ifi_index);
net->ifname_ = new_ifname;
@ -647,8 +463,6 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
spdlog::debug("network: interface {}/{} deleted", net->ifname_, net->ifid_);
net->clearIface();
// Check for a new interface and get info
net->checkNewInterface(ifi);
net->dp.emit();
}
break;
@ -720,6 +534,117 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
}
break;
}
case RTM_DELROUTE:
is_del_event = true;
case RTM_NEWROUTE: {
// Based on https://gist.github.com/Yawning/c70d804d4b8ae78cc698
// to find the interface used to reach the outside world
struct rtmsg *rtm = static_cast<struct rtmsg *>(NLMSG_DATA(nh));
ssize_t attrlen = RTM_PAYLOAD(nh);
struct rtattr *attr = RTM_RTA(rtm);
bool has_gateway = false;
bool has_destination = false;
int temp_idx = -1;
/* If we found the correct answer, skip parsing the attributes. */
if (!is_del_event && net->ifid_ != -1) {
return NL_OK;
}
/* Find the message(s) concerting the main routing table, each message
* corresponds to a single routing table entry.
*/
if (rtm->rtm_table != RT_TABLE_MAIN) {
return NL_OK;
}
/* Parse all the attributes for a single routing table entry. */
for (; RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) {
/* Determine if this routing table entry corresponds to the default
* route by seeing if it has a gateway, and if a destination addr is
* set, that it is all 0s.
*/
switch(attr->rta_type) {
case RTA_GATEWAY:
/* The gateway of the route.
*
* If someone every needs to figure out the gateway address as well,
* it's here as the attribute payload.
*/
has_gateway = true;
break;
case RTA_DST: {
/* The destination address.
* Should be either missing, or maybe all 0s. Accept both.
*/
const uint32_t nr_zeroes = (net->family_ == AF_INET) ? 4 : 16;
unsigned char c = 0;
size_t dstlen = RTA_PAYLOAD(attr);
if (dstlen != nr_zeroes) {
break;
}
for (uint32_t i = 0; i < dstlen; i += 1) {
c |= *((unsigned char *)RTA_DATA(attr) + i);
}
has_destination = (c == 0);
break;
}
case RTA_OIF:
/* The output interface index. */
temp_idx = *static_cast<int *>(RTA_DATA(attr));
break;
default:
break;
}
/* If this is the default route, and we know the interface index,
* we can stop parsing this message.
*/
if (has_gateway && !has_destination && temp_idx != -1) {
if (!is_del_event) {
net->ifid_ = temp_idx;
spdlog::debug("network: new default route via if{}", temp_idx);
/* Ask ifname associated with temp_idx as well as carrier status */
struct ifinfomsg ifinfo_hdr = {
.ifi_family = AF_UNSPEC,
.ifi_index = temp_idx,
};
int err;
err = nl_send_simple(net->ev_sock_, RTM_GETLINK, NLM_F_REQUEST,
&ifinfo_hdr, sizeof (ifinfo_hdr));
if (err < 0) {
spdlog::error("network: failed to ask link info: {}", err);
/* Ask for a dump of all links instead */
net->want_link_dump_ = true;
}
/* Also ask for the address. Asking for a addresses of a specific
* interface doesn't seems to work so ask for a dump of all
* addresses. */
net->want_addr_dump_ = true;
net->askForStateDump();
net->thread_timer_.wake_up();
} else if (is_del_event && temp_idx == net->ifid_) {
spdlog::debug("network: default route deleted {}/if{}",
net->ifname_, temp_idx);
net->ifname_.clear();
net->clearIface();
net->dp.emit();
/* Ask for a dump of all routes in case another one is already
* setup. If there's none, there'll be an event with new one
* later. */
net->want_route_dump_ = true;
net->askForStateDump();
}
}
}
break;
}
}
return NL_OK;
@ -735,7 +660,14 @@ void waybar::modules::Network::askForStateDump(void) {
.rtgen_family = AF_UNSPEC,
};
if (want_link_dump_) {
if (want_route_dump_) {
rt_hdr.rtgen_family = family_;
nl_send_simple(ev_sock_, RTM_GETROUTE, NLM_F_DUMP,
&rt_hdr, sizeof (rt_hdr));
want_route_dump_ = false;
dump_in_progress_ = true;
} else if (want_link_dump_) {
nl_send_simple(ev_sock_, RTM_GETLINK, NLM_F_DUMP,
&rt_hdr, sizeof (rt_hdr));
want_link_dump_ = false;