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(-)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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