diff options
author | Dan Williams <dan@ioncontrol.co> | 2024-11-22 20:06:43 -0600 |
---|---|---|
committer | Dan Williams <dan@ioncontrol.co> | 2025-05-08 20:37:09 -0500 |
commit | d4c0bbc8390df9dc45f726e9ff7b8d4ced17ac78 (patch) | |
tree | bc36c44cf6e732dc3ddd4c77eab9ef45b5407388 | |
parent | 4bb6026e37e74aad4faa50e89f3f4d98bec7368d (diff) |
modem-helpers: add IPv6 support to +CGCONTRDP parsing
Add some test responses from the Quectel EG915Q and some other Quectel devices.
Then try to parse those responses.
Signed-off-by: Dan Williams <dan@ioncontrol.co>
-rw-r--r-- | src/mm-modem-helpers.c | 217 | ||||
-rw-r--r-- | src/tests/test-modem-helpers.c | 38 |
2 files changed, 222 insertions, 33 deletions
diff --git a/src/mm-modem-helpers.c b/src/mm-modem-helpers.c index fcb44ad2..7a911248 100644 --- a/src/mm-modem-helpers.c +++ b/src/mm-modem-helpers.c @@ -14,6 +14,7 @@ * Copyright (C) 2009 - 2012 Red Hat, Inc. * Copyright (C) 2012 Google, Inc. * Copyright (c) 2021 Qualcomm Innovation Center, Inc. + * Copyright (C) 2024 JUCR GmbH */ #include <config.h> @@ -2339,13 +2340,89 @@ mm_3gpp_parse_crsm_response (const gchar *reply, /*************************************************************************/ /* CGCONTRDP=N response parser */ +static gchar * +dots_to_v6_address (const gchar *str, + gsize len, + GError **error) +{ + g_autoptr(GRegex) r = NULL; + g_autoptr(GMatchInfo) match_info = NULL; + guint i; + g_autoptr(GString) addr = NULL; + g_autoptr(GInetAddress) normalized = NULL; + + r = g_regex_new ("(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)", 0, 0, NULL); + g_assert (r != NULL); + + if (!g_regex_match_full (r, str, len, 0, 0, &match_info, error)) + return NULL; + + /* Remember that g_match_info_get_match_count() includes match #0 */ + if (g_match_info_get_match_count (match_info) < 17) { + g_set_error_literal (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Failed to match IPv6 address"); + return NULL; + } + + addr = g_string_sized_new (40); + + for (i = 1; i <= 16; i += 2) { + guint num1 = 0, num2 = 0; + + if (!mm_get_uint_from_match_info (match_info, i, &num1) || + !mm_get_uint_from_match_info (match_info, i + 1, &num2)) { + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Failed to parse uint from IPv6 regex match %d or %d", + i, + i + 1); + return NULL; + } + g_string_append_printf (addr, "%s%02x%02x", i > 1 ? ":" : "", num1, num2); + } + + normalized = g_inet_address_new_from_string (addr->str); + if (!normalized) { + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Failed to parse IP address '%s'", + addr->str); + return NULL; + } + + return g_inet_address_to_string (normalized); +} + +static guint +count_separators (const gchar *str, + const gchar separator, + const gchar **middle) +{ + const gchar *found; + guint count = 0; + + found = str; + while ((found = strchr (found, separator))) { + count++; + if (count == 4) { /* middle of v4 address+mask */ + if (middle) + *middle = found; + } else if (count == 16) /* middle of v6 address+mask */ + if (middle) + *middle = found; + found++; + } + + return count; +} + static gboolean split_local_address_and_subnet (const gchar *str, gchar **local_address, - gchar **subnet) + gchar **subnet, + gboolean *is_ipv6, + GError **error) { - const gchar *separator; - guint count = 0; + const gchar *middle = NULL; + guint num_dots = 0; /* E.g. split: "2.43.2.44.255.255.255.255" * into: @@ -2355,35 +2432,93 @@ split_local_address_and_subnet (const gchar *str, g_assert (str); g_assert (local_address); g_assert (subnet); - - separator = str; - while (1) { - separator = strchr (separator, '.'); - if (separator) { - count++; - if (count == 4) { - if (local_address) - *local_address = g_strndup (str, (separator - str)); - if (subnet) - *subnet = g_strdup (++separator); + g_assert (is_ipv6); + + /* Count number of dots to determine IPv4 or IPv6. Cases are: + * 0: might be IPv6 address formatted with AT+CGPIAF + * 3: IPv4 address + * 7: IPv4 address and subnet mask + * 15: IPv6 address + * 31: IPv6 address and mask/prefix + */ + num_dots = count_separators (str, '.', &middle); + if (num_dots == 0) { + if (strchr (str, ':')) { + g_autoptr(GInetAddress) a = NULL; + + /* May already be v6-formatted due to AT+CGPIAF */ + if ((a = g_inet_address_new_from_string (str))) { + *local_address = g_inet_address_to_string (a); + *is_ipv6 = TRUE; return TRUE; } - separator++; - continue; } + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Error parsing IP address '%s'", str); + return FALSE; + } - /* Not even the full IP? report error parsing */ - if (count < 3) + switch (num_dots) { + case 3: + *local_address = g_strdup (str); + *subnet = NULL; + return TRUE; + case 7: + *local_address = g_strndup (str, (middle - str)); + *subnet = g_strdup (++middle); + return TRUE; + case 15: + *local_address = dots_to_v6_address (str, -1, error); + *subnet = NULL; + *is_ipv6 = TRUE; + return (!!*local_address); + case 31: + *local_address = dots_to_v6_address (str, (middle - str), error); + if (!*local_address) return FALSE; + *subnet = dots_to_v6_address (++middle, -1, error); + *is_ipv6 = TRUE; + return (!!*subnet); + default: + break; + } - if (count == 3) { - if (local_address) - *local_address = g_strdup (str); - if (subnet) - *subnet = NULL; - return TRUE; - } + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Error parsing IP address '%s'", str); + return FALSE; +} + +static gboolean +get_normalized_address_from_match_info (GMatchInfo *match_info, + guint field_index, + gchar **out_address, + GError **error) +{ + g_autoptr(GInetAddress) a = NULL; + g_autofree gchar *address = NULL; + + g_assert (out_address && !*out_address); + + /* Missing match is not fatal */ + address = mm_get_string_unquoted_from_match_info (match_info, field_index); + if (!address) + return TRUE; + + if (count_separators (address, '.', NULL) == 15) { + *out_address = dots_to_v6_address (address, -1, error); + return TRUE; } + + a = g_inet_address_new_from_string (address); + if (!a) { + g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, + "Error normalizing IP address '%s'", + address); + return FALSE; + } + + *out_address = g_inet_address_to_string (a); + return TRUE; } gboolean @@ -2411,6 +2546,7 @@ mm_3gpp_parse_cgcontrdp_response (const gchar *response, g_autofree gchar *dns_primary_address = NULL; g_autofree gchar *dns_secondary_address = NULL; guint field_format_extra_index = 0; + gboolean is_ipv6 = FALSE; /* Response may be e.g.: * +CGCONTRDP: 4,5,"ibox.tim.it.mnc001.mcc222.gprs","2.197.17.49.255.255.255.255","2.197.17.49","10.207.43.46","10.206.56.132","0.0.0.0","0.0.0.0",0 @@ -2467,20 +2603,35 @@ mm_3gpp_parse_cgcontrdp_response (const gchar *response, * the subnet in its own comma-separated field. Try to detect that. */ local_address_and_subnet = mm_get_string_unquoted_from_match_info (match_info, 4); - if (local_address_and_subnet && !split_local_address_and_subnet (local_address_and_subnet, &local_address, &subnet)) { - g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing local address and subnet"); + if (local_address_and_subnet && !split_local_address_and_subnet (local_address_and_subnet, &local_address, &subnet, &is_ipv6, error)) return FALSE; - } - /* If we don't have a subnet in field 4, we're using the old format with subnet in an extra field */ - if (!subnet) { + /* If we have a field 4 but it doesn't contain a subnet, we're using the old + * format with subnet in an extra field. Except for IPv6, which has no subnets. + */ + if (local_address_and_subnet && !subnet && !is_ipv6) { subnet = mm_get_string_unquoted_from_match_info (match_info, 5); field_format_extra_index = 1; } - gateway_address = mm_get_string_unquoted_from_match_info (match_info, 5 + field_format_extra_index); - dns_primary_address = mm_get_string_unquoted_from_match_info (match_info, 6 + field_format_extra_index); - dns_secondary_address = mm_get_string_unquoted_from_match_info (match_info, 7 + field_format_extra_index); + if (!get_normalized_address_from_match_info (match_info, + 5 + field_format_extra_index, + &gateway_address, + error)) { + return FALSE; + } + if (!get_normalized_address_from_match_info (match_info, + 6 + field_format_extra_index, + &dns_primary_address, + error)) { + return FALSE; + } + if (!get_normalized_address_from_match_info (match_info, + 7 + field_format_extra_index, + &dns_secondary_address, + error)) { + return FALSE; + } if (out_cid) *out_cid = cid; diff --git a/src/tests/test-modem-helpers.c b/src/tests/test-modem-helpers.c index 6836bbdd..a75b2647 100644 --- a/src/tests/test-modem-helpers.c +++ b/src/tests/test-modem-helpers.c @@ -11,6 +11,7 @@ * GNU General Public License for more details: * * Copyright (C) 2010 Red Hat, Inc. + * Copyright (C) 2024 JUCR GmbH */ #include <glib.h> @@ -3834,6 +3835,43 @@ static const CgcontrdpResponseTest cgcontrdp_response_tests[] = { .apn = "ibox.tim.it.mnc001.mcc222.gprs", .local_address = "2.197.17.49", }, + { + .str = "+CGCONTRDP: 1,5,\"internetipv6.mnc003.mcc260.gprs\",\"254.128.0.0.0.0.0.0.0.0.0.37.88.136.248.1\",\"\",\"42.1.23.0.0.2.255.255.0.0.0.0.0.0.159.1\",\"42.1.23.0.0.3.255.255.0.0.0.0.0.0.152.34\",\"\",\"\",0,0", + .cid = 1, + .bearer_id = 5, + .apn = "internetipv6.mnc003.mcc260.gprs", + .local_address = "fe80::25:5888:f801", + .dns_primary_address = "2a01:1700:2:ffff::9f01", + .dns_secondary_address = "2a01:1700:3:ffff::9822" + }, + { + /* Quectel EG915Q */ + .str = "+CGCONTRDP: 2,6,\"fast.t-mobile.com\",\"38.7.251.144.154.37.192.120.172.57.189.17.176.171.138.245\",,\"253.0.151.106.0.0.0.0.0.0.0.0.0.0.0.9\",\"253.0.151.106.0.0.0.0.0.0.0.0.0.0.0.16\"", + .cid = 2, + .bearer_id = 6, + .apn = "fast.t-mobile.com", + .local_address = "2607:fb90:9a25:c078:ac39:bd11:b0ab:8af5", + .dns_primary_address = "fd00:976a::9", + .dns_secondary_address = "fd00:976a::10", + }, + { + .str = "+CGCONTRDP: 1,0,\"fast.t-mobile.com\",\"2607:FB90:B59A:02C1:0AD3:1B57:4FD4:21F4\",\"FE80:0000:0000:0000:B46D:57FF:FE45:4545\",\"fd00:976a::9\",\"fd00:976a::10\"", + .cid = 1, + .bearer_id = 0, + .apn = "fast.t-mobile.com", + .local_address = "2607:fb90:b59a:2c1:ad3:1b57:4fd4:21f4", + .gateway_address = "fe80::b46d:57ff:fe45:4545", + .dns_primary_address = "fd00:976a::9", + .dns_secondary_address = "fd00:976a::10", + }, + { + .str = "+CGCONTRDP: 2,6,\"fast.t-mobile.com\",,,\"253.0.151.106.0.0.0.0.0.0.0.0.0.0.0.9\",\"253.0.151.106.0.0.0.0.0.0.0.0.0.0.0.16\"", + .cid = 2, + .bearer_id = 6, + .apn = "fast.t-mobile.com", + .dns_primary_address = "fd00:976a::9", + .dns_secondary_address = "fd00:976a::10", + }, /* Common */ { .str = "+CGCONTRDP: 4,5,\"ibox.tim.it.mnc001.mcc222.gprs\"", |