From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202512 header.b=AUThXzLC; dkim-atps=neutral Received: from mail.ozlabs.org (gandalf.ozlabs.org [150.107.74.76]) by passt.top (Postfix) with ESMTPS id 92AFE5A0653 for ; Tue, 16 Dec 2025 04:21:32 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202512; t=1765855288; bh=ONR0SVnppiKy9eG8SeLT9Uhpiq344TVxLTxp2XT7MkI=; h=Date:From:To:Cc:Subject:References:In-Reply-To:From; b=AUThXzLCdpLxqxjsQGNNScrMLjTzRn3Fp3l/YIsFrmwdy9EoXvhfxHwjNTFMv2sbP yLlF+07Q7TWSD33GIFju94dJmCSy3fgN2HpiCkQwH4SC30koeb8gDf+DXtOh1yr9vk 90Fl0z5diH5DXm5eCfGXorRemPicJhrIcxw4tZZ8fL0v15Co5JAU8bV7KxgiNtoGY4 NHZJqbxBMCcKmSK9wEu5xUrj6S2g0OpXyTse+q4hTC0HVecmWS2T6DBqwQQ8PTPM14 FcmFHhsqtNMueV9LCZrQoQ3+CEMpBtLxbW4kuNFWgdDlBx7/UUEUnD8qL90zKD7w9I 55sU34rAuGFBg== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4dVhzX6Wj8z4wHj; Tue, 16 Dec 2025 14:21:28 +1100 (AEDT) Date: Tue, 16 Dec 2025 14:21:24 +1100 From: David Gibson To: Jon Maloy Subject: Re: [RFC 07/12] netlink: Subscribe to link/address changes in namespace Message-ID: References: <20251215015441.887736-1-jmaloy@redhat.com> <20251215015441.887736-8-jmaloy@redhat.com> <6acb974d-c0dc-4bc3-82e0-360099a67a99@redhat.com> MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha512; protocol="application/pgp-signature"; boundary="d8h3IfD7MWmTZXXB" Content-Disposition: inline In-Reply-To: <6acb974d-c0dc-4bc3-82e0-360099a67a99@redhat.com> Message-ID-Hash: 2MAHEP44C2ODSQMVWA5WVUFYCDOMXZIR X-Message-ID-Hash: 2MAHEP44C2ODSQMVWA5WVUFYCDOMXZIR X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: sbrivio@redhat.com, dgibson@redhat.com, passt-dev@passt.top X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: --d8h3IfD7MWmTZXXB Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Content-Transfer-Encoding: quoted-printable On Mon, Dec 15, 2025 at 06:25:53PM -0500, Jon Maloy wrote: >=20 >=20 > On 2025-12-15 05:32, David Gibson wrote: > > On Sun, Dec 14, 2025 at 08:54:36PM -0500, Jon Maloy wrote: > > > We add subscriptions to RTMGRP_LINK, RTMGRP_IPV4_IFADDR, and > > > RTMGRP_IPV6_IFADDR, so that we can receive notifications when link > > > state or addresses change on the namespace interface. > > >=20 > > > When addresses are discovered via netlink: > > >=20 > > > - We mark them as non-permanent, which means they can be modified or > > > deleted by subsequent events. > > > - We apply the prefix indicated in the notification. > > > - Update addr_seen to track the new address as the active one. > >=20 > > addr_seen isn't really about an "active" address. The expectation was > > that the guest would only use a single address, it just might not be > > the one we told it to. > >=20 > > Now that we're aiming to allow multiple concurrent addresses, we can > > expect the guest to use all of them actively. >=20 > Right. This makes the case for having guest/tap side subscriptions, since= we > now will now know all addresses he is using, so addr_seen > should become obsolete. Well.. it depends what you mean by obsolete. We can store multiple addresses, so it probably makes sense to store configured and observed addresses in the same table (since they'll usually be the same). Assuming we still support the guest using a non-configured address we'll still need to be able to observe them on the wire, not just via netlink, because that won't work for passt. Given that, it's not clear to me that we actually gain anything by observing addresses using guest side netlink. > > > This provides the foundation for dynamic address monitoring, > > > and supports runtime network changes. > > >=20 > > > Signed-off-by: Jon Maloy > > > --- > > > epoll_type.h | 2 + > > > netlink.c | 370 ++++++++++++++++++++++++++++++++++++++++++++++++= +++ > > > netlink.h | 3 + > > > passt.c | 5 + > > > passt.h | 1 + > > > tap.c | 6 +- > > > 6 files changed, 384 insertions(+), 3 deletions(-) > > >=20 > > > diff --git a/epoll_type.h b/epoll_type.h > > > index a90ffb6..0a16d94 100644 > > > --- a/epoll_type.h > > > +++ b/epoll_type.h > > > @@ -46,6 +46,8 @@ enum epoll_type { > > > EPOLL_TYPE_REPAIR, > > > /* Netlink neighbour subscription socket */ > > > EPOLL_TYPE_NL_NEIGH, > > > + /* Netlink link/address subscription socket */ > > > + EPOLL_TYPE_NL_LINKADDR, > > > EPOLL_NUM_TYPES, > > > }; > > > diff --git a/netlink.c b/netlink.c > > > index 82a2f0c..7492f17 100644 > > > --- a/netlink.c > > > +++ b/netlink.c > > > @@ -35,6 +35,9 @@ > > > #include "passt.h" > > > #include "log.h" > > > #include "ip.h" > > > +#include "tap.h" > > > +#include "arp.h" > > > +#include "ndp.h" > > > #include "netlink.h" > > > #include "epoll_ctl.h" > > > @@ -59,6 +62,7 @@ > > > int nl_sock =3D -1; > > > int nl_sock_ns =3D -1; > > > static int nl_sock_neigh =3D -1; > > > +static int nl_sock_linkaddr =3D -1; > > > static int nl_seq =3D 1; > > > /** > > > @@ -91,6 +95,372 @@ static int nl_sock_init_do(void *arg) > > > return 0; > > > } > > > +/** > > > + * nl_addr4_find() - Find an IPv4 address in the address array > > > + * @c: Execution context > > > + * @addr: Address to find > > > + * > > > + * Return: index if found, -1 otherwise > > > + */ > > > +static int nl_addr4_find(const struct ctx *c, const struct in_addr *= addr) > > > +{ > > > + int i; > > > + > > > + for (i =3D 0; i < c->ip4.addr_count; i++) > > > + if (IN4_ARE_ADDR_EQUAL(&c->ip4.addrs[i].addr, addr)) > > > + return (int)i; > > > + > > > + return -1; > > > +} > > > + > > > +/** > > > + * nl_addr6_find() - Find an IPv6 address in the address array > > > + * @c: Execution context > > > + * @addr: Address to find > > > + * > > > + * Return: index if found, -1 otherwise > > > + */ > > > +static int nl_addr6_find(const struct ctx *c, const struct in6_addr = *addr) > > > +{ > > > + int i; > > > + > > > + for (i =3D 0; i < c->ip6.addr_count; i++) > > > + if (IN6_ARE_ADDR_EQUAL(&c->ip6.addrs[i].addr, addr)) > > > + return (int)i; > > > + > > > + return -1; > > > +} > > > + > > > +/** > > > + * nl_addr4_add() - Add a discovered IPv4 address to the address arr= ay > > > + * @c: Execution context > > > + * @addr: Address to add > > > + * @prefix_len: Prefix length > > > + * > > > + * Return: true if added or updated, false if array full or already = permanent > > > + */ > > > +static bool nl_addr4_add(struct ctx *c, const struct in_addr *addr, > > > + int prefix_len) > > > +{ > > > + int idx =3D nl_addr4_find(c, addr); > > > + > > > + if (idx >=3D 0) { > > > + /* Address exists - if permanent, don't touch; else update */ > > > + if (c->ip4.addrs[idx].permanent) > > > + return false; > > > + c->ip4.addrs[idx].prefix_len =3D prefix_len; > > > + return true; > > > + } > > > + > > > + /* New address - add if room */ > > > + if (c->ip4.addr_count >=3D IP4_MAX_ADDRS) { > > > + debug("IPv4 address array full, ignoring discovered address"); > > > + return false; > > > + } > > > + > > > + idx =3D c->ip4.addr_count++; > > > + c->ip4.addrs[idx].addr =3D *addr; > > > + c->ip4.addrs[idx].prefix_len =3D prefix_len; > > > + c->ip4.addrs[idx].permanent =3D 0; > > > + return true; > > > +} > > > + > > > +/** > > > + * nl_addr6_add() - Add a discovered IPv6 address to the address arr= ay > > > + * @c: Execution context > > > + * @addr: Address to add > > > + * @prefix_len: Prefix length > > > + * > > > + * Return: true if added or updated, false if array full or already = permanent > > > + */ > > > +static bool nl_addr6_add(struct ctx *c, const struct in6_addr *addr, > > > + int prefix_len) > > > +{ > > > + int idx =3D nl_addr6_find(c, addr); > > > + > > > + if (idx >=3D 0) { > > > + /* Address exists - if permanent, don't touch; else update */ > > > + if (c->ip6.addrs[idx].permanent) > > > + return false; > > > + c->ip6.addrs[idx].prefix_len =3D prefix_len; > > > + return true; > > > + } > > > + > > > + /* New address - add if room */ > > > + if (c->ip6.addr_count >=3D IP6_MAX_ADDRS) { > > > + debug("IPv6 address array full, ignoring discovered address"); > > > + return false; > > > + } > > > + > > > + idx =3D c->ip6.addr_count++; > > > + c->ip6.addrs[idx].addr =3D *addr; > > > + c->ip6.addrs[idx].prefix_len =3D prefix_len; > > > + c->ip6.addrs[idx].permanent =3D 0; > > > + return true; > > > +} > > > + > > > +/** > > > + * nl_addr4_del() - Remove an IPv4 address from the array if not per= manent > > > + * @c: Execution context > > > + * @addr: Address to remove > > > + * > > > + * Return: true if removed, false if not found or permanent > > > + */ > > > +static bool nl_addr4_del(struct ctx *c, const struct in_addr *addr) > > > +{ > > > + int i, idx =3D nl_addr4_find(c, addr); > > > + > > > + if (idx < 0) > > > + return false; > > > + > > > + if (c->ip4.addrs[idx].permanent) > > > + return false; > > > + > > > + /* Shift remaining entries down */ > > > + c->ip4.addr_count--; > > > + for (i =3D idx; i < c->ip4.addr_count; i++) > > > + c->ip4.addrs[i] =3D c->ip4.addrs[i + 1]; > > > + > > > + return true; > > > +} > > > + > > > +/** > > > + * nl_addr6_del() - Remove an IPv6 address from the array if not per= manent > > > + * @c: Execution context > > > + * @addr: Address to remove > > > + * > > > + * Return: true if removed, false if not found or permanent > > > + */ > > > +static bool nl_addr6_del(struct ctx *c, const struct in6_addr *addr) > > > +{ > > > + int i, idx =3D nl_addr6_find(c, addr); > > > + > > > + if (idx < 0) > > > + return false; > > > + > > > + if (c->ip6.addrs[idx].permanent) > > > + return false; > > > + > > > + /* Shift remaining entries down */ > > > + c->ip6.addr_count--; > > > + for (i =3D idx; i < c->ip6.addr_count; i++) > > > + c->ip6.addrs[i] =3D c->ip6.addrs[i + 1]; > > > + > > > + return true; > > > +} > >=20 > > All the functions above are more to do with the data structure storing > > the addresses than they are to do with netlink. Better to move them > > into... maybe ip.c? And use them from conf.c as well. >=20 > Agreed. I'll fix that. >=20 > >=20 > > Given the amount of near-duplication here, maybe it would be better to > > have a single table for v4 and v6 using inany_addr? > Yes. > >=20 > > > +/** > > > + * nl_linkaddr_msg_read() - Parse and log a netlink link/addr message > > > + * @c: Execution context > > > + * @nh: Netlink message header > > > + */ > > > +static void nl_linkaddr_msg_read(struct ctx *c, const struct nlmsghd= r *nh) > > > +{ > > > + if (nh->nlmsg_type =3D=3D NLMSG_DONE || nh->nlmsg_type =3D=3D NLMSG= _ERROR) > > > + return; > > > + > > > + if (nh->nlmsg_type =3D=3D RTM_NEWLINK || nh->nlmsg_type =3D=3D RTM_= DELLINK) { > > > + const struct ifinfomsg *ifm =3D NLMSG_DATA(nh); > > > + struct rtattr *rta =3D IFLA_RTA(ifm); > > > + size_t na =3D IFLA_PAYLOAD(nh); > > > + const char *name =3D "?"; > > > + bool up =3D !!(ifm->ifi_flags & IFF_UP); > > > + bool running =3D !!(ifm->ifi_flags & IFF_RUNNING); > > > + > > > + for (; RTA_OK(rta, na); rta =3D RTA_NEXT(rta, na)) { > > > + if (rta->rta_type =3D=3D IFLA_IFNAME) { > > > + name =3D (const char *)RTA_DATA(rta); > > > + break; > > > + } > > > + } > > > + > > > + /* Update pasta interface UP state if this is our interface */ > > > + if (c->mode =3D=3D MODE_PASTA && > > > + (unsigned int)ifm->ifi_index =3D=3D c->pasta_ifi) { > > > + c->pasta_ifi_up =3D up; > > > + debug("Interface %s", up ? "UP" : "DOWN"); > >=20 > > This only makes sense if we're listening to netlink messages in the > > guest netns, but the address stuff only makes sense listening to > > messages in the host netns. >=20 > See previous response.> > > > + } > > > + > > > + if (nh->nlmsg_type =3D=3D RTM_NEWLINK) > > > + debug("Link %s (idx=3D%d): %s %s", name, ifm->ifi_index, > > > + up ? "UP" : "DOWN", running ? "RUNNING" : ""); > > > + else > > > + debug("Link %s (idx=3D%d): DELETED", name, ifm->ifi_index); > > > + > > > + return; > > > + } > > > + > > > + if (nh->nlmsg_type =3D=3D RTM_NEWADDR || nh->nlmsg_type =3D=3D RTM_= DELADDR) { > > > + bool is_new =3D (nh->nlmsg_type =3D=3D RTM_NEWADDR); > > > + const struct ifaddrmsg *ifa =3D NLMSG_DATA(nh); > > > + char addr_str[INET6_ADDRSTRLEN]; > > > + struct rtattr *rta =3D IFA_RTA(ifa); > > > + char ifname[IFNAMSIZ] =3D { 0 }; > > > + size_t na =3D IFA_PAYLOAD(nh); > > > + void *addr =3D NULL; > > > + for (; RTA_OK(rta, na); rta =3D RTA_NEXT(rta, na)) { > > > + if (ifa->ifa_family =3D=3D AF_INET && > > > + rta->rta_type =3D=3D IFA_LOCAL) { > > > + addr =3D RTA_DATA(rta); > > > + break; > > > + } else if (ifa->ifa_family =3D=3D AF_INET6 && > > > + rta->rta_type =3D=3D IFA_ADDRESS) { > > > + addr =3D RTA_DATA(rta); > > > + break; > > > + } > > > + } > > > + > > > + if (!addr) > > > + return; > > > + > > > + if_indextoname(ifa->ifa_index, ifname); > > > + inet_ntop(ifa->ifa_family, addr, addr_str, sizeof(addr_str)); > > > + > > > + debug("%s addr on %s (index=3D%d): %s/%i%s", > > > + is_new ? "NEW" : "DEL", ifname, ifa->ifa_index, addr_str, > > > + ifa->ifa_prefixlen, > > > + tap_is_ready(c) ? " (tap UP)" : " (tap DOWN)"); > > > + > > > + /* Only handle our pasta interface */ > > > + if (c->mode !=3D MODE_PASTA || ifa->ifa_index !=3D c->pasta_ifi) > > > + return; > >=20 > > Nope. This is a host netns event, so comparing to pasta_ifi makes no > > sense. We _should_ be comparing to ifi4 or ifi6 (depending on address > > family), and we should probably do that before we go parsing the > > details above. > >=20 > > We should also probably check for --no-copy-addrs here, too. > >=20 > > In the other direction, even for PASST mode we can store this address > > in our table, it just won't do anything until DHCP or whatever > > consults it. >=20 > ok This comment no longer makes sense, now that I understand this is intentionally observing the guest not the host. >=20 > >=20 > > > + > > > + if (ifa->ifa_family =3D=3D AF_INET) { > > > + struct in_addr *a =3D (struct in_addr *)addr; > > > + > > > + if (!is_new) { > > > + nl_addr4_del(c, a); > > > + return; > > > + } > > > + > > > + if (nl_addr4_add(c, a, ifa->ifa_prefixlen)) { > > > + c->ip4.addr_seen =3D *a; > > > + if (c->pasta_ifi_up && c->ifi4) { > > > + debug("Sending ARP"); > > > + arp_send_init_req(c); > >=20 > > What does this ARP request do? AFAICT we haven't actually added the > > address in the guest netns yet, so the guest won't respond to the ARP. >=20 > The address was set by the guest, so why wouldn't he respond? Again, I was assuming this was meant to be observing host addresses. > > > + } > > > + } > > > + } else if (ifa->ifa_family =3D=3D AF_INET6) { > > > + struct in6_addr *a =3D (struct in6_addr *)addr; > > > + > > > + if (!is_new) { > > > + nl_addr6_del(c, a); > > > + return; > > > + } > > > + > > > + if (nl_addr6_add(c, a, > > > + ifa->ifa_prefixlen)) { > > > + c->ip6.addr_seen =3D *a; > > > + if (c->pasta_ifi_up && > > > + c->ifi6 && !c->no_ndp) { > > > + debug("Sending NDP"); > > > + ndp_send_init_req(c); > >=20 > > Some question with this NDP. > >=20 > > > + } > > > + } > > > + } > > > + } > > > +} > > > + > > > +/** > > > + * nl_linkaddr_notify_handler() - Handle events from link/addr notif= ier socket > > > + * @c: Execution context > > > + */ > > > +void nl_linkaddr_notify_handler(struct ctx *c) > > > +{ > > > + char buf[NLBUFSIZ]; > > > + > > > + for (;;) { > > > + ssize_t n =3D recv(nl_sock_linkaddr, buf, sizeof(buf), MSG_DONTWAI= T); > > > + struct nlmsghdr *nh =3D (struct nlmsghdr *)buf; > > > + > > > + if (n < 0) { > > > + if (errno =3D=3D EINTR) > > > + continue; > > > + if (errno !=3D EAGAIN) > > > + debug("recv() error: %s", strerror_(errno)); > > > + break; > > > + } > > > + > > > + debug("Received %zd bytes", n); > > > + > > > + for (; NLMSG_OK(nh, n); nh =3D NLMSG_NEXT(nh, n)) > > > + nl_linkaddr_msg_read(c, nh); > > > + } > > > +} > > > + > > > +/** > > > + * nl_linkaddr_init_do() - Actually create and bind the netlink sock= et > > > + * @arg: Execution context (for namespace entry) or NULL > > > + * > > > + * Return: 0 on success, -1 on failure > > > + */ > > > +static int nl_linkaddr_init_do(void *arg) > > > +{ > > > + struct sockaddr_nl addr =3D { .nl_family =3D AF_NETLINK, > > > + .nl_groups =3D RTMGRP_LINK | RTMGRP_IPV4_IFADDR | > > > + RTMGRP_IPV6_IFADDR }; > > > + > > > + if (arg) > > > + ns_enter((struct ctx *)arg); > > > + > > > + nl_sock_linkaddr =3D socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NE= TLINK_ROUTE); > >=20 > > Is there a reason to use an additional socket, rather than adding more > > events to the neighbour listening socket? >=20 > None in particular. I'll look into it. >=20 > >=20 > > > + if (nl_sock_linkaddr < 0) { > > > + debug("socket() failed: %s", strerror_(errno)); > > > + return -1; > > > + } > > > + > > > + if (bind(nl_sock_linkaddr, (struct sockaddr *)&addr, sizeof(addr)) = < 0) { > > > + debug("bind() failed: %s", strerror_(errno)); > > > + close(nl_sock_linkaddr); > > > + nl_sock_linkaddr =3D -1; > > > + return -1; > > > + } > > > + > > > + debug("socket fd=3D%d", nl_sock_linkaddr); > > > + return 0; > > > +} > > > + > > > +/** > > > + * nl_linkaddr_notify_init() - Initialize link/address change notifi= er > > > + * @c: Execution context > > > + * > > > + * Return: 0 on success, -1 on failure > > > + */ > > > +int nl_linkaddr_notify_init(const struct ctx *c) > > > +{ > > > + union epoll_ref ref =3D { .type =3D EPOLL_TYPE_NL_LINKADDR }; > > > + struct epoll_event ev =3D { .events =3D EPOLLIN }; > > > + > > > + if (nl_sock_linkaddr >=3D 0) { > > > + debug("notifier already initialized (fd=3D%d)", nl_sock_linkaddr); > > > + return 0; > > > + } > > > + > > > + /* Open the notifier socket in the namespace for pasta mode, > > > + * or in the init namespace otherwise. > >=20 > > Definitely wrong. We're trying to watch host addresses so that we can > > copy them to the guest - therefore we always need to watch in the host > > netns. On pasta there might be reasons to *also* listen in the guest > > netns, but that would want a different handler to do different things. >=20 > Hmm. I think I'll wait for your feedback on the remaining patches before I > comment on this. So, as discussed in the call, this makes a bit more sense to me now that I know this is intending to observe what the guest is doing, not the host. That said, it still doesn't make sense to listen in different places based on the passt/pasta mode. For both passt and pasta we want something listening on the host, that will behave pretty similarly. For pasta we might want an additional listener on the guest side, but this will be separate from the former, and I'm not yet convinced it's actually helpful. --=20 David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson --d8h3IfD7MWmTZXXB Content-Type: application/pgp-signature; name=signature.asc -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEEO+dNsU4E3yXUXRK2zQJF27ox2GcFAmlA0DMACgkQzQJF27ox 2GcB4g//SeKmml/s2YSm2KNGJPilXm6OeXSBsHsB0BypHyZBn8mHPOGOUwbje7vr R7TbX9AKdINz/6BTWGVFsIlloYKR2HXb52JwvMRYqiDxFrc8NZ2xMA2RQlI3pYyz uoTEdX1P/PAe9yfWZzBVHT+iop65grREu9aVqEwS+ZR3CWOR23dsx0YgNGmF9S2g d840s8aeuc6XNlYPxoi90UzerVkaGN1YttOcJkZRigowaat8iLITuzD8LabvEMFj 16KKv/zLy+zHtCd+QXbY8UBWlnRQktMOrUBrXgR+3rVPBll7tMnHnG+yl97JhWPk IDyA6kg98WzKwF9iYK3zz12WQ6TCR47Wq4aJHTXUJ0+9TME/hHtO/4TpEXvVFXmR /E4jpS8Y+h5AgC76jZyfudDDJlc63rLwhEWkQ2ADVz3HZuCUNiOQaxC0ifiVCpV3 idUirAYfkuMSaInSvDoMlUdF8/9JPnz+oVnQ3cSxLgFXTMIfgFp4re+tSklcKTU+ 9jzf/FN77aL8jTb1fG4y7q2/fi9EkZWw4hbL9kE0c000dBifJMXkU4mRhqZEbqlt UQkfpnC6KrRu5q9ucAEO0tAbSRIm0lBJhask0blk6/ludszzm9w7Xnf/Wt87oa3r RJBCu84Oe1+CYs+tC2upDxRcSNGFfVd1iiC2277F+FH02dMj3Y4= =oPc6 -----END PGP SIGNATURE----- --d8h3IfD7MWmTZXXB--