Source code for daklib.rpc_peer
# SPDX-License-Identifier: GPL-2.0-or-later
# © 2026, Ansgar 🙀 <ansgar@debian.org>
"""
Peer address handling for the DAK RPC server.
"""
import contextvars
import ipaddress
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, override
import grpc
logger = logging.getLogger(__name__)
current_peer: contextvars.ContextVar["PeerAddress | None"] = contextvars.ContextVar(
"current_peer", default=None
)
[docs]
@dataclass(frozen=True)
class PeerAddress:
ip: ipaddress.IPv4Address | ipaddress.IPv6Address
port: int = 0
@override
def __str__(self) -> str:
if isinstance(self.ip, ipaddress.IPv6Address):
return f"[{self.ip}]:{self.port}"
return f"{self.ip}:{self.port}"
[docs]
@classmethod
def from_grpc_peer(cls, peer: str) -> "PeerAddress | None":
"""Parse a gRPC peer string like `ipv4:1.2.3.4:5678` or
`ipv6:[::1]:5678`.
Returns None for peers without an IP address (unix sockets).
"""
if peer.startswith("ipv4:"):
host, sep, port_str = peer.removeprefix("ipv4:").rpartition(":")
elif peer.startswith("ipv6:"):
host, sep, port_str = peer.removeprefix("ipv6:").rpartition(":")
if not (host.startswith("[") and host.endswith("]")):
return None
host = host[1:-1]
else:
return None
if not sep:
return None
try:
port = int(port_str)
if not 0 <= port <= 65535:
return None
return cls(ip=ipaddress.ip_address(host), port=port)
except ValueError:
return None
[docs]
class PeerAddressInterceptor(grpc.ServerInterceptor):
"""gRPC interceptor that records the client's peer address.
The address is taken from `peer_header` if configured, falling back to
the connection's transport address. The header must only be configured
when all connections arrive via a trusted reverse proxy that sets it
(e.g. via a unix socket only the proxy can reach); otherwise clients
can spoof their address.
"""
def __init__(self, peer_header: str | None = None) -> None:
# gRPC metadata keys are always lowercase
self._peer_header = peer_header.lower() if peer_header else None
[docs]
@override
def intercept_service(
self,
continuation: "Callable[[grpc.HandlerCallDetails], grpc.RpcMethodHandler[Any, Any] | None]",
handler_call_details: grpc.HandlerCallDetails,
) -> "grpc.RpcMethodHandler[Any, Any] | None":
peer: PeerAddress | None = None
if self._peer_header is not None:
metadata = dict(handler_call_details.invocation_metadata)
value = metadata.get(self._peer_header)
if isinstance(value, str):
peer = PeerAddress.from_forwarded_header(value)
if peer is None:
logger.warning(
"invalid peer address in %s header: %r",
self._peer_header,
value,
)
current_peer.set(peer)
handler = continuation(handler_call_details)
if peer is not None or handler is None or handler.unary_unary is None:
return handler
# No trusted header: resolve the transport address, which is only
# reachable via the ServicerContext inside the handler.
original_fn = handler.unary_unary
def peer_wrapper(request: Any, context: grpc.ServicerContext) -> Any:
current_peer.set(PeerAddress.from_grpc_peer(context.peer()))
return original_fn(request, context)
return grpc.unary_unary_rpc_method_handler(
peer_wrapper,
request_deserializer=handler.request_deserializer,
response_serializer=handler.response_serializer,
)