Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/scripts/smc_firewall.py
Views: 687
#!/usr/bin/env python12###############################################################################3#4# CoCalc: Collaborative Calculation5#6# Copyright (C) 2016, Sagemath Inc.7#8# This program is free software: you can redistribute it and/or modify9# it under the terms of the GNU General Public License as published by10# the Free Software Foundation, either version 3 of the License, or11# (at your option) any later version.12#13# This program is distributed in the hope that it will be useful,14# but WITHOUT ANY WARRANTY; without even the implied warranty of15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the16# GNU General Public License for more details.17#18# You should have received a copy of the GNU General Public License19# along with this program. If not, see <http://www.gnu.org/licenses/>.20#21###############################################################################2223import json, os, signal, socket, sys, time24from subprocess import Popen, PIPE252627def log(s, *args):28if args:29try:30s = str(s % args)31except Exception as mesg:32s = str(mesg) + str(s)33sys.stderr.write(s + '\n')34sys.stderr.flush()353637def cmd(s,38ignore_errors=False,39verbose=2,40timeout=None,41stdout=True,42stderr=True,43system=False):44if isinstance(s, list):45s = [str(x) for x in s]46if isinstance(s, list):47c = ' '.join([x if len(x.split()) <= 1 else "'%s'" % x for x in s])48else:49c = s50if verbose >= 1:51if isinstance(s, list):52log(c)53else:54log(s)55t = time.time()5657if system:58if os.system(c):59if verbose >= 1:60log("(%s seconds)", time.time() - t)61if ignore_errors:62return63else:64raise RuntimeError('error executing %s' % c)65return6667mesg = "ERROR"68if timeout:69mesg = "TIMEOUT: running '%s' took more than %s seconds, so killed" % (70s, timeout)7172def handle(*a):73if ignore_errors:74return mesg75else:76raise KeyboardInterrupt(mesg)7778signal.signal(signal.SIGALRM, handle)79signal.alarm(timeout)80try:81out = Popen(82s,83stdin=PIPE,84stdout=PIPE,85stderr=PIPE,86shell=not isinstance(s, list))87x = out.stdout.read() + out.stderr.read()88e = out.wait(89) # this must be *after* the out.stdout.read(), etc. above or will hang when output large!90if e:91if ignore_errors:92return (x + "ERROR").strip()93else:94raise RuntimeError(x)95if verbose >= 2:96log("(%s seconds): %s", time.time() - t, x[:500])97elif verbose >= 1:98log("(%s seconds)", time.time() - t)99return x.strip()100except IOError:101return mesg102finally:103if timeout:104signal.signal(signal.SIGALRM, signal.SIG_IGN) # cancel the alarm105106107class Firewall(object):108def iptables(self, args, **kwds):109return cmd(['iptables', '-v'] + args, **kwds)110111def insert_rule(self, rule, force=False):112if not self.exists(rule):113log("insert_rule: %s", rule)114self.iptables(['-I'] + rule)115elif force:116self.delete_rule(rule)117self.iptables(['-I'] + rule)118119def append_rule(self, rule, force=False):120if not self.exists(rule):121log("append_rule: %s", rule)122self.iptables(['-A'] + rule)123elif force:124self.delete_rule(rule, force=True)125self.iptables(['-A'] + rule)126127def delete_rule(self, rule, force=False):128if self.exists(rule):129log("delete_rule: %s", rule)130try:131self.iptables(['-D'] + rule)132except Exception as mesg:133log("delete_rule error -- %s", mesg)134# checking for exists is not 100% for uid rules module135pass136elif force:137try:138self.iptables(['-D'] + rule)139except:140pass141142def exists(self, rule):143"""144Return true if the given rule exists already.145"""146try:147self.iptables(['-C'] + rule, verbose=0)148#log("rule %s already exists", rule)149return True150except:151#log("rule %s does not exist", rule)152return False153154def clear(self):155"""156Remove all firewall rules, making everything completely open.157"""158self.iptables(['-F']) # clear the normal rules159self.iptables(160['-t', 'mangle',161'-F']) # clear the mangle rules used to shape traffic (using tc)162return {'status': 'success'}163164def show(self, names=False):165"""166Show all firewall rules. (NON-JSON interface!)167"""168if names:169os.system("iptables -v -L")170else:171os.system("iptables -v -n -L")172173def outgoing(self,174whitelist_hosts='',175whitelist_hosts_file='',176whitelist_users='',177blacklist_users='',178bandwidth_Kbps=1000):179"""180Block all outgoing traffic, except what is given181in a specific whitelist and DNS. Also throttle182bandwidth of outgoing SMC *user* traffic.183"""184if whitelist_users or blacklist_users:185self.outgoing_user(whitelist_users, blacklist_users)186187if whitelist_hosts_file:188v = []189for x in open(whitelist_hosts_file).readlines():190i = x.find('#')191if i != -1:192x = x[:i]193x = x.strip()194if x:195v.append(x)196self.outgoing_whitelist_hosts(','.join(v))197self.outgoing_whitelist_hosts(whitelist_hosts)198199# Block absolutely all outgoing traffic *from* lo to not loopback on same200# machine: this is to make it so a project201# can serve a network service listening on eth0 safely without having to worry202# about security at all, and still have it be secure, even from users on203# the same machine. We insert and remove this every time we mess with the firewall204# rules to ensure that it is at the very top.205self.insert_rule(206['OUTPUT', '-o', 'lo', '-d',207socket.gethostname(), '-j', 'REJECT'],208force=True)209210if bandwidth_Kbps:211self.configure_tc(bandwidth_Kbps)212213return {'status': 'success'}214215def configure_tc(self, bandwidth_Kbps):216try:217cmd("tc qdisc del dev eth0 root".split())218except:219pass # will fail if not already configured220try:221cmd("tc qdisc add dev eth0 root handle 1:0 htb default 99".split())222cmd((223"tc class add dev eth0 parent 1:0 classid 1:10 htb rate %sKbit ceil %sKbit prio 2"224% (bandwidth_Kbps, bandwidth_Kbps)).split())225cmd("tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10".226split())227cmd("tc filter add dev eth0 parent 1:0 protocol ip prio 1 handle 1 fw classid 1:10".228split())229except Exception:230pass # this is more serious but I don't have time to debug this231232def outgoing_whitelist_hosts(self, whitelist):233whitelist = [x.strip() for x in whitelist.split(',')]234# determine the ip addresses of our locally configured DNS servers235for x in open("/etc/resolv.conf").readlines():236v = x.split()237if v[0] == 'nameserver':238log("adding nameserver %s to whitelist", v[1])239whitelist.append(v[1])240whitelist = ','.join([x for x in whitelist if x])241log("whitelist: %s", whitelist)242243# Insert whitelist rule at the beginning of OUTPUT chain.244# Anything that matches this will immediately be accepted to go out.245if whitelist:246self.insert_rule(['OUTPUT', '-d', whitelist, '-j', 'ACCEPT'])247248# Loopback traffic: allow all OUTGOING (so the rule below doesn't cause trouble);249# needed, e.g., by Jupyter notebook and probably other services.250self.insert_rule(['OUTPUT', '-o', 'lo', '-j', 'ACCEPT'])251252# Block all new outgoing connections that we didn't allow above.253self.append_rule(254['OUTPUT', '-m', 'state', '--state', 'NEW', '-j', 'REJECT'])255256def outgoing_user(self, add='', remove=''):257def rules(user):258# returns rule for allowing this user and whether rule is already in chain259v = [[260'OUTPUT', '-m', 'owner', '--uid-owner', user, '-j', 'ACCEPT'261]]262if False and user != 'salvus' and user != 'root':263# Make it so this user has their bandwidth throttled so DOS attacks are more difficult, and also spending264# thousands in bandwidth is harder.265# -t mangle mangles packets by adding a mark, which is needed by tc.266# -p all -- match all protocols, including both tcp and udp267# ! -d 10.240.0.0/8 ensures this rule does NOT apply to any destination inside GCE.;268# CRITICAL -- I thought 10.240.0.0/16 was right because that's what it says in the google firewall rules; but with k8s269# it's definitely wrong and this mistake frickin' kills everything!!!270# -m owner --uid-owner [user] makes the rule apply only to this user271# -j MARK --set-mark 0x1 marks packet so the throttling tc filter we created elsewhere gets applied272v.append([273'OUTPUT', '-t', 'mangle', '-p', 'all', '!', '-d',274'10.240.0.0/8', '-m', 'owner', '--uid-owner', user, '-j',275'MARK', '--set-mark', '0x1'276])277return v278279for user in remove.split(','):280if user:281for x in rules(user):282self.delete_rule(x, force=True)283284for user in add.split(','):285if user:286try:287for x in rules(user):288self.insert_rule(x, force=True)289except Exception as mesg:290log("\nWARNING whitelisting user: %s\n",291str(mesg).splitlines()[:-1])292293def incoming(self, whitelist_hosts='', whitelist_ports=''):294"""295Deny all other incoming traffic, except from the296explicitly given whitelist of machines.297"""298# Allow some incoming packets from the whitelist of ports.299for p in whitelist_ports.split(','):300self.insert_rule(301['INPUT', '-p', 'tcp', '--dport', p, '-j', 'ACCEPT'])302303# Allow incoming connections/packets from anything in the whitelist304if not whitelist_hosts.strip():305v = []306for t in ['smc', 'storage', 'admin']:307s = cmd(308"curl -s http://metadata.google.internal/computeMetadata/v1/project/attributes/%s-servers -H 'Metadata-Flavor: Google'"309% t)310v.append(s.replace(' ', ','))311whitelist_hosts = ','.join(v)312313self.insert_rule(['INPUT', '-s', whitelist_hosts, '-j', 'ACCEPT'])314315# Loopback traffic: allow all INCOMING (so the rule below doesn't cause trouble);316# needed, e.g., by Jupyter notebook and probably other services.317self.append_rule(['INPUT', '-i', 'lo', '-j', 'ACCEPT'])318319# Block *new* packets arriving via a new connection from anywhere else. We320# don't want to block all packets -- e.g., if something on this machine321# connects to DNS, it should be allowed to receive the answer back.322self.append_rule(323['INPUT', '-m', 'state', '--state', 'NEW', '-j', 'DROP'])324325return {'status': 'success'}326327328if __name__ == "__main__":329330import socket331hostname = socket.gethostname()332log("hostname=%s", hostname)333if not hostname.startswith('compute') and not hostname.startswith('web'):334log("skipping firewall since this is not a production SMC machine")335sys.exit(0)336337import argparse338parser = argparse.ArgumentParser(339description="CoCalc firewall control script")340subparsers = parser.add_subparsers(help='sub-command help')341342def f(subparser):343function = subparser.prog.split()[-1]344345def g(args):346special = [k for k in args.__dict__.keys() if k not in ['func']]347out = []348errors = False349kwds = dict([(k, getattr(args, k)) for k in special])350try:351result = getattr(Firewall(), function)(**kwds)352except Exception as mesg:353raise #-- for debugging354errors = True355result = {'error': str(mesg)}356print(json.dumps(result))357if errors:358sys.exit(1)359360subparser.set_defaults(func=g)361362parser_outgoing = subparsers.add_parser(363'outgoing',364help=365'create firewall to block all outgoing traffic, except explicit whitelist)'366)367parser_outgoing.add_argument(368'--whitelist_hosts',369help="comma separated list of sites to whitelist (not run if empty)",370default='')371parser_outgoing.add_argument(372'--whitelist_hosts_file',373help=374"filename of file with one line for each host (comments and blank lines are ignored)",375default='')376parser_outgoing.add_argument(377'--whitelist_users',378help="comma separated list of users to whitelist",379default='')380parser_outgoing.add_argument(381'--blacklist_users',382help="comma separated list of users to remove from whitelist",383default='')384parser_outgoing.add_argument(385'--bandwidth_Kbps', help="throttle user bandwidth", default=1000)386f(parser_outgoing)387388parser_incoming = subparsers.add_parser(389'incoming',390help=391'create firewall to block all incoming traffic except ssh, nfs, http[s], except explicit whitelist'392)393parser_incoming.add_argument(394'--whitelist_hosts',395help=396"comma separated list of sites to whitelist (default: use metadata server to get smc vms)",397default='')398parser_incoming.add_argument(399'--whitelist_ports',400help="comma separated list of ports to whitelist",401default='22,80,111,443')402f(parser_incoming)403404f(subparsers.add_parser('clear', help='clear all rules'))405406parser_show = subparsers.add_parser('show', help='show all rules')407parser_show.add_argument(408'--names',409help="show hostnames (potentially expensive DNS lookup)",410default=False,411action="store_const",412const=True)413f(parser_show)414415args = parser.parse_args()416args.func(args)417418419