deep-packet-inspection/dpi.py
2026-10-21 11:16:01 -06:00

69 lines
2.8 KiB
Python

"""The censor's four filters. Three are given, and sni_filter is extra credit.
Each filter looks at one record (see corpus.py for the format) and returns
True to block it or False to let it through. It may read only the record and
the Censor below: a real censor sees traffic, not the answer key.
The four strategies read different parts of a record:
DNS the name the client looked up
IP the destination address
SNI the server name in the TLS ClientHello
keyword the payload, for any listed word
Python references:
str methods (lower, endswith, split): https://docs.python.org/3/library/stdtypes.html#string-methods
the `in` operator on strings: https://docs.python.org/3/reference/expressions.html#membership-test-operations
"""
from dataclasses import dataclass, field
@dataclass
class Censor:
"""What the censor has decided to block. Given; experiments.py builds it."""
hosts: list # site names, such as "freeleaf-news.example"
ips: set = field(default_factory=set) # addresses to drop, such as "203.0.113.10"
keywords: list = field(default_factory=list) # words to look for in readable payloads
def name_matches(name, hosts):
"""True when `name` is one of `hosts` or a subdomain of one.
Blocking "freeleaf-news.example" should also block "www.freeleaf-news.example",
but not "notfreeleaf-news.example". DNS names do not care about upper and
lower case, and a name may end with a dot ("freeleaf-news.example.") and
still mean the same site. `name` may be None; nothing matches None.
"""
if name is None:
return False
name = name.lower().rstrip(".")
return any(name == h or name.endswith("." + h) for h in hosts)
def dns_filter(record, censor):
"""Block when the name the client looked up is a blocked site."""
return name_matches(record["dns"], censor.hosts)
def ip_filter(record, censor):
"""Block when the destination address is one the censor drops."""
return record["ip"] in censor.ips
def sni_filter(record, censor):
"""Block when the TLS server name is a blocked site.
Extra credit. `name_matches` above already does the matching, so this is a
few lines once you see it. Leave it unwritten and part B prints one fewer
strategy.
"""
raise NotImplementedError("sni_filter")
def keyword_filter(record, censor):
"""Block when any keyword appears anywhere in the payload, in any mix of upper and lower case.
Encrypted payloads are in the corpus too; think about what this filter can
and cannot see in them.
"""
text = record["payload"].lower()
return any(k.lower() in text for k in censor.keywords)
FILTERS = {"DNS": dns_filter, "IP": ip_filter, "SNI": sni_filter, "keyword": keyword_filter}