def _prompt(finalize, conn, prompt_conn): # we match rule again here in case a rule was added while this conn was waiting in the queue rule = match_rule(*conn) if rule: action, _duration, _start = rule if action == 'deny': finalize('deny', conn) return 'deny' else: return 'allow' else: formatted = tinysnitch.dns.format(*prompt_conn) formatted = formatted.replace('$', '\$').replace('(', '\(').replace( ')', '\)').replace('`', '\`') output = tinysnitch.lib.check_output( f'su {_prompt_user} -c \'DISPLAY=:0 tinysnitch-prompt "{formatted}"\' 2>/tmp/tinysnitch_prompt.log' ) try: duration, subdomains, action, ports = output.split() except ValueError: log(f'output: {output}') log(f'FATAL failed to run tinysnitch-prompt\n' + tinysnitch.lib.check_output( 'cat /tmp/tinysnitch_prompt.log || true')) sys.exit(1) else: action = _process_rule(conn, duration, subdomains, action, ports) if action == 'deny': finalize('deny', conn) return 'deny' else: return 'allow'
def _process_rule(conn, duration, subdomains, action, ports): _src, dst, _src_port, dst_port, proto = conn if ports == "no": dst_port = '-' if duration == 'once': return action else: _duration = duration if '-minute' in duration: minutes = int(duration.split('-')[0]) duration = 60 * minutes elif '-hour' in duration: hours = int(duration.split('-')[0]) duration = 60 * 60 * hours elif duration == 'forever': duration = None if subdomains == 'yes': dst = '*.' + '.'.join(dst.split('.')[-2:]) start = time.monotonic() _add_rule(action, duration, start, dst, dst_port, proto) if duration is None: with open(state.rules_file, 'a') as f: f.write(f'{action} {dst} {dst_port} {proto}\n') log(f'INFO add permanent rule {action} {dst} {dst_port} {proto}') else: log(f'INFO add temporary rule {action} {_duration} {dst} {dst_port} {proto}' ) return action
def _process_prompt_queue(): while True: finalize, conn = state._prompt_queue.get() _src, dst, _src_port, dst_port, _proto = conn action = _prompt(finalize, conn, conn) finalize(action, conn) log('FATAL process-prompt-queue exited prematurely') sys.exit(1)
def _finalize(nfq, id, data, size, action, conn): _src, _dst, _src_port, _dst_port, proto = conn if not tinysnitch.dns.is_inbound_dns( *conn) and proto in tinysnitch.lib.protos: log(f'INFO {action} {tinysnitch.dns.format(*conn)}') if action == 'allow': lib.nfq_set_verdict(nfq, id, ONE, ZERO, NULL) elif action == 'deny': lib.nfq_set_verdict2(nfq, id, ONE, MARK, size, data) else: assert False, f'bad action: {action}'
def _gc(): while True: for k, (action, duration, start) in list(state._rules.items()): dst, dst_port, proto = k if isinstance(duration, int) and time.monotonic() - start > duration: log(f'INFO gc expired rule {action} {dst} {dst_port} {proto}') del state._rules[k] time.sleep(3) log('FATAL rules gc exited prematurely') sys.exit(1)
def update_hosts(packet): if UDP in packet and DNS in packet: addrs = [] for name, addr in _parse_dns(packet): name = name.lower() if addr and name != state._hosts.get(addr): state._hosts[addr] = name addrs.append(addr) state._new_addrs.append(addr) if addrs: log(f'INFO dns {name} {" ".join(addrs)}')
def _persister(): while True: while True: with open(_hosts_file, 'a') as f: try: addr = state._new_addrs.pop() except IndexError: break else: f.write(f'{addr} {state._hosts[addr]}\n') time.sleep(1) log('FATAL dns persister exited prematurely') sys.exit(1)
def _log_sizes(): while True: states = [ tinysnitch.dns.state, tinysnitch.rules.state, tinysnitch.netfilter.state ] sizes = [ f'{state.__module__.split(".")[-1]}.{k}:{len(v)}' for state in states for k, v in state.__dict__.items() if isinstance(v, dict) ] log(f"INFO sizes {' '.join(sizes)}") time.sleep(5) log('FATAL log sizes exited prematurely') sys.exit(1)
def _load_permanent_rules(): try: with open(state.rules_file) as f: lines = reversed(f.read().splitlines() ) # lines at top of file are higher priority except FileNotFoundError: with open(state.rules_file, 'w') as f: lines = [] i = 0 lines = [l.split('#')[-1] for l in lines] lines = [l for l in lines if l.strip()] for i, line in enumerate(lines): rule = _parse_rule(line) if rule: action, dst, dst_port, proto = rule duration = start = None _add_rule(action, duration, start, dst, dst_port, proto) log(f'INFO loaded rule {action} {dst} {dst_port} {proto}') if list(lines): log(f'INFO loaded {i + 1} rules from {state.rules_file}')
# # Software distributed under the License is distributed # on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either # express or implied. See the GPL for the specific language # governing rights and limitations. # # You should have received a copy of the GPL along with this # program. If not, go to http://www.gnu.org/licenses/gpl.html # or write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from tinysnitch.lib import log try: from tinysnitch._netfilter import ffi, lib log('use existing ffi binaries') except ModuleNotFoundError: log('recompile ffi binaries') import tinysnitch.netfilter_build import os orig = os.getcwd() os.chdir(os.path.dirname(os.path.abspath(__file__))) print(os.getcwd(), flush=True) for path in os.listdir('.'): if path.endswith('.c') or path.endswith('.o') or path.endswith('.so'): os.remove(path) tinysnitch.netfilter_build.ffibuilder.compile(verbose=True) os.chdir(orig) from tinysnitch._netfilter import ffi, lib import tinysnitch.dns
def _parse_rule(line): try: action, dst, dst_port, proto = line.split(None, 3) except ValueError: log(f'ERROR invalid rule, should have been "action dst dst_port proto", was {line}' ) traceback.print_exc() return try: if dst_port != '-': dst_port = int(dst_port) except ValueError: log(f'ERROR invalid rule {line}') log(f'ERROR ports should be numbers, was {dst_port}') return if proto not in tinysnitch.lib.protos: log(f'ERROR invalid rule {line}') log(f'ERROR bad proto, should be one of {tinysnitch.lib.protos}, was {proto}' ) return if action not in _actions: log(f'ERROR invalid rule {line}') log(f'ERROR bad action, should be one of {_actions}, was {action}') return return action, dst, dst_port, proto