Skip to content

Commit

Permalink
VPN-6363: Better LAN exclusions (#9674)
Browse files Browse the repository at this point in the history
* Remove default route supporession rule
* Add routing table setup back for Linux.

Once the default-route-suppression rule was removed, it turns out that
we really do need to setup the wireguard routing table to exclude LAN
addresses. This can easily be done using the RTN_THROW route type.

But then once the throw routes are in place, we can't actually reach
the internal Mulvad address space, so we need to add unicast routes
back too.

There is a bunch of duplicated work between the throw and unicast
routes though. So it still results in an ugly looking routing table.

* Move LAN exclusion setup down into the daemon
* Remove setupWireguardRoutingTable - it's no longer necessary
* Remove iOS special casing in getAllowedIPAddressRanges()
* Windows/MacOS: Default routes are handled by excludeLocalAddresses now
* Add rough implementation for MacOS too
* We don't need to queue routes anymore for ifup
* Implement LAN exclusions properly for Windows
* Remove old exclusion route API from WireguardUtils
* Add Windows firewall setup for LAN bypass too
* Compute combined route metric when comparing windows routes
* Create windows route monitor dynamically on up/down
* Add route capturing to WindowsRouteMonitor

This attempts to make up for the lack of policy-based routing on Windows
by interactively updating the routing table to force non-local traffic
into the VPN tunnel. The gist of the algorithm being deployed here is to
dump the routing table, and then dynamically duplicate any routes which
might try to leak traffic but with a lower metric to force it back into
the VPN anyways.

* Don't attempt to exclude non-routeable addresses
* Implement excludeLocalNetworks for mock daemon.
* Remove duplicate inclusion of daemon.cpp and friends on Linux
  • Loading branch information
oskirby authored Jul 23, 2024
1 parent 981fe1a commit a95fa8c
Show file tree
Hide file tree
Showing 19 changed files with 548 additions and 261 deletions.
5 changes: 0 additions & 5 deletions src/cmake/linux.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ if(NOT BUILD_FLATPAK)
target_sources(mozillavpn PRIVATE
${CMAKE_SOURCE_DIR}/3rdparty/wireguard-tools/contrib/embeddable-wg-library/wireguard.c
${CMAKE_SOURCE_DIR}/3rdparty/wireguard-tools/contrib/embeddable-wg-library/wireguard.h
${CMAKE_SOURCE_DIR}/src/daemon/daemon.cpp
${CMAKE_SOURCE_DIR}/src/daemon/daemon.h
${CMAKE_SOURCE_DIR}/src/daemon/dnsutils.h
${CMAKE_SOURCE_DIR}/src/daemon/iputils.h
${CMAKE_SOURCE_DIR}/src/daemon/wireguardutils.h
${CMAKE_SOURCE_DIR}/src/platforms/linux/daemon/apptracker.cpp
${CMAKE_SOURCE_DIR}/src/platforms/linux/daemon/apptracker.h
${CMAKE_SOURCE_DIR}/src/platforms/linux/daemon/dbusservice.cpp
Expand Down
26 changes: 0 additions & 26 deletions src/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -550,16 +550,6 @@ Controller::IPAddressList Controller::getExcludedIPAddressRanges() {
excludeIPv4s.append(RFC1112::ipv4MulticastAddressBlock());
excludeIPv6s.append(RFC4291::ipv6MulticastAddressBlock());

logger.debug() << "Filtering out explicitely-set network address ranges";
for (const QString& ipv4String :
SettingsHolder::instance()->excludedIpv4Addresses()) {
excludeIPv4s.append(IPAddress(ipv4String));
}
for (const QString& ipv6String :
SettingsHolder::instance()->excludedIpv6Addresses()) {
excludeIPv6s.append(IPAddress(ipv6String));
}

return IPAddressList{
.v6 = excludeIPv6s,
.v4 = excludeIPv4s,
Expand All @@ -573,20 +563,11 @@ QList<IPAddress> Controller::getAllowedIPAddressRanges(

QList<IPAddress> list;

#ifdef MZ_IOS
// Note: On iOS, we use the `excludeLocalNetworks` flag to ensure
// LAN traffic is allowed through. This is in the swift code.

Q_UNUSED(exitServer);

logger.debug() << "Catch all IPv4";
list.append(IPAddress("0.0.0.0/0"));

logger.debug() << "Catch all IPv6";
list.append(IPAddress("::0/0"));
#else

auto excludedIPs = getExcludedIPAddressRanges();

// Allow access to the internal gateway addresses.
logger.debug() << "Allow the IPv4 gateway:" << exitServer.ipv4Gateway();
Expand All @@ -598,13 +579,6 @@ QList<IPAddress> Controller::getAllowedIPAddressRanges(
list.append(
IPAddress(QHostAddress(MULLVAD_PROXY_RANGE), MULLVAD_PROXY_RANGE_LENGTH));

// Allow access to everything not covered by an excluded address.
QList<IPAddress> allowedIPv4 = {IPAddress("0.0.0.0/0")};
list.append(IPAddress::excludeAddresses(allowedIPv4, excludedIPs.v4));
QList<IPAddress> allowedIPv6 = {IPAddress("::/0")};
list.append(IPAddress::excludeAddresses(allowedIPv6, excludedIPs.v6));
#endif

return list;
}

Expand Down
28 changes: 19 additions & 9 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <QMetaEnum>
#include <QTimer>

#include "controller.h"
#include "leakdetector.h"
#include "logger.h"
#include "loghandler.h"
Expand Down Expand Up @@ -115,10 +116,28 @@ bool Daemon::activate(const InterfaceConfig& config) {

// Bring up the wireguard interface if not already done.
if (!wgutils()->interfaceExists()) {
// Create the interface.
if (!wgutils()->addInterface(config)) {
logger.error() << "Interface creation failed.";
return false;
}

// Bring the interface up.
if (supportIPUtils()) {
if (!iputils()->addInterfaceIPs(config)) {
return false;
}
if (!iputils()->setMTUAndUp(config)) {
return false;
}
}

// Configure LAN exclusion policies
auto lanAddressRanges = Controller::getExcludedIPAddressRanges().flatten();
if (!wgutils()->excludeLocalNetworks(lanAddressRanges)) {
logger.error() << "LAN exclusion failed.";
return false;
}
}

// Add the peer to this interface.
Expand All @@ -131,15 +150,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
return false;
}

if (supportIPUtils()) {
if (!iputils()->addInterfaceIPs(config)) {
return false;
}
if (!iputils()->setMTUAndUp(config)) {
return false;
}
}

// set routing
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wgutils()->updateRoutePrefix(ip)) {
Expand Down
6 changes: 1 addition & 5 deletions src/daemon/mock/wireguardutilsmock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ bool WireguardUtilsMock::deleteRoutePrefix(const IPAddress& prefix) {
return true;
}

bool WireguardUtilsMock::addExclusionRoute(const IPAddress& prefix) {
return true;
}

bool WireguardUtilsMock::deleteExclusionRoute(const IPAddress& prefix) {
bool WireguardUtilsMock::excludeLocalNetworks(const QList<IPAddress>& addrs) {
return true;
}
4 changes: 1 addition & 3 deletions src/daemon/mock/wireguardutilsmock.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ class WireguardUtilsMock final : public WireguardUtils {

bool updateRoutePrefix(const IPAddress& prefix) override;
bool deleteRoutePrefix(const IPAddress& prefix) override;

bool addExclusionRoute(const IPAddress& prefix) override;
bool deleteExclusionRoute(const IPAddress& prefix) override;
bool excludeLocalNetworks(const QList<IPAddress>& addresses) override;

signals:
void backendFailure();
Expand Down
20 changes: 3 additions & 17 deletions src/daemon/wireguardutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,9 @@ class WireguardUtils : public QObject {
virtual bool deletePeer(const InterfaceConfig& config) = 0;
virtual QList<PeerStatus> getPeerStatus() = 0;

virtual bool updateRoutePrefix(const IPAddress& prefix) {
Q_UNUSED(prefix);
return true;
}
virtual bool deleteRoutePrefix(const IPAddress& prefix) {
Q_UNUSED(prefix);
return true;
}

virtual bool addExclusionRoute(const IPAddress& prefix) {
Q_UNUSED(prefix);
return true;
}
virtual bool deleteExclusionRoute(const IPAddress& prefix) {
Q_UNUSED(prefix);
return true;
}
virtual bool updateRoutePrefix(const IPAddress& prefix) = 0;
virtual bool deleteRoutePrefix(const IPAddress& prefix) = 0;
virtual bool excludeLocalNetworks(const QList<IPAddress>& addresses) = 0;
};

#endif // WIREGUARDUTILS_H
7 changes: 6 additions & 1 deletion src/platforms/android/androidcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,13 @@ void AndroidController::activate(const InterfaceConfig& config,
jServer["publicKey"] = config.m_serverPublicKey;
jServer["port"] = (double)config.m_serverPort;

// TODO: This could be done better with VpnService.Builder.excludeRoute()
// we just need a way to get this config down that far.
const auto localRoutes = Controller::getExcludedIPAddressRanges().flatten();
const auto fullAllowedIPs =
IPAddress::excludeAddresses(config.m_allowedIPAddressRanges, localRoutes);
QJsonArray jAllowedIPs;
foreach (auto item, config.m_allowedIPAddressRanges) {
foreach (auto item, fullAllowedIPs) {
jAllowedIPs.append(QJsonValue(item.toString()));
}

Expand Down
123 changes: 64 additions & 59 deletions src/platforms/linux/daemon/wireguardutilslinux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,7 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
return false;
}

// Rules that allow traffic or not are applied on the firewall for Linux.
//
// To work around the issue, just set default routes for the exit hop.
// Configure the allowed addresses for this peer.
if ((config.m_hopType == InterfaceConfig::SingleHop) ||
(config.m_hopType == InterfaceConfig::MultiHopExit)) {
if (!config.m_deviceIpv4Address.isNull()) {
Expand All @@ -241,8 +239,7 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
}

// Direct multihop exit destinations to use the wireguard table.
int flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK;
rtmIncludePeer(RTM_NEWRULE, flags, ip);
rtmIncludePeer(RTM_NEWRULE, ip, NLM_F_CREATE | NLM_F_REPLACE);
}
}

Expand Down Expand Up @@ -290,7 +287,7 @@ bool WireguardUtilsLinux::deletePeer(const InterfaceConfig& config) {
// Clear routing policy tweaks for multihop.
if (config.m_hopType == InterfaceConfig::MultiHopEntry) {
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
rtmIncludePeer(RTM_DELRULE, NLM_F_REQUEST | NLM_F_ACK, ip);
rtmIncludePeer(RTM_DELRULE, ip);
}
}

Expand Down Expand Up @@ -360,40 +357,23 @@ QList<WireguardUtils::PeerStatus> WireguardUtilsLinux::getPeerStatus() {
return peerList;
}

bool WireguardUtilsLinux::setupWireguardRoutingTable(int family) {
constexpr size_t rtm_max_size = sizeof(struct rtmsg) +
2 * RTA_SPACE(sizeof(uint32_t)) +
RTA_SPACE(sizeof(struct in6_addr));
bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) {
return rtmSendRoute(RTM_NEWROUTE, prefix, RTN_UNICAST,
NLM_F_CREATE | NLM_F_REPLACE);
}

char buf[NLMSG_SPACE(rtm_max_size)];
struct nlmsghdr* nlmsg = reinterpret_cast<struct nlmsghdr*>(buf);
struct rtmsg* rtm = static_cast<struct rtmsg*>(NLMSG_DATA(nlmsg));
bool WireguardUtilsLinux::deleteRoutePrefix(const IPAddress& prefix) {
return rtmSendRoute(RTM_DELROUTE, prefix, RTN_UNICAST);
}

/* Create a routing policy rule that for all packets sent to the Wireguard
* routing table to just go to the Wireguard interface. This is
* equivalent to:
* ip route add default dev moz0 proto static table $WG_ROUTE_TABLE
*/
memset(buf, 0, sizeof(buf));
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlmsg->nlmsg_type = RTM_NEWROUTE;
nlmsg->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE;
nlmsg->nlmsg_pid = getpid();
nlmsg->nlmsg_seq = m_nlseq++;
rtm->rtm_family = family;
rtm->rtm_type = RTN_UNICAST;
rtm->rtm_table = RT_TABLE_UNSPEC;
rtm->rtm_protocol = RTPROT_STATIC;
rtm->rtm_scope = RT_SCOPE_LINK;
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_TABLE, WG_ROUTE_TABLE);
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_OIF, m_ifindex);
bool WireguardUtilsLinux::excludeLocalNetworks(
const QList<IPAddress>& lanAddressRanges) {
for (const IPAddress& prefix : lanAddressRanges) {
m_routesExcluded.append(prefix);
rtmSendRoute(RTM_NEWROUTE, prefix, RTN_THROW, NLM_F_CREATE | NLM_F_REPLACE);
}

struct sockaddr_nl nladdr;
memset(&nladdr, 0, sizeof(nladdr));
nladdr.nl_family = AF_NETLINK;
size_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0,
(struct sockaddr*)&nladdr, sizeof(nladdr));
return (result == nlmsg->nlmsg_len);
return true;
}

// PRIVATE METHODS
Expand Down Expand Up @@ -542,36 +522,57 @@ bool WireguardUtilsLinux::rtmSendRule(int action, int flags, int addrfamily) {
nlmsg_append_attr32(nlmsg, sizeof(buf), FRA_TABLE, WG_ROUTE_TABLE);
ssize_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0,
(struct sockaddr*)&nladdr, sizeof(nladdr));
if (result != static_cast<ssize_t>(nlmsg->nlmsg_len)) {
return (result == static_cast<ssize_t>(nlmsg->nlmsg_len));
}

bool WireguardUtilsLinux::rtmSendRoute(int action, const IPAddress& dest,
int type, int flags) {
constexpr size_t rtm_max_size = sizeof(struct rtmsg) +
2 * RTA_SPACE(sizeof(uint32_t)) +
RTA_SPACE(sizeof(struct in6_addr));

char buf[NLMSG_SPACE(rtm_max_size)];
struct nlmsghdr* nlmsg = reinterpret_cast<struct nlmsghdr*>(buf);
struct rtmsg* rtm = static_cast<struct rtmsg*>(NLMSG_DATA(nlmsg));

wg_allowedip ip;
if (!buildAllowedIp(&ip, dest)) {
logger.warning() << "Invalid destination prefix";
return false;
}

/* Create a routing policy rule to suppress zero-length prefix lookups from
* in the main routing table. This is equivalent to:
* ip rule add table main suppress_prefixlength 0
*/
memset(buf, 0, sizeof(buf));
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct fib_rule_hdr));
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlmsg->nlmsg_type = action;
nlmsg->nlmsg_flags = flags;
nlmsg->nlmsg_flags = flags | NLM_F_REQUEST | NLM_F_ACK;
nlmsg->nlmsg_pid = getpid();
nlmsg->nlmsg_seq = m_nlseq++;
rule->family = addrfamily;
rule->table = RT_TABLE_MAIN;
rule->action = FR_ACT_TO_TBL;
rule->flags = 0;
nlmsg_append_attr32(nlmsg, sizeof(buf), FRA_SUPPRESS_PREFIXLEN, 0);
result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0, (struct sockaddr*)&nladdr,
sizeof(nladdr));
if (result != static_cast<ssize_t>(nlmsg->nlmsg_len)) {
return false;
rtm->rtm_dst_len = ip.cidr;
rtm->rtm_family = ip.family;
rtm->rtm_type = type;
rtm->rtm_table = RT_TABLE_UNSPEC;
rtm->rtm_protocol = RTPROT_BOOT;
rtm->rtm_scope = RT_SCOPE_UNIVERSE;
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_TABLE, WG_ROUTE_TABLE);
if (rtm->rtm_family == AF_INET6) {
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip6, sizeof(ip.ip6));
} else {
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_DST, &ip.ip4, sizeof(ip.ip4));
}
if (type == RTN_UNICAST) {
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_OIF, m_ifindex);
}

return true;
struct sockaddr_nl nladdr;
memset(&nladdr, 0, sizeof(nladdr));
nladdr.nl_family = AF_NETLINK;
ssize_t result = sendto(m_nlsock, buf, nlmsg->nlmsg_len, 0,
(struct sockaddr*)&nladdr, sizeof(nladdr));
return (result == static_cast<ssize_t>(nlmsg->nlmsg_len));
}

bool WireguardUtilsLinux::rtmIncludePeer(int action, int flags,
const IPAddress& prefix) {
bool WireguardUtilsLinux::rtmIncludePeer(int action, const IPAddress& prefix,
int flags) {
constexpr size_t fib_max_size = sizeof(struct fib_rule_hdr) +
RTA_SPACE(sizeof(struct in6_addr)) +
2 * RTA_SPACE(sizeof(quint32));
Expand All @@ -590,7 +591,7 @@ bool WireguardUtilsLinux::rtmIncludePeer(int action, int flags,
memset(buf, 0, sizeof(buf));
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct fib_rule_hdr));
nlmsg->nlmsg_type = action;
nlmsg->nlmsg_flags = flags;
nlmsg->nlmsg_flags = flags | NLM_F_REQUEST | NLM_F_ACK;
nlmsg->nlmsg_pid = getpid();
nlmsg->nlmsg_seq = m_nlseq++;
rule->table = RT_TABLE_UNSPEC;
Expand Down Expand Up @@ -675,8 +676,6 @@ void WireguardUtilsLinux::nlsockHandleNewlink(struct nlmsghdr* nlmsg) {
// Check for the interface going up.
if ((diff & IFF_UP) && (msg->ifi_flags & IFF_UP)) {
logger.info() << "Wireguard interface is UP";
setupWireguardRoutingTable(AF_INET);
setupWireguardRoutingTable(AF_INET6);
}

logger.debug() << "RTM_NEWLINK flags:"
Expand All @@ -691,6 +690,12 @@ void WireguardUtilsLinux::nlsockHandleDellink(struct nlmsghdr* nlmsg) {
return;
}

// Clear LAN exclusions
for (const IPAddress& prefix : m_routesExcluded) {
rtmSendRoute(RTM_DELROUTE, prefix, RTN_THROW);
}
m_routesExcluded.clear();

// Interface is down!
m_ifindex = 0;
m_ifflags = 0;
Expand Down
Loading

0 comments on commit a95fa8c

Please sign in to comment.