NetworkManager/src/nm-cloud-setup/main.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1107 lines
42 KiB
C
Raw Permalink Normal View History

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "libnm-client-aux-extern/nm-default-client.h"
#include "libnm-client-aux-extern/nm-libnm-aux.h"
cloud-setup: use suppress_prefixlength rule to honor non-default-routes in the main table Background ========== Imagine you run a container on your machine. Then the routing table might look like: default via 10.0.10.1 dev eth0 proto dhcp metric 100 10.0.10.0/28 dev eth0 proto kernel scope link src 10.0.10.5 metric 100 [...] 10.42.0.0/24 via 10.42.0.0 dev flannel.1 onlink 10.42.1.2 dev cali02ad7e68ce1 scope link 10.42.1.3 dev cali8fcecf5aaff scope link 10.42.2.0/24 via 10.42.2.0 dev flannel.1 onlink 10.42.3.0/24 via 10.42.3.0 dev flannel.1 onlink That is, there are another interfaces with subnets and specific routes. If nm-cloud-setup now configures rules: 0: from all lookup local 30400: from 10.0.10.5 lookup 30400 32766: from all lookup main 32767: from all lookup default and default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 then these other subnets will also be reached via the default route. This container example is just one case where this is a problem. In general, if you have specific routes on another interface, then the default route in the 30400+ table will interfere badly. The idea of nm-cloud-setup is to automatically configure the network for secondary IP addresses. When the user has special requirements, then they should disable nm-cloud-setup and configure whatever they want. But the container use case is popular and important. It is not something where the user actively configures the network. This case needs to work better, out of the box. In general, nm-cloud-setup should work better with the existing network configuration. Change ====== Add new routing tables 30200+ with the individual subnets of the interface: 10.0.10.0/24 dev eth0 table 30200 proto static metric 10 [...] default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 Also add more important routing rules with priority 30200+, which select these tables based on the source address: 30200: from 10.0.10.5 lookup 30200 These will do source based routing for the subnets on these interfaces. Then, add a rule with priority 30350 30350: lookup main suppress_prefixlength 0 which processes the routes from the main table, but ignores the default routes. 30350 was chosen, because it's in between the rules 30200+ and 30400+, leaving a range for the user to configure their own rules. Then, as before, the rules 30400+ again look at the corresponding 30400+ table, to find a default route. Finally, process the main table again, this time honoring the default route. That is for packets that have a different source address. This change means that the source based routing is used for the subnets that are configured on the interface and for the default route. Whereas, if there are any more specific routes in the main table, they will be preferred over the default route. Apparently Amazon Linux solves this differently, by not configuring a routing table for addresses on interface "eth0". That might be an alternative, but it's not clear to me what is special about eth0 to warrant this treatment. It also would imply that we somehow recognize this primary interface. In practise that would be doable by selecting the interface with "iface_idx" zero. Instead choose this approach. This is remotely similar to what WireGuard does for configuring the default route ([1]), however WireGuard uses fwmark to match the packets instead of the source address. [1] https://www.wireguard.com/netns/#improved-rule-based-routing
2021-09-01 10:31:55 +02:00
#include <linux/rtnetlink.h>
#include "nm-cloud-setup-utils.h"
#include "nmcs-provider-ec2.h"
#include "nmcs-provider-gcp.h"
#include "nmcs-provider-azure.h"
#include "nmcs-provider-aliyun.h"
#include "nmcs-provider-oci.h"
#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
/*****************************************************************************/
typedef struct {
GCancellable *cancellable;
gboolean enabled;
gboolean signal_received;
} SigTermData;
typedef struct {
SigTermData *sigterm_data;
GMainLoop *main_loop;
GCancellable *cancellable;
NMCSProvider *provider_result;
guint detect_count;
gboolean any_provider_enabled;
} ProviderDetectData;
static void
_provider_detect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
gs_unref_object NMCSProvider *provider = NMCS_PROVIDER(source);
gs_free_error GError *error = NULL;
ProviderDetectData *dd = user_data;
gboolean success;
nm_assert(dd->detect_count > 0);
dd->detect_count--;
success = nmcs_provider_detect_finish(provider, result, &error);
nm_assert(success != (!!error));
if (nm_utils_error_is_cancelled(error)) {
_LOGD("provider %s detection cancelled", nmcs_provider_get_name(provider));
goto out;
}
if (error) {
if (nm_g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_NOT_READY)) {
/* This error tells us, that the provider was not enabled in configuration. */
} else
dd->any_provider_enabled = TRUE;
_LOGI("provider %s not detected: %s", nmcs_provider_get_name(provider), error->message);
goto out;
}
_LOGI("provider %s detected", nmcs_provider_get_name(provider));
dd->provider_result = g_steal_pointer(&provider);
g_cancellable_cancel(dd->cancellable);
out:
if (dd->detect_count == 0) {
if (!dd->provider_result) {
NMLogLevel level = LOGL_INFO;
if (dd->any_provider_enabled && !dd->sigterm_data->signal_received)
level = LOGL_WARN;
_NMLOG(level, "no provider detected");
}
g_main_loop_quit(dd->main_loop);
}
}
static void
_provider_detect_sigterm_cb(GCancellable *source, gpointer user_data)
{
ProviderDetectData *dd = user_data;
g_cancellable_cancel(dd->cancellable);
g_clear_object(&dd->provider_result);
}
static NMCSProvider *
_provider_detect(SigTermData *sigterm_data)
{
nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
gs_unref_object GCancellable *cancellable = g_cancellable_new();
gs_unref_object NMHttpClient *http_client = NULL;
ProviderDetectData dd = {
.sigterm_data = sigterm_data,
.cancellable = cancellable,
.main_loop = main_loop,
.detect_count = 0,
.provider_result = NULL,
.any_provider_enabled = FALSE,
};
const GType gtypes[] = {
NMCS_TYPE_PROVIDER_EC2,
NMCS_TYPE_PROVIDER_GCP,
NMCS_TYPE_PROVIDER_AZURE,
NMCS_TYPE_PROVIDER_ALIYUN,
NMCS_TYPE_PROVIDER_OCI,
};
int i;
gulong cancellable_signal_id;
cancellable_signal_id = g_cancellable_connect(sigterm_data->cancellable,
G_CALLBACK(_provider_detect_sigterm_cb),
&dd,
NULL);
if (!cancellable_signal_id)
goto out;
http_client = nmcs_wait_for_objects_register(nm_http_client_new());
for (i = 0; i < G_N_ELEMENTS(gtypes); i++) {
NMCSProvider *provider;
provider = g_object_new(gtypes[i], NMCS_PROVIDER_HTTP_CLIENT, http_client, NULL);
nmcs_wait_for_objects_register(provider);
_LOGD("start detecting %s provider...", nmcs_provider_get_name(provider));
dd.detect_count++;
nmcs_provider_detect(provider, cancellable, _provider_detect_cb, &dd);
}
if (dd.detect_count > 0)
g_main_loop_run(main_loop);
out:
nm_clear_g_signal_handler(sigterm_data->cancellable, &cancellable_signal_id);
return dd.provider_result;
}
/*****************************************************************************/
static const NMUtilsNamedValue *gl_interfaces_map = NULL;
static NMUtilsNamedValue *
_map_interfaces_parse(void)
{
gs_free const char **split = NULL;
NMUtilsNamedValue *map_interfaces;
const char *env_var;
gsize i;
gsize j;
gsize alloc_len;
env_var = g_getenv(NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES);
if (nm_str_is_empty(env_var))
return NULL;
split = nm_strsplit_set_full(env_var, ";", NM_STRSPLIT_SET_FLAGS_STRSTRIP);
alloc_len = NM_PTRARRAY_LEN(split) + 1u;
map_interfaces = g_new(NMUtilsNamedValue, alloc_len);
_LOGD("test: map interfaces via NM_CLOUD_SETUP_MAP_INTERFACES=\"%s\"", env_var);
for (i = 0, j = 0; split && split[i]; i++) {
NMUtilsNamedValue *m;
const char *str = split[i];
char *hwaddr;
const char *s;
s = strchr(str, '=');
if (!s || str == s)
continue;
hwaddr = nmcs_utils_hwaddr_normalize(&s[1], -1);
if (!hwaddr)
continue;
nm_assert(j < alloc_len);
m = &map_interfaces[j++];
*m = (NMUtilsNamedValue) {
.name = g_strndup(str, s - str),
.value_str = hwaddr,
};
_LOGD("test: map \"%s\" -> %s", m->name, m->value_str);
}
nm_assert(j < alloc_len);
map_interfaces[j++] = (NMUtilsNamedValue) {
.name = NULL,
.value_str = NULL,
};
return g_steal_pointer(&map_interfaces);
}
static const char *
_device_get_hwaddr(NMDevice *device)
{
const NMUtilsNamedValue *map = NULL;
nm_assert(NM_IS_DEVICE_ETHERNET(device) || NM_IS_DEVICE_MACVLAN(device)
|| NM_IS_DEVICE_VLAN(device));
if (NM_IS_DEVICE_ETHERNET(device)) {
/* Ethernet interfaces in cloud environments are identified by their permanent
* MAC address.
*
* For testing, we can set NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES
* to a ';' separate list of "$INTERFACE=$HWADDR", which means that we
* pretend that device with ip-interface "$INTERFACE" has the specified permanent
* MAC address. */
map = gl_interfaces_map;
if (G_UNLIKELY(map)) {
const char *const iface = nm_device_get_iface(NM_DEVICE(device));
/* For testing, the device<->hwaddr is remapped and the actual permanent
* MAC address of the device ignored. This mapping is configured via
* NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES environment variable. */
if (!iface)
return NULL;
for (; map->name; map++) {
if (nm_streq(map->name, iface))
return map->value_str;
}
return NULL;
}
return nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(device));
} else {
return nm_device_get_hw_address(device);
}
}
static char **
_nmc_get_ethernet_hwaddrs(NMClient *nmc)
{
gs_unref_ptrarray GPtrArray *hwaddrs = NULL;
const GPtrArray *devices;
char **hwaddrs_v;
gs_free char *str = NULL;
guint i;
devices = nm_client_get_devices(nmc);
for (i = 0; i < devices->len; i++) {
NMDevice *device = devices->pdata[i];
const char *hwaddr;
char *s;
if (!NM_IS_DEVICE_ETHERNET(device))
continue;
if (nm_device_get_state(device) < NM_DEVICE_STATE_UNAVAILABLE)
continue;
hwaddr = _device_get_hwaddr(device);
if (!hwaddr)
continue;
s = nmcs_utils_hwaddr_normalize(hwaddr, -1);
if (!s)
continue;
if (!hwaddrs)
hwaddrs = g_ptr_array_new_with_free_func(g_free);
g_ptr_array_add(hwaddrs, s);
}
if (!hwaddrs) {
_LOGD("found interfaces: none");
return NULL;
}
g_ptr_array_add(hwaddrs, NULL);
hwaddrs_v = (char **) g_ptr_array_free(g_steal_pointer(&hwaddrs), FALSE);
_LOGD("found interfaces: %s", (str = g_strjoinv(", ", hwaddrs_v)));
return hwaddrs_v;
}
static NMDevice *
_nmc_get_device_by_hwaddr(NMClient *nmc, const GType type_device, const char *hwaddr)
{
const GPtrArray *devices;
guint i;
devices = nm_client_get_devices(nmc);
for (i = 0; i < devices->len; i++) {
NMDevice *device = devices->pdata[i];
const char *hwaddr_dev;
gs_free char *s = NULL;
if (!G_TYPE_CHECK_INSTANCE_TYPE(device, type_device))
continue;
hwaddr_dev = _device_get_hwaddr(device);
if (!hwaddr_dev)
continue;
s = nmcs_utils_hwaddr_normalize(hwaddr_dev, -1);
if (s && nm_streq(s, hwaddr))
return device;
}
return NULL;
}
/*****************************************************************************/
typedef struct {
GMainLoop *main_loop;
NMCSProviderGetConfigResult *result;
} GetConfigData;
static void
_get_config_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
GetConfigData *data = user_data;
nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
gs_free_error GError *error = NULL;
result = nmcs_provider_get_config_finish(NMCS_PROVIDER(source), res, &error);
if (!result) {
if (!nm_utils_error_is_cancelled(error))
_LOGI("failure to get meta data: %s", error->message);
} else
_LOGD("meta data received");
data->result = g_steal_pointer(&result);
g_main_loop_quit(data->main_loop);
}
static NMCSProviderGetConfigResult *
_get_config(GCancellable *sigterm_cancellable, NMCSProvider *provider, NMClient *nmc)
{
nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
GetConfigData data = {
.main_loop = main_loop,
};
gs_strfreev char **hwaddrs = NULL;
hwaddrs = _nmc_get_ethernet_hwaddrs(nmc);
nmcs_provider_get_config(provider,
TRUE,
(const char *const *) hwaddrs,
sigterm_cancellable,
_get_config_cb,
&data);
g_main_loop_run(main_loop);
return data.result;
}
/*****************************************************************************/
static gboolean
_nmc_skip_connection_by_user_data(NMConnection *connection)
{
NMSettingUser *s_user;
const char *v;
#define USER_TAG_SKIP "org.freedesktop.nm-cloud-setup.skip"
nm_assert(nm_setting_user_check_key(USER_TAG_SKIP, NULL));
s_user = NM_SETTING_USER(nm_connection_get_setting(connection, NM_TYPE_SETTING_USER));
if (s_user) {
v = nm_setting_user_get_data(s_user, USER_TAG_SKIP);
if (_nm_utils_ascii_str_to_bool(v, FALSE))
return TRUE;
}
return FALSE;
}
static void
_nmc_mangle_connection(NMDevice *device,
NMConnection *connection,
const NMCSProviderGetConfigResult *result,
const NMCSProviderGetConfigIfaceData *config_data,
gboolean *out_skipped_single_addr,
gboolean *out_changed)
{
NMSettingIPConfig *s_ip;
NMActiveConnection *ac;
NMConnection *remote_connection;
NMSettingIPConfig *remote_s_ip = NULL;
gsize i;
gboolean addrs_changed = FALSE;
gboolean rules_changed = FALSE;
gboolean routes_changed = FALSE;
gs_unref_ptrarray GPtrArray *addrs_new = NULL;
gs_unref_ptrarray GPtrArray *rules_new = NULL;
gs_unref_ptrarray GPtrArray *routes_new = NULL;
NM_SET_OUT(out_skipped_single_addr, FALSE);
NM_SET_OUT(out_changed, FALSE);
if (nm_streq(nm_connection_get_connection_type(connection), NM_SETTING_MACVLAN_SETTING_NAME)) {
/* The MACVLAN just sits in between, no L3 configuration on it */
return;
} else if (!nm_streq(nm_connection_get_connection_type(connection),
NM_SETTING_VLAN_SETTING_NAME)) {
/* Preserve existing L3 configuration if not a VLAN */
if (device) {
if ((ac = nm_device_get_active_connection(device))
&& (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac))))
remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection);
}
}
s_ip = nm_connection_get_setting_ip4_config(connection);
nm_assert(NM_IS_SETTING_IP4_CONFIG(s_ip));
addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref);
rules_new =
g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref);
routes_new =
g_ptr_array_new_full(nm_g_ptr_array_len(config_data->iproutes) + !!config_data->ipv4s_len,
(GDestroyNotify) nm_ip_route_unref);
if (remote_s_ip) {
guint len;
guint j;
len = nm_setting_ip_config_get_num_addresses(remote_s_ip);
for (j = 0; j < len; j++) {
g_ptr_array_add(addrs_new,
nm_ip_address_dup(nm_setting_ip_config_get_address(remote_s_ip, j)));
}
len = nm_setting_ip_config_get_num_routes(remote_s_ip);
for (j = 0; j < len; j++) {
g_ptr_array_add(routes_new,
nm_ip_route_dup(nm_setting_ip_config_get_route(remote_s_ip, j)));
}
len = nm_setting_ip_config_get_num_routing_rules(remote_s_ip);
for (j = 0; j < len; j++) {
g_ptr_array_add(
rules_new,
nm_ip_routing_rule_ref(nm_setting_ip_config_get_routing_rule(remote_s_ip, j)));
}
}
if (result->num_valid_ifaces <= 1 && result->num_ipv4s <= 1) {
/* this setup only has one interface and one IPv4 address (or less).
* We don't need to configure policy routing in this case. */
NM_SET_OUT(out_skipped_single_addr, TRUE);
} else if (config_data->has_ipv4s && config_data->has_cidr) {
gs_unref_hashtable GHashTable *unique_subnets = g_hash_table_new(nm_direct_hash, NULL);
NMIPAddress *addr_entry;
NMIPRoute *route_entry;
NMIPRoutingRule *rule_entry;
in_addr_t gateway;
char sbuf[NM_INET_ADDRSTRLEN];
for (i = 0; i < config_data->ipv4s_len; i++) {
addr_entry = nm_ip_address_new_binary(AF_INET,
&config_data->ipv4s_arr[i],
config_data->cidr_prefix,
NULL);
nm_assert(addr_entry);
g_ptr_array_add(addrs_new, addr_entry);
}
if (config_data->has_gateway && config_data->gateway) {
gateway = config_data->gateway;
} else {
glib-aux: rename IP address related helpers from "nm-inet-utils.h" - name things related to `in_addr_t`, `struct in6_addr`, `NMIPAddr` as `nm_ip4_addr_*()`, `nm_ip6_addr_*()`, `nm_ip_addr_*()`, respectively. - we have a wrapper `nm_inet_ntop()` for `inet_ntop()`. This name of our wrapper is chosen to be familiar with the libc underlying function. With this, also name functions that are about string representations of addresses `nm_inet_*()`, `nm_inet4_*()`, `nm_inet6_*()`. For example, `nm_inet_parse_str()`, `nm_inet_is_normalized()`. <<<< R() { git grep -l "$1" | xargs sed -i "s/\<$1\>/$2/g" } R NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX R NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX R NM_UTILS_INET_ADDRSTRLEN NM_INET_ADDRSTRLEN R _nm_utils_inet4_ntop nm_inet4_ntop R _nm_utils_inet6_ntop nm_inet6_ntop R _nm_utils_ip4_get_default_prefix nm_ip4_addr_get_default_prefix R _nm_utils_ip4_get_default_prefix0 nm_ip4_addr_get_default_prefix0 R _nm_utils_ip4_netmask_to_prefix nm_ip4_addr_netmask_to_prefix R _nm_utils_ip4_prefix_to_netmask nm_ip4_addr_netmask_from_prefix R nm_utils_inet4_ntop_dup nm_inet4_ntop_dup R nm_utils_inet6_ntop_dup nm_inet6_ntop_dup R nm_utils_inet_ntop nm_inet_ntop R nm_utils_inet_ntop_dup nm_inet_ntop_dup R nm_utils_ip4_address_clear_host_address nm_ip4_addr_clear_host_address R nm_utils_ip4_address_is_link_local nm_ip4_addr_is_link_local R nm_utils_ip4_address_is_loopback nm_ip4_addr_is_loopback R nm_utils_ip4_address_is_zeronet nm_ip4_addr_is_zeronet R nm_utils_ip4_address_same_prefix nm_ip4_addr_same_prefix R nm_utils_ip4_address_same_prefix_cmp nm_ip4_addr_same_prefix_cmp R nm_utils_ip6_address_clear_host_address nm_ip6_addr_clear_host_address R nm_utils_ip6_address_same_prefix nm_ip6_addr_same_prefix R nm_utils_ip6_address_same_prefix_cmp nm_ip6_addr_same_prefix_cmp R nm_utils_ip6_is_ula nm_ip6_addr_is_ula R nm_utils_ip_address_same_prefix nm_ip_addr_same_prefix R nm_utils_ip_address_same_prefix_cmp nm_ip_addr_same_prefix_cmp R nm_utils_ip_is_site_local nm_ip_addr_is_site_local R nm_utils_ipaddr_is_normalized nm_inet_is_normalized R nm_utils_ipaddr_is_valid nm_inet_is_valid R nm_utils_ipx_address_clear_host_address nm_ip_addr_clear_host_address R nm_utils_parse_inaddr nm_inet_parse_str R nm_utils_parse_inaddr_bin nm_inet_parse_bin R nm_utils_parse_inaddr_bin_full nm_inet_parse_bin_full R nm_utils_parse_inaddr_prefix nm_inet_parse_with_prefix_str R nm_utils_parse_inaddr_prefix_bin nm_inet_parse_with_prefix_bin R test_nm_utils_ip6_address_same_prefix test_nm_ip_addr_same_prefix ./contrib/scripts/nm-code-format.sh -F
2022-08-19 13:15:20 +02:00
gateway =
nm_ip4_addr_clear_host_address(config_data->cidr_addr, config_data->cidr_prefix);
if (config_data->cidr_prefix < 32)
((guint8 *) &gateway)[3] += 1;
}
cloud-setup: use suppress_prefixlength rule to honor non-default-routes in the main table Background ========== Imagine you run a container on your machine. Then the routing table might look like: default via 10.0.10.1 dev eth0 proto dhcp metric 100 10.0.10.0/28 dev eth0 proto kernel scope link src 10.0.10.5 metric 100 [...] 10.42.0.0/24 via 10.42.0.0 dev flannel.1 onlink 10.42.1.2 dev cali02ad7e68ce1 scope link 10.42.1.3 dev cali8fcecf5aaff scope link 10.42.2.0/24 via 10.42.2.0 dev flannel.1 onlink 10.42.3.0/24 via 10.42.3.0 dev flannel.1 onlink That is, there are another interfaces with subnets and specific routes. If nm-cloud-setup now configures rules: 0: from all lookup local 30400: from 10.0.10.5 lookup 30400 32766: from all lookup main 32767: from all lookup default and default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 then these other subnets will also be reached via the default route. This container example is just one case where this is a problem. In general, if you have specific routes on another interface, then the default route in the 30400+ table will interfere badly. The idea of nm-cloud-setup is to automatically configure the network for secondary IP addresses. When the user has special requirements, then they should disable nm-cloud-setup and configure whatever they want. But the container use case is popular and important. It is not something where the user actively configures the network. This case needs to work better, out of the box. In general, nm-cloud-setup should work better with the existing network configuration. Change ====== Add new routing tables 30200+ with the individual subnets of the interface: 10.0.10.0/24 dev eth0 table 30200 proto static metric 10 [...] default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 Also add more important routing rules with priority 30200+, which select these tables based on the source address: 30200: from 10.0.10.5 lookup 30200 These will do source based routing for the subnets on these interfaces. Then, add a rule with priority 30350 30350: lookup main suppress_prefixlength 0 which processes the routes from the main table, but ignores the default routes. 30350 was chosen, because it's in between the rules 30200+ and 30400+, leaving a range for the user to configure their own rules. Then, as before, the rules 30400+ again look at the corresponding 30400+ table, to find a default route. Finally, process the main table again, this time honoring the default route. That is for packets that have a different source address. This change means that the source based routing is used for the subnets that are configured on the interface and for the default route. Whereas, if there are any more specific routes in the main table, they will be preferred over the default route. Apparently Amazon Linux solves this differently, by not configuring a routing table for addresses on interface "eth0". That might be an alternative, but it's not clear to me what is special about eth0 to warrant this treatment. It also would imply that we somehow recognize this primary interface. In practise that would be doable by selecting the interface with "iface_idx" zero. Instead choose this approach. This is remotely similar to what WireGuard does for configuring the default route ([1]), however WireGuard uses fwmark to match the packets instead of the source address. [1] https://www.wireguard.com/netns/#improved-rule-based-routing
2021-09-01 10:31:55 +02:00
for (i = 0; i < config_data->ipv4s_len; i++) {
in_addr_t a = config_data->ipv4s_arr[i];
glib-aux: rename IP address related helpers from "nm-inet-utils.h" - name things related to `in_addr_t`, `struct in6_addr`, `NMIPAddr` as `nm_ip4_addr_*()`, `nm_ip6_addr_*()`, `nm_ip_addr_*()`, respectively. - we have a wrapper `nm_inet_ntop()` for `inet_ntop()`. This name of our wrapper is chosen to be familiar with the libc underlying function. With this, also name functions that are about string representations of addresses `nm_inet_*()`, `nm_inet4_*()`, `nm_inet6_*()`. For example, `nm_inet_parse_str()`, `nm_inet_is_normalized()`. <<<< R() { git grep -l "$1" | xargs sed -i "s/\<$1\>/$2/g" } R NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX R NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX R NM_UTILS_INET_ADDRSTRLEN NM_INET_ADDRSTRLEN R _nm_utils_inet4_ntop nm_inet4_ntop R _nm_utils_inet6_ntop nm_inet6_ntop R _nm_utils_ip4_get_default_prefix nm_ip4_addr_get_default_prefix R _nm_utils_ip4_get_default_prefix0 nm_ip4_addr_get_default_prefix0 R _nm_utils_ip4_netmask_to_prefix nm_ip4_addr_netmask_to_prefix R _nm_utils_ip4_prefix_to_netmask nm_ip4_addr_netmask_from_prefix R nm_utils_inet4_ntop_dup nm_inet4_ntop_dup R nm_utils_inet6_ntop_dup nm_inet6_ntop_dup R nm_utils_inet_ntop nm_inet_ntop R nm_utils_inet_ntop_dup nm_inet_ntop_dup R nm_utils_ip4_address_clear_host_address nm_ip4_addr_clear_host_address R nm_utils_ip4_address_is_link_local nm_ip4_addr_is_link_local R nm_utils_ip4_address_is_loopback nm_ip4_addr_is_loopback R nm_utils_ip4_address_is_zeronet nm_ip4_addr_is_zeronet R nm_utils_ip4_address_same_prefix nm_ip4_addr_same_prefix R nm_utils_ip4_address_same_prefix_cmp nm_ip4_addr_same_prefix_cmp R nm_utils_ip6_address_clear_host_address nm_ip6_addr_clear_host_address R nm_utils_ip6_address_same_prefix nm_ip6_addr_same_prefix R nm_utils_ip6_address_same_prefix_cmp nm_ip6_addr_same_prefix_cmp R nm_utils_ip6_is_ula nm_ip6_addr_is_ula R nm_utils_ip_address_same_prefix nm_ip_addr_same_prefix R nm_utils_ip_address_same_prefix_cmp nm_ip_addr_same_prefix_cmp R nm_utils_ip_is_site_local nm_ip_addr_is_site_local R nm_utils_ipaddr_is_normalized nm_inet_is_normalized R nm_utils_ipaddr_is_valid nm_inet_is_valid R nm_utils_ipx_address_clear_host_address nm_ip_addr_clear_host_address R nm_utils_parse_inaddr nm_inet_parse_str R nm_utils_parse_inaddr_bin nm_inet_parse_bin R nm_utils_parse_inaddr_bin_full nm_inet_parse_bin_full R nm_utils_parse_inaddr_prefix nm_inet_parse_with_prefix_str R nm_utils_parse_inaddr_prefix_bin nm_inet_parse_with_prefix_bin R test_nm_utils_ip6_address_same_prefix test_nm_ip_addr_same_prefix ./contrib/scripts/nm-code-format.sh -F
2022-08-19 13:15:20 +02:00
a = nm_ip4_addr_clear_host_address(a, config_data->cidr_prefix);
cloud-setup: use suppress_prefixlength rule to honor non-default-routes in the main table Background ========== Imagine you run a container on your machine. Then the routing table might look like: default via 10.0.10.1 dev eth0 proto dhcp metric 100 10.0.10.0/28 dev eth0 proto kernel scope link src 10.0.10.5 metric 100 [...] 10.42.0.0/24 via 10.42.0.0 dev flannel.1 onlink 10.42.1.2 dev cali02ad7e68ce1 scope link 10.42.1.3 dev cali8fcecf5aaff scope link 10.42.2.0/24 via 10.42.2.0 dev flannel.1 onlink 10.42.3.0/24 via 10.42.3.0 dev flannel.1 onlink That is, there are another interfaces with subnets and specific routes. If nm-cloud-setup now configures rules: 0: from all lookup local 30400: from 10.0.10.5 lookup 30400 32766: from all lookup main 32767: from all lookup default and default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 then these other subnets will also be reached via the default route. This container example is just one case where this is a problem. In general, if you have specific routes on another interface, then the default route in the 30400+ table will interfere badly. The idea of nm-cloud-setup is to automatically configure the network for secondary IP addresses. When the user has special requirements, then they should disable nm-cloud-setup and configure whatever they want. But the container use case is popular and important. It is not something where the user actively configures the network. This case needs to work better, out of the box. In general, nm-cloud-setup should work better with the existing network configuration. Change ====== Add new routing tables 30200+ with the individual subnets of the interface: 10.0.10.0/24 dev eth0 table 30200 proto static metric 10 [...] default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 Also add more important routing rules with priority 30200+, which select these tables based on the source address: 30200: from 10.0.10.5 lookup 30200 These will do source based routing for the subnets on these interfaces. Then, add a rule with priority 30350 30350: lookup main suppress_prefixlength 0 which processes the routes from the main table, but ignores the default routes. 30350 was chosen, because it's in between the rules 30200+ and 30400+, leaving a range for the user to configure their own rules. Then, as before, the rules 30400+ again look at the corresponding 30400+ table, to find a default route. Finally, process the main table again, this time honoring the default route. That is for packets that have a different source address. This change means that the source based routing is used for the subnets that are configured on the interface and for the default route. Whereas, if there are any more specific routes in the main table, they will be preferred over the default route. Apparently Amazon Linux solves this differently, by not configuring a routing table for addresses on interface "eth0". That might be an alternative, but it's not clear to me what is special about eth0 to warrant this treatment. It also would imply that we somehow recognize this primary interface. In practise that would be doable by selecting the interface with "iface_idx" zero. Instead choose this approach. This is remotely similar to what WireGuard does for configuring the default route ([1]), however WireGuard uses fwmark to match the packets instead of the source address. [1] https://www.wireguard.com/netns/#improved-rule-based-routing
2021-09-01 10:31:55 +02:00
G_STATIC_ASSERT_EXPR(sizeof(gsize) >= sizeof(in_addr_t));
if (g_hash_table_add(unique_subnets, GSIZE_TO_POINTER(a))) {
route_entry =
nm_ip_route_new_binary(AF_INET, &a, config_data->cidr_prefix, NULL, 10, NULL);
nm_ip_route_set_attribute(route_entry,
NM_IP_ROUTE_ATTRIBUTE_TABLE,
g_variant_new_uint32(30200 + config_data->iface_idx));
g_ptr_array_add(routes_new, route_entry);
}
rule_entry = nm_ip_routing_rule_new(AF_INET);
nm_ip_routing_rule_set_priority(rule_entry, 30200 + config_data->iface_idx);
nm_ip_routing_rule_set_from(rule_entry,
glib-aux: rename IP address related helpers from "nm-inet-utils.h" - name things related to `in_addr_t`, `struct in6_addr`, `NMIPAddr` as `nm_ip4_addr_*()`, `nm_ip6_addr_*()`, `nm_ip_addr_*()`, respectively. - we have a wrapper `nm_inet_ntop()` for `inet_ntop()`. This name of our wrapper is chosen to be familiar with the libc underlying function. With this, also name functions that are about string representations of addresses `nm_inet_*()`, `nm_inet4_*()`, `nm_inet6_*()`. For example, `nm_inet_parse_str()`, `nm_inet_is_normalized()`. <<<< R() { git grep -l "$1" | xargs sed -i "s/\<$1\>/$2/g" } R NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX R NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX R NM_UTILS_INET_ADDRSTRLEN NM_INET_ADDRSTRLEN R _nm_utils_inet4_ntop nm_inet4_ntop R _nm_utils_inet6_ntop nm_inet6_ntop R _nm_utils_ip4_get_default_prefix nm_ip4_addr_get_default_prefix R _nm_utils_ip4_get_default_prefix0 nm_ip4_addr_get_default_prefix0 R _nm_utils_ip4_netmask_to_prefix nm_ip4_addr_netmask_to_prefix R _nm_utils_ip4_prefix_to_netmask nm_ip4_addr_netmask_from_prefix R nm_utils_inet4_ntop_dup nm_inet4_ntop_dup R nm_utils_inet6_ntop_dup nm_inet6_ntop_dup R nm_utils_inet_ntop nm_inet_ntop R nm_utils_inet_ntop_dup nm_inet_ntop_dup R nm_utils_ip4_address_clear_host_address nm_ip4_addr_clear_host_address R nm_utils_ip4_address_is_link_local nm_ip4_addr_is_link_local R nm_utils_ip4_address_is_loopback nm_ip4_addr_is_loopback R nm_utils_ip4_address_is_zeronet nm_ip4_addr_is_zeronet R nm_utils_ip4_address_same_prefix nm_ip4_addr_same_prefix R nm_utils_ip4_address_same_prefix_cmp nm_ip4_addr_same_prefix_cmp R nm_utils_ip6_address_clear_host_address nm_ip6_addr_clear_host_address R nm_utils_ip6_address_same_prefix nm_ip6_addr_same_prefix R nm_utils_ip6_address_same_prefix_cmp nm_ip6_addr_same_prefix_cmp R nm_utils_ip6_is_ula nm_ip6_addr_is_ula R nm_utils_ip_address_same_prefix nm_ip_addr_same_prefix R nm_utils_ip_address_same_prefix_cmp nm_ip_addr_same_prefix_cmp R nm_utils_ip_is_site_local nm_ip_addr_is_site_local R nm_utils_ipaddr_is_normalized nm_inet_is_normalized R nm_utils_ipaddr_is_valid nm_inet_is_valid R nm_utils_ipx_address_clear_host_address nm_ip_addr_clear_host_address R nm_utils_parse_inaddr nm_inet_parse_str R nm_utils_parse_inaddr_bin nm_inet_parse_bin R nm_utils_parse_inaddr_bin_full nm_inet_parse_bin_full R nm_utils_parse_inaddr_prefix nm_inet_parse_with_prefix_str R nm_utils_parse_inaddr_prefix_bin nm_inet_parse_with_prefix_bin R test_nm_utils_ip6_address_same_prefix test_nm_ip_addr_same_prefix ./contrib/scripts/nm-code-format.sh -F
2022-08-19 13:15:20 +02:00
nm_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
cloud-setup: use suppress_prefixlength rule to honor non-default-routes in the main table Background ========== Imagine you run a container on your machine. Then the routing table might look like: default via 10.0.10.1 dev eth0 proto dhcp metric 100 10.0.10.0/28 dev eth0 proto kernel scope link src 10.0.10.5 metric 100 [...] 10.42.0.0/24 via 10.42.0.0 dev flannel.1 onlink 10.42.1.2 dev cali02ad7e68ce1 scope link 10.42.1.3 dev cali8fcecf5aaff scope link 10.42.2.0/24 via 10.42.2.0 dev flannel.1 onlink 10.42.3.0/24 via 10.42.3.0 dev flannel.1 onlink That is, there are another interfaces with subnets and specific routes. If nm-cloud-setup now configures rules: 0: from all lookup local 30400: from 10.0.10.5 lookup 30400 32766: from all lookup main 32767: from all lookup default and default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 then these other subnets will also be reached via the default route. This container example is just one case where this is a problem. In general, if you have specific routes on another interface, then the default route in the 30400+ table will interfere badly. The idea of nm-cloud-setup is to automatically configure the network for secondary IP addresses. When the user has special requirements, then they should disable nm-cloud-setup and configure whatever they want. But the container use case is popular and important. It is not something where the user actively configures the network. This case needs to work better, out of the box. In general, nm-cloud-setup should work better with the existing network configuration. Change ====== Add new routing tables 30200+ with the individual subnets of the interface: 10.0.10.0/24 dev eth0 table 30200 proto static metric 10 [...] default via 10.0.10.1 dev eth0 table 30400 proto static metric 10 10.0.10.1 dev eth0 table 30400 proto static scope link metric 10 Also add more important routing rules with priority 30200+, which select these tables based on the source address: 30200: from 10.0.10.5 lookup 30200 These will do source based routing for the subnets on these interfaces. Then, add a rule with priority 30350 30350: lookup main suppress_prefixlength 0 which processes the routes from the main table, but ignores the default routes. 30350 was chosen, because it's in between the rules 30200+ and 30400+, leaving a range for the user to configure their own rules. Then, as before, the rules 30400+ again look at the corresponding 30400+ table, to find a default route. Finally, process the main table again, this time honoring the default route. That is for packets that have a different source address. This change means that the source based routing is used for the subnets that are configured on the interface and for the default route. Whereas, if there are any more specific routes in the main table, they will be preferred over the default route. Apparently Amazon Linux solves this differently, by not configuring a routing table for addresses on interface "eth0". That might be an alternative, but it's not clear to me what is special about eth0 to warrant this treatment. It also would imply that we somehow recognize this primary interface. In practise that would be doable by selecting the interface with "iface_idx" zero. Instead choose this approach. This is remotely similar to what WireGuard does for configuring the default route ([1]), however WireGuard uses fwmark to match the packets instead of the source address. [1] https://www.wireguard.com/netns/#improved-rule-based-routing
2021-09-01 10:31:55 +02:00
32);
nm_ip_routing_rule_set_table(rule_entry, 30200 + config_data->iface_idx);
nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
g_ptr_array_add(rules_new, rule_entry);
}
rule_entry = nm_ip_routing_rule_new(AF_INET);
nm_ip_routing_rule_set_priority(rule_entry, 30350);
nm_ip_routing_rule_set_table(rule_entry, RT_TABLE_MAIN);
nm_ip_routing_rule_set_suppress_prefixlength(rule_entry, 0);
nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
g_ptr_array_add(rules_new, rule_entry);
route_entry = nm_ip_route_new_binary(AF_INET, &nm_ip_addr_zero, 0, &gateway, 10, NULL);
nm_ip_route_set_attribute(route_entry,
NM_IP_ROUTE_ATTRIBUTE_TABLE,
g_variant_new_uint32(30400 + config_data->iface_idx));
g_ptr_array_add(routes_new, route_entry);
for (i = 0; i < config_data->ipv4s_len; i++) {
rule_entry = nm_ip_routing_rule_new(AF_INET);
nm_ip_routing_rule_set_priority(rule_entry, 30400 + config_data->iface_idx);
nm_ip_routing_rule_set_from(rule_entry,
glib-aux: rename IP address related helpers from "nm-inet-utils.h" - name things related to `in_addr_t`, `struct in6_addr`, `NMIPAddr` as `nm_ip4_addr_*()`, `nm_ip6_addr_*()`, `nm_ip_addr_*()`, respectively. - we have a wrapper `nm_inet_ntop()` for `inet_ntop()`. This name of our wrapper is chosen to be familiar with the libc underlying function. With this, also name functions that are about string representations of addresses `nm_inet_*()`, `nm_inet4_*()`, `nm_inet6_*()`. For example, `nm_inet_parse_str()`, `nm_inet_is_normalized()`. <<<< R() { git grep -l "$1" | xargs sed -i "s/\<$1\>/$2/g" } R NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX R NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX R NM_UTILS_INET_ADDRSTRLEN NM_INET_ADDRSTRLEN R _nm_utils_inet4_ntop nm_inet4_ntop R _nm_utils_inet6_ntop nm_inet6_ntop R _nm_utils_ip4_get_default_prefix nm_ip4_addr_get_default_prefix R _nm_utils_ip4_get_default_prefix0 nm_ip4_addr_get_default_prefix0 R _nm_utils_ip4_netmask_to_prefix nm_ip4_addr_netmask_to_prefix R _nm_utils_ip4_prefix_to_netmask nm_ip4_addr_netmask_from_prefix R nm_utils_inet4_ntop_dup nm_inet4_ntop_dup R nm_utils_inet6_ntop_dup nm_inet6_ntop_dup R nm_utils_inet_ntop nm_inet_ntop R nm_utils_inet_ntop_dup nm_inet_ntop_dup R nm_utils_ip4_address_clear_host_address nm_ip4_addr_clear_host_address R nm_utils_ip4_address_is_link_local nm_ip4_addr_is_link_local R nm_utils_ip4_address_is_loopback nm_ip4_addr_is_loopback R nm_utils_ip4_address_is_zeronet nm_ip4_addr_is_zeronet R nm_utils_ip4_address_same_prefix nm_ip4_addr_same_prefix R nm_utils_ip4_address_same_prefix_cmp nm_ip4_addr_same_prefix_cmp R nm_utils_ip6_address_clear_host_address nm_ip6_addr_clear_host_address R nm_utils_ip6_address_same_prefix nm_ip6_addr_same_prefix R nm_utils_ip6_address_same_prefix_cmp nm_ip6_addr_same_prefix_cmp R nm_utils_ip6_is_ula nm_ip6_addr_is_ula R nm_utils_ip_address_same_prefix nm_ip_addr_same_prefix R nm_utils_ip_address_same_prefix_cmp nm_ip_addr_same_prefix_cmp R nm_utils_ip_is_site_local nm_ip_addr_is_site_local R nm_utils_ipaddr_is_normalized nm_inet_is_normalized R nm_utils_ipaddr_is_valid nm_inet_is_valid R nm_utils_ipx_address_clear_host_address nm_ip_addr_clear_host_address R nm_utils_parse_inaddr nm_inet_parse_str R nm_utils_parse_inaddr_bin nm_inet_parse_bin R nm_utils_parse_inaddr_bin_full nm_inet_parse_bin_full R nm_utils_parse_inaddr_prefix nm_inet_parse_with_prefix_str R nm_utils_parse_inaddr_prefix_bin nm_inet_parse_with_prefix_bin R test_nm_utils_ip6_address_same_prefix test_nm_ip_addr_same_prefix ./contrib/scripts/nm-code-format.sh -F
2022-08-19 13:15:20 +02:00
nm_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
32);
nm_ip_routing_rule_set_table(rule_entry, 30400 + config_data->iface_idx);
nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
g_ptr_array_add(rules_new, rule_entry);
}
}
for (i = 0; i < nm_g_ptr_array_len(config_data->iproutes); i++)
g_ptr_array_add(routes_new, _nm_ip_route_ref(config_data->iproutes->pdata[i]));
addrs_changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip,
(NMIPAddress **) addrs_new->pdata,
addrs_new->len);
routes_changed = nmcs_setting_ip_replace_ipv4_routes(s_ip,
(NMIPRoute **) routes_new->pdata,
routes_new->len);
rules_changed = nmcs_setting_ip_replace_ipv4_rules(s_ip,
(NMIPRoutingRule **) rules_new->pdata,
rules_new->len);
NM_SET_OUT(out_changed, addrs_changed || routes_changed || rules_changed);
}
/*****************************************************************************/
static gboolean
_config_existing(SigTermData *sigterm_data,
const NMCSProviderGetConfigIfaceData *config_data,
NMClient *nmc,
const NMCSProviderGetConfigResult *result,
const char *connection_type,
NMDevice *device)
{
const char *hwaddr = config_data->hwaddr;
gs_unref_object NMConnection *applied_connection = NULL;
guint64 applied_version_id;
gs_free_error GError *error = NULL;
gboolean changed;
gboolean skipped_single_addr;
gboolean version_id_changed;
guint try_count;
gboolean any_changes;
gboolean maybe_no_preserved_external_ip;
_LOGD("config device %s: configuring \"%s\" (%s)...",
hwaddr,
nm_device_get_iface(device) ?: "/unknown/",
nm_object_get_path(NM_OBJECT(device)));
try_count = 0;
any_changes = FALSE;
try_again:
g_clear_object(&applied_connection);
g_clear_error(&error);
applied_connection = nmcs_device_get_applied_connection(device,
sigterm_data->cancellable,
&applied_version_id,
&error);
if (!applied_connection) {
if (!nm_utils_error_is_cancelled(error))
_LOGD("config device %s: device has no applied connection (%s). Skip",
hwaddr,
error->message);
return any_changes;
}
if (_nmc_skip_connection_by_user_data(applied_connection)) {
_LOGD("config device %s: skip applied connection due to user data %s",
hwaddr,
USER_TAG_SKIP);
return any_changes;
}
if (!nm_streq0(nm_connection_get_connection_type(applied_connection), connection_type)) {
_LOGD("config device %s: skip applied connection due to type mismatch", hwaddr);
return any_changes;
}
if (!nm_connection_get_setting_ip4_config(applied_connection)) {
_LOGD("config device %s: skip applied connection due to missing IPv4 configuration",
hwaddr);
return any_changes;
}
_nmc_mangle_connection(device,
applied_connection,
result,
config_data,
&skipped_single_addr,
&changed);
if (!changed) {
if (skipped_single_addr) {
_LOGD("config device %s: device needs no update to applied connection \"%s\" (%s) "
"because there are not multiple IP addresses. Skip",
hwaddr,
nm_connection_get_id(applied_connection),
nm_connection_get_uuid(applied_connection));
} else {
_LOGD(
"config device %s: device needs no update to applied connection \"%s\" (%s). Skip",
hwaddr,
nm_connection_get_id(applied_connection),
nm_connection_get_uuid(applied_connection));
}
return any_changes;
}
_LOGD("config device %s: reapply connection \"%s\" (%s)",
hwaddr,
nm_connection_get_id(applied_connection),
nm_connection_get_uuid(applied_connection));
/* we are about to call Reapply(). Even if that fails, it counts as if we changed something. */
any_changes = TRUE;
/* "preserve-external-ip" flag was only introduced in 1.41.6 (but maybe backported!).
* If we run 1.41.6+, we are sure that it's gonna work. Otherwise, we take into account
* that the call might fail due to the invalid flag and we retry. */
maybe_no_preserved_external_ip =
(nmc_client_has_version_info_v(nmc) < NM_ENCODE_VERSION(1, 41, 6));
/* Once we start reconfiguring the system, we cannot abort in the middle. From now on,
* any SIGTERM gets ignored until we are done. */
sigterm_data->enabled = FALSE;
if (!nmcs_device_reapply(device,
NULL,
applied_connection,
applied_version_id,
maybe_no_preserved_external_ip,
&version_id_changed,
&error)) {
if (version_id_changed && try_count < 5) {
_LOGD("config device %s: applied connection changed in the meantime. Retry...", hwaddr);
try_count++;
goto try_again;
}
if (!nm_utils_error_is_cancelled(error)) {
_LOGD("config device %s: failure to reapply connection \"%s\" (%s): %s",
hwaddr,
nm_connection_get_id(applied_connection),
nm_connection_get_uuid(applied_connection),
error->message);
}
return TRUE;
}
_LOGD("config device %s: connection \"%s\" (%s) reapplied",
hwaddr,
nm_connection_get_id(applied_connection),
nm_connection_get_uuid(applied_connection));
return TRUE;
}
static NMConnection *
_new_connection(void)
{
NMConnection *connection;
NMSetting *s_user;
connection = nm_simple_connection_new();
s_user = nm_setting_user_new();
nm_connection_add_setting(connection, s_user);
nm_setting_user_set_data(NM_SETTING_USER(s_user),
"org.freedesktop.NetworkManager.origin",
"nm-cloud-setup",
NULL);
return connection;
}
static gboolean
_config_ethernet(SigTermData *sigterm_data,
const NMCSProviderGetConfigIfaceData *config_data,
NMClient *nmc,
const NMCSProviderGetConfigResult *result,
gboolean allow_new_connections)
{
gs_unref_object NMDevice *device = NULL;
gs_unref_object NMConnection *connection = NULL;
gs_unref_object NMActiveConnection *active_connection = NULL;
gs_free_error GError *error = NULL;
device = nm_g_object_ref(
_nmc_get_device_by_hwaddr(nmc, NM_TYPE_DEVICE_ETHERNET, config_data->hwaddr));
if (!device) {
_LOGD("config device %s: skip because device not found", config_data->hwaddr);
return FALSE;
}
if (allow_new_connections && nm_device_get_state(device) == NM_DEVICE_STATE_DISCONNECTED) {
connection = _new_connection();
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_CONNECTION,
NM_SETTING_CONNECTION_TYPE,
NM_SETTING_WIRED_SETTING_NAME,
NULL));
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_IP4_CONFIG,
NM_SETTING_IP_CONFIG_METHOD,
NM_SETTING_IP4_CONFIG_METHOD_MANUAL,
NULL));
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_WIRED,
NM_SETTING_WIRED_MAC_ADDRESS,
config_data->hwaddr,
NULL));
_nmc_mangle_connection(device, connection, result, config_data, NULL, NULL);
active_connection = nmcs_add_and_activate(nmc, NULL, device, connection, &error);
if (!active_connection) {
if (!nm_utils_error_is_cancelled(error)) {
_LOGD("config device %s: failure to activate connection: %s",
nm_device_get_iface(NM_DEVICE(device)),
error->message);
}
return FALSE;
}
_LOGD("config device %s: connection \"%s\" (%s) created",
nm_device_get_iface(NM_DEVICE(device)),
nm_active_connection_get_id(active_connection),
nm_active_connection_get_uuid(active_connection));
return TRUE;
} else {
return _config_existing(sigterm_data,
config_data,
nmc,
result,
NM_SETTING_WIRED_SETTING_NAME,
device);
}
}
static gboolean
_oci_new_vlan_dev(SigTermData *sigterm_data,
const NMCSProviderGetConfigIfaceData *config_data,
NMClient *nmc,
const NMCSProviderGetConfigResult *result,
const char *connection_type,
const char *parent_hwaddr)
{
const char *hwaddr = config_data->hwaddr;
gs_unref_object NMConnection *connection = NULL;
gs_unref_object NMActiveConnection *active_connection = NULL;
gs_free_error GError *error = NULL;
gs_free char *macvlan_name = NULL;
gs_free char *connection_id = NULL;
char *ifname = NULL;
const char *macvlan_parent = NULL;
const char *wired_mac_addr = NULL;
const NMUtilsNamedValue *map = NULL;
const char *ip4_config_method;
connection = _new_connection();
macvlan_name = g_strdup_printf("macvlan%" G_GSSIZE_FORMAT, config_data->iface_idx);
connection_id = g_strdup_printf("%s%" G_GSSIZE_FORMAT, connection_type, config_data->iface_idx);
wired_mac_addr = parent_hwaddr;
if (nm_streq(connection_type, NM_SETTING_MACVLAN_SETTING_NAME)) {
/* In NM-ci, use macvlan.parent instead of wired.mac-address for parent matching
* because we are faking the MAC addresses via NM_CLOUD_SETUP_MAP_INTERFACES.
* The daemon still needs the real MAC, not the mapped one, so it won't work. */
map = gl_interfaces_map;
if (G_UNLIKELY(map)) {
for (; map->name; map++) {
if (nm_streq(map->value_str, parent_hwaddr)) {
macvlan_parent = map->name;
wired_mac_addr = NULL;
break;
}
}
}
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_MACVLAN,
NM_SETTING_MACVLAN_MODE,
NM_SETTING_MACVLAN_MODE_VEPA,
NM_SETTING_MACVLAN_PARENT,
macvlan_parent,
NULL));
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_IP6_CONFIG,
NM_SETTING_IP_CONFIG_METHOD,
NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
NULL));
ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_DISABLED;
ifname = macvlan_name;
} else if (nm_streq(connection_type, NM_SETTING_VLAN_SETTING_NAME)) {
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_VLAN,
NM_SETTING_VLAN_PARENT,
macvlan_name,
NM_SETTING_VLAN_ID,
config_data->priv.oci.vlan_tag,
NULL));
ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_MANUAL;
} else {
g_return_val_if_reached(FALSE);
}
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_CONNECTION,
NM_SETTING_CONNECTION_ID,
connection_id,
NM_SETTING_CONNECTION_TYPE,
connection_type,
NM_SETTING_CONNECTION_INTERFACE_NAME,
ifname,
NULL));
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_IP4_CONFIG,
NM_SETTING_IP_CONFIG_METHOD,
ip4_config_method,
NULL));
nm_connection_add_setting(connection,
g_object_new(NM_TYPE_SETTING_WIRED,
NM_SETTING_WIRED_MAC_ADDRESS,
wired_mac_addr,
NM_SETTING_WIRED_CLONED_MAC_ADDRESS,
hwaddr,
NULL));
_nmc_mangle_connection(NULL, connection, result, config_data, NULL, NULL);
_LOGD("config device %s: creating %s connection for VLAN %d on %s...",
hwaddr,
ifname ?: connection_type,
config_data->priv.oci.vlan_tag,
parent_hwaddr);
active_connection = nmcs_add_and_activate(nmc, NULL, NULL, connection, &error);
if (active_connection == NULL) {
if (!nm_utils_error_is_cancelled(error)) {
_LOGD("config device %s: failure to activate connection: %s", hwaddr, error->message);
}
return FALSE;
}
_LOGD("config device %s: connection \"%s\" (%s) created",
hwaddr,
nm_active_connection_get_id(active_connection),
nm_active_connection_get_uuid(active_connection));
return TRUE;
}
static gboolean
_oci_config_vnic_dev(SigTermData *sigterm_data,
const NMCSProviderGetConfigIfaceData *config_data,
NMClient *nmc,
const NMCSProviderGetConfigResult *result,
const GType device_type,
const char *connection_type,
const char *parent_hwaddr)
{
gs_unref_object NMDevice *device = NULL;
device = nm_g_object_ref(_nmc_get_device_by_hwaddr(nmc, device_type, config_data->hwaddr));
if (device) {
/* There is a device. Modify and reapply the currently applied connection. */
return _config_existing(sigterm_data, config_data, nmc, result, connection_type, device);
} else {
/* There is no device, but we're configuring a VLAN.
* We can just go ahead and create one with a new connection. */
return _oci_new_vlan_dev(sigterm_data,
config_data,
nmc,
result,
connection_type,
parent_hwaddr);
}
}
static gboolean
_config_one(SigTermData *sigterm_data,
NMCSProvider *provider,
NMClient *nmc,
const NMCSProviderGetConfigResult *result,
guint idx)
{
const NMCSProviderGetConfigIfaceData *config_data = result->iface_datas_arr[idx];
gboolean allow_new_connections;
gboolean any_changes;
g_main_context_iteration(NULL, FALSE);
if (g_cancellable_is_cancelled(sigterm_data->cancellable))
return FALSE;
if (!nmcs_provider_get_config_iface_data_is_valid(config_data)) {
_LOGD("config device %s: skip because meta data not successfully fetched",
config_data->hwaddr);
return FALSE;
}
if (config_data->iface_idx >= 100) {
/* since we use the iface_idx to select a table number, the range is limited from
* 0 to 99. Note that the providers are required to provide increasing numbers,
* so this means we bail out after the first 100 devices. */
_LOGD("config device %s: skip because number of supported interfaces reached",
config_data->hwaddr);
return FALSE;
}
/* Default on on OCI, with an environment variable serving as a chicken bit. */
allow_new_connections =
_nm_utils_ascii_str_to_bool(g_getenv(NMCS_ENV_NM_CLOUD_SETUP_ALLOW_NEW_CONN),
NMCS_IS_PROVIDER_OCI(provider));
if (allow_new_connections && NMCS_IS_PROVIDER_OCI(provider)
&& config_data->priv.oci.vlan_tag != 0) {
if (config_data->priv.oci.parent_hwaddr == NULL) {
_LOGW("config device %s: has vlan id %d but no parent device",
config_data->hwaddr,
config_data->priv.oci.vlan_tag);
return FALSE;
}
/* MACVLAN first, because VLAN is on top of it. */
any_changes = _oci_config_vnic_dev(sigterm_data,
config_data,
nmc,
result,
NM_TYPE_DEVICE_MACVLAN,
NM_SETTING_MACVLAN_SETTING_NAME,
config_data->priv.oci.parent_hwaddr);
any_changes += _oci_config_vnic_dev(sigterm_data,
config_data,
nmc,
result,
NM_TYPE_DEVICE_VLAN,
NM_SETTING_VLAN_SETTING_NAME,
config_data->hwaddr);
} else {
any_changes =
_config_ethernet(sigterm_data, config_data, nmc, result, allow_new_connections);
}
return any_changes;
}
static gboolean
_config_all(SigTermData *sigterm_data,
NMCSProvider *provider,
NMClient *nmc,
const NMCSProviderGetConfigResult *result)
{
gboolean any_changes = FALSE;
guint i;
for (i = 0; i < result->n_iface_datas; i++) {
if (_config_one(sigterm_data, provider, nmc, result, i))
any_changes = TRUE;
}
return any_changes;
}
/*****************************************************************************/
static gboolean
sigterm_handler(gpointer user_data)
{
SigTermData *sigterm_data = user_data;
_LOGD("SIGTERM received (%s) (%s)",
sigterm_data->signal_received ? "first time" : "again",
sigterm_data->enabled ? "cancel operation" : "ignore");
sigterm_data->signal_received = TRUE;
if (sigterm_data->enabled)
g_cancellable_cancel(sigterm_data->cancellable);
return G_SOURCE_CONTINUE;
}
/*****************************************************************************/
int
main(int argc, const char *const *argv)
{
gs_unref_object GCancellable *sigterm_cancellable = NULL;
nm_auto_destroy_and_unref_gsource GSource *sigterm_source = NULL;
gs_unref_object NMCSProvider *provider = NULL;
gs_unref_object NMClient *nmc = NULL;
nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
gs_free_error GError *error = NULL;
SigTermData sigterm_data;
_nm_logging_enabled_init(g_getenv(NMCS_ENV_NM_CLOUD_SETUP_LOG));
_LOGD("nm-cloud-setup %s starting...", NM_DIST_VERSION);
if (argc != 1) {
g_printerr("%s: no command line arguments supported\n", argv[0]);
return EXIT_FAILURE;
}
sigterm_cancellable = g_cancellable_new();
sigterm_data = (SigTermData) {
.cancellable = sigterm_cancellable,
.enabled = TRUE,
.signal_received = FALSE,
};
sigterm_source = nm_g_unix_signal_add_source(SIGTERM, sigterm_handler, &sigterm_data);
provider = _provider_detect(&sigterm_data);
if (!provider)
goto done;
nmc_client_new_waitsync(sigterm_cancellable,
&nmc,
&error,
NM_CLIENT_INSTANCE_FLAGS,
(guint) NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS,
NULL);
nmcs_wait_for_objects_register(nmc);
nmcs_wait_for_objects_register(nm_client_get_context_busy_watcher(nmc));
if (error) {
if (!nm_utils_error_is_cancelled(error))
_LOGI("failure to talk to NetworkManager: %s", error->message);
goto done;
}
if (!nm_client_get_nm_running(nmc)) {
_LOGI("NetworkManager is not running");
goto done;
}
/* Initialize map used in test scenarios. */
gl_interfaces_map = _map_interfaces_parse();
result = _get_config(sigterm_cancellable, provider, nmc);
if (!result)
goto done;
if (_config_all(&sigterm_data, provider, nmc, result))
_LOGI("some changes were applied for provider %s", nmcs_provider_get_name(provider));
else
_LOGD("no changes were applied for provider %s", nmcs_provider_get_name(provider));
done:
nm_clear_pointer(&result, nmcs_provider_get_config_result_free);
g_clear_object(&nmc);
g_clear_object(&provider);
if (!nmcs_wait_for_objects_iterate_until_done(NULL, 2000)) {
_LOGE("shutdown: timeout waiting to application to quit. This is a bug");
nm_assert_not_reached();
}
nm_clear_g_source_inst(&sigterm_source);
g_clear_object(&sigterm_cancellable);
return 0;
}