[socket] Share the server side as TcpListener (#20037)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Bascht74
2026-10-02 13:07:00 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent cbe89c940d
commit b6c79fdc9a
13 changed files with 262 additions and 70 deletions
+3
View File
@@ -7,5 +7,8 @@ uart_tcp:
uart_id: uart_bus
role: server
port: 502
allowed_ips:
- 192.168.1.10
- 192.168.1.0/24
connected:
name: UART TCP Connected
@@ -18,5 +18,13 @@ uart_tcp:
uart_id: uart_bus
role: server
port: 18126
allowed_ips:
- 127.0.0.1
connected:
name: Bridge Connected
- id: denied_bridge
uart_id: uart_bus
role: server
port: 18127
allowed_ips:
- 192.0.2.1
+10
View File
@@ -25,6 +25,7 @@ async def test_uart_tcp_bridge(
unused_tcp_port_factory,
) -> None:
server_port = unused_tcp_port_factory()
denied_port = unused_tcp_port_factory()
controller_fd, device_fd = os.openpty()
os.set_blocking(controller_fd, False)
# uart's validate_port wants a two segment device path; Linux ptys live at
@@ -32,6 +33,7 @@ async def test_uart_tcp_bridge(
pty_link = f"/tmp/uart-tcp-pty-{os.getpid()}"
pathlib.Path(pty_link).symlink_to(os.ttyname(device_fd))
yaml_config = yaml_config.replace("port: 18126", f"port: {server_port}")
yaml_config = yaml_config.replace("port: 18127", f"port: {denied_port}")
yaml_config = yaml_config.replace("PTY_PATH", pty_link)
lines = LineWaiter()
@@ -103,6 +105,14 @@ async def test_uart_tcp_bridge(
await writer.drain()
assert await read_uart(5) == b"down2"
writer.close()
# A peer outside the allow list is rejected and closed.
denied_reader, denied_writer = await asyncio.open_connection(
"127.0.0.1", denied_port
)
await lines.wait_for("Rejected 127.0.0.1")
assert await asyncio.wait_for(denied_reader.read(8), 10) == b""
denied_writer.close()
finally:
loop.remove_reader(controller_fd)
os.close(controller_fd)
@@ -18,9 +18,13 @@ def test_network_order_swaps_to_sockaddr_value() -> None:
def test_add_ipv4_allow_emits_nothing_for_an_empty_list() -> None:
setter = MagicMock()
with patch.object(socket.cg, "add") as add:
with (
patch.object(socket.cg, "add") as add,
patch.object(socket.cg, "add_define") as add_define,
):
socket.add_ipv4_allow(setter, [], "bridge")
add.assert_not_called()
add_define.assert_not_called()
setter.assert_not_called()
@@ -29,6 +33,7 @@ def test_add_ipv4_allow_wires_the_setter_with_cleared_host_bits() -> None:
networks = [IPv4Network("192.168.175.33/24", strict=False)]
with (
patch.object(socket.cg, "add") as add,
patch.object(socket.cg, "add_define") as add_define,
patch.object(socket.cg, "progmem_array") as array,
):
socket.add_ipv4_allow(setter, networks, "bridge")
@@ -37,6 +42,7 @@ def test_add_ipv4_allow_wires_the_setter_with_cleared_host_bits() -> None:
assert str(socket._network_order(IPv4Address("255.255.255.0"))) in rendered
setter.assert_called_once_with(array.return_value, 1)
add.assert_called_once()
add_define.assert_called_once_with("USE_SOCKET_IPV4_ALLOW")
def test_schema_caps_the_list_length() -> None:
@@ -13,6 +13,7 @@ def test_helper_files_filtered_until_required() -> None:
filtered = socket.FILTER_SOURCE_FILES()
assert "ipv4_resolve.cpp" in filtered
assert "tcp_client_link.cpp" in filtered
assert "tcp_listener.cpp" in filtered
mock_core.defines = {Define("USE_SOCKET_IPV4_RESOLVE")}
filtered = socket.FILTER_SOURCE_FILES()
@@ -22,10 +23,12 @@ def test_helper_files_filtered_until_required() -> None:
mock_core.defines = {
Define("USE_SOCKET_IPV4_RESOLVE"),
Define("USE_SOCKET_TCP_CLIENT_LINK"),
Define("USE_SOCKET_TCP_LISTENER"),
}
filtered = socket.FILTER_SOURCE_FILES()
assert "ipv4_resolve.cpp" not in filtered
assert "tcp_client_link.cpp" not in filtered
assert "tcp_listener.cpp" not in filtered
def test_require_tcp_client_link_pulls_in_the_resolver() -> None:
@@ -36,3 +39,13 @@ def test_require_tcp_client_link_pulls_in_the_resolver() -> None:
"USE_SOCKET_IPV4_RESOLVE",
"USE_SOCKET_TCP_CLIENT_LINK",
}
def test_require_tcp_listener_pulls_in_the_link() -> None:
with patch.object(socket.cg, "add_define") as add_define:
socket.require_tcp_listener()
assert {call.args[0] for call in add_define.call_args_list} == {
"USE_SOCKET_IPV4_RESOLVE",
"USE_SOCKET_TCP_CLIENT_LINK",
"USE_SOCKET_TCP_LISTENER",
}