From: Oleksandr Andrushchenko Date: Wed, 3 Sep 2025 07:20:24 +0000 (+0300) Subject: network: add ModemManager support X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f8a4c3d375b83f3ee249ca3f4b7f407b618a9491;p=thirdparty%2Fsystemd.git network: add ModemManager support [Match] Name=wwan* [Network] LLDP=no LinkLocalAddressing=no IPv6AcceptRA=no [ModemManager] SimpleConnectPropertie]s=apn=internet ip-type=ipv4 allow-roaming=no pin=1111 operator-id=25503 RouteMetric=200 UseGateway=yes Co-authored-by: Yu Watanabe --- diff --git a/man/systemd.network.xml b/man/systemd.network.xml index b00c2c67957..3b08a292e0d 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -6444,6 +6444,117 @@ ServerAddress=192.168.0.1/24 + + [ModemManager] Section Options + + This section configures the default setting of the ModemManager integration. See + for more information about ModemManager. + + Regardless of the [ModemManager] section settings consider using the following for LTE modems (take into account + that LTE modems do not typically support LLDP because LLDP is a Layer 2 protocol for Ethernet networks and an LTE + modem connects to a cellular network, not a local Ethernet LAN): + [Network] +LLDP=no +LinkLocalAddressing=no +IPv6AcceptRA=no + + + + The following options are available in the [ModemManager] section: + + + SimpleConnectProperties= + + Specifies the white-space separated list of simple connect properties used to connect a modem. See + for more + information about simple connect. If no properties provided then the connection is not initiated. + + + =NAME + An Access Point Name (APN) is the name of a gateway between a mobile network + (GSM, GPRS, 3G, 4G and 5G) and another computer network. Required in 3GPP. + + + + + + =METHOD + Authentication method to use. Takes one of "none", "pap", "chap", "mschap", "mschapv2" or "eap". + Optional in 3GPP. + + + + + + =NAME + User name (if any) required by the network. Optional in 3GPP. + + + + + + =PASSWORD + Password (if any) required by the network. Optional in 3GPP. + + + + + + =TYPE + Addressing type. Takes one of "none", "ipv4", "ipv6", "ipv4v6" or "any". + Optional in 3GPP and CDMA. + + + + + + =BOOL + A boolean. When true, connection is allowed during roaming. When false, + connection is not allowed during roaming. Optional in 3GPP. + + + + + + =PIN + SIM-PIN unlock code. + + + + + + =ID + ETSI MCC-MNC of a network to force registration. + + + + + + + + + RouteMetric= + + Set the routing metric for routes specified by the mobile network (including the prefix route + added for the specified prefix). Takes an unsigned integer in the range 0…4294967295. + When unset or set to 0, the kernel's default value will be used. + + + + + + + UseGateway= + + When true (the default), the router address will be configured as the default gateway. + + + + + + + + Examples @@ -6771,6 +6882,29 @@ Xfrm=xfrm0 This allows hardware based ipsec offloading to the eth0 nic. If offloading is not needed, xfrm interfaces can be assigned to the lo device. + + + + + Connecting to a cellular network with ModemManager + # /etc/systemd/network/27-wwan0.network +[Match] +Name=wwan0 + +[Network] +LLDP=no +LinkLocalAddressing=no +IPv6AcceptRA=no + +[ModemManager] +SimpleConnectProperties=apn=internet pin=1111 +RouteMetric=30 +UseGateway=yes + + This connects a cellular modem to a broadband network matched with the network interface wwan0, + with APN name internet, SIM card pin unlock code 1111 and sets up a default + gateway with route metric of 30. + diff --git a/src/network/meson.build b/src/network/meson.build index d872510b24e..4457e836444 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -89,6 +89,8 @@ systemd_networkd_extract_sources = files( 'networkd-util.c', 'networkd-wifi.c', 'networkd-wiphy.c', + 'networkd-wwan.c', + 'networkd-wwan-bus.c', 'tc/cake.c', 'tc/codel.c', 'tc/drr.c', diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index 102047a4e43..f274a0c4d94 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -29,6 +29,7 @@ #include "networkd-route.h" #include "networkd-setlink.h" #include "networkd-state-file.h" +#include "networkd-wwan.h" #include "parse-util.h" #include "set.h" #include "socket-util.h" @@ -1770,6 +1771,9 @@ int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) { if (!link->dhcp_client) return 0; + if (link_dhcp_enabled_by_bearer(link, AF_INET) == 0) + return 0; + if (!link_has_carrier(link)) return 0; diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index 5da83d5aeb3..26ab7ceb529 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -24,6 +24,7 @@ #include "networkd-queue.h" #include "networkd-route.h" #include "networkd-state-file.h" +#include "networkd-wwan.h" #include "set.h" #include "string-table.h" #include "string-util.h" @@ -494,6 +495,9 @@ int dhcp6_start(Link *link) { if (!link_dhcp6_enabled(link)) return 0; + if (link_dhcp_enabled_by_bearer(link, AF_INET6) == 0) + return 0; + if (!link_has_carrier(link)) return 0; diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c index 584f07249d4..123594179cb 100644 --- a/src/network/networkd-json.c +++ b/src/network/networkd-json.c @@ -26,6 +26,7 @@ #include "networkd-route.h" #include "networkd-route-util.h" #include "networkd-routing-policy-rule.h" +#include "networkd-wwan.h" #include "ordered-set.h" #include "set.h" #include "string-util.h" @@ -541,6 +542,15 @@ static int dns_append_json(Link *link, sd_json_variant **v) { return r; } + Bearer *b; + + if (link_get_bearer(link, &b) >= 0) + FOREACH_ARRAY(dns, b->dns, b->n_dns) { + r = dns_append_json_one(link, *dns, NETWORK_CONFIG_SOURCE_MODEM_MANAGER, NULL, &array); + if (r < 0) + return r; + } + if (link->dhcp_lease && link_get_use_dns(link, NETWORK_CONFIG_SOURCE_DHCP4)) { const struct in_addr *dns; union in_addr_union s; diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index 2a19a6003b9..cb79ddb9eb7 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -63,6 +63,7 @@ #include "networkd-state-file.h" #include "networkd-sysctl.h" #include "networkd-wifi.h" +#include "networkd-wwan-bus.h" #include "ordered-set.h" #include "parse-util.h" #include "set.h" @@ -529,6 +530,9 @@ void link_check_ready(Link *link) { if (!link->sr_iov_configured) return (void) log_link_debug(link, "%s(): SR-IOV is not configured.", __func__); + if (!link->bearer_configured) + return (void) log_link_debug(link, "%s(): Bearer has not been applied.", __func__); + /* IPv6LL is assigned after the link gains its carrier. */ if (!link->network->configure_without_carrier && link_ipv6ll_enabled(link) && @@ -538,7 +542,7 @@ void link_check_ready(Link *link) { /* All static addresses must be ready. */ bool has_static_address = false; SET_FOREACH(a, link->addresses) { - if (a->source != NETWORK_CONFIG_SOURCE_STATIC) + if (!IN_SET(a->source, NETWORK_CONFIG_SOURCE_STATIC, NETWORK_CONFIG_SOURCE_MODEM_MANAGER)) continue; if (!address_is_ready(a)) return (void) log_link_debug(link, "%s(): static address %s is not ready.", __func__, @@ -1293,6 +1297,10 @@ static int link_configure(Link *link) { if (r < 0) return r; + r = link_modem_reconfigure(link); + if (r < 0) + return r; + if (!link_has_carrier(link)) return 0; diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index 845b6f211f9..c5b9421bc0b 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -108,6 +108,7 @@ typedef struct Link { unsigned set_link_messages; unsigned set_flags_messages; unsigned create_stacked_netdev_messages; + unsigned bearer_messages; Set *addresses; Set *neighbors; @@ -141,6 +142,7 @@ typedef struct Link { bool master_set:1; bool stacked_netdevs_created:1; bool bridge_vlan_set:1; + bool bearer_configured:1; sd_dhcp_server *dhcp_server; diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c index e3c794b82b8..dbda6311d9a 100644 --- a/src/network/networkd-manager.c +++ b/src/network/networkd-manager.c @@ -46,6 +46,7 @@ #include "networkd-state-file.h" #include "networkd-wifi.h" #include "networkd-wiphy.h" +#include "networkd-wwan-bus.h" #include "ordered-set.h" #include "qdisc.h" #include "set.h" @@ -96,6 +97,8 @@ static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *r if (m->product_uuid_requested) (void) manager_request_product_uuid(m); + (void) manager_notify_mm_bus_connected(m); + return 0; } @@ -145,6 +148,8 @@ static int manager_connect_bus(Manager *m) { if (r < 0) log_warning_errno(r, "Failed to request match for PrepareForSleep, ignoring: %m"); + (void) manager_match_mm_signals(m); + return 0; } @@ -733,6 +738,9 @@ Manager* manager_free(Manager *m) { set_free(m->rules); + sd_bus_slot_unref(m->slot_mm); + hashmap_free(m->modems_by_path); + sd_netlink_unref(m->rtnl); sd_netlink_unref(m->genl); sd_netlink_unref(m->nfnl); diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h index 4017a92abd9..13467d963e5 100644 --- a/src/network/networkd-manager.h +++ b/src/network/networkd-manager.h @@ -99,6 +99,10 @@ typedef struct Manager { Hashmap *wiphy_by_index; Hashmap *wiphy_by_name; + /* ModemManager support */ + sd_bus_slot *slot_mm; + Hashmap *modems_by_path; + /* For link speed meter */ bool use_speed_meter; sd_event_source *speed_meter_event_source; diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 7f3f4c53113..10566b7a4ed 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -40,6 +40,7 @@ _Pragma("GCC diagnostic ignored \"-Wzero-as-null-pointer-constant\"") #include "networkd-radv.h" #include "networkd-route.h" #include "networkd-routing-policy-rule.h" +#include "networkd-wwan.h" #include "qdisc.h" #include "socket-util.h" #include "tclass.h" @@ -492,6 +493,9 @@ CAN.ClassicDataLengthCode, config_parse_can_control_mode, CAN.Termination, config_parse_can_termination, 0, 0 IPoIB.Mode, config_parse_ipoib_mode, 0, offsetof(Network, ipoib_mode) IPoIB.IgnoreUserspaceMulticastGroups, config_parse_tristate, 0, offsetof(Network, ipoib_umcast) +ModemManager.SimpleConnectProperties, config_parse_strv, 0, offsetof(Network, mm_simple_connect_props) +ModemManager.RouteMetric, config_parse_mm_route_metric, 0, 0 +ModemManager.UseGateway, config_parse_tristate, 0, offsetof(Network, mm_use_gateway) QDisc.Parent, config_parse_qdisc_parent, _QDISC_KIND_INVALID, 0 QDisc.Handle, config_parse_qdisc_handle, _QDISC_KIND_INVALID, 0 BFIFO.Parent, config_parse_qdisc_parent, QDISC_KIND_BFIFO, 0 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 45456d677d2..8141a45432e 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -512,6 +512,8 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .ipoib_mode = _IP_OVER_INFINIBAND_MODE_INVALID, .ipoib_umcast = -1, + + .mm_use_gateway = -1, }; r = config_parse_many_full( @@ -551,6 +553,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi "LLDP\0" "TrafficControlQueueingDiscipline\0" "CAN\0" + "ModemManager\0" "QDisc\0" "BFIFO\0" "CAKE\0" @@ -847,6 +850,9 @@ static Network *network_free(Network *network) { hashmap_free(network->qdiscs_by_section); hashmap_free(network->tclasses_by_section); + /* ModemManager */ + strv_free(network->mm_simple_connect_props); + return mfree(network); } diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index b6534c9510a..dcd9f68e781 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -414,6 +414,12 @@ typedef struct Network { /* NTP */ char **ntp; + + /* ModemManager support */ + char **mm_simple_connect_props; + int mm_use_gateway; + uint32_t mm_route_metric; + bool mm_route_metric_set; } Network; DECLARE_TRIVIAL_REF_UNREF_FUNC(Network, network); diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index c57fc8462d6..5e485f9895f 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -23,6 +23,7 @@ #include "networkd-network.h" #include "networkd-ntp.h" #include "networkd-state-file.h" +#include "networkd-wwan.h" #include "ordered-set.h" #include "set.h" #include "string-util.h" @@ -109,6 +110,14 @@ static int link_put_dns(Link *link, OrderedSet **s) { if (r < 0) return r; + Bearer *b; + + if (link_get_bearer(link, &b) >= 0) { + r = ordered_set_put_dns_servers(s, link->ifindex, b->dns, b->n_dns); + if (r < 0) + return r; + } + if (link->dhcp_lease && link_get_use_dns(link, NETWORK_CONFIG_SOURCE_DHCP4)) { const struct in_addr *addresses; @@ -801,6 +810,11 @@ static int link_save(Link *link) { space = false; link_save_dns(link, f, link->network->dns, link->network->n_dns, &space); + Bearer *b; + + if (link_get_bearer(link, &b) >= 0) + link_save_dns(link, f, b->dns, b->n_dns, &space); + /* DNR resolvers are not required to provide Do53 service, however resolved doesn't * know how to handle such a server so for now Do53 service is required, and * assumed. */ diff --git a/src/network/networkd-util.c b/src/network/networkd-util.c index 4e1a12d8d3b..b818b6121de 100644 --- a/src/network/networkd-util.c +++ b/src/network/networkd-util.c @@ -17,14 +17,15 @@ /* This is used in log messages, and never used in parsing settings. So, upper cases are OK. */ static const char * const network_config_source_table[_NETWORK_CONFIG_SOURCE_MAX] = { - [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign", - [NETWORK_CONFIG_SOURCE_STATIC] = "static", - [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL", - [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4", - [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6", - [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD", - [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc", - [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime", + [NETWORK_CONFIG_SOURCE_FOREIGN] = "foreign", + [NETWORK_CONFIG_SOURCE_STATIC] = "static", + [NETWORK_CONFIG_SOURCE_IPV4LL] = "IPv4LL", + [NETWORK_CONFIG_SOURCE_DHCP4] = "DHCPv4", + [NETWORK_CONFIG_SOURCE_DHCP6] = "DHCPv6", + [NETWORK_CONFIG_SOURCE_DHCP_PD] = "DHCP-PD", + [NETWORK_CONFIG_SOURCE_NDISC] = "NDisc", + [NETWORK_CONFIG_SOURCE_RUNTIME] = "runtime", + [NETWORK_CONFIG_SOURCE_MODEM_MANAGER] = "ModemManager", }; DEFINE_STRING_TABLE_LOOKUP(network_config_source, NetworkConfigSource); diff --git a/src/network/networkd-util.h b/src/network/networkd-util.h index 16bfb599023..e7da47d7b22 100644 --- a/src/network/networkd-util.h +++ b/src/network/networkd-util.h @@ -14,6 +14,7 @@ typedef enum NetworkConfigSource { NETWORK_CONFIG_SOURCE_DHCP_PD, NETWORK_CONFIG_SOURCE_NDISC, NETWORK_CONFIG_SOURCE_RUNTIME, /* through D-Bus method */ + NETWORK_CONFIG_SOURCE_MODEM_MANAGER, _NETWORK_CONFIG_SOURCE_MAX, _NETWORK_CONFIG_SOURCE_INVALID = -EINVAL, } NetworkConfigSource; diff --git a/src/network/networkd-wwan-bus.c b/src/network/networkd-wwan-bus.c new file mode 100644 index 00000000000..e5752726966 --- /dev/null +++ b/src/network/networkd-wwan-bus.c @@ -0,0 +1,1341 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * 1. ModemManager (MM) integration consists of two big parts: things + * we do on the networkd start and what we do during the run-time. + * + * 2. Initialization phase + * 2.1. Wait for networkd to connect to D-Bus + * 2.2. Setup D-Bus handlers for the essential signals: + * - /org/freedesktop/DBus, org.freedesktop.DBus, NameOwnerChanged - to track MM service availability + * - /org/freedesktop/ModemManager1, org.freedesktop.DBus.ObjectManager Interfaces{Added|Removed} - + * to track modem plug/unplug + * - /org/freedesktop/ModemManager1/Bearer org.freedesktop.DBus.Properties PropertiesChanged + * to track bearers + * 2.3. Check if MM service is yet available: for that call /org/freedesktop/DBus, org.freedesktop.DBus + * ListNames method and see if MM is available. If it is not, then wait for the NameOwnerChanged + * signal and see when it is; finish initialization phase. + * 2.4. If MM is available - enumerate modems, see p.4. + * 2.5. Finish initialization phase. + * + * 3. Run-time + * 3.1. During the run-time we track MM service availability. When it is gone we remove all the modems + * and bearers. + * 3.2. When MM is connected we do modem enumeration to get in sync with their current state. + * 3.3. If a modem was removed we also remove all its bearers. + * 3.4. If a modem was added we try to start a simple connect. + * 3.5. If connection was interrupted, e.g. modem changed its network connection from connected state + * we start an automatic reconnect. + * + * 4. Modem enumeration + * 4.1. Modem enumeration is done by calling GetManagedObjects. + * 4.2. By receiving managed objects we try to instantiate all new modems found. + * 4.3. For that we inspect all bearers available for that modem and add all new bearers found. + * 4.4. We also read modem ports to detect WWAN interface name assigned to this modem, e.g. "wwan0" etc. + * N.B. As we only get the interface name known that late and the corresponding .network file was + * already used by the networkd to match interfaces etc. it is not possible + * to do things like matching APN to .network and so on. + * + * 5. Simple (re)connect + * 5.1. Connection is done by calling org.freedesktop.ModemManager1.Modem.Simple Connect method for + * the relevant modem. + * 5.2. It is possible that at the time of connect the operation may fail. For that reason and to ensure + * we are always connected we employ a periodic timer which will re-try connection hoping it will + * be successful this time or when modem has recovered after an error state and so on. + * 5.3. networkd will automatically start reconnection if any external entity disconnects modem from + * the network. + */ + +#include "af-list.h" +#include "alloc-util.h" +#include "bus-error.h" +#include "bus-internal.h" +#include "bus-map-properties.h" +#include "bus-message.h" +#include "bus-util.h" +#include "event-util.h" +#include "hashmap.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-wwan.h" +#include "networkd-wwan-bus.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" + +#define RECONNECT_TIMEOUT_USEC (30 * USEC_PER_SEC) + +static const char * const modem_state_failed_reason_str[__MM_MODEM_STATE_FAILED_REASON_MAX] = { + [MM_MODEM_STATE_FAILED_REASON_NONE] = "No error", + [MM_MODEM_STATE_FAILED_REASON_UNKNOWN] = "Unknown error", + [MM_MODEM_STATE_FAILED_REASON_SIM_MISSING] = "SIM is required, but missing", + [MM_MODEM_STATE_FAILED_REASON_SIM_ERROR] = "SIM is available, but unusable", + [MM_MODEM_STATE_FAILED_REASON_UNKNOWN_CAPABILITIES] = "Unknown modem capabilities", + [MM_MODEM_STATE_FAILED_REASON_ESIM_WITHOUT_PROFILES] = "eSIM is not initialized", +}; + +static int map_name( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + Bearer *b = ASSERT_PTR(userdata); + const char *s; + int r; + + assert(m); + + /* + * If name is already set - do not wipe it on disconnect, so we can work with link and other code which + * relies on the interface name. + */ + r = sd_bus_message_read_basic(m, 's', &s); + if (r < 0) + return r; + + if (!isempty(b->name)) + return 0; + + return bearer_set_name(b, s); +} + +static int map_dns( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + Bearer *b = ASSERT_PTR(userdata); + struct in_addr_full *a; + const char *s; + int r; + + assert(m); + + r = sd_bus_message_read_basic(m, 's', &s); + if (r < 0) + return r; + + r = in_addr_full_new_from_string(s, &a); + if (r < 0) + return r; + + if (!GREEDY_REALLOC(b->dns, b->n_dns + 1)) + return -ENOMEM; + + b->dns[b->n_dns++] = TAKE_PTR(a); + + return 0; +} + +static int map_in_addr( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata, + int family) { + + union in_addr_union *addr = ASSERT_PTR(userdata); + const char *s; + int r; + + assert(m); + assert(IN_SET(family, AF_INET, AF_INET6)); + + r = sd_bus_message_read_basic(m, 's', &s); + if (r < 0) + return r; + + return in_addr_from_string(family, s, addr); +} + +static int map_in4( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + return map_in_addr(bus, member, m, error, userdata, AF_INET); +} + +static int map_in6( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + return map_in_addr(bus, member, m, error, userdata, AF_INET6); +} + +static int map_prefixlen( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata, + int family) { + + unsigned *prefixlen = ASSERT_PTR(userdata); + unsigned p; + int r; + + assert(m); + + r = sd_bus_message_read_basic(m, 'u', &p); + if (r < 0) + return r; + + if (p > FAMILY_ADDRESS_SIZE(family) * 8) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Bearer has invalid prefix length %u for %s address, ignoring.", + p, af_to_ipv4_ipv6(family)); + + *prefixlen = p; + + return 0; +} + +static int map_prefixlen4( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + return map_prefixlen(bus, member, m, error, userdata, AF_INET); +} + +static int map_prefixlen6( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + return map_prefixlen(bus, member, m, error, userdata, AF_INET6); +} + +static int map_ip4_config( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + static const struct bus_properties_map map[] = { + { "method", "u", NULL, offsetof(Bearer, ip4_method) }, + { "address", "s", map_in4, offsetof(Bearer, ip4_address) }, + { "prefix", "u", map_prefixlen4, offsetof(Bearer, ip4_prefixlen) }, + { "dns1", "s", map_dns, 0, }, + { "dns2", "s", map_dns, 0, }, + { "dns3", "s", map_dns, 0, }, + { "gateway", "s", map_in4, offsetof(Bearer, ip4_gateway) }, + { "mtu", "u", NULL, offsetof(Bearer, ip4_mtu) }, + {} + }; + Bearer *b = ASSERT_PTR(userdata); + + /* + * The "Ip4Config" property: if the bearer was configured for IPv4 addressing, upon activation this + * property contains the addressing details for assignment to the data interface. + * We may have both IPv4 and IPv6 configured. + */ + b->ip_type |= ADDRESS_FAMILY_IPV4; + + return bus_message_map_all_properties(m, map, /* flags= */ 0, error, userdata); +} + +static int map_ip6_config( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + static const struct bus_properties_map map[] = { + { "method", "u", NULL, offsetof(Bearer, ip6_method) }, + { "address", "s", map_in6, offsetof(Bearer, ip6_address) }, + { "prefix", "u", map_prefixlen6, offsetof(Bearer, ip6_prefixlen) }, + { "dns1", "s", map_dns, 0, }, + { "dns2", "s", map_dns, 0, }, + { "dns3", "s", map_dns, 0, }, + { "gateway", "s", map_in6, offsetof(Bearer, ip6_gateway) }, + { "mtu", "u", NULL, offsetof(Bearer, ip6_mtu) }, + {} + }; + Bearer *b = ASSERT_PTR(userdata); + + /* + * The "Ip6Config" property: if the bearer was configured for IPv6 addressing, upon activation this + * property contains the addressing details for assignment to the data interface. + * We may have both IPv4 and IPv6 configured. + */ + b->ip_type |= ADDRESS_FAMILY_IPV6; + + return bus_message_map_all_properties(m, map, /* flags= */ 0, error, userdata); +} + +static int map_properties( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + static const struct bus_properties_map map[] = { + { "apn", "s", NULL, offsetof(Bearer, apn) }, + {} + }; + + return bus_message_map_all_properties(m, map, BUS_MAP_STRDUP, error, userdata); +} + +static int bus_message_contains_properties( + sd_bus_message *m, + const struct bus_properties_map *map, + sd_bus_error *error) { + + unsigned found_cnt; + int r; + + assert(m); + assert(map); + + found_cnt = 0; + + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "{sv}"); + if (r < 0) + return bus_log_parse_error_debug(r); + + while ((r = sd_bus_message_enter_container(m, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0) { + const struct bus_properties_map *prop = NULL; + const char *member; + + r = sd_bus_message_read_basic(m, SD_BUS_TYPE_STRING, &member); + if (r < 0) + return bus_log_parse_error_debug(r); + + for (unsigned i = 0; map[i].member; i++) + if (streq(map[i].member, member)) { + prop = &map[i]; + break; + } + + r = sd_bus_message_skip(m, "v"); + if (r < 0) + return bus_log_parse_error_debug(r); + if (prop) + found_cnt++; + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error_debug(r); + } + if (r < 0) + return bus_log_parse_error_debug(r); + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error_debug(r); + + return found_cnt; +} + +static int bearer_get_all_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + static const struct bus_properties_map map[] = { + { "Interface", "s", map_name, 0, }, + { "Connected", "b", NULL, offsetof(Bearer, connected) }, + { "Ip4Config", "a{sv}", map_ip4_config, 0, }, + { "Ip6Config", "a{sv}", map_ip6_config, 0, }, + { "Properties", "a{sv}", map_properties, 0, }, + {} + }; + + Bearer *b = ASSERT_PTR(userdata); + const sd_bus_error *e; + int r; + + assert(message); + + b->slot_getall = sd_bus_slot_unref(b->slot_getall); + + e = sd_bus_message_get_error(message); + if (e) { + bool removed = false; + + if (sd_bus_error_has_name(e, SD_BUS_ERROR_UNKNOWN_METHOD)) + /* The path is already removed? */ + removed = true; + + r = sd_bus_error_get_errno(e); + log_full_errno(removed ? LOG_DEBUG : LOG_WARNING, r, + "Could not get properties of bearer \"%s\": %s", + b->path, bus_error_message(e, r)); + + bearer_drop(b); + return 0; + } + + /* skip name: string "org.freedesktop.ModemManager1.Bearer" */ + r = sd_bus_message_skip(message, "s"); + if (r < 0) + return log_warning_errno(r, "Failed while parsing properties of bearer \"%s\": %s", + b->path, bus_error_message(ret_error, r)); + + r = bus_message_contains_properties(message, map, ret_error); + if (r < 0) + return log_warning_errno(r, "Failed to check properties of bearer \"%s\": %s", + b->path, bus_error_message(ret_error, r)); + + /* + * We do not want to update link status on properties change which come more or less frequently + * and do not involve link state change, e.g. we do not want to bearer_update_link on Rx/Tx counters + * change. So, see if this callback was called with the changes we want to track. + */ + if (r == 0) + return 0; + + r = sd_bus_message_rewind(message, true); + if (r < 0) + return log_warning_errno(r, "Failed to rewind properties of bearer \"%s\"", b->path); + /* skip name: string "org.freedesktop.ModemManager1.Bearer" */ + r = sd_bus_message_skip(message, "s"); + if (r < 0) + return log_warning_errno(r, "Failed while parsing properties of bearer \"%s\": %s", + b->path, bus_error_message(ret_error, r)); + + r = bus_message_map_all_properties(message, map, BUS_MAP_BOOLEAN_AS_BOOL, ret_error, b); + if (r < 0) + return log_warning_errno(r, "Failed to parse properties of bearer \"%s\": %s", + b->path, bus_error_message(ret_error, r)); + + if (b->name) + log_info("%s: ModemManager announces %s %s is%s connected.", + b->name, b->modem->manufacturer, b->modem->model, + b->connected ? "" : " not"); + + if (b->connected) + b->modem->reconnect_state = MODEM_RECONNECT_DONE; + + return bearer_update_link(b); +} + +static int bearer_initialize(Bearer *b) { + int r; + + assert(b); + assert(b->modem); + assert(b->modem->manager); + assert(sd_bus_is_ready(b->modem->manager->bus) > 0); + assert(b->path); + + b->slot_getall = sd_bus_slot_unref(b->slot_getall); + + r = sd_bus_call_method_async( + b->modem->manager->bus, + &b->slot_getall, + "org.freedesktop.ModemManager1", + b->path, + "org.freedesktop.DBus.Properties", + "GetAll", + bearer_get_all_handler, + b, "s", "org.freedesktop.ModemManager1.Bearer"); + if (r < 0) + return log_warning_errno(r, "Could not get properties of bearer \"%s\": %m", b->path); + + return 0; +} + +static int bearer_new_and_initialize(Modem *modem, const char *path) { + _cleanup_(bearer_freep) Bearer *b = NULL; + int r; + + assert(modem); + assert(modem->manager); + assert(path); + + r = bearer_new(modem, path, &b); + if (r < 0) { + if (r == -EEXIST) + return 0; + return log_warning_errno(r, "Failed to allocate new bearer \"%s\": %m", path); + } + + r = bearer_initialize(b); + if (r < 0) + return r; + + TAKE_PTR(b); + return 0; +} + +static int modem_connect_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Modem *modem = ASSERT_PTR(userdata); + const sd_bus_error *e; + const char *new_bearer; + int r; + + assert(message); + + modem->slot_connect = sd_bus_slot_unref(modem->slot_connect); + + e = sd_bus_message_get_error(message); + if (e) { + r = sd_bus_error_get_errno(e); + log_full_errno(LOG_ERR, r, + "Could not connect modem %s %s: %s", + modem->manufacturer, modem->model, + bus_error_message(e, r)); + + modem->reconnect_state = MODEM_RECONNECT_WAITING; + return 0; + } + + sd_bus_message_read(message, "o", &new_bearer); + log_debug("ModemManager: %s %s connected, bearer is at %s", + modem->manufacturer, modem->model, new_bearer); + + return 0; +} + +static MMBearerIpFamily prop_iptype_lookup(const char *key) { + static const struct { + MMBearerIpFamily family; + const char *str; + } table[] = { + { MM_BEARER_IP_FAMILY_NONE, "none" }, + { MM_BEARER_IP_FAMILY_IPV4, "ipv4" }, + { MM_BEARER_IP_FAMILY_IPV6, "ipv6" }, + { MM_BEARER_IP_FAMILY_IPV4V6, "ipv4v6" }, + { MM_BEARER_IP_FAMILY_ANY, "any" }, + {} + }; + + assert(key); + + FOREACH_ELEMENT(item, table) + if (streq(item->str, key)) + return item->family; + + log_warning("ModemManager: ignoring unknown ip-type: %s, using any", key); + return MM_BEARER_IP_FAMILY_ANY; +} + +static MMBearerAllowedAuth prop_auth_lookup(const char *key) { + static const struct { + MMBearerAllowedAuth auth; + const char *str; + } table[] = { + { MM_BEARER_ALLOWED_AUTH_NONE, "none" }, + { MM_BEARER_ALLOWED_AUTH_PAP, "pap" }, + { MM_BEARER_ALLOWED_AUTH_CHAP, "chap" }, + { MM_BEARER_ALLOWED_AUTH_MSCHAP, "mschap" }, + { MM_BEARER_ALLOWED_AUTH_MSCHAPV2, "mschapv2" }, + { MM_BEARER_ALLOWED_AUTH_EAP, "eap" }, + {} + }; + + assert(key); + + FOREACH_ELEMENT(item, table) + if (streq(item->str, key)) + return item->auth; + + log_warning("ModemManager: ignoring unknown allowed-auth: %s, using none", key); + return MM_BEARER_ALLOWED_AUTH_NONE; +} + +static const char* prop_type_lookup(const char *key) { + static const struct { + const char *prop; + const char *type; + } table[] = { + { "apn", "s" }, + { "allowed-auth", "u" }, + { "user", "s" }, + { "password", "s" }, + { "ip-type", "u" }, + { "allow-roaming", "b" }, + { "pin", "s" }, + { "operator-id", "s" }, + {} + }; + + if (!key) + return NULL; + + FOREACH_ELEMENT(item, table) + if (streq(item->prop, key)) + return item->type; + return NULL; +} + +static int bus_call_method_async_props( + sd_bus *bus, + sd_bus_slot **slot, + const char *destination, + const char *path, + const char *interface, + const char *member, + sd_bus_message_handler_t callback, + void *userdata, + Link *link) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + int r; + + assert(bus); + + r = sd_bus_message_new_method_call(bus, &m, destination, path, interface, member); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "{sv}"); + if (r < 0) + return bus_log_create_error(r); + + STRV_FOREACH(prop, link->network->mm_simple_connect_props) { + const char *type; + _cleanup_free_ char *left = NULL, *right = NULL; + + r = split_pair(*prop, "=", &left, &right); + if (r < 0) + return log_warning_errno(SYNTHETIC_ERRNO(r), + "ModemManager: failed to parse simple connect option: %s, file: %s", + *prop, link->network->filename); + + type = prop_type_lookup(left); + if (!type) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "ModemManager: unknown simple connect option: %s, file: %s", + *prop, link->network->filename); + + if (streq(left, "ip-type")) { + MMBearerIpFamily ip_type = prop_iptype_lookup(right); + + r = sd_bus_message_append(m, "{sv}", left, type, (uint32_t)ip_type); + } if (streq(left, "allowed-auth")) { + MMBearerAllowedAuth auth = prop_auth_lookup(right); + + r = sd_bus_message_append(m, "{sv}", left, type, (uint32_t)auth); + } else if (streq(type, "b")) { + r = parse_boolean(right); + if (r < 0) + return -EINVAL; + r = sd_bus_message_append(m, "{sv}", left, type, r); + } else if (streq(type, "s")) + r = sd_bus_message_append(m, "{sv}", left, type, right); + + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + return sd_bus_call_async(bus, slot, m, callback, userdata, 0); +} + +static void modem_simple_connect(Modem *modem) { + Link *link; + int r; + + assert(modem); + + /* Already have simple connect in progress? */ + if (modem->slot_connect) + return; + + if (modem->reconnect_state != MODEM_RECONNECT_SCHEDULED) + return; + + /* + * If port name is not known yet then wait for the reconnect + * timer to trigger reconnection later on. + */ + if (!modem->port_name) + return; + + (void) link_get_by_name(modem->manager, modem->port_name, &link); + if (!link) + return (void) log_debug("ModemManager: cannot find link for %s", modem->port_name); + + /* Check if .network file found at all */ + if (!link->network) + return (void) log_debug("ModemManager: no .network file provideded for %s", + modem->port_name); + + /* Check if we are provided with simple connection properties */ + if (!link->network->mm_simple_connect_props) + return (void) log_debug("ModemManager: no simple connect properties provided for %s", + modem->port_name); + + log_info("ModemManager: starting simple connect on %s %s interface %s", + modem->manufacturer, modem->model, modem->port_name); + r = bus_call_method_async_props( + modem->manager->bus, + &modem->slot_connect, + "org.freedesktop.ModemManager1", + modem->path, + "org.freedesktop.ModemManager1.Modem.Simple", + "Connect", + modem_connect_handler, modem, link); + /* + * If we failed to (re)start the connection now then rely on the periodic + * timer and wait when it retries the connection attempt. + */ + if (r < 0) + log_warning_errno(r, "Could not start modem connection %s %s, will retry: %m", + modem->manufacturer, modem->model); +} + +static void modem_simple_disconnect(Modem *modem) { + int r; + + assert(modem); + + r = sd_bus_call_method_async( + modem->manager->bus, + /* ret_slot= */ NULL, + "org.freedesktop.ModemManager1", + modem->path, + "org.freedesktop.ModemManager1.Modem.Simple", + "Disconnect", + /* callback= */ NULL, + /* userdata= */ NULL, + "o", "/"); + if (r < 0) + log_warning_errno(r, "Could not disconnect modem %s %s: %m", + modem->manufacturer, modem->model); +} + +static int reset_timer(Manager *m, sd_event *e, sd_event_source **s); + +static int on_periodic_timer(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + Modem *modem; + sd_event *e; + int r; + + assert(s); + + e = sd_event_source_get_event(s); + + HASHMAP_FOREACH(modem, manager->modems_by_path) { + /* + * We might be rate limiting the reconnection, e.g. if wrong simple connect options are + * provided modem manager might try to connect (registered->connecting) and fail soon + * (connecting->registered). To rate limit such a case we set MODEM_RECONNECT_WAITING state, + * so using this timer we can limit the requests and wait, for example, for network + * reconfigure wwanX. Still do not try to reconnect modems in failed state yet. + */ + if (modem->reconnect_state == MODEM_RECONNECT_WAITING) { + if (modem->state == MM_MODEM_STATE_LOCKED) + /* If SIM is locked do not try to make it worse with applying wrong configuration again. */ + continue; + if (modem->state_fail_reason == MM_MODEM_STATE_FAILED_REASON_NONE) + modem->reconnect_state = MODEM_RECONNECT_SCHEDULED; + } + modem_simple_connect(modem); + } + + r = reset_timer(manager, e, &s); + if (r < 0) + log_warning_errno(r, "ModemManager: Failed to reset periodic timer event source, ignoring: %m"); + + return 0; +} + +static int reset_timer(Manager *m, sd_event *e, sd_event_source **s) { + return event_reset_time_relative( + e, + s, + CLOCK_MONOTONIC, + RECONNECT_TIMEOUT_USEC, + /* accuracy= */ 0, + on_periodic_timer, + m, + /* priority= */ 0, + "modem-periodic-timer-event-source", + /* force_reset= */ false); +} + +static int setup_periodic_timer(Manager *m, sd_event *event) { + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int r; + + assert(event); + assert(m); + + r = reset_timer(m, event, &s); + if (r < 0) + return r; + + return sd_event_source_set_floating(s, true); +} + +int link_modem_reconfigure(Link *link) { + Modem *modem; + int r; + + assert(link); + + if (link_get_modem(link, &modem) >= 0) { + modem_simple_disconnect(modem); + /* .network has changed: start (re)connect if failed before. */ + if (modem->reconnect_state == MODEM_RECONNECT_WAITING && + modem->state_fail_reason == MM_MODEM_STATE_FAILED_REASON_NONE) { + modem->reconnect_state = MODEM_RECONNECT_SCHEDULED; + modem_simple_connect(modem); + } + } + + r = link_apply_bearer(link); + if (r < 0) + return r; + + return 0; +} + +static int modem_on_state_change( + Modem *modem, + MMModemState old_state, + MMModemStateFailedReason old_fail_reason) { + + assert(modem); + + if (IN_SET(modem->state, MM_MODEM_STATE_CONNECTING, MM_MODEM_STATE_CONNECTED)) + /* + * Connection is ok or reconnect is already in progress: either initiataed by us or an + * external entity. Make sure we do not try to start reconnection logic and wait for th + * modem state change signal and then decide if need be. + * We assume that it is not possible to be in the above modem states e.g. + * connecting|connected if failed reason is not NONE, e.g. modem is all good. + */ + return 0; + + /* Check if modem is still in failed state. */ + if (modem->state_fail_reason != MM_MODEM_STATE_FAILED_REASON_NONE) { + if (modem->state_fail_reason != old_fail_reason) { + log_error("ModemManager: cannot schedule reconnect for %s %s, modem is in failed state: %s", + modem->manufacturer, modem->model, + modem->state_fail_reason < __MM_MODEM_STATE_FAILED_REASON_MAX ? + modem_state_failed_reason_str[modem->state_fail_reason] : + "unknown reason"); + + /* Do not try to reconnect until modem has recovered. */ + modem->reconnect_state = MODEM_RECONNECT_WAITING; + } + return 0; + } + + if (modem->reconnect_state == MODEM_RECONNECT_SCHEDULED) + /* We are reconnecting now. */ + return 0; + + /* + * Modem is not in failed state and is not connected: try now. It is ok to fail and re-try to + * connect with periodic timer later on. + */ + modem->reconnect_state = MODEM_RECONNECT_SCHEDULED; + modem_simple_connect(modem); + + return 0; +} + +static int bearer_properties_changed_handler( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Manager *manager = ASSERT_PTR(userdata); + const char *path; + Modem *modem; + Bearer *b; + + assert(message); + + path = sd_bus_message_get_path(message); + if (!path) + return 0; + + if (bearer_get_by_path(manager, path, &modem, &b) < 0) { + /* + * Have new bearer: check if we have the corresponding modem + * for it which we might not during initialization. + */ + if (modem) + (void) bearer_new_and_initialize(modem, path); + return 0; + } + + if (b->slot_getall) { + /* Not initialized yet. Re-initialize it. */ + (void) bearer_initialize(b); + return 0; + } + + (void) bearer_get_all_handler(message, b, error); + return 0; +} + +static int modem_map_bearers( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + Modem *modem = ASSERT_PTR(userdata); + _cleanup_strv_free_ char **paths = NULL; + int r; + + r = sd_bus_message_read_strv(m, &paths); + if (r < 0) + return bus_log_parse_error(r); + + STRV_FOREACH(path, paths) { + log_info("ModemManager: bearer found at path %s", *path); + (void) bearer_new_and_initialize(modem, *path); + } + + return 0; +} + +static int modem_map_ports( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + Modem *modem = ASSERT_PTR(userdata); + const char *port_name; + uint32_t port_type; + int r; + + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, NULL); + if (r < 0) + return bus_log_parse_error_debug(r); + + while ((r = sd_bus_message_read(m, "(su)", &port_name, &port_type)) > 0) + if (port_type == MM_MODEM_PORT_TYPE_NET) { + r = free_and_strdup_warn(&modem->port_name, port_name); + if (r < 0) + return r; + break; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error(r); + + return 0; +} + +static int modem_properties_changed_signal( + sd_bus_message *message, + void *userdata, + sd_bus_error *ret_error) { + + static const struct bus_properties_map map[] = { + { "Bearers", "a{sv}", modem_map_bearers, 0, }, + { "State", "i", NULL, offsetof(Modem, state) }, + { "StateFailedReason", "u", NULL, offsetof(Modem, state_fail_reason) }, + { "Manufacturer", "s", NULL, offsetof(Modem, manufacturer) }, + { "Model", "s", NULL, offsetof(Modem, model) }, + { "Ports", "a{su}", modem_map_ports, 0, }, + {} + }; + Modem *modem = ASSERT_PTR(userdata); + MMModemState old_state; + MMModemStateFailedReason old_fail_reason; + int r; + + /* skip name: string "org.freedesktop.ModemManager1.Modem" */ + r = sd_bus_message_skip(message, "s"); + if (r < 0) + return log_warning_errno(r, "Failed while parsing properties of modem %s: %s", + modem->path, bus_error_message(ret_error, r)); + + r = bus_message_contains_properties(message, map, ret_error); + if (r < 0) + return log_warning_errno(r, "Failed to check changed properties of modem %s: %s", + modem->path, bus_error_message(ret_error, r)); + + if (r == 0) + return 0; + + r = sd_bus_message_rewind(message, true); + if (r < 0) + return log_warning_errno(r, "Failed to rewind properties of modem %s", modem->path); + old_state = modem->state; + old_fail_reason = modem->state_fail_reason; + + /* skip name: string "org.freedesktop.ModemManager1.Bearer" */ + r = sd_bus_message_skip(message, "s"); + if (r < 0) + return log_warning_errno(r, "Failed while parsing properties of modem %s: %s", + modem->path, bus_error_message(ret_error, r)); + + r = bus_message_map_all_properties(message, map, + BUS_MAP_BOOLEAN_AS_BOOL | BUS_MAP_STRDUP, + ret_error, modem); + if (r < 0) + return log_warning_errno(r, "Failed to parse properties of modem %s: %s", + modem->path, bus_error_message(ret_error, r)); + + return modem_on_state_change(modem, old_state, old_fail_reason); +} + +static int modem_properties_changed_installed( + sd_bus_message *message, + void *userdata, + sd_bus_error *ret_error) { + + Modem *modem = ASSERT_PTR(userdata); + + /* + * As soon as the signal handler installed we can start reconnect + * so we don't miss any property changed. + */ + return modem_on_state_change(modem, MM_MODEM_STATE_UNKNOWN, MM_MODEM_STATE_FAILED_REASON_UNKNOWN); +} + +static int modem_match_properties_changed(Modem *modem, const char *path) { + int r; + + assert(modem); + assert(modem->manager); + assert(modem->manager->bus); + + r = sd_bus_match_signal_async(modem->manager->bus, &modem->slot_propertieschanged, + "org.freedesktop.ModemManager1", path, + "org.freedesktop.DBus.Properties", "PropertiesChanged", + modem_properties_changed_signal, modem_properties_changed_installed, modem); + if (r < 0) + return log_error_errno(r, "Failed to request match for PropertiesChanged for modem %s: %m", path); + + return 0; +} + +static int modem_add(Manager *m, const char *path, sd_bus_message *message, sd_bus_error *ret_error) { + static const struct bus_properties_map map[] = { + { "Bearers", "ao", modem_map_bearers, 0, }, + { "State", "i", NULL, offsetof(Modem, state) }, + { "StateFailedReason", "u", NULL, offsetof(Modem, state_fail_reason) }, + { "Manufacturer", "s", NULL, offsetof(Modem, manufacturer) }, + { "Model", "s", NULL, offsetof(Modem, model) }, + { "Ports", "a{su}", modem_map_ports, 0, }, + {} + }; + Modem *modem; + int r; + + r = modem_get_by_path(m, path, &modem); + if (r != -ENOENT) + return sd_bus_message_skip(message, "a{sv}"); + + log_info("ModemManager: modem found at %s\n", path); + + r = modem_new(m, path, &modem); + if (r < 0) + return log_warning_errno(r, "Failed to initialize modem at %s, ignoring", path); + + r = modem_match_properties_changed(modem, path); + if (r < 0) + return log_warning_errno(r, "Failed to match on properties changed at %s, ignoring", path); + + r = bus_message_map_all_properties(message, map, BUS_MAP_STRDUP, ret_error, modem); + if (r < 0) + return log_warning_errno(r, "Failed to map properties at %s, ignoring", path); + + return 0; +} + +static void modem_remove(Manager *m, const char *path) { + Modem *modem; + int r; + + r = modem_get_by_path(m, path, &modem); + if (r < 0) + return; + + log_error("ModemManager: %s %s %s removed", modem->manufacturer, modem->model, modem->port_name); + modem_free(modem); +} + +static int enumerate_modems_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Manager *manager = ASSERT_PTR(userdata); + const sd_bus_error *e; + const char *modem_path; + int r; + + assert(message); + + e = sd_bus_message_get_error(message); + if (e) { + r = sd_bus_error_get_errno(e); + log_warning_errno(r, "Could not get managed objects: %s", bus_error_message(e, r)); + return 0; + } + + r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{oa{sa{sv}}}"); + if (r < 0) + return bus_log_parse_error_debug(r); + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "oa{sa{sv}}")) > 0) { + r = sd_bus_message_read_basic(message, SD_BUS_TYPE_OBJECT_PATH, &modem_path); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{sa{sv}}"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "sa{sv}")) > 0) { + const char *interface_name = NULL; + + r = sd_bus_message_read_basic(message, 's', &interface_name); + if (r < 0) + return bus_log_parse_error(r); + + if (streq("org.freedesktop.ModemManager1.Modem", interface_name)) { + r = modem_add(manager, modem_path, message, ret_error); + if (r < 0) + return log_error_errno(r, "Failed to add modem at %s: %m", modem_path); + } else { + r = sd_bus_message_skip(message, "a{sv}"); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(message); + if (r < 0) + return bus_log_parse_error(r); + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return bus_log_parse_error(r); + + return 0; +} + +static int enumerate_modems(Manager *manager) { + int r; + + log_debug("ModemManager: enumerate modems"); + /* Enumerate all modems and add new and drop removed. */ + + assert(manager); + assert(sd_bus_is_ready(manager->bus) > 0); + + r = sd_bus_call_method_async( + manager->bus, + /* ret_slot= */ NULL, + "org.freedesktop.ModemManager1", + "/org/freedesktop/ModemManager1", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", + enumerate_modems_handler, + manager, + /* types= */ NULL); + if (r < 0) + return log_error_errno(r, "Could not get managed objects: %m"); + + return 0; +} + +static int interface_add_remove_signal(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + assert(manager); + assert(message); + + manager->slot_mm = sd_bus_slot_unref(manager->slot_mm); + + if (streq(message->member, "InterfacesAdded")) + log_info("ModemManager: modem(s) added"); + else { + const char *path; + + r = sd_bus_message_read_basic(message, 'o', &path); + if (r < 0) + return r; + + modem_remove(manager, path); + return 0; + } + + return enumerate_modems(manager); +} + +static int name_owner_changed_signal(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *manager = ASSERT_PTR(userdata); + const char *name; + const char *new_owner; + int r; + + assert(manager); + assert(message); + + r = sd_bus_message_read(message, "sss", &name, NULL, &new_owner); + if (r < 0) { + bus_log_parse_error(r); + return 0; + } + + if (!streq(name, "org.freedesktop.ModemManager1")) + return 0; + + if (!isempty(new_owner)) + log_info("ModemManager: service is available"); + else { + log_info("ModemManager: service is not available"); + hashmap_clear(manager->modems_by_path); + return 0; + } + return enumerate_modems(manager); +} + +int manager_match_mm_signals(Manager *manager) { + int r; + + assert(manager); + assert(manager->bus); + + r = sd_bus_match_signal_async( + manager->bus, + /* ret= */ NULL, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "NameOwnerChanged", + name_owner_changed_signal, + /* install_callback= */ NULL, + manager); + if (r < 0) + return log_error_errno(r, "Failed to request signal for NameOwnerChanged"); + + r = sd_bus_match_signal_async( + manager->bus, + /* ret= */ NULL, + "org.freedesktop.ModemManager1", + "/org/freedesktop/ModemManager1", + "org.freedesktop.DBus.ObjectManager", + "InterfacesAdded", + interface_add_remove_signal, + /* install_callback= */ NULL, + manager); + if (r < 0) + return log_error_errno(r, "Failed to request signal for IntefaceAdded"); + + r = sd_bus_match_signal_async( + manager->bus, + /* ret= */ NULL, + "org.freedesktop.ModemManager1", + "/org/freedesktop/ModemManager1", + "org.freedesktop.DBus.ObjectManager", + "InterfacesRemoved", + interface_add_remove_signal, + /* install_callback= */ NULL, + manager); + if (r < 0) + return log_error_errno(r, "Failed to request signal for IntefaceRemoved"); + + /* N.B. We need "path_namespace" for bearers, not "path", */ + r = sd_bus_add_match_async( + manager->bus, + /* ret_slot= */ NULL, + "type='signal'," + "sender='org.freedesktop.ModemManager1'," + "path_namespace='/org/freedesktop/ModemManager1/Bearer'," + "interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged'", + bearer_properties_changed_handler, + /* install_callback= */ NULL, + manager); + if (r < 0) + return log_error_errno(r, "Failed to request signal for PropertiesChanged in ModemManager bearers"); + + return 0; +} + +static int list_names_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Manager *manager = ASSERT_PTR(userdata); + _cleanup_strv_free_ char **names = NULL; + int r; + + assert(manager); + assert(message); + + manager->slot_mm = sd_bus_slot_unref(manager->slot_mm); + + /* Read the list of available services. */ + r = sd_bus_message_read_strv(message, &names); + if (r < 0) + return bus_log_parse_error(r); + + if (!strv_contains(names, "org.freedesktop.ModemManager1")) + /* If not found yet then wait for NameOwnerChanged signal. */ + return 0; + + log_info("ModemManager: service available"); + return enumerate_modems(manager); +} + +int manager_notify_mm_bus_connected(Manager *m) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + /* + * Called on D-Bus connected. + * Check if ModemManager is available. If it is then initialize. + * If not then wait for the serivce to be available. + */ + assert(m); + assert(sd_bus_is_ready(m->bus) > 0); + + m->slot_mm = sd_bus_slot_unref(m->slot_mm); + + r = sd_bus_call_method_async( + m->bus, + &m->slot_mm, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "ListNames", + list_names_handler, + m, + /* types= */ NULL); + if (r < 0) + return log_warning_errno(r, "Could not LsitNames: %m"); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to initialize sd-event: %m"); + + r = setup_periodic_timer(m, event); + if (r < 0) + return log_error_errno(r, "Failed to set up periodic timer: %m"); + + return 0; +} diff --git a/src/network/networkd-wwan-bus.h b/src/network/networkd-wwan-bus.h new file mode 100644 index 00000000000..a6697cc2195 --- /dev/null +++ b/src/network/networkd-wwan-bus.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Manager Manager; +typedef struct Link Link; + +/* From ModemManager-enums.h */ +typedef enum { + MM_BEARER_IP_FAMILY_NONE = 0, + MM_BEARER_IP_FAMILY_IPV4 = 1 << 0, + MM_BEARER_IP_FAMILY_IPV6 = 1 << 1, + MM_BEARER_IP_FAMILY_IPV4V6 = 1 << 2, + MM_BEARER_IP_FAMILY_ANY = 0xFFFFFFFF, +} MMBearerIpFamily; + +typedef enum { + MM_BEARER_TYPE_UNKNOWN = 0, + MM_BEARER_TYPE_DEFAULT = 1, + MM_BEARER_TYPE_DEFAULT_ATTACH = 2, + MM_BEARER_TYPE_DEDICATED = 3, +} MMBearerType; + +typedef enum { + MM_MODEM_STATE_FAILED = -1, + MM_MODEM_STATE_UNKNOWN = 0, + MM_MODEM_STATE_INITIALIZING = 1, + MM_MODEM_STATE_LOCKED = 2, + MM_MODEM_STATE_DISABLED = 3, + MM_MODEM_STATE_DISABLING = 4, + MM_MODEM_STATE_ENABLING = 5, + MM_MODEM_STATE_ENABLED = 6, + MM_MODEM_STATE_SEARCHING = 7, + MM_MODEM_STATE_REGISTERED = 8, + MM_MODEM_STATE_DISCONNECTING = 9, + MM_MODEM_STATE_CONNECTING = 10, + MM_MODEM_STATE_CONNECTED = 11, +} MMModemState; + +typedef enum { /*< underscore_name=mm_modem_state_failed_reason >*/ + MM_MODEM_STATE_FAILED_REASON_NONE = 0, + MM_MODEM_STATE_FAILED_REASON_UNKNOWN = 1, + MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2, + MM_MODEM_STATE_FAILED_REASON_SIM_ERROR = 3, + MM_MODEM_STATE_FAILED_REASON_UNKNOWN_CAPABILITIES = 4, + MM_MODEM_STATE_FAILED_REASON_ESIM_WITHOUT_PROFILES = 5, + __MM_MODEM_STATE_FAILED_REASON_MAX = 6, +} MMModemStateFailedReason; + +typedef enum { + MM_BEARER_IP_METHOD_UNKNOWN = 0, + MM_BEARER_IP_METHOD_PPP = 1, + MM_BEARER_IP_METHOD_STATIC = 2, + MM_BEARER_IP_METHOD_DHCP = 3, +} MMBearerIpMethod; + +typedef enum { /*< underscore_name=mm_modem_port_type >*/ + MM_MODEM_PORT_TYPE_UNKNOWN = 1, + MM_MODEM_PORT_TYPE_NET = 2, + MM_MODEM_PORT_TYPE_AT = 3, + MM_MODEM_PORT_TYPE_QCDM = 4, + MM_MODEM_PORT_TYPE_GPS = 5, + MM_MODEM_PORT_TYPE_QMI = 6, + MM_MODEM_PORT_TYPE_MBIM = 7, + MM_MODEM_PORT_TYPE_AUDIO = 8, + MM_MODEM_PORT_TYPE_IGNORED = 9, + MM_MODEM_PORT_TYPE_XMMRPC = 10, +} MMModemPortType; + +typedef enum { /*< underscore_name=mm_bearer_allowed_auth >*/ + MM_BEARER_ALLOWED_AUTH_UNKNOWN = 0, + /* bits 0..4 order match Ericsson device bitmap */ + MM_BEARER_ALLOWED_AUTH_NONE = 1 << 0, + MM_BEARER_ALLOWED_AUTH_PAP = 1 << 1, + MM_BEARER_ALLOWED_AUTH_CHAP = 1 << 2, + MM_BEARER_ALLOWED_AUTH_MSCHAP = 1 << 3, + MM_BEARER_ALLOWED_AUTH_MSCHAPV2 = 1 << 4, + MM_BEARER_ALLOWED_AUTH_EAP = 1 << 5, +} MMBearerAllowedAuth; + +typedef enum { + MODEM_RECONNECT_DONE, /* No reconnect is required, e.g. connected. */ + MODEM_RECONNECT_SCHEDULED, /* Reconnect is in progress. */ + MODEM_RECONNECT_WAITING, /* Waiting for modem to recover. */ +} ModemReconnectState; + +int manager_notify_mm_bus_connected(Manager *manager); +int manager_match_mm_signals(Manager *manager); +int link_modem_reconfigure(Link *link); diff --git a/src/network/networkd-wwan.c b/src/network/networkd-wwan.c new file mode 100644 index 00000000000..64dd37eb6b1 --- /dev/null +++ b/src/network/networkd-wwan.c @@ -0,0 +1,641 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bus-util.h" +#include "hashmap.h" +#include "networkd-address.h" +#include "networkd-dhcp4.h" +#include "networkd-dhcp6.h" +#include "networkd-link.h" +#include "networkd-setlink.h" +#include "networkd-manager.h" +#include "networkd-ndisc.h" +#include "networkd-route.h" +#include "networkd-wwan.h" +#include "parse-util.h" +#include "sd-dhcp-client.h" +#include "sd-dhcp6-client.h" +#include "sd-ndisc.h" +#include "set.h" +#include "string-util.h" + +Bearer* bearer_free(Bearer *b) { + if (!b) + return NULL; + + if (b->modem) { + if (b->path) + hashmap_remove_value(b->modem->bearers_by_path, b->path, b); + if (b->name) + hashmap_remove_value(b->modem->bearers_by_name, b->name, b); + } + + sd_bus_slot_unref(b->slot_getall); + + free(b->path); + free(b->name); + free(b->apn); + + in_addr_full_array_free(b->dns, b->n_dns); + + return mfree(b); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + bearer_hash_ops, + char, + string_hash_func, + string_compare_func, + Bearer, + bearer_free); + +int bearer_new(Modem *modem, const char *path, Bearer **ret) { + _cleanup_(bearer_freep) Bearer *b = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(modem); + assert(path); + + if (hashmap_contains(modem->bearers_by_path, path)) + return -EEXIST; + + p = strdup(path); + if (!p) + return -ENOMEM; + + b = new(Bearer, 1); + if (!b) + return -ENOMEM; + + *b = (Bearer) { + .modem = modem, + .path = TAKE_PTR(p), + }; + + r = hashmap_ensure_put(&modem->bearers_by_path, &bearer_hash_ops, b->path, b); + if (r < 0) + return r; + + if (ret) + *ret = b; + TAKE_PTR(b); + return 0; +} + +int bearer_set_name(Bearer *b, const char *name) { + Bearer *old; + int r; + + assert(b); + assert(b->modem); + assert(name); + + if (streq_ptr(b->name, name)) + return 0; + + if (b->name) + hashmap_remove_value(b->modem->bearers_by_name, b->name, b); + + if (isempty(name)) { + b->name = mfree(b->name); + return 0; + } + + r = free_and_strdup(&b->name, name); + if (r < 0) + return r; + + /* + * FIXME: it is possible during reconnect that an interface is already + * registered in the hash map: if simple connect options + * are changed, e.g. externally modified .network file and then + * reloaded with 'networkctl reload'. This may create a new bearer + * attached to the same inerface name, e.g. "wwan0". The order in which + * we parse the bearer properties is undetermined and it can be that we + * need to raplce the old one with the new one now, so only one bearer + * with the given interface name exists. + */ + old = hashmap_get(b->modem->bearers_by_name, name); + if (old) { + hashmap_remove_value(old->modem->bearers_by_name, name, old); + old->name = mfree(old->name); + } + + return hashmap_ensure_put(&b->modem->bearers_by_name, &bearer_hash_ops, b->name, b); +} + +int bearer_get_by_path(Manager *manager, const char *path, Modem **ret_modem, Bearer **ret_bearer) { + Modem *modem; + Bearer *b; + + assert(manager); + assert(path); + + HASHMAP_FOREACH(modem, manager->modems_by_path) { + b = hashmap_get(modem->bearers_by_path, path); + if (!b) + continue; + + if (ret_bearer) + *ret_bearer = b; + if (ret_modem) + *ret_modem = modem; + return 0; + } + + return -ENOENT; +} + +Modem* modem_free(Modem *modem) { + if (!modem) + return NULL; + + if (modem->bearers_by_name) + hashmap_free(modem->bearers_by_name); + + if (modem->bearers_by_path) + hashmap_free(modem->bearers_by_path); + + if (modem->manager) + hashmap_remove_value(modem->manager->modems_by_path, modem->path, modem); + + sd_bus_slot_unref(modem->slot_propertieschanged); + sd_bus_slot_unref(modem->slot_statechanged); + sd_bus_slot_unref(modem->slot_connect); + + free(modem->path); + free(modem->manufacturer); + free(modem->model); + free(modem->port_name); + + return mfree(modem); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + modems_hash_ops, + char, + string_hash_func, + string_compare_func, + Modem, + modem_free); + +int modem_new(Manager *m, const char *path, Modem **ret) { + _cleanup_(modem_freep) Modem *modem = NULL; + _cleanup_free_ char *p = NULL; + int r; + + assert(m); + assert(path); + + if (hashmap_contains(m->modems_by_path, path)) + return -EEXIST; + + p = strdup(path); + if (!p) + return -ENOMEM; + + modem = new(Modem, 1); + if (!modem) + return -ENOMEM; + + *modem = (Modem) { + .manager = m, + .path = TAKE_PTR(p), + }; + + r = hashmap_ensure_put(&m->modems_by_path, &modems_hash_ops, modem->path, modem); + if (r < 0) + return r; + + if (ret) + *ret = modem; + + TAKE_PTR(modem); + return 0; +} + +int modem_get_by_path(Manager *m, const char *path, Modem **ret) { + Modem *modem; + + assert(m); + assert(path); + + modem = hashmap_get(m->modems_by_path, path); + if (!modem) + return -ENOENT; + + if (ret) + *ret = modem; + + return 0; +} + +int link_get_modem(Link *link, Modem **ret) { + Modem *modem; + + assert(link); + assert(link->manager); + assert(link->ifname); + + HASHMAP_FOREACH(modem, link->manager->modems_by_path) + if (modem->port_name && streq(modem->port_name, link->ifname)) { + *ret = modem; + return 0; + } + + return -ENOENT; +} + +int link_get_bearer(Link *link, Bearer **ret) { + Modem *modem; + + assert(link); + assert(link->manager); + assert(link->ifname); + + HASHMAP_FOREACH(modem, link->manager->modems_by_path) { + Bearer *b; + + b = hashmap_get(modem->bearers_by_name, link->ifname); + if (!b) + continue; + + if (ret) + *ret = b; + return 0; + } + + return -ENOENT; +} + +int link_dhcp_enabled_by_bearer(Link *link, int family) { + Bearer *b; + int r; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + + r = link_get_bearer(link, &b); + if (r < 0) + return r; + + if (!b->connected) + return false; + + if (!FLAGS_SET(b->ip_type, family == AF_INET ? ADDRESS_FAMILY_IPV4 : ADDRESS_FAMILY_IPV6)) + return false; + + return (family == AF_INET ? b->ip4_method : b->ip6_method) == MM_BEARER_IP_METHOD_DHCP; +} + +static int bearer_address_handler( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + Address *address) { + + int r; + + assert(link); + + r = address_configure_handler_internal(m, link, address); + if (r <= 0) + return r; + + if (link->bearer_messages == 0) { + link->bearer_configured = true; + link_check_ready(link); + } + + return 0; +} + +static int link_request_bearer_address( + Link *link, + int family, + const union in_addr_union *addr, + unsigned prefixlen) { + + _cleanup_(address_unrefp) Address *address = NULL; + Address *existing; + int r; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(addr); + + if (!in_addr_is_set(family, addr)) + return 0; + + r = address_new(&address); + if (r < 0) + return log_oom(); + + address->source = NETWORK_CONFIG_SOURCE_MODEM_MANAGER; + address->family = family; + address->in_addr = *addr; + address->prefixlen = prefixlen; + + if (address_get(link, address, &existing) < 0) /* The address is new. */ + link->bearer_configured = false; + else + address_unmark(existing); + + r = link_request_address(link, address, &link->bearer_messages, + bearer_address_handler, /* ret = */ NULL); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request address provided by bearer: %m"); + + return 0; +} + +static int bearer_route_handler( + sd_netlink *rtnl, + sd_netlink_message *m, + Request *req, + Link *link, + Route *route) { + + int r; + + assert(link); + + r = route_configure_handler_internal(m, req, route); + if (r <= 0) + return r; + + if (link->bearer_messages == 0) { + link->bearer_configured = true; + link_check_ready(link); + } + + return 0; +} + +static int link_request_bearer_route( + Link *link, + int family, + const union in_addr_union *gw, + const union in_addr_union *prefsrc) { + + _cleanup_(route_unrefp) Route *route = NULL; + Route *existing; + int r; + + assert(link); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(gw); + assert(prefsrc); + + if (!in_addr_is_set(family, gw)) + return 0; + + if (link->network->mm_use_gateway == 0) + return 0; + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->source = NETWORK_CONFIG_SOURCE_MODEM_MANAGER; + route->family = family; + route->nexthop.family = family; + route->nexthop.gw = *gw; + if (link->network->mm_route_metric_set) { + route->priority = link->network->mm_route_metric; + route->priority_set = true; + } + + if (prefsrc) + route->prefsrc = *prefsrc; + + if (route_get(link->manager, route, &existing) < 0) /* This is a new route. */ + link->bearer_configured = false; + else + route_unmark(existing); + + r = link_request_route(link, route, &link->bearer_messages, bearer_route_handler); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to request gateway provided by bearer: %m"); + + return 0; +} + +static int link_apply_bearer_impl(Link *link, Bearer *b) { + Address *address; + Route *route; + int r, ret = 0; + + assert(link); + + if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) + return 0; + + /* First, mark bearer configs. */ + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER) + continue; + + address_mark(address); + } + + SET_FOREACH(route, link->manager->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER) + continue; + + route_mark(route); + } + + if (b && FLAGS_SET(b->ip_type, ADDRESS_FAMILY_IPV4)) { + if (b->connected && b->ip4_method == MM_BEARER_IP_METHOD_STATIC) { + r = link_request_bearer_address(link, AF_INET, &b->ip4_address, b->ip4_prefixlen); + if (r < 0) + return r; + + r = link_request_bearer_route(link, AF_INET, &b->ip4_gateway, &b->ip4_address); + if (r < 0) + return r; + } + + if (b->connected && b->ip4_method == MM_BEARER_IP_METHOD_DHCP) { + if (!link_dhcp4_enabled(link)) + log_link_notice(link, "The WWAN connection requested DHCPv4 client, but it is disabled."); + + r = dhcp4_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv4 client: %m"); + } else { + r = sd_dhcp_client_stop(link->dhcp_client); + if (r < 0) + ret = log_link_warning_errno(link, r, "Could not stop DHCPv4 client: %m"); + } + } + + if (b && FLAGS_SET(b->ip_type, ADDRESS_FAMILY_IPV6)) { + if (b->connected && b->ip6_method == MM_BEARER_IP_METHOD_STATIC) { + r = link_request_bearer_address(link, AF_INET6, &b->ip6_address, b->ip6_prefixlen); + if (r < 0) + return r; + + r = link_request_bearer_route(link, AF_INET6, &b->ip6_gateway, NULL); + if (r < 0) + return r; + } + + if (b->connected && b->ip6_method == MM_BEARER_IP_METHOD_DHCP) { + if (!link_ndisc_enabled(link) && !link_dhcp6_enabled(link)) + log_link_notice(link, + "The WWAN connection requested IPv6 dynamic address configuration," + "but both IPv6 Router Discovery and DHCPv6 client are disabled."); + + r = ndisc_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start IPv6 Router Discovery: %m"); + + r = dhcp6_start(link); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to start DHCPv6 client: %m"); + } else { + r = sd_dhcp6_client_stop(link->dhcp6_client); + if (r < 0) + ret = log_link_warning_errno(link, r, "Could not stop DHCPv6 client: %m"); + + r = sd_ndisc_stop(link->ndisc); + if (r < 0) + ret = log_link_warning_errno(link, r, "Could not stop IPv6 Router Discovery: %m"); + } + } + + /* Finally, remove all marked configs. */ + SET_FOREACH(address, link->addresses) { + if (address->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER) + continue; + + if (!address_is_marked(address)) + continue; + + r = address_remove(address, link); + if (r < 0) + ret = r; + } + + SET_FOREACH(route, link->manager->routes) { + if (route->source != NETWORK_CONFIG_SOURCE_MODEM_MANAGER) + continue; + + if (!route_is_marked(route)) + continue; + + r = route_remove(route, link->manager); + if (ret) + ret = r; + } + + if (ret < 0) + return ret; + + if (link->bearer_messages == 0) + link->bearer_configured = true; + + if (!link->bearer_configured) + link_set_state(link, LINK_STATE_CONFIGURING); + + link_check_ready(link); + + return 0; +} + +int link_apply_bearer(Link *link) { + Bearer *b = NULL; + int r; + + assert(link); + + (void) link_get_bearer(link, &b); + + r = link_apply_bearer_impl(link, b); + if (r < 0) + link_enter_failed(link); + + return r; +} + +int bearer_update_link(Bearer *b) { + Link *link; + int r; + + assert(b); + assert(b->modem); + assert(b->modem->manager); + + if (!b->name) + return 0; + + if (link_get_by_name(b->modem->manager, b->name, &link) < 0) + return 0; + + r = link_apply_bearer_impl(link, b); + if (r < 0) + link_enter_failed(link); + + /* + * Need to bring up the interface after the modem has connected. + * This is because ModemManger does the following while connecting: + * [1755871777.322239] [modem2] state changed (registered -> connecting) + * [1755871777.325012] [modem2/bearer5] launching connection with QMI port (cdc-wdm0) and data port (wwan0) (multiplex none) + * [1755871777.327665] [cdc-wdm0/qmi] bringing down data interface 'wwan0' + * [1755871777.330108] [modem2/wwan0/net] interface index: 9 + * [1755871777.335265] [cdc-wdm0/qmi] deleting all links in data interface 'wwan0' + */ + + r = link_request_to_bring_up_or_down(link, b->connected); + if (r < 0) + link_enter_failed(link); + + return 0; +} + +void bearer_drop(Bearer *b) { + assert(b); + + b->connected = false; + b->apn = mfree(b->apn); + + (void) bearer_update_link(b); + + bearer_free(b); +} + +int config_parse_mm_route_metric( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + network->mm_route_metric_set = false; + return 0; + } + + r = safe_atou32(rvalue, &network->mm_route_metric); + if (r < 0) + return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue); + + network->mm_route_metric_set = true; + return 0; +} diff --git a/src/network/networkd-wwan.h b/src/network/networkd-wwan.h new file mode 100644 index 00000000000..962f76be2ec --- /dev/null +++ b/src/network/networkd-wwan.h @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser-forward.h" +#include "in-addr-util.h" +#include "network-util.h" +#include "networkd-wwan-bus.h" + +typedef struct Modem Modem; + +typedef struct Bearer { + Modem *modem; + + sd_bus_slot *slot_getall; /* for GetAll method */ + + char *path; /* DBus path e.g /org/freedesktop/ModemManager/Bearer/0 */ + char *name; /* Interface property, e.g. wwan0 */ + char *apn; /* "apn" field in Properties */ + AddressFamily ip_type; /* "ip-type" field in Properties */ + + /* Ip4Config or IP6Config property */ + MMBearerIpMethod ip4_method; + MMBearerIpMethod ip6_method; + unsigned ip4_prefixlen; + unsigned ip6_prefixlen; + union in_addr_union ip4_address; + union in_addr_union ip6_address; + union in_addr_union ip4_gateway; + union in_addr_union ip6_gateway; + struct in_addr_full **dns; + size_t n_dns; + uint32_t ip4_mtu; + uint32_t ip6_mtu; + + bool connected; /* Connected property */ +} Bearer; + +typedef struct Modem { + Manager *manager; + + sd_bus_slot *slot_propertieschanged; + sd_bus_slot *slot_statechanged; + sd_bus_slot *slot_connect; + + char *path; /* DBus path e.g /org/freedesktop/ModemManager/Modem/0 */ + char *manufacturer; /* The "Manufacturer" property */ + char *model; /* The "Model" property */ + char *port_name; /* MM_MODEM_PORT_TYPE_NET of Ports property */ + + Hashmap *bearers_by_path; + Hashmap *bearers_by_name; + + MMModemState state; + MMModemStateFailedReason state_fail_reason; + ModemReconnectState reconnect_state; +} Modem; + +int bearer_new(Modem *modem, const char *path, Bearer **ret); +Bearer* bearer_free(Bearer *b); +DEFINE_TRIVIAL_CLEANUP_FUNC(Bearer*, bearer_free); + +int bearer_set_name(Bearer *b, const char *name); + +int bearer_get_by_path(Manager *manager, const char *path, Modem **ret_modem, Bearer **ret_bearer); +int link_get_bearer(Link *link, Bearer **ret); + +int link_dhcp_enabled_by_bearer(Link *link, int family); + +int link_apply_bearer(Link *link); +int bearer_update_link(Bearer *b); +void bearer_drop(Bearer *b); + +int modem_new(Manager *m, const char *path, Modem **ret); +Modem* modem_free(Modem *modem); +DEFINE_TRIVIAL_CLEANUP_FUNC(Modem*, modem_free); + +int modem_get_by_path(Manager *m, const char *path, Modem **ret); +int link_get_modem(Link *link, Modem **ret); + +CONFIG_PARSER_PROTOTYPE(config_parse_mm_route_metric); diff --git a/tools/command_ignorelist b/tools/command_ignorelist index 5859e5def94..548acff8bd0 100644 --- a/tools/command_ignorelist +++ b/tools/command_ignorelist @@ -573,3 +573,4 @@ loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="rebo loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="reboot"] loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="shutdown"] varlinkctl.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="--system"] +systemd.network.xml ./refsect1[title="[ModemManager] Section Options"]/variablelist/varlistentry[term="SimpleConnectProperties="]