Use `client.id` as connection id where possible
diff --git a/mitmproxy/addons/proxyserver.py b/mitmproxy/addons/proxyserver.py
index 58e44acde3..820a30cc71 100644
--- a/mitmproxy/addons/proxyserver.py
+++ b/mitmproxy/addons/proxyserver.py
@@ -108,7 +108,7 @@ class Proxyserver(ServerManager):
     This addon runs the actual proxy server.
     """
 
-    connections: dict[tuple, ProxyConnectionHandler]
+    connections: dict[tuple | str, ProxyConnectionHandler]
     servers: Servers
 
     is_running: bool
@@ -125,7 +125,7 @@ def __repr__(self):
 
     @contextmanager
     def register_connection(
-        self, connection_id: tuple, handler: ProxyConnectionHandler
+        self, connection_id: tuple | str, handler: ProxyConnectionHandler
     ):
         self.connections[connection_id] = handler
         try:
@@ -278,6 +278,7 @@ def configure(self, updated) -> None:
                 self._update_task = asyncio.create_task(self.servers.update(modes))
 
     async def setup_servers(self) -> bool:
+        """Setup proxy servers. This may take an indefinite amount of time to complete (e.g. on permission prompts)."""
         return await self.servers.update(
             [mode_specs.ProxyMode.parse(m) for m in ctx.options.mode]
         )
@@ -286,11 +287,15 @@ def listen_addrs(self) -> list[Address]:
         return [addr for server in self.servers for addr in server.listen_addrs]
 
     def inject_event(self, event: events.MessageInjected):
-        connection_id = (
-            event.flow.client_conn.transport_protocol,
-            event.flow.client_conn.peername,
-            event.flow.client_conn.sockname,
-        )
+        connection_id: str | tuple
+        if event.flow.client_conn.transport_protocol != "udp":
+            connection_id = event.flow.client_conn.id
+        else:  # pragma: no cover
+            # temporary workaround: for UDP we don't have persistent client IDs yet.
+            connection_id = (
+                event.flow.client_conn.peername,
+                event.flow.client_conn.sockname,
+            )
         if connection_id not in self.connections:
             raise ValueError("Flow is not from a live connection.")
         self.connections[connection_id].server_event(event)
diff --git a/mitmproxy/master.py b/mitmproxy/master.py
index 651acb98b3..4af6cd98da 100644
--- a/mitmproxy/master.py
+++ b/mitmproxy/master.py
@@ -52,7 +52,14 @@ async def run(self) -> None:
             if ec := self.addons.get("errorcheck"):
                 await ec.shutdown_if_errored()
             if ps := self.addons.get("proxyserver"):
-                await ps.setup_servers()
+                # This may block for some proxy modes, so we also monitor should_exit.
+                await asyncio.wait(
+                    [
+                        asyncio.create_task(ps.setup_servers()),
+                        asyncio.create_task(self.should_exit.wait()),
+                    ],
+                    return_when=asyncio.FIRST_COMPLETED,
+                )
             if ec := self.addons.get("errorcheck"):
                 await ec.shutdown_if_errored()
                 ec.finish()
diff --git a/mitmproxy/proxy/mode_servers.py b/mitmproxy/proxy/mode_servers.py
index d991dee5a3..1d505ae777 100644
--- a/mitmproxy/proxy/mode_servers.py
+++ b/mitmproxy/proxy/mode_servers.py
@@ -76,11 +76,12 @@ async def handle_hook(self, hook: commands.StartHook) -> None:
 
 
 class ServerManager(typing.Protocol):
-    connections: dict[tuple, ProxyConnectionHandler]
+    # temporary workaround: for UDP, we use the 4-tuple because we don't have a uuid.
+    connections: dict[tuple | str, ProxyConnectionHandler]
 
     @contextmanager
     def register_connection(
-        self, connection_id: tuple, handler: ProxyConnectionHandler
+        self, connection_id: tuple | str, handler: ProxyConnectionHandler
     ):
         ...  # pragma: no cover
 
@@ -200,14 +201,11 @@ async def handle_tcp_connection(
                 handler.layer.context.client.sockname = original_dst
                 handler.layer.context.server.address = original_dst
         elif isinstance(self.mode, (mode_specs.WireGuardMode, mode_specs.OsProxyMode)):
-            handler.layer.context.server.address = handler.layer.context.client.sockname
+            handler.layer.context.server.address = writer.get_extra_info(
+                "destination_address", handler.layer.context.client.sockname
+            )
 
-        connection_id = (
-            handler.layer.context.client.transport_protocol,
-            handler.layer.context.client.peername,
-            handler.layer.context.client.sockname,
-        )
-        with self.manager.register_connection(connection_id, handler):
+        with self.manager.register_connection(handler.layer.context.client.id, handler):
             await handler.handle_client()
 
     def handle_udp_datagram(
@@ -217,7 +215,8 @@ def handle_udp_datagram(
         remote_addr: Address,
         local_addr: Address,
     ) -> None:
-        connection_id = ("udp", remote_addr, local_addr)
+        # temporary workaround: we don't have a client uuid here.
+        connection_id = (remote_addr, local_addr)
         if connection_id not in self.manager.connections:
             reader = udp.DatagramReader()
             writer = udp.DatagramWriter(transport, remote_addr, reader)
diff --git a/test/mitmproxy/addons/test_proxyserver.py b/test/mitmproxy/addons/test_proxyserver.py
index c197d7ce08..97a1412db7 100644
--- a/test/mitmproxy/addons/test_proxyserver.py
+++ b/test/mitmproxy/addons/test_proxyserver.py
@@ -301,9 +301,7 @@ async def test_dns(caplog_async) -> None:
         resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE))
         assert req.id == resp.id and "8.8.8.8" in str(resp)
         assert len(ps.connections) == 1
-        dns_layer = ps.connections[
-            ("udp", w.get_extra_info("sockname"), dns_addr)
-        ].layer
+        dns_layer = ps.connections[(w.get_extra_info("sockname"), dns_addr)].layer
         assert isinstance(dns_layer, layers.DNSLayer)
         assert len(dns_layer.flows) == 2
 

This fixes compatibilit with the new macOS transparent mode, where the client's peername is unspecified (which otherwise leads to duplicates).