feat: report listening sockets
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 37s
CI / test (3.10) (pull_request) Successful in 42s
CI / test (3.11) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 38s
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 37s
CI / test (3.10) (pull_request) Successful in 42s
CI / test (3.11) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 38s
get_listening_sockets() in the shape of napalm-device-types' ListeningSocketsMixin, whose ss/cgroup reading is Linux's, and with its rule: read as root first, and without root when that brings nothing back, which the reading reports as `attributed: False`. - FreeBSD: `sockstat -46lq -P tcp,udp`, which sees every socket either way. - OpenBSD: `fstat -n` as root (the sockets nothing is connected to; port 0 is unbound), `netstat -an` without root, as fstat shows a user only their own processes; those sockets come without a process. - Addresses as ss prints them: `*` becomes 0.0.0.0 or :: by family, IPv6 without brackets, a zone (`fe80::1%lo0`) becomes the interface. One entry per socket, the first process holding it. No get_kernel_facts on purpose: KernelFactsMixin reports a Linux kernel's modules and CONFIG_ options, which a BSD kernel does not have. Fixtures recorded on the FreeBSD 15.1 and OpenBSD 7.9 VMs with test listeners on 127.0.0.1 and ::1 (NetOrk/netork#798).
This commit is contained in:
@@ -257,3 +257,93 @@ def boottime(text: str) -> Optional[int]:
|
||||
"""``sysctl -n kern.boottime``: FreeBSD ``{ sec = N, … }``, OpenBSD ``N``."""
|
||||
match = re.search(r"sec = (\d+)", text) or re.fullmatch(r"\s*(\d+)\s*", text)
|
||||
return int(match[1]) if match else None
|
||||
|
||||
|
||||
def _socket(
|
||||
proto: str, local: str, family: str, process: Optional[str], pid: Optional[int]
|
||||
) -> Optional[dict[str, Any]]:
|
||||
"""A ListeningSocketDict from a local endpoint: ``*:22``, ``[::1]:25``,
|
||||
``127.0.0.1:25``, ``[fe80::1%lo0]:25`` (sockstat, fstat) or ``*.22``,
|
||||
``::1.25``, ``fe80::1%lo0.25`` (netstat)."""
|
||||
if local.startswith("["):
|
||||
address, _, port = local[1:].partition("]:")
|
||||
elif local.count(":") == 1 or local.startswith("*:"):
|
||||
address, _, port = local.rpartition(":")
|
||||
else:
|
||||
address, _, port = local.rpartition(".")
|
||||
if not port.isdigit() or int(port) == 0:
|
||||
return None
|
||||
address, _, zone = address.partition("%")
|
||||
if address == "*":
|
||||
address = "0.0.0.0" if family == "4" else "::"
|
||||
return {
|
||||
"proto": proto,
|
||||
"address": address,
|
||||
"port": int(port),
|
||||
"interface": zone or None,
|
||||
"process": process,
|
||||
"pid": pid,
|
||||
"unit": None,
|
||||
"container_id": None,
|
||||
}
|
||||
|
||||
|
||||
def _unique(sockets: list[Optional[dict[str, Any]]]) -> list[dict[str, Any]]:
|
||||
"""One entry per socket, the first process holding it, as ss reports it."""
|
||||
seen: set[tuple] = set()
|
||||
result = []
|
||||
for s in sockets:
|
||||
if s is None:
|
||||
continue
|
||||
key = (s["proto"], s["address"], s["port"], s["interface"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append(s)
|
||||
return result
|
||||
|
||||
|
||||
def sockstat(text: str) -> list[dict[str, Any]]:
|
||||
"""FreeBSD ``sockstat -46lq -P tcp,udp``: USER COMMAND PID FD PROTO LOCAL FOREIGN."""
|
||||
sockets = []
|
||||
for line in text.splitlines():
|
||||
words = line.split()
|
||||
if len(words) < 7 or not words[2].isdigit() or words[4][:3] not in ("tcp", "udp"):
|
||||
continue
|
||||
proto, family = words[4][:3], words[4][3:]
|
||||
sockets.append(_socket(proto, words[5], family, words[1], int(words[2])))
|
||||
return _unique(sockets)
|
||||
|
||||
|
||||
def fstat_sockets(text: str) -> list[dict[str, Any]]:
|
||||
"""OpenBSD ``fstat -n``: the internet sockets nothing is connected to.
|
||||
|
||||
``USER CMD PID FD internet[6] stream|dgram tcp|udp [0xPCB] LOCAL [ARROW REMOTE]``;
|
||||
a socket with an arrow is connected, one bound to port 0 is not bound.
|
||||
"""
|
||||
sockets = []
|
||||
for line in text.splitlines():
|
||||
words = line.split()
|
||||
if len(words) < 8 or words[4] not in ("internet", "internet6"):
|
||||
continue
|
||||
if any(arrow in words for arrow in ("<--", "-->", "<->")):
|
||||
continue
|
||||
family = "6" if words[4] == "internet6" else "4"
|
||||
sockets.append(_socket(words[6], words[-1], family, words[1], int(words[2])))
|
||||
return _unique(sockets)
|
||||
|
||||
|
||||
def netstat_listening(text: str) -> list[dict[str, Any]]:
|
||||
"""``netstat -an``: listening TCP and bound UDP sockets, without their process."""
|
||||
sockets = []
|
||||
for line in text.splitlines():
|
||||
words = line.split()
|
||||
if len(words) < 5 or words[0][:3] not in ("tcp", "udp"):
|
||||
continue
|
||||
proto, family = words[0][:3], "6" if words[0].endswith("6") else "4"
|
||||
local, foreign = words[3], words[4]
|
||||
if proto == "tcp" and words[-1] != "LISTEN":
|
||||
continue
|
||||
if proto == "udp" and foreign != "*.*":
|
||||
continue
|
||||
sockets.append(_socket(proto, local, family, None, None))
|
||||
return _unique(sockets)
|
||||
|
||||
Reference in New Issue
Block a user