get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/2003663/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2003663,
    "url": "http://patchwork.ozlabs.org/api/patches/2003663/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/openvswitch/patch/20241029101608.2991596-3-i.maximets@ovn.org/",
    "project": {
        "id": 47,
        "url": "http://patchwork.ozlabs.org/api/projects/47/?format=api",
        "name": "Open vSwitch",
        "link_name": "openvswitch",
        "list_id": "ovs-dev.openvswitch.org",
        "list_email": "ovs-dev@openvswitch.org",
        "web_url": "http://openvswitch.org/",
        "scm_url": "git@github.com:openvswitch/ovs.git",
        "webscm_url": "https://github.com/openvswitch/ovs",
        "list_archive_url": "",
        "list_archive_url_format": "",
        "commit_url_format": ""
    },
    "msgid": "<20241029101608.2991596-3-i.maximets@ovn.org>",
    "list_archive_url": null,
    "date": "2024-10-29T10:15:00",
    "name": "[ovs-dev,2/9] ipsec: libreswan: Reconcile missing connections periodically.",
    "commit_ref": null,
    "pull_url": null,
    "state": "changes-requested",
    "archived": false,
    "hash": "b54518c93625583682c70df52940050ef400da4f",
    "submitter": {
        "id": 76798,
        "url": "http://patchwork.ozlabs.org/api/people/76798/?format=api",
        "name": "Ilya Maximets",
        "email": "i.maximets@ovn.org"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/openvswitch/patch/20241029101608.2991596-3-i.maximets@ovn.org/mbox/",
    "series": [
        {
            "id": 430270,
            "url": "http://patchwork.ozlabs.org/api/series/430270/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/openvswitch/list/?series=430270",
            "date": "2024-10-29T10:14:58",
            "name": "ipsec: Resiliency to Libreswan failures.",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/430270/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2003663/comments/",
    "check": "fail",
    "checks": "http://patchwork.ozlabs.org/api/patches/2003663/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<ovs-dev-bounces@openvswitch.org>",
        "X-Original-To": [
            "incoming@patchwork.ozlabs.org",
            "ovs-dev@openvswitch.org"
        ],
        "Delivered-To": [
            "patchwork-incoming@legolas.ozlabs.org",
            "ovs-dev@lists.linuxfoundation.org"
        ],
        "Authentication-Results": [
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=2605:bc80:3010::136; helo=smtp3.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)",
            "smtp3.osuosl.org;\n dmarc=none (p=none dis=none) header.from=ovn.org"
        ],
        "Received": [
            "from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4Xd5l202jXz1xwn\n\tfor <incoming@patchwork.ozlabs.org>; Tue, 29 Oct 2024 21:16:30 +1100 (AEDT)",
            "from localhost (localhost [127.0.0.1])\n\tby smtp3.osuosl.org (Postfix) with ESMTP id 2723A60D66;\n\tTue, 29 Oct 2024 10:16:28 +0000 (UTC)",
            "from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id izhmc8paSzHF; Tue, 29 Oct 2024 10:16:24 +0000 (UTC)",
            "from lists.linuxfoundation.org (lf-lists.osuosl.org\n [IPv6:2605:bc80:3010:104::8cd3:938])\n\tby smtp3.osuosl.org (Postfix) with ESMTPS id 2B23F60B43;\n\tTue, 29 Oct 2024 10:16:23 +0000 (UTC)",
            "from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id AE4DDC08A8;\n\tTue, 29 Oct 2024 10:16:22 +0000 (UTC)",
            "from smtp3.osuosl.org (smtp3.osuosl.org [140.211.166.136])\n by lists.linuxfoundation.org (Postfix) with ESMTP id 45F95C08A3\n for <ovs-dev@openvswitch.org>; Tue, 29 Oct 2024 10:16:21 +0000 (UTC)",
            "from localhost (localhost [127.0.0.1])\n by smtp3.osuosl.org (Postfix) with ESMTP id 31357608F8\n for <ovs-dev@openvswitch.org>; Tue, 29 Oct 2024 10:16:21 +0000 (UTC)",
            "from smtp3.osuosl.org ([127.0.0.1])\n by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id PeWOos4JKmfW for <ovs-dev@openvswitch.org>;\n Tue, 29 Oct 2024 10:16:20 +0000 (UTC)",
            "from mail-wm1-f68.google.com (mail-wm1-f68.google.com\n [209.85.128.68])\n by smtp3.osuosl.org (Postfix) with ESMTPS id A86B4608FA\n for <ovs-dev@openvswitch.org>; Tue, 29 Oct 2024 10:16:19 +0000 (UTC)",
            "by mail-wm1-f68.google.com with SMTP id\n 5b1f17b1804b1-4315baec69eso52922245e9.2\n for <ovs-dev@openvswitch.org>; Tue, 29 Oct 2024 03:16:19 -0700 (PDT)",
            "from im-t490s.redhat.com (ip-86-49-44-151.bb.vodafone.cz.\n [86.49.44.151]) by smtp.gmail.com with ESMTPSA id\n 5b1f17b1804b1-431b4594ec3sm20279685e9.1.2024.10.29.03.16.16\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Tue, 29 Oct 2024 03:16:17 -0700 (PDT)"
        ],
        "X-Virus-Scanned": [
            "amavis at osuosl.org",
            "amavis at osuosl.org"
        ],
        "X-Comment": "SPF check N/A for local connections -\n client-ip=2605:bc80:3010:104::8cd3:938; helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ",
        "DKIM-Filter": [
            "OpenDKIM Filter v2.11.0 smtp3.osuosl.org 2B23F60B43",
            "OpenDKIM Filter v2.11.0 smtp3.osuosl.org A86B4608FA"
        ],
        "Received-SPF": "Pass (mailfrom) identity=mailfrom; client-ip=209.85.128.68;\n helo=mail-wm1-f68.google.com; envelope-from=i.maximets.ovn@gmail.com;\n receiver=<UNKNOWN>",
        "DMARC-Filter": "OpenDMARC Filter v1.4.2 smtp3.osuosl.org A86B4608FA",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20230601; t=1730196978; x=1730801778;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=XcLX1R2RCUTDZJ0rGOyMKMdS9E0JbeYQugbPk0/UTlg=;\n b=q1Du1Wwzw6zAEtE9R5r4STQ0ks8me/HKn4Vxd2z/9jGkWEbV6vsTitfG/yClHAPVC3\n BPw1iYG9yb5Zv0ML5aHl1l7yyQs+pV/QPqbsmHO9LT7OMezDPVAe7bSTVHfB3L7rJF67\n VBIr87BtxKmhWJ2puoZDn8GQLAUaz0E3QMqYnIKicmYUxtMdixjhA0vvT2K3JSjLUeQ2\n dyB/75s+XQ02otECT3QHUIfWt24r/sohW0WIBobXRYwTzECCYLO3NX9mxmJbggD+Coml\n 4ymbeQI1x2kQmnMtaFNgpFDJiTXwzUTZCIkIJvMa3m4vyfNbklxwl3L+0bYmRiHIPErB\n pKkQ==",
        "X-Gm-Message-State": "AOJu0YwEGFI5rhom72+fx5IJKkuDFguwfXC9KXrW+ehxTxnw2o07m0/I\n 2WMfjNw9p1TgJX4l38JyWhMbKyCWVNeskyj85AjGAeBa2mlaUuuIw2QOUint",
        "X-Google-Smtp-Source": "\n AGHT+IHVAVMiwoUjTMFjcMx6W+8Xkmf1im8rsfJvRbixEXzcJzc/tFnmGBMrRy25h190asWXlmVTLA==",
        "X-Received": "by 2002:a05:600c:4708:b0:426:6308:e2f0 with SMTP id\n 5b1f17b1804b1-4319ad024d7mr110774825e9.26.1730196977399;\n Tue, 29 Oct 2024 03:16:17 -0700 (PDT)",
        "From": "Ilya Maximets <i.maximets@ovn.org>",
        "To": "ovs-dev@openvswitch.org",
        "Date": "Tue, 29 Oct 2024 11:15:00 +0100",
        "Message-ID": "<20241029101608.2991596-3-i.maximets@ovn.org>",
        "X-Mailer": "git-send-email 2.46.0",
        "In-Reply-To": "<20241029101608.2991596-1-i.maximets@ovn.org>",
        "References": "<20241029101608.2991596-1-i.maximets@ovn.org>",
        "MIME-Version": "1.0",
        "Subject": "[ovs-dev] [PATCH 2/9] ipsec: libreswan: Reconcile missing\n connections periodically.",
        "X-BeenThere": "ovs-dev@openvswitch.org",
        "X-Mailman-Version": "2.1.30",
        "Precedence": "list",
        "List-Id": "<ovs-dev.openvswitch.org>",
        "List-Unsubscribe": "<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>",
        "List-Archive": "<http://mail.openvswitch.org/pipermail/ovs-dev/>",
        "List-Post": "<mailto:ovs-dev@openvswitch.org>",
        "List-Help": "<mailto:ovs-dev-request@openvswitch.org?subject=help>",
        "List-Subscribe": "<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>",
        "Cc": "Ilya Maximets <i.maximets@ovn.org>",
        "Content-Type": "text/plain; charset=\"us-ascii\"",
        "Content-Transfer-Encoding": "7bit",
        "Errors-To": "ovs-dev-bounces@openvswitch.org",
        "Sender": "\"dev\" <ovs-dev-bounces@openvswitch.org>"
    },
    "content": "There are cases where ipsec commands may fail to add new connections or\nremove the old ones.  Unfortunately, this means that those connections\nmay actually never be added or removed, since ovs-monitor-ipsec will\nnot re-visit them, unless something else changes.\n\nWake up the monitor periodically to check if something changed in the\nsystem or if some connections still need loading.\n\nThis addresses two main use cases:\n\n  1. Connection failed to start for some reason and was not added\n     to pluto or properly started.  The logic will go over all the\n     desired, loaded and active connections and make sure that\n     any undesired connections are removed, non-loaded connections\n     are loaded and non-active connections are brought UP.\n\n  2. If pluto re-starts it loads all the connections, but doesn't\n     bring them up, because we're using route (ondemand) activation\n     strategy.  This change in this commit will notice all the\n     loaded but not active connections and will bring them up.\n     This helps avoiding packet drops on first packets until the\n     connection activates.\n\nChoosing 15 seconds as an interval to wake up to give pluto some\nbreathing room, i.e. a chance to activate the connections properly\nbefore we start poking them.  And also if pluto is down, 15 second\ninterval will create less spam in the logs.\n\nStrongSwan doesn't need such a logic, because it supports a single\ncommand 'ipsec update' that re-loads the config as a whole and\nfigures out what configuration changes are needed.  But since we're\nstarting all the connections separately with Libreswan, we have to\nkeep track and reconcile manually.\n\nSome more details of the logic are in the comments in the code.\n\nSigned-off-by: Ilya Maximets <i.maximets@ovn.org>\n---\n ipsec/ovs-monitor-ipsec.in | 178 ++++++++++++++++++++++++-------------\n 1 file changed, 116 insertions(+), 62 deletions(-)",
    "diff": "diff --git a/ipsec/ovs-monitor-ipsec.in b/ipsec/ovs-monitor-ipsec.in\nindex 19a401609..09a29e2ca 100755\n--- a/ipsec/ovs-monitor-ipsec.in\n+++ b/ipsec/ovs-monitor-ipsec.in\n@@ -295,6 +295,9 @@ conn prevent_unencrypted_vxlan\n \n         return conns\n \n+    def need_to_reconcile(self, monitor):\n+        return False\n+\n     def config_init(self):\n         self.conf_file = open(self.IPSEC_CONF, \"w\")\n         self.secrets_file = open(self.IPSEC_SECRETS, \"w\")\n@@ -622,51 +625,50 @@ conn prevent_unencrypted_vxlan\n                                        \"--config\", self.IPSEC_CONF,\n                                        \"--rereadsecrets\"],\n                     \"re-read secrets\")\n-        tunnels = set(monitor.tunnels.keys())\n-\n-        # Delete old connections\n-        conns_dict = self.get_active_conns()\n-        for ifname, conns in conns_dict.items():\n-            tunnel = monitor.tunnels.get(ifname)\n-\n-            for conn in conns:\n-                # IPsec \"connection\" names must start with Interface name\n-                if not conn.startswith(ifname):\n-                    vlog.err(\"%s does not start with %s\" % (conn, ifname))\n-                    continue\n \n-                # version number should be the first integer after\n-                # interface name in IPsec \"connection\"\n-                try:\n-                    ver = int(re.findall(r'\\d+', conn[len(ifname):])[0])\n-                except ValueError:\n-                    vlog.err(\"%s does not contain version number\")\n-                    continue\n-                except IndexError:\n-                    vlog.err(\"%s does not contain version number\")\n-                    continue\n-\n-                if not tunnel or tunnel.version != ver:\n-                    vlog.info(\"%s is outdated %u\" % (conn, ver))\n-                    run_command(self.IPSEC_AUTO +\n-                                [\"--ctlsocket\", self.IPSEC_CTL,\n-                                 \"--config\", self.IPSEC_CONF,\n-                                 \"--delete\", conn], \"delete %s\" % conn)\n-                elif ifname in tunnels:\n-                    tunnels.remove(ifname)\n-\n-        # Activate new connections\n-        for name in tunnels:\n-            ver = monitor.tunnels[name].version\n-\n-            if monitor.tunnels[name].conf[\"tunnel_type\"] == \"gre\":\n-                conn = \"%s-%s\" % (name, ver)\n-                self._start_ipsec_connection(conn)\n+        loaded_conns = self.get_loaded_conns()\n+        active_conns = self.get_active_conns()\n+\n+        all_names = set(monitor.tunnels.keys()) | \\\n+                    set(loaded_conns.keys()) | \\\n+                    set(active_conns.keys())\n+\n+        for name in all_names:\n+            desired = set(self.get_conn_names(monitor, name))\n+            loaded = set(loaded_conns.get(name, dict()).keys())\n+            active = set(active_conns.get(name, dict()).keys())\n+\n+            # Remove all the loaded or active but not desired connections.\n+            for conn in loaded | active:\n+                if conn not in desired:\n+                    self._delete_ipsec_connection(conn, \"is outdated\")\n+                    loaded.discard(conn)\n+                    active.discard(conn)\n+\n+            # If not all desired are loaded, remove all the loaded and\n+            # active for this tunnel and re-load only the desired ones.\n+            # Need to do that, because connections for the same tunnel\n+            # may share SAs.  If one is loaded and the other is not,\n+            # it means the second one failed, so the shared SA may be in\n+            # a broken state.\n+            if desired != loaded:\n+                for conn in loaded | active:\n+                    self._delete_ipsec_connection(conn, \"is half-loaded\")\n+                    loaded.discard(conn)\n+                    active.discard(conn)\n+\n+                for conn in desired:\n+                    vlog.info(\"Starting ipsec connection %s\" % conn)\n+                    self._start_ipsec_connection(conn, \"start\")\n             else:\n-                conn_in = \"%s-in-%s\" % (name, ver)\n-                conn_out = \"%s-out-%s\" % (name, ver)\n-                self._start_ipsec_connection(conn_in)\n-                self._start_ipsec_connection(conn_out)\n+                # Ask pluto to bring UP connections that are loaded,\n+                # but not active for some reason.\n+                #\n+                # desired == loaded and desired >= loaded + active,\n+                # so loaded >= active\n+                for conn in loaded - active:\n+                    vlog.info(\"Bringing up ipsec connection %s\" % conn)\n+                    self._start_ipsec_connection(conn, \"up\")\n \n         # Update shunt policy if changed\n         if monitor.conf_in_use[\"skb_mark\"] != monitor.conf[\"skb_mark\"]:\n@@ -713,23 +715,26 @@ conn prevent_unencrypted_vxlan\n                             \"--delete\",\n                             \"--asynchronous\", \"prevent_unencrypted_vxlan\"])\n             monitor.conf_in_use[\"skb_mark\"] = monitor.conf[\"skb_mark\"]\n+        vlog.info(\"Refreshing is done.\")\n \n-    def get_active_conns(self):\n+    def get_conns_from_status(self, pattern):\n         \"\"\"This function parses output from 'ipsec status' command.\n         It returns dictionary where <key> is interface name (as in OVSDB)\n         and <value> is another dictionary.  This another dictionary\n         uses LibreSwan connection name as <key> and more detailed\n-        sample line from the parsed outpus as <value>. \"\"\"\n+        sample line from the parsed outpus as <value>. 'pattern' should\n+        be a regular expression that parses out the connection name.\n+        Only the lines that match the pattern will be parsed. \"\"\"\n \n         conns = {}\n         ret, pout, perr = run_command([self.IPSEC, 'status',\n                                       '--ctlsocket', self.IPSEC_CTL],\n-                                      \"get active connections\")\n+                                      \"get ipsec status\")\n         if ret:\n             return conns\n \n         for line in pout.decode().splitlines():\n-            m = re.search(r\"#\\d+: \\\"(.*)\\\".*\", line)\n+            m = re.search(pattern, line)\n             if not m:\n                 continue\n \n@@ -748,25 +753,73 @@ conn prevent_unencrypted_vxlan\n \n         return conns\n \n-    def _start_ipsec_connection(self, conn):\n-        # In a corner case, LibreSwan daemon restarts for some reason and\n-        # the \"ipsec auto --start\" command is lost. Just retry to make sure\n-        # the command is received by LibreSwan.\n-        while True:\n-            ret, pout, perr = run_command(self.IPSEC_AUTO +\n-                                          [\"--config\", self.IPSEC_CONF,\n-                                          \"--ctlsocket\", self.IPSEC_CTL,\n-                                          \"--start\",\n-                                          \"--asynchronous\", conn],\n-                                          \"start %s\" % conn)\n-            if not re.match(r\".*Connection refused.*\", perr.decode()) and \\\n-                    not re.match(r\".*need --listen.*\", pout.decode()):\n-                break\n+    def get_active_conns(self):\n+        return self.get_conns_from_status(r\"#\\d+: \\\"(.*)\\\".*\")\n+\n+    def get_loaded_conns(self):\n+        return self.get_conns_from_status(r\"\\\"(.*)\\\":.*(===|---).*\")\n+\n+    def get_conn_names(self, monitor, ifname):\n+        conns = []\n+        if ifname not in monitor.tunnels:\n+            return conns\n+\n+        tunnel = monitor.tunnels.get(ifname)\n+        ver = tunnel.version\n+\n+        if tunnel.conf[\"tunnel_type\"] == \"gre\":\n+            conns.append(\"%s-%s\" % (ifname, ver))\n+        else:\n+            conns.append(\"%s-in-%s\" % (ifname, ver))\n+            conns.append(\"%s-out-%s\" % (ifname, ver))\n+\n+        return conns\n+\n+    def need_to_reconcile(self, monitor):\n+        conns_dict = self.get_active_conns()\n+        for ifname, tunnel in monitor.tunnels.items():\n+            if ifname not in conns_dict:\n+                vlog.info(\"Connection for port %s is not active, \"\n+                          \"need to reconcile\" % ifname)\n+                return True\n+\n+            existing_conns = conns_dict.get(ifname)\n+            desired_conns = self.get_conn_names(monitor, ifname)\n+\n+            if set(existing_conns.keys()) != set(desired_conns):\n+                vlog.info(\"Active connections for port %s %s do not match \"\n+                          \"desired %s, need to reconcile\"\n+                          % (ifname, list(existing_conns.keys()),\n+                             desired_conns))\n+                return True\n+\n+        return False\n+\n+    def _delete_ipsec_connection(self, conn, reason):\n+        vlog.info(\"%s %s, removing\" % (conn, reason))\n+        run_command(self.IPSEC_AUTO +\n+                    [\"--ctlsocket\", self.IPSEC_CTL,\n+                     \"--config\", self.IPSEC_CONF,\n+                     \"--delete\", conn], \"delete %s\" % conn)\n+\n+    def _start_ipsec_connection(self, conn, action):\n+        ret, pout, perr = run_command(self.IPSEC_AUTO +\n+                                      [\"--config\", self.IPSEC_CONF,\n+                                      \"--ctlsocket\", self.IPSEC_CTL,\n+                                      \"--\" + action,\n+                                      \"--asynchronous\", conn],\n+                                      \"%s %s\" % (action, conn))\n \n         if re.match(r\".*[F|f]ailed to initiate connection.*\", pout.decode()):\n             vlog.err('Failed to initiate connection through'\n                     ' Interface %s.\\n' % (conn.split('-')[0]))\n             vlog.err(\"stdout: %s\" % pout)\n+            ret = 1\n+\n+        if ret:\n+            # We don't know in which state the connection was left on\n+            # failure.  Try to clean it up.\n+            self._delete_ipsec_connection(conn, \"--%s failed\" % action)\n \n     def _nss_clear_database(self):\n         \"\"\"Remove all OVS IPsec related state from the NSS database\"\"\"\n@@ -1192,7 +1245,7 @@ class IPsecMonitor(object):\n                 self.ike_helper.clear_tunnel_state(self.tunnels[name])\n             del self.tunnels[name]\n \n-        if needs_refresh:\n+        if needs_refresh or self.ike_helper.need_to_reconcile(self):\n             self.ike_helper.refresh(self)\n \n     def _get_cn_from_cert(self, cert):\n@@ -1365,6 +1418,7 @@ def main():\n         poller = ovs.poller.Poller()\n         unixctl_server.wait(poller)\n         idl.wait(poller)\n+        poller.timer_wait(15000)\n         poller.block()\n \n     unixctl_server.close()\n",
    "prefixes": [
        "ovs-dev",
        "2/9"
    ]
}