Commit graph

125 commits

Author SHA1 Message Date
Thomas Haller
7a71aedf46
libnm: optimize NM_CONNECTION_GET_PRIVATE() for NMSimpleConnection
NMConnection is a glib interface, implemented only by NMSimpleConnection
and NMRemoteConnection.

Inside the daemon, every NMConnection instance is always a NMSimpleConnection.

Using glib interfaces has an overhead, for example NM_IS_CONNECTION() needs
to search the implemented types for the pointer. And NM_CONNECTION_GET_PRIVATE()
is implemented by attaching user data to the GObject instance. Both have measurable
overhead.

Special case them for NMSimpleConnection.

This optimizes primarily the call to nm_connection_get_setting_connection(),
which easily gets called millions of times. This is easily measurable.
2021-06-17 17:48:11 +02:00
Thomas Haller
f3abf2491a
libnm: add code comment about preserving ABI for libnm GObject structs 2021-06-17 17:48:10 +02:00
Thomas Haller
b0f4bb84bf
libnm: avoid cloning buffer for nm_connection_get_settings() in nm_keyfile_write() 2021-06-17 17:48:10 +02:00
Thomas Haller
5aef93355f
libnm: add _nm_connection_get_settings_arr() helper 2021-06-17 17:48:09 +02:00
Thomas Haller
207b101238
libnm: take reference to settings in nm_connection_for_each_setting_value()
As we iterate over the settings, let's ensure that they stay
alive while we call back to the user data.
2021-06-17 17:48:09 +02:00
Thomas Haller
d829849a7b
libnm: avoid cloning list of settings in nm_connection_to_dbus_full() 2021-06-17 17:48:09 +02:00
Thomas Haller
97eef2bf6d
libnm: implement nm_connection_get_setting*() via NMMetaSettingType
The NM_TYPE_SETTING_* macros are really function calls (to a GType/gsize which is
guarded by an atomic operation for thread safe initialization). Also, finding
the setting_info based on the GType requires additional lookups.

It's no longer necessary. We can directly find the setting using the
well known index.
2021-06-17 17:48:08 +02:00
Thomas Haller
c8c606b323
libnm: avoid cloning list of settings in _nm_connection_verify() 2021-06-17 17:48:08 +02:00
Thomas Haller
91aacbef41
libnm: refactor tracking of NMSetting in NMConnection
A NMConnection tracks a list of NMSetting instances. For
each setting type, it only can track one instance, as is
clear by the API nm_connection_get_setting().

The number of different setting types is known at compile time,
currently it is 52. Also, we have an NMMetaSettingType enum,
which assigns each type a number.

Previously, we were tracking the settings in a GHashTable.
Rework that, to instead use a fixed size array.

Now every NMConnection instance consumes 52 * sizeof(pointer)
for the settings array. Previously, the GHashTable required to malloc
the "struct _GHashTable" (on 64bit that is about the size of 12
pointers) and for N settings it allocated two buffers (for
the key and the values) plus one buffer for the hash values. So,
it may or may not consume a bit more memory now, but also can lookup
settings directly without hashing.

When looking at all settings, we iterate the entire array. Most
entries will be NULL, so it's a question whether this could be done
better. But as the array is of a fixed, small size, naive iteration
is probably still faster and simpler than anything else.

---

Test: compiled with -O2, x86_64:

  $ T=src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh; \
    make -j 8 "$T" && \
    "$T" 1>/dev/null && \
    perf stat -r 200 -B "$T" 1>/dev/null

Before:

 Performance counter stats for 'src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh' (200 runs):

            338.39 msec task-clock:u              #    0.962 CPUs utilized            ( +-  0.68% )
                 0      context-switches:u        #    0.000 K/sec
                 0      cpu-migrations:u          #    0.000 K/sec
             1,121      page-faults:u             #    0.003 M/sec                    ( +-  0.03% )
     1,060,001,815      cycles:u                  #    3.132 GHz                      ( +-  0.50% )
     1,877,905,122      instructions:u            #    1.77  insn per cycle           ( +-  0.01% )
       374,065,113      branches:u                # 1105.429 M/sec                    ( +-  0.01% )
         6,862,991      branch-misses:u           #    1.83% of all branches          ( +-  0.36% )

           0.35185 +- 0.00247 seconds time elapsed  ( +-  0.70% )

After:

 Performance counter stats for 'src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh' (200 runs):

            328.07 msec task-clock:u              #    0.959 CPUs utilized            ( +-  0.39% )
                 0      context-switches:u        #    0.000 K/sec
                 0      cpu-migrations:u          #    0.000 K/sec
             1,130      page-faults:u             #    0.003 M/sec                    ( +-  0.03% )
     1,034,858,368      cycles:u                  #    3.154 GHz                      ( +-  0.33% )
     1,846,714,951      instructions:u            #    1.78  insn per cycle           ( +-  0.00% )
       369,754,267      branches:u                # 1127.052 M/sec                    ( +-  0.01% )
         6,594,396      branch-misses:u           #    1.78% of all branches          ( +-  0.23% )

           0.34193 +- 0.00145 seconds time elapsed  ( +-  0.42% )
2021-06-17 17:48:08 +02:00
Thomas Haller
042cd99049
libnm/tests: test consistency for nm_meta_setting_types_by_priority 2021-06-17 17:48:08 +02:00
Thomas Haller
b7a7cc1b13
libnm: add nm_meta_setting_types_by_priority array for sorting settings
nm_meta_setting_infos is a list of all NMMetaSettingInfo, sorted by name.
Add nm_meta_setting_types_by_priority which provides a mapping with a
different sort order (first by priority). We need that sometimes.
2021-06-17 17:48:08 +02:00
Thomas Haller
16b01233fa
libnm: add nm_meta_setting_info helpers 2021-06-17 17:48:07 +02:00
Thomas Haller
05f2a0b024
libnm: expose nm_ip_address_dup(), nm_ip_route_dup() API in libnm
This fixes commit 21c8a6b20e ('libnm-core, all: merge IPv4 and IPv6
address/route types'), which introduced this API but didn't export it
in the library. In practice this API is thus only usable since 1.32.0.
2021-06-15 19:11:57 +02:00
Thomas Haller
14ec96f262
libnm/tests: avoid coverity warning in test_setting_connection_secondaries_verify()
nm_strvarray_get_strv() returns the input pointer itself, if _secondaries is NULL.
It does so intentionally and correctly to create an artificial empty strv array.

Coverity doesn't like this. Try to workaround the warning:

    Error: ARRAY_VS_SINGLETON (CWE-119): [#def484]
    NetworkManager-1.31.90/src/libnm-core-impl/tests/test-setting.c:4544: address_of: Taking address with "&_secondaries" yields a singleton pointer.
    NetworkManager-1.31.90/src/libnm-core-impl/tests/test-setting.c:4544: identity_transfer: Passing "&_secondaries" as argument 1 to function "nm_strvarray_get_strv", which returns that argument.
    NetworkManager-1.31.90/src/libnm-core-impl/tests/test-setting.c:4544: callee_ptr_arith: Passing "_Generic (nm_strvarray_get_strv(&_secondaries, NULL), char const * const * : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char const ** : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char * const * : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char ** : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), void const * : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), void * : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char const * const * const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char const ** const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char * const * const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), char ** const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), void const * const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL), void * const : (char const * const *)nm_strvarray_get_strv(&_secondaries, NULL))" to function "_nm_utils_strv_cmp_n" which uses it as an array. This might corrupt or misinterpret adjacent memory locations.
    # 4542|       G_STMT_END
    # 4543|
    # 4544|->         _assert_secondaries(s_con, (const char *const *) arr->pdata);
    # 4545|
    # 4546|           /* reimplement the normalization that we expect to happen and
2021-06-11 22:42:45 +02:00
Thomas Haller
860b280248
libnm: hide NMSimpleConnection type from public headers 2021-06-11 22:32:24 +02:00
Thomas Haller
e46d484fae
libnm: hide NMSetting types from public headers
When subclassing a GObject type, the class and object structs
must be available and defined in the header.

For libnm, and in particular for NMSetting classes, we don't want
users to subclass NMSetting. It also doesn't work, because libnm
has internal code that is necessary to hook up the NMSetting class.
You cannot define your own type and make it work together with
libnm.

Having the structs in public headers limits what we can do with them.
For example, we could embed the private data directly in the structures
and avoid the additional indirection.

This is an API break, but for something that most likely nobody cares
about. Or better, nobody should care about. API is not what is
accidentally defined in a header, API was the library provides to
meaningfully use. Subclassing these types is not meaningful and was
only accidentally possible so far.

Only hide the structs for now. More cleanup is possible later. We shall
however aim to keep the padding and struct layout to not also break ABI.
2021-06-11 22:32:24 +02:00
Simon McVittie
18c76388f0
libnm: Don't crash if service tells us a new key management mode
The NetworkManager service sometimes adds new key management modes.
If it does, an older client library (perhaps in a container, or loaded
into a pre-existing process before an upgrade) shouldn't crash when
talking to a newer NetworkManager service over D-Bus.

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/744

Signed-off-by: Simon McVittie <smcv@collabora.com>

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/887
2021-06-10 23:16:59 +02:00
Thomas Haller
7d063726c2
libnm/tests: fix compilation error with old gcc not supporting __auto_type
Fixes: 23adeed244 ('glib-aux: use NM_VA_ARGS_FOREACH() to implement NM_HASH_COMBINE_BOOLS()')
2021-06-10 13:28:15 +02:00
Thomas Haller
2b2c818e03
glib-aux/uuid: use NMUuid typed argument for nm_uuid_generate_from_string*()
nm_uuid_generate_from_string*() accepts an optional namespace parameter,
to seed the hashing. This previously was a UUID in string format, so it
first had to be parsed.

Rework the code to pass a NMUuid instance that can be used directly.
Also, as the type_args parameter is always of the same type, change
the argument from a void pointer to "const NMUuid *" pointer.
2021-06-08 08:24:14 +02:00
Thomas Haller
10e5f10f9d
glib-aux/uuid: add NM_UUID_INIT() macro 2021-06-08 08:24:13 +02:00
Thomas Haller
23adeed244
glib-aux: use NM_VA_ARGS_FOREACH() to implement NM_HASH_COMBINE_BOOLS() 2021-06-08 08:24:12 +02:00
Thomas Haller
1ccfde7ee6
libnm/tests: add test for NM_NARG() with 120 parameters 2021-06-08 08:24:11 +02:00
Thomas Haller
3699c31eb1
libnm/tests: add test for normalizing "connection.secondaries" 2021-06-04 09:42:38 +02:00
Thomas Haller
890df48d14
libnm: verify and normalize "connection.secondaries"
So far, we didn't verify the secondary connections at all.
But these really are supposed to be UUIDs.

As we now also normalize "connection.uuid" to be in a strict
format, the user might have profiles with non-normalized UUIDs.
In that case, the "connection.uuid" would be normalized, but
"connection.secondaries" no longer matches. We can fix that by
also normalizing "connection.secondaries". OK, this is not a very good
reason, because it's unlikely to affect any users in practice ('though
it's easy to reproduce).

A better reason is that the secondary setting really should be well
defined and verified. As we didn't do that so far, we cannot simply
outright reject invalid settings. What this patch does instead, is
silently changing the profile to only contain valid settings.
That has it's own problems, like that the user setting an invalid
value does not get an error nor the desired(?) outcome.
But of all the bad choices, normalizing seems the most sensible
one.

Note that in practice, most client applications don't rely on setting
arbitrary (invalid) "UUIDs". They simply expect to be able to set valid
UUIDs, which they still are. For example, nm-connection-editor presents
a drop down list of VPN profile, and nmcli also resolves connection IDs
to the UUID. That is, clients already have an intimate understanding of
this setting, and don't blindly set arbitrary values. Hence, this
normalization is unlikely to hit users in practice. But what it gives
is the guarantee that a verified connection only contains valid UUIDs.

Now all UUIDs will be normalized, invalid entries removed, and the list
made unique.
2021-06-04 09:29:25 +02:00
Thomas Haller
3acf62f8be
libnm: use GArray to track "connection.secondaries" property instead of GSList
GSList requires an additional allocation for the container struct for each
element. Also, it does not have O(1) direct access. It's a pretty bad
data structure, especially if the underlying data is in form of a strv
array.

Use a GArray instead and the nm_strvarray_*() helpers.
2021-06-04 09:29:24 +02:00
Thomas Haller
92136135ad
libnm: don't reject empty strings in add/remove API
For example for NM_SETTING_CONNECTION_SECONDARIES, the user can set
the GObject property to a string list that includes empty strings.

The C accessors (add/remove-by-value) should also accept any strings that
are accepted otherwise. Asserting against empty strings is wrong. If the
setting wants to reject empty strings, then it should use verify().
2021-06-04 09:29:24 +02:00
Thomas Haller
46533cd15f
libnm: use nm_strvarray_get_strv_non_empty_dup() in "nm-setting-match.c" 2021-06-04 09:29:24 +02:00
Thomas Haller
75c6c4abf8
libnm: use nm_strvarray_get_idx() in "nm-setting-match.c" 2021-06-04 09:29:23 +02:00
Thomas Haller
6f2ae46b37
all: use nm_uuid_is_normalized() for checking valid UUID for "connection.uuid"
"connection.uuid" gets normalized. When we check for a valid UUID, we expect
it to be normalized.
2021-06-04 09:29:23 +02:00
Beniamino Galvani
6a88d4e55c ifcfg-rh: preserve an empty tc configuration
If the TC setting contains no qdiscs and filters, it is lost after a
write-read cycle. Fix this by adding a new property to indicate the
presence of the (empty) setting.
2021-06-03 09:02:07 +02:00
Beniamino Galvani
a48edd0410 core,libnm: don't touch device TC configuration by default
NetworkManager supports a very limited set of qdiscs. If users want to
configure a unsupported qdisc, they need to do it outside of
NetworkManager using tc.

The problem is that NM also removes all qdiscs and filters during
activation if the connection doesn't contain a TC setting. Therefore,
setting TC configuration outside of NM is hard because users need to
do it *after* the connection is up (for example through a dispatcher
script).

Let NM consider the presence (or absence) of a TC setting in the
connection to determine whether NM should configure (or not) qdiscs
and filters on the interface. We already do something similar for
SR-IOV configuration.

Since new connections don't have the TC setting, the new behavior
(ignore existing configuration) will be the default. The impact of
this change in different scenarios is:

 - the user previously configured TC settings via NM. This continues
   to work as before;

 - the user didn't set any qdiscs or filters in the connection, and
   expected NM to clear them from the interface during activation.
   Here there is a change in behavior, but it seems unlikely that
   anybody relied on the old one;

 - the user didn't care about qdiscs and filters; NM removed all
   qdiscs upon activation, and so the default qdisc from kernel was
   used. After this change, NM will not touch qdiscs and the default
   qdisc will be used, as before;

 - the user set a different qdisc via tc and NM cleared it during
   activation. Now this will work as expected.

So, the new default behavior seems better than the previous one.

https://bugzilla.redhat.com/show_bug.cgi?id=1928078
2021-06-03 09:01:57 +02:00
Thomas Haller
dd4cccbe66
lgtm: suppress [cpp/alloca-in-loop] warnings for lgtm.com
The warning is not right. There is no unguarded number of alloca()
calls inside a loop. Suppress the warnings.
2021-06-01 17:45:39 +02:00
Andrew Zaborowski
3bc16323fa libnm: Fix error message conditions in verity_ttls
In two similar ``if () {} else if () {} else if () {} else {}`` sequences
the latter two {} blocks were unreachable.  In the
identity/anonymous-identity case, anonymous-identity is optional,
wpa_supplicant will fall back to identity, so only check that (a likely
privacy issue because no NM or wpa_s documentation explains that the
"secure" identity is also sent in plaintext if anonymous_identity is
missing.)

In the phase2_auth/phase2_autheap case change the message to make it
clear that exactly one of the properties is expected to be present.
Drop the empty string checks because those cases is validated later in
verify() anyway.
2021-06-01 09:02:42 +00:00
Thomas Haller
5ce91adeff
libnm/trivial: rename _nm_keyfile_equals() to _nm_keyfile_equal() 2021-05-27 09:56:41 +02:00
Thomas Haller
cbaeadb0a4
libnm/docs: document nm_utils_is_uuid() as deprecated
However, don't also use the NM_DEPRECATED_IN_1_32 macro, because that
causes annoying compiler warnings.

There is no replacement for the function in libnm, nor is it planned
to add one. So users may still call it, but they are now warned by
documentation that it may not be a good idea.
2021-05-27 09:29:22 +02:00
Thomas Haller
d5e8d1382b
all/trivial: add code comment about using alloca() inside a loop
lgtm.com warns about these uses. They are correct though. Maybe the code should
not use alloca() simply to suppress the warning. Instead, add a comment pointing
out that this is in fact correct.
2021-05-27 09:09:29 +02:00
Thomas Haller
a559950d41
libnm/tests: avoid Coverity warning in test code _do_test_utils_str_utf8safe_unescape()
Error: FORWARD_NULL (CWE-476): [#def435]
    NetworkManager-1.31.5/src/libnm-core-impl/tests/test-general.c:9084: var_compare_op: Comparing "str" to null implies that "str" might be null.
    NetworkManager-1.31.5/src/libnm-core-impl/tests/test-general.c:9105: var_deref_model: Passing null pointer "str" to "strchr", which dereferences it.
    # 9103|           s = nm_utils_str_utf8safe_unescape(str, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &str_free_1);
    # 9104|           g_assert_cmpstr(s, ==, expected);
    # 9105|->         if (strchr(str, '\\')) {
    # 9106|               g_assert(str_free_1 != str);
    # 9107|               g_assert(s == str_free_1);
2021-05-26 15:46:00 +02:00
Thomas Haller
6646ee6546
libnm/tests: avoid potential crash in test code test_nm_utils_escaped_tokens()
It causes a Coverity warning, so let's work around it.
2021-05-26 15:46:00 +02:00
Wen Liang
18839361ac
bond: support tlb_dynamic_lb in balance-alb mode
In kernel, `tlb_dynamic_lb` is supported to configure in bonding mode
`balance-alb`. Therefore, add the support in NetworkManager to avoid
undesirable limitation.

Kernel previously had such limitation and it was removed in
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e79c1055749e3183a2beee04a24da378623329c5.

Signed-off-by: Wen Liang <liangwen12year@gmail.com>

https://bugzilla.redhat.com/show_bug.cgi?id=1959934

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/868
2021-05-26 14:57:21 +02:00
Thomas Haller
8ba66f8ec9
trivial: improve code comments 2021-05-26 12:07:11 +02:00
Thomas Haller
ca6d30cb24
libnm: comment "olpc-mesh.dhcp-anycast-address" only working with dhclient 2021-05-18 09:41:52 +02:00
Thomas Haller
dfc5667603
libnm: reject setting ethtool.pause-autoneg while setting pause-rx/pause-tx
Setting pause-rx/pause-tx to an explicit value, implies that the user
does not want to enable autoneg. Reject that as invalid value in the
connection profile.
2021-05-17 23:31:21 +02:00
Gris Ge
652ddca04c
ethtool: Introducing PAUSE support
Introducing ethtool PAUSE support with:

 * ethtool.pause-autoneg on/off
 * ethtool.pause-rx on/off
 * ethtool.pause-tx on/off

Limitations:
 * When `ethtool.pause-autoneg` is set to true, the `ethtool.pause-rx`
   and `ethtool.pause-tx` will be ignored. We don't have warning for
   this yet.

Unit test case included.

Signed-off-by: Gris Ge <fge@redhat.com>

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/829
2021-05-12 18:04:46 +02:00
Thomas Haller
90f5d9114f
libnm: use nm_auto_unref_tc_action cleanup macro in nm_utils_tc_action_from_str() 2021-05-11 13:56:45 +02:00
Thomas Haller
3cd56e92d4
libnm: fix leak in nm_utils_tc_tfilter_from_str()
Found by Coverity:

  Error: RESOURCE_LEAK (CWE-772):
  NetworkManager-1.31.3/src/libnm-core-impl/nm-utils.c:2772: alloc_fn: Storage is returned from allocation function "nm_utils_tc_action_from_str".
  NetworkManager-1.31.3/src/libnm-core-impl/nm-utils.c:2772: var_assign: Assigning: "action" = storage returned from "nm_utils_tc_action_from_str(extra_opts, error)".
  NetworkManager-1.31.3/src/libnm-core-impl/nm-utils.c:2785: leaked_storage: Variable "action" going out of scope leaks the storage it points to.
  # 2783|       tfilter = nm_tc_tfilter_new(kind, parent, error);
  # 2784|       if (!tfilter)
  # 2785|->         return NULL;
  # 2786|
  # 2787|       nm_tc_tfilter_set_handle(tfilter, handle);

Fixes: de41c45e61 ('libnm-core: add functionality for dealing with tc-style traffic filter specifiers')
2021-05-11 13:56:45 +02:00
Thomas Haller
72e8336fdc
libnm: add internal accessors for _nm_setting_bridge{,_port}_get_vlans()
For internal code, when we control what we are doing, make it possible
to directly access the internal GPtrArray.
2021-05-11 13:53:52 +02:00
Jonas Dreßler
a4b95a9fcd
libnm-core: Don't allow disabling PMF when using WPA3 key management
Modern WPA3 authentication methods like SAE and WPA-EAP-SUITE-B-192 need
to have management frame protection set to required according to the
standard. Since the last commit, we enforce this automatically when
key-mgmt is set to 'owe', 'sae' or 'wpa-eap-suite-b-192', so disabling
it manually should not be possible.

Add a check to the pmf property that makes sure it can't be set to
'disabled' or 'optional' when one of those key-mgmt methods is used.
2021-05-06 22:23:28 +02:00
Jonas Dreßler
e06f9508d1
libnm-core: Rewrite comment of key-mgmt property
The key-mgmt property of NMSettingWirelessSecurity is slightly confusing
when you know there's also a wpa_supplicant configuration option called
"key_mgmt". Our property is not the same as that supplicant option even
though they do have things in common. NMs key-mgmt is not exactly meant
to configure which AKM suites you want to use, but rather which method
of wifi security is being used (so "wpa2+wpa3 personal", "wpa3 personal
only" or "wpa3 enterprise only").

Try to make this a bit clearer in the documentation of the property by
rewriting it and listing those security methods.
2021-05-06 22:23:28 +02:00
Thomas Haller
207cf3d5d4
libnm: normalize "connection.uuid"
For NetworkManager profiles, "connection.uuid" is the identifier of the
profile. It is supposed to be a UUID, however:

- the UUID was not ensured to be all-lower case. We should make sure
  that our UUIDs are in a consistent manner, so that users can rely
  on the format of the string.

- the UUID was never actually interpreted as a UUID. It only was some
  opaque string, that we use as identifier. We had nm_utils_is_uuid()
  which checks that the format is valid, however that did not fully
  validate the format, like it would accept "----7daf444dd78741a59e1ef1b3c8b1c0e8"
  and "549fac10a25f4bcc912d1ae688c2b4987daf444d" (40 hex characters).

Both invalid UUIDs and non-normalized UUID should be normalized. We
don't want to break existing profiles that use such UUIDs, thus we don't
outright reject them. Let's instead mangle them during
nm_connection_normalize().
2021-05-04 15:51:59 +02:00
Thomas Haller
5bd03fd9a3
libnm: use nm_uuid_is_valid_nmlegacy() to implement nm_utils_is_uuid() 2021-05-04 15:51:57 +02:00