From 00009b2a238555b6777aab79b3190a8a46df56d2 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 19 Nov 2013 22:28:36 -0600 Subject: [PATCH] core: capture DNS configuration from resolv.conf when generating connections If the interface who's IP configuration is being captured has the default route, then read DNS servers from resolv.conf into the NMIP[4|6]Config. --- .gitignore | 1 + src/NetworkManagerUtils.c | 138 ++++++++++++++++++++++++++++ src/NetworkManagerUtils.h | 8 ++ src/devices/nm-device.c | 10 +- src/nm-ip4-config.c | 13 ++- src/nm-ip4-config.h | 2 +- src/nm-ip6-config.c | 11 ++- src/nm-ip6-config.h | 2 +- src/tests/Makefile.am | 12 ++- src/tests/test-resolvconf-capture.c | 176 ++++++++++++++++++++++++++++++++++++ 10 files changed, 362 insertions(+), 11 deletions(-) create mode 100644 src/tests/test-resolvconf-capture.c diff --git a/src/NetworkManagerUtils.c b/src/NetworkManagerUtils.c index c2cf5e7..5a5fb7f 100644 --- a/src/NetworkManagerUtils.c +++ b/src/NetworkManagerUtils.c @@ -21,14 +21,15 @@ #include #include #include #include #include #include +#include #include "NetworkManagerUtils.h" #include "nm-utils.h" #include "nm-logging.h" #include "nm-device.h" #include "nm-setting-connection.h" #include "nm-setting-ip4-config.h" @@ -607,7 +608,136 @@ nm_utils_complete_generic (NMConnection *connection, char * nm_utils_new_vlan_name (const char *parent_iface, guint32 vlan_id) { /* Basically VLAN_NAME_TYPE_RAW_PLUS_VID_NO_PAD */ return g_strdup_printf ("%s.%d", parent_iface, vlan_id); } +static GPtrArray * +read_nameservers (const char *test_rc_contents) +{ + GPtrArray *nameservers = NULL; + char *contents = NULL; + char **lines, **iter; + char *p; + + if (test_rc_contents) + contents = g_strdup (test_rc_contents); + else { + if (!g_file_get_contents (_PATH_RESCONF, &contents, NULL, NULL)) + return NULL; + } + + nameservers = g_ptr_array_new_full (3, g_free); + + lines = g_strsplit_set (contents, "\r\n", -1); + for (iter = lines; iter && *iter; iter++) { + if (!g_str_has_prefix (*iter, "nameserver")) + continue; + p = *iter + strlen ("nameserver"); + while (g_ascii_isspace (*p)) + p++; + + g_ptr_array_add (nameservers, g_strdup (p)); + } + + g_free (contents); + g_strfreev (lines); + return nameservers; +} + +#define IS_10(a) ((ntohl (a) & 0xff000000) == 0x0a000000) +#define IS_172(a) ((ntohl (a) & 0xfff00000) == 0xac100000) +#define IS_192(a) ((ntohl (a) & 0xffff0000) == 0xc0a80000) + +/** + * nm_utils_capture_resolv_conf_ip4(): + * @addresses: array of #NMPlatformIP4Address structures + * @nameservers: array of guint32 + * @test_rc_contents: for testing; the contents of resolv.conf + * + * Reads all /etc/resolv.conf IPv4 nameservers and adds them to @nameservers, + * which checks to ensure that private-network nameservers are not added if + * @addresses does not contain an address in that private network. + */ +void +nm_utils_capture_resolv_conf_ip4 (const GArray *addresses, + GArray *nameservers, + const char *test_rc_contents) +{ + GPtrArray *read_ns; + gboolean has_10 = FALSE, has_172 = FALSE, has_192 = FALSE; + guint i; + + g_return_if_fail (nameservers != NULL); + + read_ns = read_nameservers (test_rc_contents); + if (!read_ns) + return; + + for (i = 0; addresses && i < addresses->len; i++) { + NMPlatformIP4Address *addr = &g_array_index (addresses, NMPlatformIP4Address, i); + + if (IS_10 (addr->address)) + has_10 = TRUE; + else if (IS_172 (addr->address)) + has_172 = TRUE; + else if (IS_192 (addr->address)) + has_192 = TRUE; + } + + for (i = 0; i < read_ns->len; i++) { + const char *s = g_ptr_array_index (read_ns, i); + guint32 ns = 0; + + if (!inet_pton (AF_INET, s, (void *) &ns) || !ns) + continue; + + /* Ignore any private-network addresses that aren't in this + * interface's address space. + */ + if ( (IS_10 (ns) && !has_10) + || (IS_172 (ns) && !has_172) + || (IS_192 (ns) && !has_192)) + continue; + + g_array_append_val (nameservers, ns); + } + + g_ptr_array_unref (read_ns); +} + +/** + * nm_utils_capture_resolv_conf_ip6(): + * @addresses: array of #NMPlatformIP6Address structures + * @nameservers: array of struct in6_addr + * @test_rc_contents: for testing; the contents of resolv.conf + * + * Reads all /etc/resolv.conf IPv6 nameservers and adds them to @nameservers. + */ +void +nm_utils_capture_resolv_conf_ip6 (const GArray *addresses, + GArray *nameservers, + const char *test_rc_contents) +{ + GPtrArray *read_ns; + guint i; + + g_return_if_fail (nameservers != NULL); + + read_ns = read_nameservers (test_rc_contents); + if (!read_ns) + return; + + for (i = 0; i < read_ns->len; i++) { + const char *s = g_ptr_array_index (read_ns, i); + struct in6_addr ns = IN6ADDR_ANY_INIT; + + if (!inet_pton (AF_INET6, s, (void *) &ns) || IN6_IS_ADDR_UNSPECIFIED (&ns)) + continue; + + g_array_append_val (nameservers, ns); + } + + g_ptr_array_unref (read_ns); +} + diff --git a/src/NetworkManagerUtils.h b/src/NetworkManagerUtils.h index 93ec111..cd30ece 100644 --- a/src/NetworkManagerUtils.h +++ b/src/NetworkManagerUtils.h @@ -80,8 +80,16 @@ void nm_utils_complete_generic (NMConnection *connection, const GSList *existing, const char *format, const char *preferred, gboolean default_enable_ipv6); char *nm_utils_new_vlan_name (const char *parent_iface, guint32 vlan_id); +void nm_utils_capture_resolv_conf_ip4 (const GArray *addresses, + GArray *nameservers, + const char *test_rc_contents); + +void nm_utils_capture_resolv_conf_ip6 (const GArray *addresses, + GArray *nameservers, + const char *test_rc_contents); + #endif /* NETWORK_MANAGER_UTILS_H */ diff --git a/src/devices/nm-device.c b/src/devices/nm-device.c index cc678ce..f03ecbb 100644 --- a/src/devices/nm-device.c +++ b/src/devices/nm-device.c @@ -353,15 +353,15 @@ static void cp_connection_updated (NMConnectionProvider *cp, NMConnection *conne static const char *state_to_string (NMDeviceState state); static void link_changed_cb (NMPlatform *platform, int ifindex, NMPlatformLink *info, NMPlatformReason reason, NMDevice *device); static void check_carrier (NMDevice *device); static void nm_device_queued_ip_config_change_clear (NMDevice *self); -static void update_ip_config (NMDevice *self, gboolean capture_dhcp); +static void update_ip_config (NMDevice *self, gboolean initial); static void device_ip_changed (NMPlatform *platform, int ifindex, gpointer platform_object, NMPlatformReason reason, gpointer user_data); static void nm_device_slave_notify_enslave (NMDevice *dev, gboolean success); static void nm_device_slave_notify_release (NMDevice *dev, gboolean master_failed); static void addrconf6_start_with_link_ready (NMDevice *self); @@ -6493,44 +6493,44 @@ capture_lease_config (NMDevice *device, if (out_ip6_config && strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_AUTO) == 0) { /* FIXME: implement find_ip6_lease_config() */ } } } static void -update_ip_config (NMDevice *self, gboolean capture_dhcp) +update_ip_config (NMDevice *self, gboolean initial) { NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self); int ifindex; gboolean linklocal6_just_completed = FALSE; ifindex = nm_device_get_ip_ifindex (self); if (!ifindex) return; /* IPv4 */ g_clear_object (&priv->ext_ip4_config); - priv->ext_ip4_config = nm_ip4_config_capture (ifindex); + priv->ext_ip4_config = nm_ip4_config_capture (ifindex, initial); if (priv->ext_ip4_config) { - if (capture_dhcp) { + if (initial) { g_clear_object (&priv->dev_ip4_config); capture_lease_config (self, priv->ext_ip4_config, &priv->dev_ip4_config, NULL, NULL); } if (priv->dev_ip4_config) nm_ip4_config_subtract (priv->ext_ip4_config, priv->dev_ip4_config); if (priv->vpn4_config) nm_ip4_config_subtract (priv->ext_ip4_config, priv->vpn4_config); ip4_config_merge_and_apply (self, NULL, FALSE, NULL); } /* IPv6 */ g_clear_object (&priv->ext_ip6_config); - priv->ext_ip6_config = nm_ip6_config_capture (ifindex); + priv->ext_ip6_config = nm_ip6_config_capture (ifindex, initial); if (priv->ext_ip6_config) { /* Check this before modifying ext_ip6_config */ linklocal6_just_completed = priv->linklocal6_timeout_id && linklocal6_config_is_ready (priv->ext_ip6_config); if (priv->ac_ip6_config) diff --git a/src/nm-ip4-config.c b/src/nm-ip4-config.c index 5229ef9..e27a557 100644 --- a/src/nm-ip4-config.c +++ b/src/nm-ip4-config.c @@ -25,14 +25,15 @@ #include "libgsystem.h" #include "nm-platform.h" #include "nm-utils.h" #include "nm-dbus-manager.h" #include "nm-dbus-glib-types.h" #include "nm-ip4-config-glue.h" +#include "NetworkManagerUtils.h" G_DEFINE_TYPE (NMIP4Config, nm_ip4_config, G_TYPE_OBJECT) #define NM_IP4_CONFIG_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_IP4_CONFIG, NMIP4ConfigPrivate)) typedef struct { @@ -121,20 +122,20 @@ static gboolean routes_are_duplicate (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, gboolean consider_gateway_and_metric) { return a->network == b->network && a->plen == b->plen && (!consider_gateway_and_metric || (a->gateway == b->gateway && a->metric == b->metric)); } NMIP4Config * -nm_ip4_config_capture (int ifindex) +nm_ip4_config_capture (int ifindex, gboolean capture_resolv_conf) { NMIP4Config *config; NMIP4ConfigPrivate *priv; - guint i; gboolean gateway_changed = FALSE; + guint i; /* Slaves have no IP configuration */ if (nm_platform_link_get_master (ifindex) > 0) return NULL; config = nm_ip4_config_new (); priv = NM_IP4_CONFIG_GET_PRIVATE (config); @@ -156,14 +157,22 @@ nm_ip4_config_capture (int ifindex) } /* Remove the default route from the list */ g_array_remove_index (priv->routes, i); break; } } + /* If the interface has the default route, and has IPv4 addresses, capture + * nameservers from /etc/resolv.conf. + */ + if (priv->addresses->len && priv->gateway && capture_resolv_conf) { + nm_utils_capture_resolv_conf_ip4 (priv->addresses, priv->nameservers, NULL); + _NOTIFY (config, PROP_NAMESERVERS); + } + /* actually, nobody should be connected to the signal, just to be sure, notify */ _NOTIFY (config, PROP_ADDRESSES); _NOTIFY (config, PROP_ROUTES); if (gateway_changed) _NOTIFY (config, PROP_GATEWAY); return config; diff --git a/src/nm-ip4-config.h b/src/nm-ip4-config.h index 3b2b250..4ecfb76 100644 --- a/src/nm-ip4-config.h +++ b/src/nm-ip4-config.h @@ -55,15 +55,15 @@ GType nm_ip4_config_get_type (void); NMIP4Config * nm_ip4_config_new (void); /* D-Bus integration */ void nm_ip4_config_export (NMIP4Config *config); const char * nm_ip4_config_get_dbus_path (const NMIP4Config *config); /* Integration with nm-platform and nm-setting */ -NMIP4Config *nm_ip4_config_capture (int ifindex); +NMIP4Config *nm_ip4_config_capture (int ifindex, gboolean capture_resolv_conf); gboolean nm_ip4_config_commit (const NMIP4Config *config, int ifindex, int priority); void nm_ip4_config_merge_setting (NMIP4Config *config, NMSettingIP4Config *setting); void nm_ip4_config_update_setting (const NMIP4Config *config, NMSettingIP4Config *setting); /* Utility functions */ void nm_ip4_config_merge (NMIP4Config *dst, const NMIP4Config *src); void nm_ip4_config_subtract (NMIP4Config *dst, const NMIP4Config *src); diff --git a/src/nm-ip6-config.c b/src/nm-ip6-config.c index 7522164..726e658 100644 --- a/src/nm-ip6-config.c +++ b/src/nm-ip6-config.c @@ -26,14 +26,15 @@ #include "nm-glib-compat.h" #include "libgsystem.h" #include "nm-platform.h" #include "nm-utils.h" #include "nm-dbus-manager.h" #include "nm-dbus-glib-types.h" #include "nm-ip6-config-glue.h" +#include "NetworkManagerUtils.h" G_DEFINE_TYPE (NMIP6Config, nm_ip6_config, G_TYPE_OBJECT) #define NM_IP6_CONFIG_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_IP6_CONFIG, NMIP6ConfigPrivate)) typedef struct { char *path; @@ -121,15 +122,15 @@ static gboolean routes_are_duplicate (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, gboolean consider_gateway_and_metric) { return IN6_ARE_ADDR_EQUAL (&a->network, &b->network) && a->plen == b->plen && (!consider_gateway_and_metric || (IN6_ARE_ADDR_EQUAL (&a->gateway, &b->gateway) && a->metric == b->metric)); } NMIP6Config * -nm_ip6_config_capture (int ifindex) +nm_ip6_config_capture (int ifindex, gboolean capture_resolv_conf) { NMIP6Config *config; NMIP6ConfigPrivate *priv; guint i; gboolean gateway_changed = FALSE; /* Slaves have no IP configuration */ @@ -156,14 +157,22 @@ nm_ip6_config_capture (int ifindex) } /* Remove the default route from the list */ g_array_remove_index (priv->routes, i); break; } } + /* If the interface has the default route, and has IPv4 addresses, capture + * nameservers from /etc/resolv.conf. + */ + if (priv->addresses->len && !IN6_IS_ADDR_UNSPECIFIED (&priv->gateway) && capture_resolv_conf) { + nm_utils_capture_resolv_conf_ip6 (priv->addresses, priv->nameservers, NULL); + _NOTIFY (config, PROP_NAMESERVERS); + } + /* actually, nobody should be connected to the signal, just to be sure, notify */ _NOTIFY (config, PROP_ADDRESSES); _NOTIFY (config, PROP_ROUTES); if (gateway_changed) _NOTIFY (config, PROP_GATEWAY); return config; diff --git a/src/nm-ip6-config.h b/src/nm-ip6-config.h index 538490a..2b1ba8b 100644 --- a/src/nm-ip6-config.h +++ b/src/nm-ip6-config.h @@ -54,15 +54,15 @@ GType nm_ip6_config_get_type (void); NMIP6Config * nm_ip6_config_new (void); /* D-Bus integration */ void nm_ip6_config_export (NMIP6Config *config); const char * nm_ip6_config_get_dbus_path (const NMIP6Config *config); /* Integration with nm-platform and nm-setting */ -NMIP6Config *nm_ip6_config_capture (int ifindex); +NMIP6Config *nm_ip6_config_capture (int ifindex, gboolean initial); gboolean nm_ip6_config_commit (const NMIP6Config *config, int ifindex, int priority); void nm_ip6_config_merge_setting (NMIP6Config *config, NMSettingIP6Config *setting); void nm_ip6_config_update_setting (const NMIP6Config *config, NMSettingIP6Config *setting); /* Utility functions */ void nm_ip6_config_merge (NMIP6Config *dst, const NMIP6Config *src); void nm_ip6_config_subtract (NMIP6Config *dst, const NMIP6Config *src); diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am index 9d17e9a..eaa9d51 100644 --- a/src/tests/Makefile.am +++ b/src/tests/Makefile.am @@ -12,15 +12,16 @@ AM_CPPFLAGS = \ noinst_PROGRAMS = \ test-dhcp-options \ test-policy-hosts \ test-wifi-ap-utils \ test-ip4-config \ test-ip6-config \ - test-dcb + test-dcb \ + test-resolvconf-capture ####### DHCP options test ####### test_dhcp_options_SOURCES = \ test-dhcp-options.c test_dhcp_options_CPPFLAGS = \ @@ -67,21 +68,30 @@ test_ip6_config_LDADD = \ test_dcb_SOURCES = \ test-dcb.c test_dcb_LDADD = \ $(top_builddir)/src/libNetworkManager.la +####### resolv.conf capture test ####### + +test_resolvconf_capture_SOURCES = \ + test-resolvconf-capture.c + +test_resolvconf_capture_LDADD = \ + $(top_builddir)/src/libNetworkManager.la + ####### secret agent interface test ####### EXTRA_DIST = test-secret-agent.py ########################################### check-local: test-dhcp-options test-policy-hosts test-wifi-ap-utils test-ip4-config test-ip6-config $(abs_builddir)/test-dhcp-options $(abs_builddir)/test-policy-hosts $(abs_builddir)/test-wifi-ap-utils $(abs_builddir)/test-ip4-config $(abs_builddir)/test-ip6-config $(abs_builddir)/test-dcb + $(abs_builddir)/test-resolvconf-capture diff --git a/src/tests/test-resolvconf-capture.c b/src/tests/test-resolvconf-capture.c new file mode 100644 index 0000000..0343d17 --- /dev/null +++ b/src/tests/test-resolvconf-capture.c @@ -0,0 +1,176 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2013 Red Hat, Inc. + * + */ + +#include +#include + +#include "NetworkManagerUtils.h" +#include "nm-platform.h" + +static void +test_capture_empty (void) +{ + GArray *ns4 = g_array_new (FALSE, FALSE, sizeof (guint32)); + GArray *ns6 = g_array_new (FALSE, FALSE, sizeof (struct in6_addr)); + + nm_utils_capture_resolv_conf_ip4 (NULL, ns4, ""); + g_assert_cmpint (ns4->len, ==, 0); + + nm_utils_capture_resolv_conf_ip6 (NULL, ns6, ""); + g_assert_cmpint (ns6->len, ==, 0); + + g_array_free (ns4, TRUE); + g_array_free (ns6, TRUE); +} + +static void +assert_dns4_entry (const GArray *a, guint i, const char *s) +{ + guint32 n, m; + + g_assert (inet_aton (s, (void *) &n) != 0); + m = g_array_index (a, guint32, i); + g_assert_cmpint (m, ==, n); +} + +static void +assert_dns6_entry (const GArray *a, guint i, const char *s) +{ + struct in6_addr n = IN6ADDR_ANY_INIT; + struct in6_addr *m; + + g_assert (inet_pton (AF_INET6, s, (void *) &n) == 1); + m = &g_array_index (a, struct in6_addr, i); + g_assert (IN6_ARE_ADDR_EQUAL (&n, m)); +} + +static void +test_capture_basic4 (void) +{ + GArray *ns4 = g_array_new (FALSE, FALSE, sizeof (guint32)); + const char *rc = +"# neato resolv.conf\r\n" +"domain foobar.com\r\n" +"search foobar.com\r\n" +"nameserver 4.2.2.1\r\n" +"nameserver 4.2.2.2\r\n" +"nameserver 192.168.1.1\r\n"; + + nm_utils_capture_resolv_conf_ip4 (NULL, ns4, rc); + g_assert_cmpint (ns4->len, ==, 2); + assert_dns4_entry (ns4, 0, "4.2.2.1"); + assert_dns4_entry (ns4, 1, "4.2.2.2"); + /* 192.x not present because no addresses given */ + + g_array_free (ns4, TRUE); +} + +static void +test_capture_basic6 (void) +{ + GArray *ns6 = g_array_new (FALSE, FALSE, sizeof (struct in6_addr)); + const char *rc = +"# neato resolv.conf\r\n" +"domain foobar.com\r\n" +"search foobar.com\r\n" +"nameserver 2001:4860:4860::8888\r\n" +"nameserver 2001:4860:4860::8844\r\n"; + + nm_utils_capture_resolv_conf_ip6 (NULL, ns6, rc); + g_assert_cmpint (ns6->len, ==, 2); + assert_dns6_entry (ns6, 0, "2001:4860:4860::8888"); + assert_dns6_entry (ns6, 1, "2001:4860:4860::8844"); + + g_array_free (ns6, TRUE); +} + +static void +test_capture_private_net4 (void) +{ + GArray *ns4 = g_array_new (FALSE, FALSE, sizeof (guint32)); + const char *rc = +"# neato resolv.conf\r\n" +"domain foobar.com\r\n" +"search foobar.com\r\n" +"nameserver 4.2.2.1\r\n" +"nameserver 4.2.2.2\r\n" +"nameserver 10.2.3.4\r\n" +"nameserver 172.17.6.3\r\n" +"nameserver 192.168.1.1\r\n"; + GArray *addrs = g_array_new (FALSE, FALSE, sizeof (NMPlatformIP4Address)); + NMPlatformIP4Address a, *b; + + memset (&a, 0, sizeof (a)); + a.address = htonl (0x0a000006); + a.plen = 8; + g_array_append_val (addrs, a); + + /* 10.x test */ + nm_utils_capture_resolv_conf_ip4 (addrs, ns4, rc); + g_assert_cmpint (ns4->len, ==, 3); + assert_dns4_entry (ns4, 0, "4.2.2.1"); + assert_dns4_entry (ns4, 1, "4.2.2.2"); + assert_dns4_entry (ns4, 2, "10.2.3.4"); + + /* 172.x test */ + b = &g_array_index (addrs, NMPlatformIP4Address, 0); + b->address = htonl (0xac100003); + + g_array_set_size (ns4, 0); + nm_utils_capture_resolv_conf_ip4 (addrs, ns4, rc); + g_assert_cmpint (ns4->len, ==, 3); + assert_dns4_entry (ns4, 0, "4.2.2.1"); + assert_dns4_entry (ns4, 1, "4.2.2.2"); + assert_dns4_entry (ns4, 2, "172.17.6.3"); + + /* 192.x test */ + b = &g_array_index (addrs, NMPlatformIP4Address, 0); + b->address = htonl (0xc0a80010); + + g_array_set_size (ns4, 0); + nm_utils_capture_resolv_conf_ip4 (addrs, ns4, rc); + g_assert_cmpint (ns4->len, ==, 3); + assert_dns4_entry (ns4, 0, "4.2.2.1"); + assert_dns4_entry (ns4, 1, "4.2.2.2"); + assert_dns4_entry (ns4, 2, "192.168.1.1"); + + g_array_free (ns4, TRUE); + g_array_free (addrs, TRUE); +} + +/*******************************************/ + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + +#if !GLIB_CHECK_VERSION (2,36,0) + g_type_init (); +#endif + + g_test_add_func ("/resolvconf-capture/empty", test_capture_empty); + g_test_add_func ("/resolvconf-capture/basic4", test_capture_basic4); + g_test_add_func ("/resolvconf-capture/basic6", test_capture_basic6); + g_test_add_func ("/resolvconf-capture/private-net4", test_capture_private_net4); + + return g_test_run (); +} + -- 1.8.3.1