aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Williams <dan@ioncontrol.co>2024-11-22 20:06:43 -0600
committerDan Williams <dan@ioncontrol.co>2025-05-08 20:37:09 -0500
commitd4c0bbc8390df9dc45f726e9ff7b8d4ced17ac78 (patch)
treebc36c44cf6e732dc3ddd4c77eab9ef45b5407388
parent4bb6026e37e74aad4faa50e89f3f4d98bec7368d (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.c217
-rw-r--r--src/tests/test-modem-helpers.c38
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\"",