69 lines
2.8 KiB
Python
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}
|