7797b7df创建于 2025年9月1日历史提交
From e10c8809e8dcf1b05c9d111a2551c33adaf7edbc Mon Sep 17 00:00:00 2001
From: Brett Holman <brett.holman@canonical.com>
Date: Thu, 28 Aug 2025 05:27:56 -0600
Subject: [PATCH] feat: support nmap in socket protocol (#6339)

Nmap's netcat implementation doesn't support creating a return
socket for datagram mode. Switch the socket mode to stream for
better compatibility.

Fixes GH-6136
---
 cloudinit/socket.py                     | 20 ++++++++------------
 systemd/cloud-config.service            |  2 +-
 systemd/cloud-final.service             |  2 +-
 systemd/cloud-init-local.service.tmpl   |  2 +-
 systemd/cloud-init-network.service.tmpl |  2 +-
 tests/unittests/test_all_stages.py      |  3 +--
 6 files changed, 13 insertions(+), 18 deletions(-)

diff --git a/cloudinit/socket.py b/cloudinit/socket.py
index 98c82886f..0a5485a07 100644
--- a/cloudinit/socket.py
+++ b/cloudinit/socket.py
@@ -5,6 +5,7 @@ import os
 import socket
 import sys
 from contextlib import suppress
+from typing import Dict
 
 from cloudinit import performance
 from cloudinit.settings import DEFAULT_RUN_DIR
@@ -55,16 +56,16 @@ class SocketSync:
         :param names: stage names, used as a unique identifiers
         """
         self.stage = ""
-        self.remote = ""
         self.first_exception = ""
         self.systemd_exit_code = 0
         self.experienced_any_error = False
         self.sockets = {
             name: socket.socket(
-                socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC
+                socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_CLOEXEC
             )
             for name in names
         }
+        self.connections: Dict[str, socket.socket] = {}
         # ensure the directory exists
         os.makedirs(f"{DEFAULT_RUN_DIR}/share", mode=0o700, exist_ok=True)
         # removing stale sockets and bind
@@ -73,6 +74,7 @@ class SocketSync:
             with suppress(FileNotFoundError):
                 os.remove(socket_path)
             sock.bind(socket_path)
+            sock.listen()
 
     def __call__(self, stage: str):
         """Set the stage before entering context.
@@ -116,19 +118,14 @@ class SocketSync:
         #     reply, which is expected to be /path/to/{self.stage}-return.sock
         sock = self.sockets[self.stage]
         with performance.Timed(f"Waiting to start stage {self.stage}"):
-            chunk, self.remote = sock.recvfrom(5)
+            connection, _ = sock.accept()
+            chunk, _ = connection.recvfrom(5)
+            self.connections[self.stage] = connection
 
         if b"start" != chunk:
             # The protocol expects to receive a command "start"
             self.__exit__(None, None, None)
             raise ValueError(f"Received invalid message: [{str(chunk)}]")
-        elif f"{DEFAULT_RUN_DIR}/share/{self.stage}-return.sock" != str(
-            self.remote
-        ):
-            # assert that the return path is in a directory with appropriate
-            # permissions
-            self.__exit__(None, None, None)
-            raise ValueError(f"Unexpected path to unix socket: {self.remote}")
 
         sd_notify(f"STATUS=Running ({self.stage} stage)")
         return self
@@ -156,8 +153,7 @@ class SocketSync:
         self.experienced_any_error = self.experienced_any_error or bool(
             self.systemd_exit_code
         )
-        sock = self.sockets[self.stage]
-        sock.connect(self.remote)
+        sock = self.connections[self.stage]
 
         # the returned message will be executed in a subshell
         # hardcode this message rather than sending a more informative message
diff --git a/systemd/cloud-config.service b/systemd/cloud-config.service
index 68f80d2b3..3fe62f9d9 100644
--- a/systemd/cloud-config.service
+++ b/systemd/cloud-config.service
@@ -16,7 +16,7 @@ Type=oneshot
 # process has completed this stage. The output from the return socket is piped
 # into a shell so that the process can send a completion message (defaults to
 # "done", otherwise includes an error message) and an exit code to systemd.
-ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/config.sock -s /run/cloud-init/share/config-return.sock | sh'
+ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/config.sock | sh'
 RemainAfterExit=yes
 TimeoutSec=0
 
diff --git a/systemd/cloud-final.service b/systemd/cloud-final.service
index fb74a47c8..e7e892ab9 100644
--- a/systemd/cloud-final.service
+++ b/systemd/cloud-final.service
@@ -19,7 +19,7 @@ Type=oneshot
 # process has completed this stage. The output from the return socket is piped
 # into a shell so that the process can send a completion message (defaults to
 # "done", otherwise includes an error message) and an exit code to systemd.
-ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/final.sock -s /run/cloud-init/share/final-return.sock | sh'
+ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/final.sock | sh'
 RemainAfterExit=yes
 TimeoutSec=0
 TasksMax=infinity
diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl
index 26a6aee1d..b8a2f3311 100644
--- a/systemd/cloud-init-local.service.tmpl
+++ b/systemd/cloud-init-local.service.tmpl
@@ -33,7 +33,7 @@ ExecStartPre=/sbin/restorecon /run/cloud-init
 # process has completed this stage. The output from the return socket is piped
 # into a shell so that the process can send a completion message (defaults to
 # "done", otherwise includes an error message) and an exit code to systemd.
-ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/local.sock -s /run/cloud-init/share/local-return.sock | sh'
+ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/local.sock | sh'
 RemainAfterExit=yes
 TimeoutSec=0
 
diff --git a/systemd/cloud-init-network.service.tmpl b/systemd/cloud-init-network.service.tmpl
index 61425b4a9..9658af1d6 100644
--- a/systemd/cloud-init-network.service.tmpl
+++ b/systemd/cloud-init-network.service.tmpl
@@ -56,7 +56,7 @@ Type=oneshot
 # process has completed this stage. The output from the return socket is piped
 # into a shell so that the process can send a completion message (defaults to
 # "done", otherwise includes an error message) and an exit code to systemd.
-ExecStart=sh -c 'echo "start" | nc -Uu -W1 /run/cloud-init/share/network.sock -s /run/cloud-init/share/network-return.sock | sh'
+ExecStart=sh -c 'echo "start" | nc -U /run/cloud-init/share/network.sock | sh'
 RemainAfterExit=yes
 TimeoutSec=0
 
diff --git a/tests/unittests/test_all_stages.py b/tests/unittests/test_all_stages.py
index 90bde5e1a..1b66e6955 100644
--- a/tests/unittests/test_all_stages.py
+++ b/tests/unittests/test_all_stages.py
@@ -15,9 +15,8 @@ class Sync:
     """
 
     def __init__(self, name: str, path: str):
-        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
         self.sock.connect(f"{path}/share/{name}.sock")
-        self.sock.bind(f"{path}/share/{name}-return.sock")
         self.sock.sendall(b"start")
 
     def receive(self):
-- 
2.43.0