This commit is contained in:
commit
70bec7b842
2
.drone.yml
Normal file
2
.drone.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
kind: template
|
||||||
|
load: docker.yaml
|
5
Dockerfile
Normal file
5
Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM harbor.k-space.ee/k-space/microservice-base
|
||||||
|
RUN pip install psutil scapy
|
||||||
|
RUN apk add libpcap
|
||||||
|
ADD netstat-sniffer.py /netstat-sniffer.py
|
||||||
|
ENTRYPOINT /netstat-sniffer.py
|
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
version: '3.7'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: harbor.k-space.ee/k-space/netstat-sniffer
|
||||||
|
network_mode: host
|
||||||
|
build:
|
||||||
|
context: .
|
131
netstat-sniffer.py
Executable file
131
netstat-sniffer.py
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import psutil
|
||||||
|
from datetime import datetime
|
||||||
|
from math import inf, ceil
|
||||||
|
from prometheus_client import start_http_server, Counter, Gauge, Histogram
|
||||||
|
from scapy.all import AsyncSniffer, DNS
|
||||||
|
from time import sleep, time
|
||||||
|
|
||||||
|
# For interesting ports we will record entries even if reverse DNS lookup fails
|
||||||
|
INTERESTING_PORTS = 53, 80, 443, 27017, 3306, 5432, 6379, 16379, 26379, 11211
|
||||||
|
EPHEMERAL_PORT_RANGE_START = 32000
|
||||||
|
|
||||||
|
reverse_mapping = {}
|
||||||
|
mapping = {}
|
||||||
|
|
||||||
|
dns_snooped_packets = Counter(
|
||||||
|
"netstat_sniffer_dns_snooped_packets",
|
||||||
|
"Count of snooped packets on port 53")
|
||||||
|
dns_reverse_map_entries = Gauge(
|
||||||
|
"netstat_sniffer_dns_reverse_map_entries",
|
||||||
|
"Number of entries in the DNS reverse lookup table")
|
||||||
|
dns_reverse_lookup_failures_total = Counter(
|
||||||
|
"netstat_sniffer_dns_reverse_lookup_failures_total",
|
||||||
|
"Reverse DNS records missing in the DNS snooper cache")
|
||||||
|
histogram_outbound = Histogram(
|
||||||
|
"netstat_sniffer_outbound_connection_duration_seconds",
|
||||||
|
"Outbound connection duration in seconds",
|
||||||
|
labelnames=("addr", "port"),
|
||||||
|
buckets=(1, 5, 10, 50, 100, 500, 1000, inf))
|
||||||
|
outbound_connection_count = Gauge(
|
||||||
|
"netstat_sniffer_outbound_connection_count",
|
||||||
|
"Total outbound connections",
|
||||||
|
labelnames=("addr", "port", "status"))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_addr(j):
|
||||||
|
if j.startswith("::ffff:"):
|
||||||
|
# Normalize IPv6 mapped addresses
|
||||||
|
j = j[7:]
|
||||||
|
return j
|
||||||
|
|
||||||
|
|
||||||
|
def poller():
|
||||||
|
prev_labels = set()
|
||||||
|
while True:
|
||||||
|
counts = collections.Counter()
|
||||||
|
now = time()
|
||||||
|
sleep(ceil(now) - now)
|
||||||
|
now = datetime.utcnow()
|
||||||
|
for j in psutil.net_connections():
|
||||||
|
if not j.raddr:
|
||||||
|
# Skip listening sockets
|
||||||
|
continue
|
||||||
|
laddr, raddr = normalize_addr(j.laddr[0]), normalize_addr(j.raddr[0])
|
||||||
|
if laddr.startswith("127."):
|
||||||
|
# Skip localhost
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = laddr, j.laddr[1], raddr, j.raddr[1]
|
||||||
|
|
||||||
|
if key not in mapping:
|
||||||
|
mapping[key] = {"first_seen": now}
|
||||||
|
|
||||||
|
mapping[key]["last_seen"] = now
|
||||||
|
mapping[key]["status"] = j.status
|
||||||
|
|
||||||
|
# Observe connection duration when connection disappears
|
||||||
|
for key, value in list(mapping.items()):
|
||||||
|
laddr, lport, raddr, rport = key
|
||||||
|
duration = (value["last_seen"] - value["first_seen"]).total_seconds()
|
||||||
|
if duration > 2:
|
||||||
|
if value["status"] == "SYN_SENT":
|
||||||
|
counts[(raddr, rport, "pending")] += 1
|
||||||
|
elif value["status"] == "ESTABLISHED" and lport >= EPHEMERAL_PORT_RANGE_START and rport < EPHEMERAL_PORT_RANGE_START:
|
||||||
|
counts[(raddr, rport, "established")] += 1
|
||||||
|
|
||||||
|
if value["last_seen"] < now:
|
||||||
|
mapping.pop(key)
|
||||||
|
if rport >= EPHEMERAL_PORT_RANGE_START:
|
||||||
|
NotImplemented
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raddr = reverse_mapping[raddr]
|
||||||
|
except KeyError:
|
||||||
|
dns_reverse_lookup_failures_total.inc()
|
||||||
|
else:
|
||||||
|
histogram_outbound.labels(raddr, rport).observe(duration)
|
||||||
|
|
||||||
|
used_labels = set()
|
||||||
|
for (raddr, rport, status), count in counts.items():
|
||||||
|
try:
|
||||||
|
raddr = reverse_mapping[raddr]
|
||||||
|
except KeyError:
|
||||||
|
dns_reverse_lookup_failures_total.inc()
|
||||||
|
key = raddr, rport, status
|
||||||
|
outbound_connection_count.labels(*key).set(count)
|
||||||
|
used_labels.add(key)
|
||||||
|
prev_labels.discard(key)
|
||||||
|
print("Adding label:", key)
|
||||||
|
|
||||||
|
for key in prev_labels:
|
||||||
|
print("Removing label:", key)
|
||||||
|
outbound_connection_count.remove(*key)
|
||||||
|
|
||||||
|
prev_labels = used_labels
|
||||||
|
|
||||||
|
|
||||||
|
def process_packet(p):
|
||||||
|
dns_snooped_packets.inc()
|
||||||
|
if not p.haslayer(DNS):
|
||||||
|
return
|
||||||
|
if not p.an:
|
||||||
|
return
|
||||||
|
for an in p.an:
|
||||||
|
reverse_mapping[an.rdata] = an.rrname.decode("ascii").lower().rstrip(".")
|
||||||
|
dns_reverse_map_entries.set(len(reverse_mapping))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
start_http_server(39680)
|
||||||
|
|
||||||
|
# Sniff default route (?)
|
||||||
|
sniffer = AsyncSniffer(prn=process_packet, filter="port 53", store=False)
|
||||||
|
sniffer.start()
|
||||||
|
|
||||||
|
# Sniff local DNS cache
|
||||||
|
sniffer2 = AsyncSniffer(iface="lo", prn=process_packet, filter="port 53", store=False)
|
||||||
|
sniffer2.start()
|
||||||
|
poller()
|
Loading…
Reference in New Issue
Block a user