208 lines
6.0 KiB
Python
208 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from .db import RespoDB
|
|
from .ips import blocks, is_bogon
|
|
from .structs import Options, Entry
|
|
from .util import right_now, read_ips, getaddrs, detect_gfw, make_pooler
|
|
from asyncio import run, sleep
|
|
from sys import argv, stdin, stderr, exit
|
|
import respodns.checks as chk
|
|
|
|
def process_result(res, ip, check, opts: Options):
|
|
# TODO: get more accurate times by inserting start-end into getaddrs.
|
|
now = right_now()
|
|
assert len(res) > 0
|
|
reason = None
|
|
|
|
if "Timeout" in res:
|
|
reason = "timeout"
|
|
|
|
elif check.kind.startswith("bad"):
|
|
reason = "okay" if "NXDOMAIN" in res else "redirect"
|
|
|
|
elif any(is_bogon(r) for r in res):
|
|
reason = "block"
|
|
elif any(blocked in res for blocked in blocks):
|
|
reason = "block"
|
|
|
|
elif not any(len(r) > 0 and r[0].isdigit() for r in res):
|
|
# TODO: check for no alias on common.
|
|
reason = "missing"
|
|
|
|
else:
|
|
for r in res:
|
|
if len(r) == 0 or not r[0].isdigit():
|
|
continue
|
|
if detect_gfw(r, ip, check):
|
|
reason = "gfw"
|
|
break
|
|
else:
|
|
reason = "okay"
|
|
|
|
assert reason is not None, (res, ip, check)
|
|
|
|
addrs = list(filter(lambda r: len(r) > 0 and r[0].isdigit(), res))
|
|
exception = res[0] if len(addrs) == 0 else None
|
|
|
|
return Entry(
|
|
date=now,
|
|
success=reason == "okay",
|
|
server=ip,
|
|
kind=check.kind,
|
|
domain=check.domain,
|
|
exception=exception,
|
|
addrs=addrs,
|
|
reason=reason,
|
|
execution=opts.execution,
|
|
)
|
|
|
|
async def try_ip(db, server_ip, checks, opts: Options):
|
|
entries = []
|
|
|
|
success = True
|
|
def finisher(done, pending):
|
|
nonlocal success
|
|
for task in done:
|
|
res, ip, check = task.result()
|
|
entry = process_result(res, ip, check, opts)
|
|
entries.append(entry)
|
|
if not entry.success:
|
|
if opts.early_stopping and success: # only cancel once
|
|
for pend in pending:
|
|
#print("CANCEL", file=stderr)
|
|
# FIXME: this can still, somehow, cancel the main function.
|
|
pend.cancel()
|
|
success = False
|
|
|
|
pooler = make_pooler(opts.domain_simul, finisher)
|
|
|
|
async def getaddrs_wrapper(ip, check):
|
|
# NOTE: could put right_now() stuff here!
|
|
# TODO: add duration field given in milliseconds (integer)
|
|
# by subtracting start and end datetimes.
|
|
res = await getaddrs(ip, check.domain, opts)
|
|
return res, ip, check
|
|
|
|
for i, check in enumerate(checks):
|
|
first = i == 0
|
|
if not first:
|
|
await sleep(opts.domain_wait)
|
|
await pooler(getaddrs_wrapper(server_ip, check))
|
|
if first:
|
|
# limit to one connection for the first check.
|
|
await pooler()
|
|
if not success:
|
|
if opts.early_stopping or first:
|
|
break
|
|
else:
|
|
await pooler()
|
|
|
|
if not opts.dry:
|
|
for entry in entries:
|
|
db.push_entry(entry)
|
|
db.commit()
|
|
|
|
if not success:
|
|
first_failure = None
|
|
assert len(entries) > 0
|
|
for entry in entries:
|
|
#print(entry, file=stderr)
|
|
if not entry.success:
|
|
first_failure = entry
|
|
break
|
|
else:
|
|
assert 0, ("no failures found:", entries)
|
|
return server_ip, first_failure
|
|
return server_ip, None
|
|
|
|
async def main(db, filepath, checks, opts: Options):
|
|
def finisher(done, pending):
|
|
for task in done:
|
|
ip, first_failure = task.result()
|
|
if first_failure is None:
|
|
print(ip)
|
|
elif opts.dry:
|
|
ff = first_failure
|
|
if ff.kind in ("shock", "adware"):
|
|
print(ip, ff.reason, ff.kind, sep="\t")
|
|
else:
|
|
print(ip, ff.reason, ff.kind, ff.domain, sep="\t")
|
|
|
|
pooler = make_pooler(opts.ip_simul, finisher)
|
|
|
|
f = stdin if filepath == "" else open(filepath, "r")
|
|
for i, ip in enumerate(read_ips(f)):
|
|
first = i == 0
|
|
if opts.progress:
|
|
print(f"#{i}: {ip}", file=stderr)
|
|
stderr.flush()
|
|
if not first:
|
|
await sleep(opts.ip_wait)
|
|
await pooler(try_ip(db, ip, checks, opts))
|
|
if f != stdin:
|
|
f.close()
|
|
|
|
await pooler()
|
|
|
|
def ui(program, args):
|
|
from argparse import ArgumentParser
|
|
|
|
name = "respodns6"
|
|
parser = ArgumentParser(name,
|
|
description=name + ": test and log DNS records")
|
|
|
|
# TODO: support multiple paths. nargs="+", iterate with pooling?
|
|
parser.add_argument(
|
|
"path", metavar="file-path",
|
|
help="a path to a file containing IPv4 addresses which host DNS servers")
|
|
|
|
parser.add_argument(
|
|
"--database",
|
|
help="specify database for logging")
|
|
|
|
a = parser.parse_args(args)
|
|
|
|
checks = []
|
|
checks += chk.first
|
|
#checks += chk.new
|
|
checks += chk.likely
|
|
#checks += chk.unlikely
|
|
#checks += chk.top100
|
|
|
|
opts = Options()
|
|
opts.dry = a.database is None
|
|
opts.early_stopping = opts.dry
|
|
|
|
if a.database is not None:
|
|
if a.database.startswith("sqlite:"):
|
|
uri = a.database
|
|
else:
|
|
uri = "sqlite:///" + a.database
|
|
|
|
def runwrap(db, debug=False):
|
|
if debug:
|
|
import logging
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
run(main(db, a.path, checks, opts), debug=True)
|
|
else:
|
|
run(main(db, a.path, checks, opts))
|
|
|
|
if opts.dry:
|
|
runwrap(None)
|
|
else:
|
|
# log to a database.
|
|
db = RespoDB(uri, create=True)
|
|
with db: # TODO: .open and .close methods for manual invocation.
|
|
with db.execution as execution: # TODO: clean up this interface.
|
|
opts.execution = execution
|
|
runwrap(db)
|
|
|
|
if __name__ == "__main__":
|
|
if len(argv) == 0:
|
|
print("You've met with a terrible fate.", file=stderr)
|
|
ret = 2
|
|
else:
|
|
ret = ui(argv[0], argv[1:])
|
|
if ret is not None:
|
|
exit(ret)
|