├── .gitignore ├── README.md ├── RULES.md ├── config.py ├── contrack.py ├── egress.py ├── examples ├── addr-port.json ├── conntrack.json ├── dst_port_block.json ├── example.json ├── portknock.json └── src_port_block.json ├── main.py ├── packets.py ├── py_log.py ├── pywall.py ├── requirements.txt ├── rules ├── __init__.py ├── ip_rules.py ├── port_filter.py ├── port_ip_rule.py ├── port_knocking.py ├── print_rule.py ├── tcp_rules.py └── true_rule.py ├── run-acceptance-tests.py ├── run-integration-tests.py ├── test ├── __init__.py ├── acceptance │ ├── __init__.py │ ├── local │ │ ├── __init__.py │ │ ├── block_unreg_dst_ports.json │ │ ├── block_unreg_ports.json │ │ ├── block_unreg_src_ports.json │ │ ├── conf.py │ │ ├── example_config.json │ │ ├── listeners.py │ │ ├── port_filter_test.py │ │ ├── pywall_acceptance_test_case.py │ │ ├── tcp_listener.py │ │ └── test_connection.py │ └── remote │ │ ├── __init__.py │ │ ├── example_test_remote.py │ │ └── port_filter_remote.py ├── integration │ ├── __init__.py │ ├── block_local_dst_web.json │ ├── block_local_src_web.json │ ├── block_local_web.json │ ├── block_unreg_dst_ports.json │ ├── block_unreg_ports.json │ ├── block_unreg_src_ports.json │ ├── destination_ip_rule_in_range_accept.json │ ├── destination_ip_rule_in_range_drop.json │ ├── destination_ip_rule_out_of_range.json │ ├── destination_ip_rule_test.py │ ├── port_filter_test.py │ ├── port_knock_test.py │ ├── port_knocking_test.json │ ├── pywall_test_case.py │ ├── source_ip_rule_in_range_accept.json │ ├── source_ip_rule_in_range_drop.json │ ├── source_ip_rule_out_of_range.json │ ├── source_ip_rule_test.py │ ├── tcp_connection.json │ ├── tcp_connection_test.py │ ├── tcp_server_process.py │ ├── udp_connection_test.py │ └── udp_server_process.py └── unit │ ├── __init__.py │ ├── fake_packet.py │ ├── test_ip_rules.py │ ├── test_port_filter.py │ ├── test_port_ip_rule.py │ └── test_tcp_rules.py └── unit_test /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | *.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyWall 2 | ====== 3 | 4 | A Python firewall: Because slow networks are secure networks. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | This section assumes that you are installing this program on Ubuntu 14.04 LTS. 11 | This firewall should work on other Linux systems, but safety not guaranteed. 12 | 13 | First, install the required packages. On Ubuntu, these are `iptables`, `python`, 14 | `python-pip`, `build-essential`, `python-dev`, and 15 | `libnetfilter-queue-dev`. Next, use `pip2` to install the project dependencies, 16 | which can be found in `requirements.txt`. 17 | 18 | The commands for both these operations are: 19 | 20 | sudo apt-get install python python-pip iptables build-essential python-dev libnetfilter-queue-dev 21 | pip install --user -r requirements.txt 22 | 23 | 24 | Running 25 | ------- 26 | 27 | The main file is `main.py`, which needs to be run as root to modify IPTables. 28 | Additionally, main needs to receive a JSON configuration file as its first 29 | argument. If running with the example configuration, the command is: 30 | 31 | `sudo python2 main.py examples/example.json` 32 | 33 | To stop PyWall, press Control-C. 34 | 35 | 36 | Troubleshooting 37 | --------------- 38 | 39 | PyWall should undo its changes to IPTables after exiting. However, if you are 40 | unable to access the internet after exiting PyWall, view existing 41 | IPTables rules with `sudo iptables -nL`. If a rule with the target chain 42 | `NFQueue` lingers, delete it with 43 | `sudo iptables -D INPUT -j NFQUEUE --queue-num [undesired-queue-number]`. 44 | 45 | For INPUT rules, the command is `sudo iptables -D INPUT -j NFQUEUE --queue-num 1`. 46 | For OUTPUT rules, the command is `sudo iptables -D OUTPUT -j NFQUEUE --queue-num 2`. 47 | 48 | In case PyWall gives a message that another application has the xtables lock, 49 | Control-C the server, ensure that all the IPTables rules are cleared, and 50 | restart PyWall. 51 | -------------------------------------------------------------------------------- /RULES.md: -------------------------------------------------------------------------------- 1 | # Rule Documentation 2 | 3 | The following serves to document the possible arguments that each of our rules 4 | takes, to ease users in generating config files. 5 | 6 | ## `IPRangeRule` 7 | 8 | - `action`: Chain to send the packet to if it matches. 9 | - `cidr_range`: The IP address range to match on, specified in CIDR (Classless Inter-Domain Routing) format. 10 | 11 | ### `SourceIPRule` and `DestinationIPRule` 12 | 13 | Both derive from `IPRangeRule`, so their arguments are the same. 14 | 15 | ## `PortRule` 16 | 17 | - `action`: Chain to send the packet to if it matches. 18 | - `protocol`: Which protocol to match on. 'TCP' or 'UDP'. 19 | - `src_port`: Which source port to match on. 20 | - `dst_port`: Which destination port to match on. 21 | 22 | Only one of `src_port` and `dst_port` is required for the rule to function. 23 | 24 | ## `PortRangeRule` 25 | 26 | - `action`: Chain to send the packet to if it matches. 27 | - `protocol`: Which protocol to match on. 'TCP' or 'UDP'. 28 | - `src_lo`: Lower bound of the source port range. Inclusive. 29 | - `src_hi`: Upper bound of the source port range. Inclusive. 30 | - `dst_lo`: Lower bound of the destination port range. Inclusive. 31 | - `dst_hi`: Upper bound of the destination port range. Inclusive. 32 | 33 | Either a source port range or destination port range must be specified, though 34 | you can use both in conjunction. 35 | 36 | ## `IPPortRule` 37 | 38 | This rule is a composition of the `IPRangeRule` and `PortRangeRule` so most of 39 | its arguments are derived from there. Note: 40 | 41 | - `src_ip`: The CIDR range to match on source addresses. 42 | - `dst_ip`: The CIDR range to match on destination addresses. 43 | 44 | Only one of `src_ip` and `dst_ip` is required. 45 | 46 | ## `TCPRule` 47 | 48 | - `action` 49 | 50 | ## `TCPStateRule` 51 | 52 | - `action` 53 | - `match_if`: A set of TCP states. The rule will match on packets whose TCP state is in this set. 54 | - `match_if_not`: A set of TCP states. The rule will match on packets whose TCP state is not in this set. 55 | 56 | You may only specify one of `match_if` and `match_if_not`. They cannot be used 57 | in conjunction. 58 | 59 | ## `PortKnocking` 60 | 61 | - `protocol`: Which protocol to match on. 'TCP' or 'UDP'. 62 | - `src_port`: Source port that ultimately will be connected to, upon successful knocking. 63 | - `timeout`: How long the rule should maintain state about a knocking attempt. 64 | - `doors`: Doors to knock on. A list of (protocol, port) tuples. 65 | 66 | ## Example Configuration 67 | 68 | ``` 69 | { 70 | "default_chain": "ACCEPT", 71 | "INPUT":[ 72 | {"name":"PrintRule"}, 73 | {"name":"PortKnocking", 74 | "src_port": 9001, 75 | "protocol": "TCP", 76 | "port": 2222, 77 | "timeout": 20, 78 | "doors": [["TCP", 49001], ["UDP", 49011]]}, 79 | {"name":"TCPStateRule", 80 | "match_if": ["CLOSED"], 81 | "action": "DROP"}, 82 | {"name":"SourceIPRule", 83 | "action": "DROP", 84 | "cidr_range": "221.224.0.0/13"} 85 | ] 86 | } 87 | ``` 88 | 89 | You can create more chains than just the `INPUT` chain above -- simply copy and 90 | rename the `INPUT` chain above, and modify its rules. Then, you can use your 91 | new chain as the destination ("action") for other rules, to create even more 92 | complicated configurations. 93 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """PyWall instance creator from config file.""" 2 | 3 | from __future__ import print_function 4 | import json 5 | 6 | import rules 7 | # This import must be here to trigger all rules to be imported and register. 8 | from rules import * 9 | from pywall import PyWall 10 | 11 | 12 | class PyWallConfig(object): 13 | """Creates instances of PyWall from a configuration file.""" 14 | 15 | def __init__(self, filename): 16 | """Constructor - takes filename, but doesn't open.""" 17 | self.filename = filename 18 | 19 | def create_pywall(self, *args): 20 | """Read the configuration file and create an instance of PyWall. 21 | 22 | Any arguments will be passed to the constructor of PyWall. 23 | 24 | """ 25 | cfg = json.load(open(self.filename)) 26 | default = cfg.pop('default_chain', 'ACCEPT') 27 | 28 | the_wall = PyWall(*args, default=default) 29 | 30 | for chain, rule_list in cfg.items(): 31 | the_wall.add_chain(chain) 32 | 33 | for rule in rule_list: 34 | name = rule.pop('name', None) 35 | rule_class = rules.rules[name] 36 | rule_instance = rule_class(**rule) 37 | the_wall.add_brick(chain, rule_instance) 38 | 39 | return the_wall 40 | -------------------------------------------------------------------------------- /contrack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """TCP Connection Tracking.""" 3 | 4 | from __future__ import print_function 5 | 6 | import select 7 | import logging 8 | 9 | 10 | class PyWallCracker(object): 11 | """Central TCP connection tracking process and class. 12 | 13 | Receives TCP packets from ingress and egress, and updates connection status 14 | accordingly. Also, receives connection status queries from the firewall, 15 | and responds to them. 16 | 17 | Connection tuple is defined as: 18 | (remote_ip, remote_port, local_ip, local_port) 19 | 20 | Objects placed into the queues should be: 21 | (connection tuple, syn, ack, fin) 22 | 23 | """ 24 | 25 | def __init__(self, ingress_queue, egress_queue, query_pipe): 26 | """Create an instance of the Cracker, given three IPC's. 27 | 28 | - ingress_queue - Queue of flags from ingress TCP packets. 29 | - egress_queue - Queue of flags from egress TCP packets. 30 | - query_pipe - Pipe for queries and responses from PyWall. 31 | """ 32 | self.ingress_queue = ingress_queue 33 | self.egress_queue = egress_queue 34 | self.query_pipe = query_pipe 35 | self.connections = {} 36 | 37 | def handle_ingress(self, report): 38 | """Handle an ingress packet 'report'. 39 | 40 | Updates the state table for the given packet and flags by following our 41 | TCP state diagram (only including transitions for ingress packets). 42 | 43 | """ 44 | tup, syn, ack, fin = report 45 | curr = self.connections.get(tup, 'CLOSED') 46 | l = logging.getLogger('pywall.contrack') 47 | new = None 48 | if curr == "CLOSED": 49 | if syn: 50 | new = 'SYN_RCVD1' 51 | else: # Otherwise, assume this was started before firewall ran. 52 | new = 'ESTABLISHED' 53 | elif curr == 'SYN_RCVD2': 54 | if ack: 55 | new = 'ESTABLISHED' 56 | elif curr == 'SYN_SENT1': 57 | if syn and ack: 58 | new = 'SYN_SENT2' 59 | elif syn: 60 | new = 'SYN_SENT3' 61 | elif curr == 'ESTABLISHED': 62 | if fin: 63 | new = 'CLOSE_WAIT1' 64 | else: 65 | new = 'ESTABLISHED' 66 | elif curr == 'FIN_WAIT_1': 67 | if fin and ack: 68 | new = 'FIN_WAIT_3' 69 | elif ack: 70 | new = 'FIN_WAIT_2' 71 | elif fin: 72 | new = 'CLOSING' 73 | elif curr == 'FIN_WAIT_2': 74 | if fin: 75 | new = 'FIN_WAIT_3' 76 | elif curr == 'CLOSING': 77 | if ack: 78 | new = 'FIN_WAIT_3' 79 | elif curr == 'CLOSING2': 80 | if ack: 81 | new = 'CLOSED' 82 | elif curr == 'LAST_ACK': 83 | if ack: 84 | new = 'CLOSED' 85 | 86 | if new is None: 87 | # Log undefined transitions and don't change the state of the 88 | # connection. 89 | new = curr 90 | l.error('RCV: %r (%s): syn=%r, ack=%r, fin=%r => %s' 91 | ' (UNDEFINED TRANSITION)' % 92 | (tup, curr, syn, ack, fin, new)) 93 | else: 94 | # Log other transitions at the lowest level, in case we need to 95 | # debug. 96 | l.debug('RCV: %r (%s): syn=%r, ack=%r, fin=%r => %s' % 97 | (tup, curr, syn, ack, fin, new)) 98 | 99 | # Update the connection status. 100 | self.connections[tup] = new 101 | 102 | def handle_egress(self, report): 103 | """Handle an egress packet 'report'. 104 | 105 | Updates the state table for the given packet and flags by following our 106 | TCP state diagram (only including transitions for egress packets). 107 | 108 | """ 109 | tup, syn, ack, fin = report 110 | curr = self.connections.get(tup, 'CLOSED') 111 | l = logging.getLogger('pywall.contrack') 112 | new = None 113 | if curr == 'CLOSED': 114 | if syn: 115 | new = 'SYN_SENT1' 116 | else: # Assume this was running before hand. 117 | new = 'ESTABLISHED' 118 | elif curr == 'SYN_SENT1': 119 | if syn: 120 | new = 'SYN_SENT1' # This means we are retrying a connection. 121 | elif curr == 'SYN_RCVD1': 122 | if syn and ack: 123 | new = 'SYN_RCVD2' 124 | elif curr == 'SYN_RCVD2': 125 | if fin: 126 | new = 'FIN_WAIT_1' 127 | elif curr == 'SYN_SENT3': 128 | if ack: 129 | new = 'SYN_RCVD2' 130 | elif curr == 'SYN_SENT2': 131 | if ack: 132 | new = 'ESTABLISHED' 133 | elif curr == 'ESTABLISHED': 134 | if fin: 135 | new = 'FIN_WAIT_1' 136 | else: 137 | new = 'ESTABLISHED' 138 | elif curr == 'CLOSE_WAIT1': 139 | if fin and ack: 140 | new = 'LAST_ACK' 141 | elif ack: 142 | new = 'CLOSE_WAIT2' 143 | elif curr == 'CLOSE_WAIT2': 144 | if fin: 145 | new = 'LAST_ACK' 146 | elif curr == 'CLOSING': 147 | if ack: 148 | new = 'CLOSING2' 149 | elif curr == 'FIN_WAIT_3': 150 | if ack: 151 | new = 'CLOSED' 152 | 153 | if new is None: 154 | # Log undefined transitions and don't change the state of the 155 | # connection. 156 | new = curr 157 | l.error('SND: %r (%s): syn=%r, ack=%r, fin=%r => %s' 158 | ' (UNDEFINED TRANSITION)' % 159 | (tup, curr, syn, ack, fin, new)) 160 | else: 161 | # Log other transitions at the lowest level, in case we need to 162 | # debug. 163 | l.debug('SND: %r (%s): syn=%r, ack=%r, fin=%r => %s' % 164 | (tup, curr, syn, ack, fin, new)) 165 | 166 | self.connections[tup] = new 167 | 168 | def handle_query(self, con_tuple): 169 | """Take a query, load the state, and return it in the query pipe.""" 170 | self.query_pipe.send(self.connections.get(con_tuple, 'CLOSED')) 171 | 172 | def run(self): 173 | """Run the connection tracking process. 174 | 175 | Selects on the IPC, waiting for input. 176 | """ 177 | while True: 178 | egress_fd = self.egress_queue._reader.fileno() 179 | ingress_fd = self.ingress_queue._reader.fileno() 180 | query_fd = self.query_pipe.fileno() 181 | 182 | # Use select to get a list of file descriptors ready to be read. 183 | ready, _, _ = select.select([egress_fd, ingress_fd, query_fd], [], []) 184 | for ready_fd in ready: 185 | if ready_fd == egress_fd: 186 | egress_packet = self.egress_queue.get_nowait() 187 | self.handle_egress(egress_packet) 188 | elif ready_fd == ingress_fd: 189 | ingess_packet = self.ingress_queue.get_nowait() 190 | self.handle_ingress(ingess_packet) 191 | elif ready_fd == query_fd: 192 | self.handle_query(self.query_pipe.recv()) 193 | -------------------------------------------------------------------------------- /egress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """TCP Packet egress reporter.""" 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import logging 8 | 9 | import netfilterqueue as nfq 10 | 11 | from packets import IPPacket, TCPPacket, to_tuple 12 | 13 | 14 | class PyWallEgress(object): 15 | """Egress monitoring process. 16 | 17 | This class/process doesn't perform any packet filtering. It simply accepts 18 | egress packets, and reports them to the Cracker if they are TCP. 19 | 20 | """ 21 | 22 | def __init__(self, mp_queue, queue_num=2): 23 | """Create the egress process. 24 | 25 | Takes the mp_queue, which is where we report the TCP packets. queue-num 26 | is the iptables queue to bind to. 27 | 28 | """ 29 | self.queue_num = queue_num 30 | self.mp_queue = mp_queue 31 | self._nfq_init = 'iptables -I OUTPUT -j NFQUEUE --queue-num %d' 32 | self._nfq_close = 'iptables -D OUTPUT -j NFQUEUE --queue-num %d' 33 | 34 | def run(self): 35 | """Run the egress process.""" 36 | # Create IPTables commands. 37 | setup = self._nfq_init % self.queue_num 38 | teardown = self._nfq_close % self.queue_num 39 | 40 | # Set up IPTables to receive egress packets. 41 | os.system(setup) 42 | print('Set up IPTables: ' + setup) 43 | 44 | # Create and run NFQ. 45 | nfqueue = nfq.NetfilterQueue() 46 | nfqueue.bind(self.queue_num, self.callback) 47 | try: 48 | nfqueue.run() 49 | finally: 50 | # Tear down IPTables rules. 51 | os.system(teardown) 52 | print('\nTore down IPTables: ' + teardown + '\n') 53 | 54 | def callback(self, packet): 55 | """The callback called by IPTables for each egress packet.""" 56 | # Parse packet 57 | ip_packet = IPPacket(packet.get_payload()) 58 | tcp_packet = ip_packet.get_payload() 59 | logging.getLogger('pywall.egress').debug(unicode(ip_packet)) 60 | 61 | # Accept non-TCP packets. 62 | if type(tcp_packet) is not TCPPacket: 63 | packet.accept() 64 | return 65 | 66 | # Send the packet to the connection tracker. 67 | tup = to_tuple(ip_packet, flip=True) 68 | self.mp_queue.put((tup, bool(tcp_packet.flag_syn), 69 | bool(tcp_packet.flag_ack), 70 | bool(tcp_packet.flag_fin))) 71 | packet.accept() 72 | -------------------------------------------------------------------------------- /examples/addr-port.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT":[ 4 | {"name":"PrintRule"}, 5 | {"name":"IPPortRule", 6 | "protocol": "TCP", 7 | "dst_lo": 2222, 8 | "dst_hi": 2222, 9 | "src_ip": "172.20.33.164", 10 | "action": "DROP"} 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/conntrack.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT":[ 4 | {"name":"PrintRule"}, 5 | {"name":"TCPStateRule", 6 | "match_if": ["CLOSED"], 7 | "action": "DROP"} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/dst_port_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "INPUT":[ 3 | {"name":"PrintRule"}, 4 | {"name":"PortFilter", "protocol":"UDP", "dst_port":53, "action":"DROP"}, 5 | {"name":"PortRangeRule", "protocol":"TCP", "dst_lo":49152, "dst_hi":65535, "action":"DROP"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT":[ 4 | {"name":"TCPStateRule", 5 | "match_if": ["CLOSED"], 6 | "action": "DROP"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/portknock.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT":[ 4 | {"name":"PrintRule"}, 5 | {"name":"PortKnocking", 6 | "src_port": 9001, 7 | "protocol": "TCP", 8 | "port": 2222, 9 | "timeout": 20, 10 | "doors": [["TCP", 49001], ["UDP", 49011]]}, 11 | {"name":"TCPStateRule", 12 | "match_if": ["CLOSED"], 13 | "action": "DROP"} 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/src_port_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "INPUT":[ 3 | {"name":"PrintRule"}, 4 | {"name":"PortRule", "protocol":"UDP", "src_port":53, "action":"DROP"}, 5 | {"name":"PortRangeRule", "protocol":"TCP", "src_lo":49152, "src_hi":65535, "action":"DROP"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """Main function for PyWall.""" 3 | 4 | from __future__ import print_function 5 | import multiprocessing as mp 6 | import logging 7 | import argparse 8 | 9 | import config 10 | import egress 11 | import contrack 12 | from py_log import initialize_logging, log_server 13 | 14 | 15 | def run_pywall(conf, packet_queue, query_pipe, kwargs): 16 | """Utility function to run PyWall. (target function for the Process) 17 | 18 | Run PyWall with a configuration file, as well as the queue for reporting 19 | TCP packets, and the query pipe for querying state. KWargs are passed to 20 | the erect() function of PyWall. 21 | 22 | """ 23 | # Get logging information from the kwargs, so we can setup logging. 24 | logqueue = kwargs.pop('logqueue', mp.Queue()) 25 | loglevel = kwargs.pop('loglevel', logging.INFO) 26 | initialize_logging(loglevel, logqueue) 27 | 28 | cfg = config.PyWallConfig(conf) 29 | the_wall = cfg.create_pywall(packet_queue, query_pipe) 30 | the_wall.erect(**kwargs) 31 | 32 | 33 | def run_egress(packet_queue, loglevel, logqueue): 34 | """Utility function to run the egress function. (target of Process) 35 | 36 | Given the queue to report TCP connections, as well as logging variables, 37 | run the egress monitor. 38 | 39 | """ 40 | initialize_logging(loglevel, logqueue) 41 | ct = egress.PyWallEgress(packet_queue) 42 | ct.run() 43 | 44 | 45 | def main(conf, loglevel, filename, **kwargs): 46 | """Main function of the whole program. 47 | 48 | Runs a PyWall given a configuration file, a loglevel, and a filename. This 49 | spawns three processes (log_process, egress_process, and pywall_process). 50 | It then runs the connection tracker on thes process (the "master process"). 51 | 52 | """ 53 | # Create multiprocessing queues for IPC. 54 | egress_queue = mp.Queue() 55 | ingress_queue = mp.Queue() 56 | log_queue = mp.Queue() 57 | query_pywall, query_contrack = mp.Pipe() 58 | kwargs['loglevel'] = loglevel 59 | kwargs['logqueue'] = log_queue 60 | 61 | # Start logging for the connection tracker. 62 | initialize_logging(loglevel, log_queue) 63 | 64 | # Initialize the connection tracker with the IPC channels. 65 | ct = contrack.PyWallCracker(ingress_queue, egress_queue, query_contrack) 66 | 67 | # Create and start log_process. 68 | log_process = mp.Process(target=log_server, args=(loglevel, log_queue, 69 | filename)) 70 | log_process.start() 71 | 72 | # Create and start egress_process. 73 | egress_process = mp.Process(target=run_egress, args=(egress_queue, 74 | loglevel, log_queue)) 75 | egress_process.start() 76 | 77 | # Create and start PyWall process. 78 | pywall_process = mp.Process(target=run_pywall, args=(conf, ingress_queue, 79 | query_pywall, kwargs)) 80 | pywall_process.start() 81 | 82 | # Run the connection tracker on the "master process." 83 | ct.run() 84 | 85 | 86 | if __name__ == '__main__': 87 | # This is run if main.py is executed. Gets arguments from the command line. 88 | parser = argparse.ArgumentParser(description='Build a PyWall') 89 | parser.add_argument('config', help='JSON configuration file') 90 | parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 91 | 'WARNING', 'ERROR', 92 | 'CRITICAL'], 93 | help='set verbosity of logging', default='INFO') 94 | parser.add_argument('-f', '--log-file', help='set log file', default=None) 95 | args = parser.parse_args() 96 | main(args.config, args.log_level, args.log_file) 97 | -------------------------------------------------------------------------------- /packets.py: -------------------------------------------------------------------------------- 1 | """Contains Python objects for IP and network layer datagrams/segments. 2 | 3 | These generally take a buffer in their constructor, and parse the header fields 4 | into class member fields so that we can access them easily. 5 | 6 | """ 7 | 8 | from __future__ import unicode_literals 9 | from struct import unpack 10 | from abc import ABCMeta 11 | from abc import abstractmethod 12 | import socket 13 | 14 | # A list of IP Protocol numbers, taken directly from IANA. 15 | PROTO_NUMS = { 16 | 0: 'HOPOPT', 17 | 1: 'ICMP', 18 | 2: 'IGMP', 19 | 3: 'GGP', 20 | 4: 'IPv4', 21 | 5: 'ST', 22 | 6: 'TCP', 23 | 7: 'CBT', 24 | 8: 'EGP', 25 | 9: 'IGP', 26 | 10: 'BBN-RCC-MON', 27 | 11: 'NVP-II', 28 | 12: 'PUP', 29 | 13: 'ARGUS', 30 | 14: 'EMCON', 31 | 15: 'XNET', 32 | 16: 'CHAOS', 33 | 17: 'UDP', 34 | 18: 'MUX', 35 | 19: 'DCN-MEAS', 36 | 20: 'HMP', 37 | 21: 'PRM', 38 | 22: 'XNS-IDP', 39 | 23: 'TRUNK-1', 40 | 24: 'TRUNK-2', 41 | 25: 'LEAF-1', 42 | 26: 'LEAF-2', 43 | 27: 'RDP', 44 | 28: 'IRTP', 45 | 29: 'ISO-TP4', 46 | 30: 'NETBLT', 47 | 31: 'MFE-NSP', 48 | 32: 'MERIT-INP', 49 | 33: 'DCCP', 50 | 34: '3PC', 51 | 35: 'IDPR', 52 | 36: 'XTP', 53 | 37: 'DDP', 54 | 38: 'IDPR-CMTP', 55 | 39: 'TP++', 56 | 40: 'IL', 57 | 41: 'IPv6', 58 | 42: 'SDRP', 59 | 43: 'IPv6-Route', 60 | 44: 'IPv6-Frag', 61 | 45: 'IDRP', 62 | 46: 'RSVP', 63 | 47: 'GRE', 64 | 48: 'DSR', 65 | 49: 'BNA', 66 | 50: 'ESP', 67 | 51: 'AH', 68 | 52: 'I-NLSP', 69 | 53: 'SWIPE (deprecated)', 70 | 54: 'NARP', 71 | 55: 'MOBILE', 72 | 56: 'TLSP', 73 | 57: 'SKIP', 74 | 58: 'IPv6-ICMP', 75 | 59: 'IPv6-NoNxt', 76 | 60: 'IPv6-Opts', 77 | 62: 'CFTP', 78 | 64: 'SAT-EXPAK', 79 | 65: 'KRYPTOLAN', 80 | 66: 'RVD', 81 | 67: 'IPPC', 82 | 69: 'SAT-MON', 83 | 70: 'VISA', 84 | 71: 'IPCV', 85 | 72: 'CPNX', 86 | 73: 'CPHB', 87 | 74: 'WSN', 88 | 75: 'PVP', 89 | 76: 'BR-SAT-MON', 90 | 77: 'SUN-ND', 91 | 78: 'WB-MON', 92 | 79: 'WB-EXPAK', 93 | 80: 'ISO-IP', 94 | 81: 'VMTP', 95 | 82: 'SECURE-VMTP', 96 | 83: 'VINES', 97 | 84: 'TTP', 98 | 84: 'IPTM', 99 | 85: 'NSFNET-IGP', 100 | 86: 'DGP', 101 | 87: 'TCF', 102 | 88: 'EIGRP', 103 | 89: 'OSPFIGP', 104 | 90: 'Sprite-RPC', 105 | 91: 'LARP', 106 | 92: 'MTP', 107 | 93: 'AX.25', 108 | 94: 'IPIP', 109 | 95: 'MICP (deprecated)', 110 | 96: 'SCC-SP', 111 | 97: 'ETHERIP', 112 | 98: 'ENCAP', 113 | 100: 'GMTP', 114 | 101: 'IFMP', 115 | 102: 'PNNI', 116 | 103: 'PIM', 117 | 104: 'ARIS', 118 | 105: 'SCPS', 119 | 106: 'QNX', 120 | 107: 'A/N', 121 | 108: 'IPComp', 122 | 109: 'SNP', 123 | 110: 'Compaq-Peer', 124 | 111: 'IPX-in-IP', 125 | 112: 'VRRP', 126 | 113: 'PGM', 127 | 115: 'L2TP', 128 | 116: 'DDX', 129 | 117: 'IATP', 130 | 118: 'STP', 131 | 119: 'SRP', 132 | 120: 'UTI', 133 | 121: 'SMP', 134 | 122: 'SM', 135 | 123: 'PTP', 136 | 124: 'ISIS over IPv4', 137 | 125: 'FIRE', 138 | 126: 'CRTP', 139 | 127: 'CRUDP', 140 | 128: 'SSCOPMCE', 141 | 129: 'IPLT', 142 | 130: 'SPS', 143 | 131: 'PIPE', 144 | 132: 'SCTP', 145 | 133: 'FC', 146 | 134: 'RSVP-E2E-IGNORE', 147 | 135: 'Mobility Header', 148 | 136: 'UDPLite', 149 | 137: 'MPLS-in-IP', 150 | 138: 'manet', 151 | 139: 'HIP', 152 | 140: 'Shim6', 153 | 141: 'WESP', 154 | 142: 'ROHC' 155 | } 156 | 157 | 158 | def payload_builder(payload_buff, protocol): 159 | """If `protocol` is supported, builds packet object from buff.""" 160 | if protocol == socket.IPPROTO_TCP: 161 | return TCPPacket(payload_buff) 162 | elif protocol == socket.IPPROTO_UDP: 163 | return UDPPacket(payload_buff) 164 | else: 165 | return None 166 | 167 | 168 | def to_tuple(ippacket, flip=False): 169 | """Create a tuple from a TCP packet. 170 | 171 | The flip argument flips the source and destination port, so that they will 172 | be consistent between ingress and egress. 173 | 174 | """ 175 | payload = ippacket.get_payload() 176 | if type(payload) is TCPPacket and not flip: 177 | tup = (ippacket.get_src_ip(), payload.get_src_port(), # remote 178 | ippacket.get_dst_ip(), payload.get_dst_port()) # local 179 | return tup 180 | elif type(payload) is TCPPacket and flip: 181 | tup = (ippacket.get_dst_ip(), payload.get_dst_port(), # remote 182 | ippacket.get_src_ip(), payload.get_src_port()) # local 183 | else: 184 | tup = None 185 | return tup 186 | 187 | 188 | def proto_to_string(proto): 189 | """Convert protocol number to a string.""" 190 | return PROTO_NUMS.get(proto, 'unknown') 191 | 192 | 193 | class Packet(object): 194 | """Base class for all packets""" 195 | __metaclass__ = ABCMeta 196 | 197 | @abstractmethod 198 | def get_header_len(self): 199 | pass 200 | 201 | @abstractmethod 202 | def get_data_len(self): 203 | pass 204 | 205 | 206 | class TransportLayerPacket(Packet): 207 | """Base class packets at the transport layer """ 208 | __metaclass__ = ABCMeta 209 | 210 | @abstractmethod 211 | def get_body(self): 212 | pass 213 | 214 | 215 | class IPPacket(Packet): 216 | """Base class for all packets""" 217 | 218 | def __init__(self, buf): 219 | """Create packet from raw data.""" 220 | self.buf = buf 221 | self._src_ip = socket.inet_ntoa(buf[12:16]) 222 | self._dst_ip = socket.inet_ntoa(buf[16:20]) 223 | # Internal Header Length, in bytes 224 | self._ihl = (unpack('!B', buf[0])[0] & 0xF) * 4 225 | self._proto = unpack('!B', buf[9])[0] 226 | self._payload = payload_builder(buf[self._ihl:], self._proto) 227 | 228 | def get_src_ip(self): 229 | return self._src_ip 230 | 231 | def get_dst_ip(self): 232 | return self._dst_ip 233 | 234 | def get_protocol(self): 235 | return self._proto 236 | 237 | def get_payload(self): 238 | return self._payload 239 | 240 | def get_header_len(self): 241 | return self._ihl 242 | 243 | def get_data_len(self): 244 | return len(self.buf) - self._ihl 245 | 246 | def __unicode__(self): 247 | return 'IP Packet %s => %s, proto=%s' % (self._src_ip, self._dst_ip, 248 | proto_to_string(self._proto)) 249 | 250 | 251 | class TCPPacket(TransportLayerPacket): 252 | """TCP Packet object.""" 253 | 254 | def __init__(self, buff): 255 | self._parse_header(buff) 256 | 257 | def _parse_header(self, buff): 258 | self._src_port, self._dst_port = unpack('!HH', buff[0:4]) 259 | self._seq_num, self._ack_num = unpack('!II', buff[4:12]) 260 | flags, self._win_size = unpack('!HH', buff[12:16]) 261 | self._data_offset = flags & 0xF000 262 | self.flag_ns = flags & 0x0100 263 | self.flag_cwr = flags & 0x0080 264 | self.flag_ece = flags & 0x0040 265 | self.flag_urg = flags & 0x0020 266 | self.flag_ack = flags & 0x0010 267 | self.flag_psh = flags & 0x0008 268 | self.flag_rst = flags & 0x0004 269 | self.flag_syn = flags & 0x0002 270 | self.flag_fin = flags & 0x0001 271 | self._checksum, self._urg_ptr = unpack('!HH', buff[16:20]) 272 | # can be parsed later if we care: 273 | self._options = buff[20:(self._data_offset * 4)] 274 | self._total_length = len(buff) 275 | self._body = buff[self.get_header_len():] 276 | 277 | def get_header_len(self): 278 | return self._data_offset * 4 279 | 280 | def get_data_len(self): 281 | return self._total_length - self.get_header_len() 282 | 283 | def get_src_port(self): 284 | return self._src_port 285 | 286 | def get_dst_port(self): 287 | return self._dst_port 288 | 289 | def get_body(self): 290 | return str(self._body) 291 | 292 | def __unicode__(self): 293 | """Returns a printable version of the TCP header""" 294 | return u'TCP from %d to %d' % (self._src_port, self._dst_port) 295 | 296 | 297 | class UDPPacket(TransportLayerPacket): 298 | """UDP Packet object.""" 299 | 300 | def __init__(self, buff): 301 | self._parse_header(buff) 302 | 303 | def _parse_header(self, buff): 304 | self._src_port, self._dst_port = unpack('!HH', buff[0:4]) 305 | self._length, self._checksum = unpack('!HH', buff[4:8]) 306 | self._total_length = len(buff) 307 | self._body = buff[self.get_header_len():] 308 | 309 | def get_header_len(self): 310 | return 8 311 | 312 | def get_data_len(self): 313 | return self._total_length - self.get_header_len() 314 | 315 | def get_src_port(self): 316 | return self._src_port 317 | 318 | def get_dst_port(self): 319 | return self._dst_port 320 | 321 | def get_body(self): 322 | return str(self._body) 323 | 324 | def __unicode__(self): 325 | """Returns a printable version of the UDP header""" 326 | return u'UDP from %d to %d' % (self._src_port, self._dst_port) 327 | -------------------------------------------------------------------------------- /py_log.py: -------------------------------------------------------------------------------- 1 | """Contains code to setup logging, and to run the logger process. 2 | 3 | Sadly, logging in multiprocessing programs is not trivial. Happily, Python 4 | provides wonderful logging utilities that make multiprocessing logging much 5 | nicer to setup. We use these utilities here. The external "logutils" module 6 | actually contains logging library items that are present in Python 3, 7 | backported to Python 2. 8 | 9 | """ 10 | 11 | import logging 12 | import time 13 | from logging import StreamHandler, FileHandler 14 | 15 | from logutils.queue import QueueHandler, QueueListener 16 | 17 | 18 | def _get_formatter(): 19 | """Creates a formatter with our specified format for log messages.""" 20 | return logging.Formatter(fmt='[%(asctime)s][%(levelname)s] %(message)s') 21 | 22 | 23 | def initialize_logging(level, queue): 24 | """Setup logging for a process. 25 | 26 | Creates a base logger for pywall. Installs a single handler, which will 27 | send packets across a queue to the logger process. This function should be 28 | called by each of the three worker processes before they start. 29 | 30 | """ 31 | formatter = _get_formatter() 32 | 33 | logger = logging.getLogger('pywall') 34 | logger.setLevel(level) 35 | 36 | handler = QueueHandler(queue) 37 | handler.setLevel(level) 38 | handler.setFormatter(formatter) 39 | 40 | logger.addHandler(handler) 41 | 42 | 43 | def log_server(level, queue, filename, mode='w'): 44 | """Run the logging server. 45 | 46 | This listens to the queue of log messages, and handles them using Python's 47 | logging handlers. It prints to stderr, as well as to a specified file, if 48 | it is given. 49 | 50 | """ 51 | formatter = _get_formatter() 52 | handlers = [] 53 | 54 | sh = StreamHandler() 55 | sh.setFormatter(formatter) 56 | sh.setLevel(level) 57 | handlers.append(sh) 58 | 59 | if filename: 60 | fh = FileHandler(filename, mode) 61 | fh.setFormatter(formatter) 62 | fh.setLevel(level) 63 | handlers.append(fh) 64 | 65 | listener = QueueListener(queue, *handlers) 66 | listener.start() 67 | 68 | # For some reason, queuelisteners run on a separate thread, so now we just 69 | # "busy wait" until terminated. 70 | try: 71 | while True: 72 | time.sleep(1) 73 | except KeyboardInterrupt: 74 | pass 75 | finally: 76 | listener.stop() 77 | -------------------------------------------------------------------------------- /pywall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """Contains PyWall class, the main class for our Python firewall.""" 3 | 4 | from __future__ import print_function 5 | import os 6 | import logging 7 | 8 | import netfilterqueue as nfq 9 | 10 | from packets import IPPacket, TCPPacket, to_tuple 11 | 12 | _NFQ_INIT = 'iptables -I INPUT -j NFQUEUE --queue-num %d' 13 | _NFQ_CLOSE = 'iptables -D INPUT -j NFQUEUE --queue-num %d' 14 | _pipe = None 15 | 16 | 17 | def get_pipe(): 18 | """This function returns the query pipe for TCP connection state. 19 | 20 | Sadly, this breaks some modularity with rule classes. However, it is a 21 | quick fix for the one rule (TCPStateRule) that needs access to TCP 22 | conenction state. 23 | 24 | """ 25 | global _pipe 26 | return _pipe 27 | 28 | 29 | class PyWall(object): 30 | """The main class for PyWall. 31 | 32 | This class contains all rules for the firewall. Furthermore, it waits on 33 | NetfilterQueue for packets, runs them through rules, and ultimately accepts 34 | or drops the packets. 35 | """ 36 | 37 | def __init__(self, tcp_queue, query_pipe, queue_num=1, default='DROP'): 38 | """Create a PyWall object, specifying NFQueue queue number.""" 39 | global _pipe 40 | _pipe = query_pipe 41 | self.queue_num = queue_num 42 | self.tcp_queue = tcp_queue 43 | self.query_pipe = query_pipe 44 | self.chains = {'INPUT': [], 'ACCEPT': None, 'DROP': None} 45 | self.default = default 46 | self._start = 'INPUT' 47 | self._old_handler = None 48 | 49 | def add_chain(self, chain_name): 50 | """Add a new, empty chain.""" 51 | self.chains[chain_name] = [] 52 | 53 | def add_brick(self, chain, rule): 54 | """Add a rule to a chain.""" 55 | self.chains[chain].append(rule) 56 | 57 | def _apply_chain(self, chain, nfqueue_packet, pywall_packet): 58 | """Run the packet through a chain.""" 59 | l = logging.getLogger('pywall.pywall') 60 | if chain == 'ACCEPT': 61 | payload = pywall_packet.get_payload() 62 | # We don't want to tell the connection tracker that we've accepted 63 | # a TCP connection until we're sure that we have. 64 | l.debug('ACCEPT %s' % unicode(pywall_packet)) 65 | if type(payload) is TCPPacket: 66 | tup = to_tuple(pywall_packet) 67 | if self.tcp_queue is not None: 68 | self.tcp_queue.put((tup, bool(payload.flag_syn), 69 | bool(payload.flag_ack), 70 | bool(payload.flag_fin))) 71 | nfqueue_packet.accept() 72 | elif chain == 'DROP': 73 | l.info('DROP %s' % unicode(pywall_packet)) 74 | nfqueue_packet.drop() 75 | else: 76 | # Match against every rule: 77 | for rule in self.chains[chain]: 78 | result = rule(pywall_packet) 79 | # If it matches, execute the rule. 80 | if result: 81 | return self._apply_chain(result, nfqueue_packet, 82 | pywall_packet) 83 | # If no matches, run the default rule. 84 | return self._apply_chain(self.default, nfqueue_packet, 85 | pywall_packet) 86 | 87 | def callback(self, packet): 88 | """Accept packets from NFQueue.""" 89 | pywall_packet = IPPacket(packet.get_payload()) 90 | self._apply_chain(self._start, packet, pywall_packet) 91 | 92 | def erect(self, **kwargs): 93 | """Run the PyWall!""" 94 | # Setup firewall rule. 95 | setup = _NFQ_INIT % self.queue_num 96 | os.system(setup) 97 | print('Set up IPTables: ' + setup) 98 | 99 | # Bind and run NFQ. 100 | nfqueue = nfq.NetfilterQueue() 101 | nfqueue.bind(self.queue_num, self.callback) 102 | if kwargs.get('test', False): 103 | lock = kwargs.get('lock', None) 104 | if lock: 105 | lock.release() 106 | 107 | try: 108 | nfqueue.run() 109 | except KeyboardInterrupt: 110 | pass 111 | finally: 112 | # Always remove the firewall rule when we're done. 113 | teardown = _NFQ_CLOSE % self.queue_num 114 | os.system(teardown) 115 | print('\nTore down IPTables: ' + teardown) 116 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | logutils==0.3.3 2 | netaddr==0.7.14 3 | NetfilterQueue==0.3 4 | -------------------------------------------------------------------------------- /rules/__init__.py: -------------------------------------------------------------------------------- 1 | """Collection of rules for PyWall.""" 2 | 3 | import os 4 | import glob 5 | from abc import ABCMeta 6 | from abc import abstractmethod 7 | 8 | rules = {} 9 | modules = glob.glob(os.path.dirname(__file__)+"/*.py") 10 | __all__ = [os.path.basename(f)[:-3] for f in modules] 11 | 12 | 13 | class Rule(object): 14 | """ 15 | One rule class to rule them all. 16 | 17 | Generic Rule class. All other rules should inherit from here, passing 18 | their **kwargs up to the super constructor. To function as a rule, each 19 | subclass should provide its own implementation of __call__. 20 | 21 | This class should be extended instead of SimpleRule if multiple actions 22 | need to be supported. 23 | """ 24 | __metaclass__ = ABCMeta 25 | 26 | def __init__(self, **kwargs): 27 | self._action = kwargs.get('action') 28 | 29 | @abstractmethod 30 | def __call__(self, pywall_packet): 31 | """ 32 | Return False to pass packet down the chain, "ACCEPT" to 33 | explicitly accept and "DROP" to explicitly drop. 34 | """ 35 | pass 36 | 37 | 38 | class SimpleRule(Rule): 39 | """ 40 | Class for simple rules (i.e. ones that perform one action if some condition 41 | is met, pass down the chain otherwise). 42 | """ 43 | __metaclass__ = ABCMeta 44 | 45 | def __call__(self, pywall_packet): 46 | """ 47 | Packet filtering logic. This is the same for all simple rules, so this 48 | method should never be overridden. To get the correct behavior for your 49 | rules, provide your own implementation of the filter_condition method. 50 | """ 51 | if self.filter_condition(pywall_packet): 52 | return self._action 53 | else: 54 | return False 55 | 56 | @abstractmethod 57 | def filter_condition(self, pywall_packet): 58 | """ 59 | Return True to perform default action, return False to pass packet 60 | down the chain. Override this to define correct behavior for your rule. 61 | """ 62 | return True 63 | 64 | 65 | def register(rule_class): 66 | """This function must be called in every rule class.""" 67 | rules[rule_class.__name__] = rule_class 68 | -------------------------------------------------------------------------------- /rules/ip_rules.py: -------------------------------------------------------------------------------- 1 | """Contains rules for filtering by IP address.""" 2 | import netaddr 3 | 4 | from rules import register, SimpleRule 5 | 6 | 7 | class IPRangeRule(SimpleRule): 8 | """Filter IP packets based on source/dest address.""" 9 | 10 | def __init__(self, **kwargs): 11 | """Create an IPRangeRule, taking the cidr_range.""" 12 | SimpleRule.__init__(self, **kwargs) 13 | self._ip_range = netaddr.IPNetwork(kwargs['cidr_range']) 14 | 15 | 16 | class SourceIPRule(IPRangeRule): 17 | """Filter IP packets based on source address""" 18 | 19 | def __init__(self, **kwargs): 20 | """Takes single argument, 'cidr_range', passed to super.""" 21 | IPRangeRule.__init__(self, **kwargs) 22 | 23 | def filter_condition(self, pywall_packet): 24 | """ 25 | Filter packets if their source address falls within the ip_range. 26 | """ 27 | return pywall_packet.get_src_ip() in self._ip_range 28 | 29 | 30 | class DestinationIPRule(IPRangeRule): 31 | """Filter IP packets based on destination address""" 32 | 33 | def __init__(self, **kwargs): 34 | """Takes single argument, 'cidr_range', passed to super.""" 35 | IPRangeRule.__init__(self, **kwargs) 36 | 37 | def filter_condition(self, pywall_packet): 38 | """True if destination address falls within the ip_range.""" 39 | return pywall_packet.get_dst_ip() in self._ip_range 40 | 41 | register(SourceIPRule) 42 | register(DestinationIPRule) 43 | -------------------------------------------------------------------------------- /rules/port_filter.py: -------------------------------------------------------------------------------- 1 | """Contains rules for filtering by TCP/UDP port.""" 2 | 3 | import socket 4 | 5 | from rules import register 6 | from rules import SimpleRule 7 | 8 | 9 | class PortRule(SimpleRule): 10 | """Class for filtering out packets to/from a single port""" 11 | 12 | def __init__(self, **kwargs): 13 | """Create a rule for a single source and/or destination port.""" 14 | protocol = kwargs.get('protocol', None) 15 | self._src_port = kwargs.get('src_port', None) 16 | self._dst_port = kwargs.get('dst_port', None) 17 | self._action = kwargs.get('action', 'DROP') 18 | 19 | if protocol == 'TCP': 20 | self._protocol = socket.IPPROTO_TCP 21 | elif protocol == 'UDP': 22 | self._protocol = socket.IPPROTO_UDP 23 | else: 24 | raise ValueError('protocol should be either TCP or UDP') 25 | 26 | if self._src_port is None and self._dst_port is None: 27 | raise ValueError('At least one of src_port or dst_port should be' 28 | ' non-None') 29 | 30 | def filter_condition(self, packet): 31 | """Condition to jump to action chain.""" 32 | match = (packet.get_protocol() == self._protocol) 33 | match = match and (packet.get_payload() is not None) 34 | match = match and (self._src_port is None or 35 | packet.get_payload().get_src_port() == self._src_port) 36 | match = match and (self._dst_port is None or 37 | packet.get_payload().get_dst_port() == self._dst_port) 38 | if match: 39 | print('PortRule: %s' % str(self._action)) 40 | return match 41 | 42 | 43 | class PortRangeRule(SimpleRule): 44 | """Blocks all packets with given protocol on inclusive range [lo, hi].""" 45 | 46 | def __init__(self, **kwargs): 47 | """Creates a rule that takes matches port ranges.""" 48 | protocol = kwargs.get('protocol', None) 49 | self._src_lo = kwargs.get('src_lo', None) 50 | self._src_hi = kwargs.get('src_hi', None) 51 | self._dst_lo = kwargs.get('dst_lo', None) 52 | self._dst_hi = kwargs.get('dst_hi', None) 53 | self._src_range = (self._src_lo, self._src_hi) 54 | self._dst_range = (self._dst_lo, self._dst_hi) 55 | self._action = kwargs.get('action', 'DROP') 56 | 57 | if protocol == 'TCP': 58 | self._protocol = socket.IPPROTO_TCP 59 | elif protocol == 'UDP': 60 | self._protocol = socket.IPPROTO_UDP 61 | else: 62 | raise ValueError('protocol should be either TCP or UDP') 63 | 64 | if self._src_range == (None, None) and self._dst_range == (None, None): 65 | raise ValueError('At least one of src_port or dst_port should be' 66 | ' non-None') 67 | elif not self._is_port_range_valid(self._src_lo, self._src_hi): 68 | raise ValueError('Invalid source port range') 69 | elif not self._is_port_range_valid(self._dst_lo, self._dst_hi): 70 | raise ValueError('Invalid destination port range') 71 | 72 | def _is_port_range_valid(self, port_lo, port_hi): 73 | """Return true if a port range is valid.""" 74 | valid = (port_hi is None and port_lo is None) or \ 75 | (port_hi is not None and port_lo is not None) 76 | valid = valid and (port_lo <= port_hi) 77 | return valid 78 | 79 | def filter_condition(self, packet): 80 | """Condition to jump to action chain.""" 81 | match = (packet.get_payload() is not None) 82 | match = match and (packet.get_protocol() == self._protocol) 83 | match = match and ((self._src_range == (None, None)) or 84 | (self._src_lo <= packet.get_payload().get_src_port() <= self._src_hi)) 85 | match = match and ((self._dst_range == (None, None)) or 86 | (self._dst_lo <= packet.get_payload().get_dst_port() <= self._dst_hi)) 87 | if match: 88 | print('PortRangeRule: %s' % str(self._action)) 89 | return match 90 | 91 | 92 | register(PortRule) 93 | register(PortRangeRule) 94 | -------------------------------------------------------------------------------- /rules/port_ip_rule.py: -------------------------------------------------------------------------------- 1 | """Contains rules that act on a combination of IP and port.""" 2 | 3 | from rules import register, SimpleRule 4 | from rules.port_filter import PortRangeRule 5 | from rules.ip_rules import SourceIPRule, DestinationIPRule 6 | 7 | 8 | class IPPortRule(SimpleRule): 9 | 10 | def __init__(self, **kwargs): 11 | """Creates a combination of rules.""" 12 | SimpleRule.__init__(self, **kwargs) 13 | 14 | # Create a port rule. 15 | self.port_rule = PortRangeRule(**kwargs) 16 | 17 | # Create a source IP rule. 18 | source_args = kwargs.copy() 19 | source_ip = source_args.pop('src_ip', None) 20 | if source_ip: 21 | source_args['cidr_range'] = source_ip 22 | self.ip_src_rule = SourceIPRule(**source_args) 23 | else: 24 | self.ip_src_rule = None 25 | 26 | # Create a dest IP rule. 27 | dest_args = kwargs.copy() 28 | dest_ip = dest_args.pop('dst_ip', None) 29 | if dest_ip: 30 | dest_args['cidr_range'] = dest_ip 31 | self.ip_dst_rule = DestinationIPRule(**dest_args) 32 | else: 33 | self.ip_dst_rule = None 34 | 35 | def filter_condition(self, packet): 36 | """Condition to filter on.""" 37 | res = self.port_rule.filter_condition(packet) 38 | if self.ip_src_rule: 39 | res = res and self.ip_src_rule.filter_condition(packet) 40 | if self.ip_dst_rule: 41 | res = res and self.ip_dst_rule.filter_condition(packet) 42 | return res 43 | 44 | register(IPPortRule) 45 | -------------------------------------------------------------------------------- /rules/port_knocking.py: -------------------------------------------------------------------------------- 1 | """Contains rule for port knocking. 2 | 3 | Port knocking is a technique where an external host will make connection 4 | requests to a series of ports, frequently with a "token" of some sort. When 5 | the correct sequence of attempts is made, the host is allowed through the 6 | firewall. 7 | 8 | """ 9 | from __future__ import print_function 10 | from datetime import datetime 11 | from datetime import timedelta 12 | import socket 13 | 14 | from rules import Rule, register 15 | 16 | 17 | class PortKnocking(Rule): 18 | """Stateful Port Knocking rule. 19 | 20 | This doesn't inherit from SimpleRule since it needs more fine-grained 21 | control over the chains that the packets go through. 22 | 23 | """ 24 | 25 | def __init__(self, **kwargs): 26 | """Create the port knocking rule.""" 27 | self._protocol = self._proto_to_const(kwargs.get('protocol', None)) 28 | self._port = kwargs.get('port', None) 29 | self._src_port = kwargs.get('src_port', None) 30 | self._body = kwargs.get('body' 'knock-knock') 31 | self._timeout = kwargs.get('timeout', 60) 32 | self._doors = self._convert_doors(kwargs.get('doors', [])) 33 | self._activity = {} # IP -> (state, timestamp) 34 | 35 | def _proto_to_const(self, protocol_str): 36 | """Convert a string protocol to the IP Protocol number.""" 37 | if protocol_str == 'TCP': 38 | return socket.IPPROTO_TCP 39 | elif protocol_str == 'UDP': 40 | return socket.IPPROTO_UDP 41 | else: 42 | raise ValueError('Missing or invalid protocol') 43 | 44 | def _convert_doors(self, user_doors): 45 | """Parse the user-provided list of port knock doors.""" 46 | final_doors = [] 47 | for proto, port in user_doors: 48 | if proto == 'TCP' and 0 <= port <= 65535: 49 | final_doors.append((socket.IPPROTO_TCP, port)) 50 | elif proto == 'UDP' and 0 <= port <= 65535: 51 | final_doors.append((socket.IPPROTO_UDP, port)) 52 | else: 53 | raise ValueError('Invalid door: (%s, %d)' % (proto, port)) 54 | 55 | if len(final_doors) == 0: 56 | raise ValueError('No doors given') 57 | return final_doors 58 | 59 | def __call__(self, pywall_packet): 60 | """Return the destination chain for a packet, or False.""" 61 | act_def = (0, datetime.now()) 62 | src_ip = pywall_packet.get_src_ip() 63 | payload = pywall_packet.get_payload() 64 | 65 | # get the latest activity 66 | i, last_activity = self._activity.get(src_ip, act_def) 67 | # clear if we have timed out 68 | if last_activity + timedelta(seconds=self._timeout) < datetime.now(): 69 | print('PortKnocking: timeout -- fall through: %s' % (src_ip)) 70 | del self._activity[src_ip] 71 | i, last_activity = act_def 72 | 73 | if i >= len(self._doors): 74 | if (self._protocol == pywall_packet.get_protocol() and 75 | self._port == payload.get_dst_port()): 76 | print('PortKnocking: accepting from %s' % (src_ip)) 77 | return 'ACCEPT' 78 | else: 79 | print('PortKnocking: fall through from recognized ip: %s' % 80 | (src_ip)) 81 | return False 82 | else: 83 | cur_proto, cur_port = self._doors[i] 84 | if (cur_proto == pywall_packet.get_protocol() and 85 | cur_port == payload.get_dst_port() and 86 | self._src_port == payload.get_src_port()): 87 | i += 1 88 | self._activity[src_ip] = (i, datetime.now()) 89 | print('PortKnocking: advance to %d' % i) 90 | return 'DROP' 91 | else: 92 | print('PortKnocking: unrecognized -- fall-through') 93 | return False 94 | 95 | 96 | register(PortKnocking) 97 | -------------------------------------------------------------------------------- /rules/print_rule.py: -------------------------------------------------------------------------------- 1 | """Printout rule for PyWall.""" 2 | 3 | from __future__ import print_function 4 | from rules import register, SimpleRule 5 | 6 | 7 | class PrintRule(SimpleRule): 8 | """Rule that just prints the socket and its payload. 9 | 10 | This is mostly irrelevent now that logging is enabled. 11 | 12 | """ 13 | 14 | def filter_condition(self, pywall_packet): 15 | """Prints out packet information at the IP level.""" 16 | print(unicode(pywall_packet)) 17 | print(unicode(pywall_packet.get_payload())) 18 | # Action should not be applied. Ever. 19 | return False 20 | 21 | 22 | register(PrintRule) 23 | -------------------------------------------------------------------------------- /rules/tcp_rules.py: -------------------------------------------------------------------------------- 1 | """Contains rules that match TCP packets, and track state.""" 2 | import socket 3 | 4 | from rules import register, SimpleRule 5 | from packets import to_tuple 6 | from pywall import get_pipe 7 | 8 | 9 | class TCPRule(SimpleRule): 10 | """Returns True when a packet is TCP.""" 11 | 12 | def filter_condition(self, pywall_packet): 13 | return pywall_packet.get_protocol() == socket.IPPROTO_TCP 14 | 15 | 16 | class TCPStateRule(TCPRule): 17 | """A rule that matches TCP packets in a certain state. 18 | 19 | This rule works with the PyWallCracker. It takes two arguments: 20 | - match_if: A list of TCP states the rule will match. 21 | - match_if_not: A list of TCP states the rule will fail to match. 22 | 23 | You cannot provide both arguments. Only one. The rule queries the state 24 | table, and then matches if it is in the the match_if set, or fails if it is 25 | in the match_if_not. 26 | 27 | """ 28 | 29 | def __init__(self, **kwargs): 30 | """Create rule with arguments.""" 31 | TCPRule.__init__(self, **kwargs) 32 | self.match_if = set(kwargs.get('match_if', [])) 33 | self.match_if_not = set(kwargs.get('match_if_not', [])) 34 | if self.match_if and self.match_if_not: 35 | raise ValueError('You may only provide one of "match_if" and' 36 | ' "match_if_not".') 37 | if not self.match_if and not self.match_if_not: 38 | raise ValueError('You must provide one of "match_if" and' 39 | ' "match_if_not".') 40 | 41 | def add_connection(self, pywall_packet): 42 | """ 43 | Add a connection to the Rule. Connections are represented as the TCP 44 | 4-tuple. See the TCPPacket class for more info. 45 | """ 46 | self._existing_connections.add(pywall_packet.to_tuple()) 47 | 48 | def filter_condition(self, pywall_packet): 49 | if not TCPRule.filter_condition(self, pywall_packet): 50 | return False 51 | pipe = get_pipe() 52 | pipe.send(to_tuple(pywall_packet)) 53 | state = pipe.recv() 54 | if self.match_if: 55 | return state in self.match_if 56 | else: 57 | return state not in self.match_if_not 58 | 59 | 60 | register(TCPRule) 61 | register(TCPStateRule) 62 | -------------------------------------------------------------------------------- /rules/true_rule.py: -------------------------------------------------------------------------------- 1 | """Rule that is always true.""" 2 | from rules import register, SimpleRule 3 | 4 | 5 | class TrueRule(SimpleRule): 6 | """Rule that is always true.""" 7 | 8 | def filter_condition(self, pckt): 9 | return True 10 | 11 | register(TrueRule) 12 | -------------------------------------------------------------------------------- /run-acceptance-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | from importlib import import_module 5 | import sys 6 | import os 7 | import glob 8 | 9 | 10 | if __name__ == '__main__': 11 | # This is run when you run this on the command line. 12 | 13 | # Determine the test modules to run. 14 | if len(sys.argv) > 1: 15 | if sys.argv[1] == 'test_connection': 16 | modules = ['./test/acceptance/local/test_connection.py'] 17 | else: 18 | modules = sys.argv[1:] 19 | else: 20 | modules = glob.glob(os.path.join(os.path.dirname(__file__), 'test/acceptance/local', '*_test.py')) 21 | print(modules) 22 | 23 | # Iterate through every test module, running all the tests within. 24 | test_results = [] 25 | for module in modules: 26 | module_name = module[2:-3].replace('/', '.') 27 | print('Importing module: %s' % module_name) 28 | mod = import_module(module_name, '') 29 | print('Running module: %s' % module_name) 30 | tests = getattr(mod, 'tests') 31 | 32 | # Iterate through every test class. 33 | for test_name, test_class in tests: 34 | print('Running test: %s' % test_name) 35 | result = False 36 | 37 | # Run test, catching all exceptions. 38 | try: 39 | result = test_class.run() 40 | except Exception as e: 41 | print('Exception in test case: %s' % test_name) 42 | print(e) 43 | result = False 44 | 45 | if result: 46 | print('Test PASSED!') 47 | else: 48 | print('Test FAILED!') 49 | test_results.append((test_name, result)) 50 | print("\n") 51 | 52 | # Since output is jumbled for our test runner, print a summary at the end. 53 | for name, res in test_results: 54 | res_str = "PASSED" if res else "FAILED" 55 | print("%s: %s" % (name, res_str)) 56 | print("P:F = %d:%d" % (len([test_name for test_name, result in test_results if result]), 57 | len([test_name for test_name, result in test_results if not result]))) 58 | print('yay!') 59 | -------------------------------------------------------------------------------- /run-integration-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | from importlib import import_module 5 | import sys 6 | import os 7 | import glob 8 | 9 | 10 | if __name__ == '__main__': 11 | # This is run when you run this on the command line. 12 | 13 | # Get test modules to import. 14 | if len(sys.argv) > 1: 15 | modules = sys.argv[1:] 16 | else: 17 | modules = glob.glob(os.path.join(os.path.dirname(__file__), 'test/integration', '*_test.py')) 18 | print(modules) 19 | 20 | # Load each test module. 21 | test_results = [] 22 | for module in modules: 23 | module_name = module[2:-3].replace('/', '.') 24 | print('Importing module: %s' % module_name) 25 | mod = import_module(module_name, '') 26 | print('Running module: %s' % module_name) 27 | tests = getattr(mod, 'tests') 28 | 29 | # Run each test class in the test module. 30 | for test_name, test_class in tests: 31 | print('Running test: %s' % test_name) 32 | result = False 33 | 34 | # Run the test class, taching all errors. 35 | try: 36 | result = test_class.run() 37 | except Exception as e: 38 | print(e) 39 | result = False 40 | 41 | if result: 42 | print('Test PASSED!') 43 | else: 44 | print('Test FAILED!') 45 | test_results.append((test_name, result)) 46 | print("\n") 47 | 48 | # Print summary of results. 49 | for name, res in test_results: 50 | res_str = "PASSED" if res else "FAILED" 51 | print("%s: %s" % (name, res_str)) 52 | print("P:F = %d:%d" % (len([test_name for test_name, result in test_results if result]), 53 | len([test_name for test_name, result in test_results if not result]))) 54 | print('yay!') 55 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/__init__.py -------------------------------------------------------------------------------- /test/acceptance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/acceptance/__init__.py -------------------------------------------------------------------------------- /test/acceptance/local/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/acceptance/local/__init__.py -------------------------------------------------------------------------------- /test/acceptance/local/block_unreg_dst_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRule", 5 | "protocol": "TCP", 6 | "src_port": 22, 7 | "action": "ACCEPT"}, 8 | {"name": "PortRule", 9 | "protocol": "TCP", 10 | "dst_port": 22, 11 | "action": "ACCEPT"}, 12 | {"name": "PortRangeRule", 13 | "protocol": "TCP", 14 | "dst_lo": 49151, 15 | "dst_hi": 65535, 16 | "action": "DROP"}, 17 | {"name": "PortRangeRule", 18 | "protocol": "UDP", 19 | "dst_lo": 49151, 20 | "dst_hi": 65535, 21 | "action": "DROP"} 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/acceptance/local/block_unreg_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRule", 5 | "protocol": "TCP", 6 | "src_port": 22, 7 | "action": "ACCEPT"}, 8 | {"name": "PortRule", 9 | "protocol": "TCP", 10 | "dst_port": 22, 11 | "action": "ACCEPT"}, 12 | {"name": "PortRangeRule", 13 | "protocol": "TCP", 14 | "src_lo": 49151, 15 | "src_hi": 65535, 16 | "action": "DROP"}, 17 | {"name": "PortRangeRule", 18 | "protocol": "UDP", 19 | "src_lo": 49151, 20 | "src_hi": 65535, 21 | "action": "DROP"}, 22 | {"name": "PortRangeRule", 23 | "protocol": "TCP", 24 | "dst_lo": 49151, 25 | "dst_hi": 65535, 26 | "action": "DROP"}, 27 | {"name": "PortRangeRule", 28 | "protocol": "UDP", 29 | "dst_lo": 49151, 30 | "dst_hi": 65535, 31 | "action": "DROP"} 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/acceptance/local/block_unreg_src_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRule", 5 | "protocol": "TCP", 6 | "src_port": 22, 7 | "action": "ACCEPT"}, 8 | {"name": "PortRule", 9 | "protocol": "TCP", 10 | "dst_port": 22, 11 | "action": "ACCEPT"}, 12 | {"name": "PortRangeRule", 13 | "protocol": "TCP", 14 | "src_lo": 49151, 15 | "src_hi": 65535, 16 | "action": "DROP"}, 17 | {"name": "PortRangeRule", 18 | "protocol": "UDP", 19 | "src_lo": 49151, 20 | "src_hi": 65535, 21 | "action": "DROP"} 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/acceptance/local/conf.py: -------------------------------------------------------------------------------- 1 | CONF = { 2 | 'key_file': '/home/jeffrey/.ssh/id_rsa', 3 | 'remote_host': 'bertha.case.edu', 4 | 'target_host': 'fuego.case.edu', 5 | 'port': 59001, 6 | 'user': 'jeffrey', 7 | } 8 | -------------------------------------------------------------------------------- /test/acceptance/local/example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRule", 5 | "protocol": "TCP", 6 | "src_port": 22, 7 | "action": "ACCEPT"}, 8 | {"name": "PortRule", 9 | "protocol": "TCP", 10 | "dst_port": 22, 11 | "action": "ACCEPT"}, 12 | {"name": "PrintRule"} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/acceptance/local/listeners.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timedelta 3 | from abc import ABCMeta 4 | from abc import abstractmethod 5 | from datetime import datetime 6 | from datetime import timedelta 7 | import socket 8 | 9 | 10 | 11 | class BaseListener(object): 12 | """ 13 | Base class for listeners. Extend and implement listen() to suit 14 | your acceptance test. 15 | """ 16 | __metaclass__ = ABCMeta 17 | def __init__(self, msg='knock-knock'): 18 | self._msg = msg 19 | 20 | @abstractmethod 21 | def listen(self, queue, sem): 22 | """Puts True in queue if it gets message on socket, False otherwise""" 23 | pass 24 | 25 | 26 | class TCPListener(BaseListener): 27 | def __init__(self, host, port, msg='knock-knock', timeout=5): 28 | BaseListener.__init__(self, msg=msg) 29 | self._port = port 30 | self._remote_host_ip = socket.gethostbyname(host) 31 | self._timeout = 5 32 | 33 | def listen(self, queue, sem): 34 | print('listening') 35 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 36 | try: 37 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 38 | sock.bind(('0.0.0.0', self._port)) 39 | sock.listen(5) 40 | sock.settimeout(self._timeout) 41 | except: 42 | print('failed to set up TCP listener socket') 43 | finally: 44 | sem.release() 45 | 46 | start = datetime.now() 47 | print('before loop') 48 | try: 49 | while datetime.now() < start + timedelta(seconds=self._timeout): 50 | s, (host_ip, host_port) = sock.accept() 51 | if host_ip == self._remote_host_ip: 52 | msg = s.recv(1024) 53 | print(msg) 54 | queue.put(msg == self._msg) 55 | s.close() 56 | break 57 | s.close() 58 | except: 59 | queue.put(False) 60 | sock.close() 61 | print('done listening') 62 | 63 | 64 | class UDPListener(BaseListener): 65 | def __init__(self, port, msg='knock-knock', timeout=5): 66 | BaseListener.__init__(self, msg=msg) 67 | self._port = port 68 | self._timeout = 5 69 | 70 | def listen(self, queue, sem): 71 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 72 | try: 73 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 74 | sock.bind(('0.0.0.0', self._port)) 75 | sock.settimeout(self._timeout) 76 | except Exception as e: 77 | print(e) 78 | print('failed to set up UDP listener socket') 79 | finally: 80 | sem.release() 81 | 82 | start = datetime.now() 83 | print('listening...') 84 | try: 85 | while datetime.now() < start + timedelta(seconds=self._timeout): 86 | msg = sock.recv(1024) 87 | if msg == self._msg: 88 | queue.put(True) 89 | break 90 | except: 91 | queue.put(False) 92 | sock.close() 93 | print('done listening') 94 | -------------------------------------------------------------------------------- /test/acceptance/local/port_filter_test.py: -------------------------------------------------------------------------------- 1 | from conf import CONF 2 | from pywall_acceptance_test_case import PyWallAcceptanceTestCase 3 | from listeners import TCPListener 4 | from listeners import UDPListener 5 | import socket 6 | 7 | 8 | class TestBlockTCP(PyWallAcceptanceTestCase): 9 | def __init__(self, config, port=9001, key_file=None, remote_module='', remote_args=[]): 10 | listener = TCPListener(CONF['remote_host'], port) 11 | PyWallAcceptanceTestCase.__init__(self, config, '@'.join([CONF['user'], CONF['remote_host']]), 12 | remote_module=remote_module, 13 | remote_args=remote_args, 14 | key_file=CONF['key_file'], 15 | listener=listener) 16 | 17 | def run(self): 18 | return not bool(PyWallAcceptanceTestCase.run(self)) 19 | 20 | 21 | class TestBlockUDP(PyWallAcceptanceTestCase): 22 | def __init__(self, config, port=9001, key_file=None, remote_module='', remote_args=[]): 23 | listener = UDPListener(port) 24 | PyWallAcceptanceTestCase.__init__(self, config, '@'.join([CONF['user'], CONF['remote_host']]), 25 | remote_module=remote_module, 26 | remote_args=remote_args, 27 | key_file=CONF['key_file'], 28 | listener=listener) 29 | 30 | def run(self): 31 | return not bool(PyWallAcceptanceTestCase.run(self)) 32 | 33 | 34 | tests = [ 35 | # Tests destination port filtering 36 | ('TCPUnregDstPort1', TestBlockTCP('block_unreg_dst_ports.json', 49151, 37 | remote_module='port_filter_remote.py', 38 | remote_args=[CONF['target_host'], str(49151), 'TCP', 'dst'])), 39 | ('TCPUnregDstPort2', TestBlockTCP('block_unreg_dst_ports.json', 49152, 40 | remote_module='port_filter_remote.py', 41 | remote_args=[CONF['target_host'], str(49152), 'TCP', 'dst'])), 42 | ('TCPUnregDstPort3', TestBlockTCP('block_unreg_dst_ports.json', 65535, 43 | remote_module='port_filter_remote.py', 44 | remote_args=[CONF['target_host'], str(65535), 'TCP', 'dst'])), 45 | ('UDPUnregDstPort1', TestBlockUDP('block_unreg_dst_ports.json', 49151, 46 | remote_module='port_filter_remote.py', 47 | remote_args=[CONF['target_host'], str(49151), 'UDP', 'dst'])), 48 | ('UDPUnregDstPort2', TestBlockUDP('block_unreg_dst_ports.json', 49152, 49 | remote_module='port_filter_remote.py', 50 | remote_args=[CONF['target_host'], str(49152), 'UDP', 'dst'])), 51 | ('UDPUnregDstPort3', TestBlockUDP('block_unreg_dst_ports.json', 65535, 52 | remote_module='port_filter_remote.py', 53 | remote_args=[CONF['target_host'], str(65535), 'UDP', 'dst'])), 54 | 55 | # Test source port filtering 56 | ('TCPUnregSrcPort1', TestBlockTCP('block_unreg_src_ports.json', 49151, 57 | remote_module='port_filter_remote.py', 58 | remote_args=[CONF['target_host'], str(49151), 'TCP', 'src'])), 59 | ('TCPUnregSrcPort2', TestBlockTCP('block_unreg_src_ports.json', 49152, 60 | remote_module='port_filter_remote.py', 61 | remote_args=[CONF['target_host'], str(49152), 'TCP', 'src'])), 62 | ('TCPUnregSrcPort3', TestBlockTCP('block_unreg_src_ports.json', 65535, 63 | remote_module='port_filter_remote.py', 64 | remote_args=[CONF['target_host'], str(65535), 'TCP', 'src'])), 65 | ('UDPUnregSrcPort1', TestBlockUDP('block_unreg_src_ports.json', 49151, 66 | remote_module='port_filter_remote.py', 67 | remote_args=[CONF['target_host'], str(49151), 'UDP', 'src'])), 68 | ('UDPUnregSrcPort2', TestBlockUDP('block_unreg_src_ports.json', 49152, 69 | remote_module='port_filter_remote.py', 70 | remote_args=[CONF['target_host'], str(49152), 'UDP', 'src'])), 71 | ('UDPUnregSrcPort3', TestBlockUDP('block_unreg_src_ports.json', 65535, 72 | remote_module='port_filter_remote.py', 73 | remote_args=[CONF['target_host'], str(65535), 'UDP', 'src'])), 74 | 75 | # Conbined source/dstination port filtering 76 | ('TCPUnregPort1', TestBlockTCP('block_unreg_ports.json', 49151, 77 | remote_module='port_filter_remote.py', 78 | remote_args=[CONF['target_host'], str(49151), 'TCP', 'src'])), 79 | ('TCPUnregPort2', TestBlockTCP('block_unreg_ports.json', 49152, 80 | remote_module='port_filter_remote.py', 81 | remote_args=[CONF['target_host'], str(49152), 'TCP', 'src'])), 82 | ('TCPUnregPort3', TestBlockTCP('block_unreg_ports.json', 65535, 83 | remote_module='port_filter_remote.py', 84 | remote_args=[CONF['target_host'], str(65535), 'TCP', 'src'])), 85 | ('UDPUnregPort1', TestBlockUDP('block_unreg_ports.json', 49151, 86 | remote_module='port_filter_remote.py', 87 | remote_args=[CONF['target_host'], str(49151), 'UDP', 'src'])), 88 | ('UDPUnregPort2', TestBlockUDP('block_unreg_ports.json', 49152, 89 | remote_module='port_filter_remote.py', 90 | remote_args=[CONF['target_host'], str(49152), 'UDP', 'src'])), 91 | ('UDPUnregPort3', TestBlockUDP('block_unreg_ports.json', 65535, 92 | remote_module='port_filter_remote.py', 93 | remote_args=[CONF['target_host'], str(65535), 'UDP', 'src'])), 94 | ] 95 | -------------------------------------------------------------------------------- /test/acceptance/local/pywall_acceptance_test_case.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | from __future__ import print_function 3 | from os.path import join as pjoin 4 | from subprocess import Popen 5 | from datetime import timedelta 6 | from datetime import datetime 7 | from abc import ABCMeta 8 | from abc import abstractmethod 9 | import time 10 | import multiprocessing as mp 11 | import Queue 12 | import os 13 | import signal 14 | 15 | 16 | # copied from test/integraiton/pywall_test_case.py 17 | def run_pywall(config_file, **kwargs): 18 | import main 19 | main.run_pywall(config_file, None, None, kwargs) 20 | 21 | 22 | def Popen_wait(proc, timeout, interval=1): 23 | start = datetime.now() 24 | t = timedelta(seconds=timeout) 25 | while datetime.now() < start + t and proc.poll() is None: 26 | time.sleep(interval) 27 | try: 28 | proc.kill() 29 | except Exception as e: 30 | pass # not actually an error 31 | 32 | 33 | 34 | class PyWallAcceptanceTestCase(object): 35 | __metaclass__ = ABCMeta 36 | 37 | def __init__(self, config, host, remote_module='', 38 | remote_args=[], listener=None, key_file=None, timeout=3): 39 | self._config = pjoin('test/acceptance/local', config) 40 | self._host = host 41 | self._timeout = timeout 42 | self._key_file = key_file 43 | self._listener = listener 44 | self._remote_args = remote_args 45 | self._remote_module = remote_module 46 | 47 | def run_test_on_host(self, *args): 48 | cmd = ' '.join(['/usr/bin/env python2', 49 | pjoin('pywall/test/acceptance/remote', self._remote_module) 50 | ] + list(args)) 51 | ssh_args = ['ssh', self._host, 52 | '-i', self._key_file, 53 | cmd, 54 | ] 55 | return Popen(ssh_args) 56 | 57 | def run(self): 58 | passed = False 59 | 60 | # set up PyWall 61 | print('set up PyWall') 62 | sem = mp.Semaphore(0) 63 | pywall = mp.Process(target=run_pywall, args=(self._config,), kwargs={'test': True, 'lock': sem}) 64 | pywall.start() 65 | sem.acquire() # by here, pywall is running 66 | 67 | # set up listener 68 | print('set up listener') 69 | res_queue = mp.Queue() 70 | listener = mp.Process(target=self._listener.listen, args=(res_queue, sem)) 71 | listener.start() 72 | sem.acquire() # by here, listener is ready 73 | 74 | # call out to host 75 | print('call out to host') 76 | remote_proc = self.run_test_on_host(*self._remote_args) 77 | 78 | # wait a bit 79 | print('wait a bit') 80 | time.sleep(self._timeout) 81 | 82 | # merge in listener 83 | print('merge in listener') 84 | listener.join(3) 85 | try: 86 | passed = res_queue.get(timeout=1) 87 | except Queue.Empty as e: 88 | print(e) 89 | passed = True 90 | 91 | # merge in host 92 | print('merge in host') 93 | Popen_wait(remote_proc, self._timeout) 94 | 95 | # kill pywall 96 | print('kill pywall') 97 | os.kill(pywall.pid, signal.SIGINT) 98 | pywall.join(3) 99 | 100 | print('returning') 101 | return passed 102 | 103 | 104 | -------------------------------------------------------------------------------- /test/acceptance/local/tcp_listener.py: -------------------------------------------------------------------------------- 1 | from pywall_acceptance_test_case import BaseListener 2 | from datetime import datetime 3 | from datetime import timedelta 4 | 5 | 6 | class TCPListener(BaseListener): 7 | def __init__(self, host, port): 8 | self._port = port 9 | self._timeout = 5 10 | self._remote_host_ip = socket.gethostbyname(host) 11 | 12 | def listen(self, queue, sem): 13 | print('listening') 14 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | sock.bind(('0.0.0.0', self._port)) 16 | sock.listen(5) 17 | sock.settimeout(self._timeout) 18 | sem.release() 19 | 20 | start = datetime.now() 21 | print('before loop') 22 | try: 23 | while start + timedelta(seconds=self._timeout) < datetime.now(): 24 | s, (host_ip, host_port) = sock.accept() 25 | if host_ip == self._remote_host_ip: 26 | msg = s.recv(1024) 27 | print(msg) 28 | queue.put(msg == 'knock-knock') 29 | s.close() 30 | break 31 | s.close() 32 | sock.close() 33 | except socket.timeout: 34 | sock.close() 35 | queue.put(False) 36 | print('done listening') 37 | -------------------------------------------------------------------------------- /test/acceptance/local/test_connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple test case that checks whether the connection between remote and target 3 | host is working properly. 4 | """ 5 | from conf import CONF 6 | from pywall_acceptance_test_case import PyWallAcceptanceTestCase 7 | from listeners import TCPListener 8 | from listeners import UDPListener 9 | import socket 10 | 11 | 12 | class ExampleAcceptanceTestTCP(PyWallAcceptanceTestCase): 13 | def __init__(self, config, key_file=None, remote_module='', remote_args=[]): 14 | listener = TCPListener(CONF['remote_host'], CONF['port']) 15 | PyWallAcceptanceTestCase.__init__(self, config, '@'.join([CONF['user'], CONF['remote_host']]), 16 | remote_module=remote_module, 17 | remote_args=remote_args, 18 | key_file=CONF['key_file'], 19 | listener=listener) 20 | 21 | def run(self): 22 | return bool(PyWallAcceptanceTestCase.run(self)) 23 | 24 | 25 | class ExampleAcceptanceTestUDP(PyWallAcceptanceTestCase): 26 | def __init__(self, config, key_file=None, remote_module='', remote_args=[]): 27 | listener = UDPListener(CONF['port']) 28 | PyWallAcceptanceTestCase.__init__(self, config, '@'.join([CONF['user'], CONF['remote_host']]), 29 | remote_module=remote_module, 30 | remote_args=remote_args, 31 | key_file=CONF['key_file'], 32 | listener=listener) 33 | 34 | def run(self): 35 | return bool(PyWallAcceptanceTestCase.run(self)) 36 | 37 | 38 | tests = [ 39 | ('ExampleAcceptanceTest (TCP)', ExampleAcceptanceTestTCP('example_config.json', 40 | remote_module='example_test_remote.py', 41 | remote_args=[CONF['target_host'], str(CONF['port'])])), 42 | ('ExampleAcceptanceTest (UDP)', ExampleAcceptanceTestUDP('example_config.json', 43 | remote_module='example_test_remote.py', 44 | remote_args=[CONF['target_host'], str(CONF['port']), 45 | 'UDP'])), 46 | ] 47 | -------------------------------------------------------------------------------- /test/acceptance/remote/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/acceptance/remote/__init__.py -------------------------------------------------------------------------------- /test/acceptance/remote/example_test_remote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | from __future__ import print_function 3 | import socket 4 | import sys 5 | import time 6 | 7 | 8 | def rprint(msg): 9 | """Print a message labelled from remote""" 10 | print('[REMOTE] %s' % msg) 11 | 12 | 13 | if __name__ == '__main__': 14 | rprint('hello from remote!') 15 | target_host = socket.gethostbyname(sys.argv[1]) 16 | target_port = int(sys.argv[2]) if len(sys.argv) >= 3 else 9001 17 | target = (target_host, target_port) 18 | rprint('target: %s' % str(target)) 19 | sock_type = sys.argv[3] if len(sys.argv) >= 4 else 'TCP' 20 | timeout = int(sys.argv[4]) if len(sys.argv) >= 5 else 5 21 | 22 | time.sleep(2) 23 | 24 | if sock_type == 'TCP': 25 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 26 | s.settimeout(timeout) 27 | try: 28 | s.connect(target) 29 | s.send('knock-knock') 30 | except socket.timeout: 31 | rprint('remote socket timeout') 32 | elif sock_type == 'UDP': 33 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 34 | s.settimeout(timeout) 35 | try: 36 | s.sendto('knock-knock', target) 37 | except socket.timeout: 38 | rprint('remote socket timeout') 39 | else: 40 | rprint('invalid socket type') 41 | 42 | rprint('remote out') 43 | -------------------------------------------------------------------------------- /test/acceptance/remote/port_filter_remote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | from __future__ import print_function 3 | import socket 4 | import sys 5 | import time 6 | 7 | 8 | def rprint(msg): 9 | """Print a message labelled from remote""" 10 | print('[REMOTE] %s' % msg) 11 | 12 | 13 | 14 | if __name__ == '__main__': 15 | rprint('hello from remote!') 16 | if len(sys.argv) >= 5: 17 | target_host = socket.gethostbyname(sys.argv[1]) 18 | target_port = int(sys.argv[2]) 19 | target = (target_host, target_port) 20 | 21 | timeout = 5 22 | sock_type = sys.argv[3] 23 | port_type = sys.argv[4] 24 | 25 | time.sleep(2) 26 | 27 | if sock_type == 'TCP': 28 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | s.settimeout(timeout) 30 | if port_type == 'src': 31 | s.bind(('', target_port)) 32 | try: 33 | s.connect(target) 34 | s.send('knock-knock') 35 | except socket.timeout: 36 | rprint('remote socket timeout') 37 | s.close() 38 | elif sock_type == 'UDP': 39 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | s.settimeout(timeout) 41 | if port_type == 'src': 42 | s.bind(('', target_port)) 43 | try: 44 | s.sendto('knock-knock', target) 45 | except socket.timeout: 46 | rprint('remote socket timeout') 47 | s.close() 48 | else: 49 | rprint('invlaid socket type') 50 | else: 51 | rprint('Insufficient number of args: %d' % len(sys.argv)) 52 | rprint('remote out') 53 | -------------------------------------------------------------------------------- /test/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/integration/__init__.py -------------------------------------------------------------------------------- /test/integration/block_local_dst_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PrintRule"}, 5 | {"name": "PortRule", 6 | "protocol": "TCP", 7 | "dst_port": 80, 8 | "action": "DROP"}, 9 | {"name": "PortRule", 10 | "protocol": "UDP", 11 | "dst_port": 53, 12 | "action": "DROP"} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/block_local_src_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PrintRule"}, 5 | {"name": "PortRule", 6 | "protocol": "TCP", 7 | "src_port": 80, 8 | "action": "DROP"}, 9 | {"name": "PortRule", 10 | "protocol": "UDP", 11 | "src_port": 53, 12 | "action": "DROP"} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/block_local_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PrintRule"}, 5 | {"name": "PortRule", 6 | "protocol": "TCP", 7 | "src_port": 80, 8 | "action": "DROP"}, 9 | {"name": "PortRule", 10 | "protocol": "TCP", 11 | "dst_port": 80, 12 | "action": "DROP"}, 13 | {"name": "PortRule", 14 | "protocol": "UDP", 15 | "src_port": 53, 16 | "action": "DROP"}, 17 | {"name": "PortRule", 18 | "protocol": "UDP", 19 | "dst_port": 53, 20 | "action": "DROP"} 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/integration/block_unreg_dst_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRangeRule", 5 | "protocol": "TCP", 6 | "dst_lo": 49151, 7 | "dst_hi": 65535, 8 | "action": "DROP"}, 9 | {"name": "PortRangeRule", 10 | "protocol": "UDP", 11 | "dst_lo": 49151, 12 | "dst_hi": 65535, 13 | "action": "DROP"} 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/integration/block_unreg_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PrintRule"}, 5 | {"name": "PortRangeRule", 6 | "protocol": "TCP", 7 | "src_lo": 49151, 8 | "src_hi": 65535, 9 | "action": "DROP"}, 10 | {"name": "PortRangeRule", 11 | "protocol": "TCP", 12 | "dst_lo": 49151, 13 | "dst_hi": 65535, 14 | "action": "DROP"}, 15 | {"name": "PortRangeRule", 16 | "protocol": "UDP", 17 | "src_lo": 49151, 18 | "src_hi": 65535, 19 | "action": "DROP"}, 20 | {"name": "PortRangeRule", 21 | "protocol": "UDP", 22 | "dst_lo": 49151, 23 | "dst_hi": 65535, 24 | "action": "DROP"} 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/integration/block_unreg_src_ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "PortRangeRule", 5 | "protocol": "TCP", 6 | "src_lo": 49151, 7 | "src_hi": 65535, 8 | "action": "DROP"}, 9 | {"name": "PortRangeRule", 10 | "protocol": "UDP", 11 | "src_lo": 49151, 12 | "src_hi": 65535, 13 | "action": "DROP"} 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/integration/destination_ip_rule_in_range_accept.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "DROP", 3 | "INPUT": [ 4 | {"name": "DestinationIPRule", 5 | "cidr_range": "127.0.0.0/24", 6 | "action": "ACCEPT"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/destination_ip_rule_in_range_drop.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "DestinationIPRule", 5 | "cidr_range": "127.0.0.0/24", 6 | "action": "DROP"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/destination_ip_rule_out_of_range.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "DestinationIPRule", 5 | "cidr_range": "127.0.255.0/24", 6 | "action": "DROP"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/destination_ip_rule_test.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import PyWallTestCase 2 | from tcp_server_process import TCPServerProcess 3 | import socket 4 | import select 5 | 6 | 7 | class DestinationIPRuleTest(PyWallTestCase): 8 | def __init__(self, config_filename, port, client_timeout=1, server_timeout=5, expected_num_connections=1): 9 | self.port = port 10 | self.timeout = client_timeout 11 | PyWallTestCase.__init__(self, config_filename, TCPServerProcess(port, server_timeout, expected_num_connections)) 12 | 13 | def client_request(self): 14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | s.settimeout(self.timeout) 16 | try: 17 | s.connect(('localhost', self.port)) 18 | except socket.timeout: 19 | return 20 | finally: 21 | s.close() 22 | 23 | 24 | tests = [ 25 | ('DestinationIPRuleOutOfRangeTest', DestinationIPRuleTest('test/integration/destination_ip_rule_out_of_range.json', 58008, client_timeout=1, server_timeout=5)), 26 | ('DestinationIPRuleInRangeAcceptTest', DestinationIPRuleTest('test/integration/destination_ip_rule_in_range_accept.json', 58008, client_timeout=1, server_timeout=5)), 27 | ('DestinationIPRuleInRangeDropTest', DestinationIPRuleTest('test/integration/destination_ip_rule_in_range_drop.json', 58008, client_timeout=1, server_timeout=5, expected_num_connections=0)), 28 | ] 29 | -------------------------------------------------------------------------------- /test/integration/port_filter_test.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import PyWallTestCase 2 | from tcp_server_process import TCPServerProcess 3 | from tcp_connection_test import TCPConnectionTest 4 | from udp_connection_test import UDPConnectionTest 5 | 6 | import socket 7 | import select 8 | 9 | tests = [ 10 | # Tests src_port filtering 11 | ('TCPUnregSrcPort1', TCPConnectionTest('test/integration/block_unreg_src_ports.json', 49151, src_port=49251, expected_num_connections=0)), 12 | ('TCPUnregSrcPort2', TCPConnectionTest('test/integration/block_unreg_src_ports.json', 49152, src_port=49252, expected_num_connections=0)), 13 | ('TCPUnregSrcPort3', TCPConnectionTest('test/integration/block_unreg_src_ports.json', 65535, src_port=65435, expected_num_connections=0)), 14 | ('UDPUnregSrcPort1', UDPConnectionTest('test/integration/block_unreg_src_ports.json', 49151, src_port=49251, expected_num_connections=0)), 15 | ('UDPUnregSrcPort2', UDPConnectionTest('test/integration/block_unreg_src_ports.json', 49152, src_port=49252, expected_num_connections=0)), 16 | ('UDPUnregSrcPort3', UDPConnectionTest('test/integration/block_unreg_src_ports.json', 65535, src_port=64535, expected_num_connections=0)), 17 | 18 | # Tests dst_port filters 19 | ('TCPUnregDstPort1', TCPConnectionTest('test/integration/block_unreg_dst_ports.json', 49151, expected_num_connections=0)), 20 | ('TCPUnregDstPort2', TCPConnectionTest('test/integration/block_unreg_dst_ports.json', 49152, expected_num_connections=0)), 21 | ('TCPUnregDstPort3', TCPConnectionTest('test/integration/block_unreg_dst_ports.json', 65535, expected_num_connections=0)), 22 | ('UDPUnregDstPort1', UDPConnectionTest('test/integration/block_unreg_dst_ports.json', 49151, expected_num_connections=0)), 23 | ('UDPUnregDstPort2', UDPConnectionTest('test/integration/block_unreg_dst_ports.json', 49152, expected_num_connections=0)), 24 | ('UDPUnregDstPort3', UDPConnectionTest('test/integration/block_unreg_dst_ports.json', 65535, expected_num_connections=0)), 25 | 26 | # Tests combo dst/src_port filters 27 | ('TCPUnregPort1', TCPConnectionTest('test/integration/block_unreg_ports.json', 49151, src_port=49251, expected_num_connections=0)), 28 | ('TCPUnregPort2', TCPConnectionTest('test/integration/block_unreg_ports.json', 49152, src_port=49252, expected_num_connections=0)), 29 | ('TCPUnregPort3', TCPConnectionTest('test/integration/block_unreg_ports.json', 65535, src_port=65435, expected_num_connections=0)), 30 | ('UDPUnregPort1', UDPConnectionTest('test/integration/block_unreg_ports.json', 49151, src_port=49251, expected_num_connections=0)), 31 | ('UDPUnregPort2', UDPConnectionTest('test/integration/block_unreg_ports.json', 49152, src_port=49252, expected_num_connections=0)), 32 | ('UDPUnregPort3', UDPConnectionTest('test/integration/block_unreg_ports.json', 65535, src_port=65435, expected_num_connections=0)), 33 | ] 34 | -------------------------------------------------------------------------------- /test/integration/port_knock_test.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import PyWallTestCase 2 | from tcp_server_process import TCPServerProcess 3 | import socket 4 | import time 5 | 6 | 7 | class PKTest(PyWallTestCase): 8 | def __init__(self, config_filename, doors, port, src_port): 9 | self._timeout = 1 10 | self._doors = doors 11 | PyWallTestCase.__init__(self, config_filename, TCPServerProcess(port, timeout=(5*self._timeout))) 12 | self._port = port 13 | self._src_port = src_port 14 | self._body = 'knock-knock' 15 | 16 | def client_request(self): 17 | print(self._doors) 18 | for proto, port in self._doors: 19 | print(proto) 20 | if proto == 'TCP': 21 | print('prepping tcp') 22 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 | s.settimeout(self._timeout) 24 | s.bind(('', self._src_port)) 25 | try: 26 | s.connect(('', port)) 27 | s.close() 28 | except socket.error: 29 | # if firewall is doing its job, connection should be refused. 30 | pass 31 | elif proto == 'UDP': 32 | print('prepping udp') 33 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 34 | s.settimeout(self._timeout) 35 | s.bind(('', self._src_port)) 36 | s.sendto(self._body, ('', port)) 37 | s.close() 38 | time.sleep(1) 39 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | s.settimeout(self._timeout) 41 | try: 42 | s.connect(('localhost', self._port)) 43 | except socket.timeout: 44 | print('Final socket timeout!!') 45 | 46 | 47 | tests = [ 48 | ('PortKnockingTest', PKTest('test/integration/port_knocking_test.json', [('TCP', 49001), ('UDP', 49011)], 52222, 9001)), 49 | ] 50 | -------------------------------------------------------------------------------- /test/integration/port_knocking_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT":[ 4 | {"name":"PrintRule"}, 5 | {"name":"PortKnocking", 6 | "src_port": 9001, 7 | "protocol": "TCP", 8 | "port": 2222, 9 | "timeout": 20, 10 | "doors": [["TCP", 49001], ["UDP", 49011]]}, 11 | {"name": "PortRule", 12 | "protocol": "TCP", 13 | "dst_port": 2222, 14 | "action": "DROP"} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/integration/pywall_test_case.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """Test bench for PyWall.""" 3 | 4 | from __future__ import print_function 5 | import multiprocessing as mp 6 | import socket 7 | import os 8 | import signal 9 | import time 10 | import select 11 | 12 | 13 | def run_pywall(config_file, **kwargs): 14 | import main 15 | main.run_pywall(config_file, None, None, kwargs) 16 | 17 | 18 | class ServerProcess(object): 19 | def run(self, q): 20 | self.setup_socket() 21 | res = self.wait_socket() 22 | print('after wait_socket') 23 | q.put(res) 24 | print('put in queue') 25 | 26 | 27 | class PyWallTestCase(object): 28 | def __init__(self, config_filename, server, server_sleep_time=1): 29 | self.config_filename = config_filename 30 | self.server_sleep_time = server_sleep_time 31 | self.set_server(server) 32 | 33 | def set_server(self, server): 34 | self.queue = mp.Queue() 35 | self.server = server 36 | self.server_process = mp.Process(target=server.run, 37 | args=(self.queue,)) 38 | 39 | def run(self): 40 | time.sleep(self.server_sleep_time) 41 | sem = mp.Semaphore(0) 42 | self.wall_process = mp.Process(target=run_pywall, 43 | args=(self.config_filename,), kwargs={'test':True, 'lock':sem}) 44 | self.wall_process.start() 45 | sem.acquire() # firewall is ready here 46 | self.server_process.start() 47 | time.sleep(self.server_sleep_time) 48 | self.client_request() 49 | self.server_process.join() 50 | passed = self.queue.get() 51 | os.kill(self.wall_process.pid, signal.SIGINT) 52 | self.wall_process.join() 53 | return passed 54 | -------------------------------------------------------------------------------- /test/integration/source_ip_rule_in_range_accept.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "DROP", 3 | "INPUT": [ 4 | {"name": "SourceIPRule", 5 | "cidr_range": "127.0.0.0/24", 6 | "action": "ACCEPT"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/source_ip_rule_in_range_drop.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "SourceIPRule", 5 | "cidr_range": "127.0.0.0/24", 6 | "action": "DROP"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/source_ip_rule_out_of_range.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "ACCEPT", 3 | "INPUT": [ 4 | {"name": "SourceIPRule", 5 | "cidr_range": "127.0.255.0/24", 6 | "action": "DROP"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/source_ip_rule_test.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import PyWallTestCase 2 | from tcp_server_process import TCPServerProcess 3 | import socket 4 | import select 5 | 6 | 7 | class SourceIPRuleTest(PyWallTestCase): 8 | def __init__(self, config_filename, port, client_timeout=1, server_timeout=5, expected_num_connections=1): 9 | self.port = port 10 | self.timeout = client_timeout 11 | PyWallTestCase.__init__(self, config_filename, TCPServerProcess(port, server_timeout, expected_num_connections)) 12 | 13 | def client_request(self): 14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | s.settimeout(self.timeout) 16 | try: 17 | s.connect(('localhost', self.port)) 18 | except socket.timeout: 19 | return 20 | finally: 21 | s.close() 22 | 23 | 24 | tests = [ 25 | ('SourceIPRuleOutOfRangeTest', SourceIPRuleTest('test/integration/source_ip_rule_out_of_range.json', 58008, client_timeout=1, server_timeout=5)), 26 | ('SourceIPRuleInRangeAcceptTest', SourceIPRuleTest('test/integration/source_ip_rule_in_range_accept.json', 58008, client_timeout=1, server_timeout=5)), 27 | ('SourceIPRuleInRangeDropTest', SourceIPRuleTest('test/integration/source_ip_rule_in_range_drop.json', 58008, client_timeout=1, server_timeout=5, expected_num_connections=0)), 28 | ] 29 | -------------------------------------------------------------------------------- /test/integration/tcp_connection.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_chain": "DROP", 3 | "INPUT": [ 4 | {"name": "SourceIPRule", 5 | "cidr_range": "127.0.0.1", 6 | "action": "ACCEPT"} 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/tcp_connection_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from pywall_test_case import PyWallTestCase 4 | from tcp_server_process import TCPServerProcess 5 | import socket 6 | 7 | 8 | class TCPConnectionTest(PyWallTestCase): 9 | def __init__(self, config_filename, port, src_port=None, client_timeout=1, 10 | server_timeout=5, expected_num_connections=1): 11 | self.port = port 12 | self.src_port = src_port 13 | self.timeout = client_timeout 14 | PyWallTestCase.__init__(self, config_filename, 15 | TCPServerProcess(port, server_timeout, expected_num_connections)) 16 | 17 | def client_request(self): 18 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 19 | s.settimeout(self.timeout) 20 | if self.src_port: 21 | s.bind(('', self.src_port)) 22 | try: 23 | s.connect(('localhost', self.port)) 24 | except socket.timeout: 25 | print('socket timeout') 26 | return 27 | 28 | 29 | tests = [ 30 | ('TCPConnectionTest', TCPConnectionTest('test/integration/tcp_connection.json', 58008, client_timeout=1, server_timeout=5)), 31 | ] 32 | -------------------------------------------------------------------------------- /test/integration/tcp_server_process.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import ServerProcess 2 | 3 | import socket 4 | import select 5 | 6 | 7 | class TCPServerProcess(ServerProcess): 8 | def __init__(self, port, timeout=5, expected_num_connections=1): 9 | self.port = port 10 | self.timeout = timeout 11 | self.expected_num_connections = expected_num_connections 12 | 13 | def setup_socket(self): 14 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 16 | self.sock.bind(('localhost', self.port)) 17 | self.sock.listen(5) 18 | 19 | def wait_socket(self): 20 | print('Waiting on server socket.') 21 | rlist, _, __ = select.select([self.sock], [], [], self.timeout) 22 | print('tcp_rlist_len: %d' % (len(rlist))) 23 | self.sock.close() 24 | return (len(rlist) == self.expected_num_connections) 25 | -------------------------------------------------------------------------------- /test/integration/udp_connection_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from pywall_test_case import PyWallTestCase 4 | from udp_server_process import UDPServerProcess 5 | import socket 6 | 7 | 8 | class UDPConnectionTest(PyWallTestCase): 9 | def __init__(self, config_filename, port, src_port=None, client_timeout=1, 10 | server_timeout=5, expected_num_connections=1): 11 | self.port = port 12 | self.src_port = src_port 13 | self.timeout = client_timeout 14 | PyWallTestCase.__init__(self, config_filename, 15 | UDPServerProcess(port, server_timeout, expected_num_connections)) 16 | 17 | def client_request(self): 18 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | if self.src_port: 20 | s.bind(('', self.src_port)) 21 | s.sendto("hi!", ('localhost', self.port)) 22 | s.close() 23 | 24 | 25 | tests = [ 26 | ('UDPConnectionTest', UDPConnectionTest('test/integration/tcp_connection.json', 58008, client_timeout=1, server_timeout=5)), 27 | ] 28 | -------------------------------------------------------------------------------- /test/integration/udp_server_process.py: -------------------------------------------------------------------------------- 1 | from pywall_test_case import ServerProcess 2 | 3 | import socket 4 | import select 5 | 6 | 7 | class UDPServerProcess(ServerProcess): 8 | def __init__(self, port, timeout=5, expected_num_connections=1): 9 | self.port = port 10 | self.timeout = timeout 11 | self.expected_num_connections = expected_num_connections 12 | 13 | def setup_socket(self): 14 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 15 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 16 | self.sock.bind(('localhost', self.port)) 17 | 18 | def wait_socket(self): 19 | print('Waiting on server socket.') 20 | rlist, _, __ = select.select([self.sock], [], [], self.timeout) 21 | print('udp_rlist_len: %d' % (len(rlist))) 22 | self.sock.close() 23 | return (len(rlist) == self.expected_num_connections) 24 | -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brenns10/pywall/63a5fc71e5a6cc88dfa11a85fb11ac07e9ed983f/test/unit/__init__.py -------------------------------------------------------------------------------- /test/unit/fake_packet.py: -------------------------------------------------------------------------------- 1 | class FakePacket(object): 2 | 3 | def __init__(self, **kwargs): 4 | self._src_ip = kwargs.get('src_ip') 5 | self._dst_ip = kwargs.get('dst_ip') 6 | self._protocol = kwargs.get('protocol') 7 | self._payload = FakeTransportPacket(**kwargs) 8 | 9 | def get_src_ip(self): 10 | return self._src_ip 11 | 12 | def get_dst_ip(self): 13 | return self._dst_ip 14 | 15 | def get_protocol(self): 16 | return self._protocol 17 | 18 | def get_payload(self): 19 | return self._payload 20 | 21 | 22 | class FakeTransportPacket(object): 23 | 24 | def __init__(self, **kwargs): 25 | self._src_port = kwargs.get('src_port') 26 | self._dst_port = kwargs.get('dst_port') 27 | 28 | def get_src_port(self): 29 | return self._src_port 30 | 31 | def get_dst_port(self): 32 | return self._dst_port 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/unit/test_ip_rules.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from rules import ip_rules 3 | from test.unit.fake_packet import FakePacket 4 | 5 | 6 | class TestSourceIPRule(unittest.TestCase): 7 | 8 | def test_in_range(self): 9 | rule = ip_rules.SourceIPRule(cidr_range='127.0.0.0/24') 10 | packet = FakePacket(src_ip='127.0.0.0') 11 | self.assertTrue(rule.filter_condition(packet)) 12 | 13 | def test_out_of_range(self): 14 | rule = ip_rules.SourceIPRule(cidr_range='127.0.0.0/24') 15 | packet = FakePacket(src_ip='128.0.0.0') 16 | self.assertFalse(rule.filter_condition(packet)) 17 | 18 | def test_without_cidr_range(self): 19 | rule = ip_rules.SourceIPRule(cidr_range='127.0.0.0') 20 | packet1 = FakePacket(src_ip='127.0.0.0') 21 | packet2 = FakePacket(src_ip='127.0.0.1') 22 | self.assertTrue(rule.filter_condition(packet1)) 23 | self.assertFalse(rule.filter_condition(packet2)) 24 | 25 | 26 | class TestDestinationIPRule(unittest.TestCase): 27 | 28 | def test_in_range(self): 29 | rule = ip_rules.DestinationIPRule(cidr_range='127.0.0.0/24') 30 | packet = FakePacket(dst_ip='127.0.0.0') 31 | self.assertTrue(rule.filter_condition(packet)) 32 | 33 | def test_out_of_range(self): 34 | rule = ip_rules.DestinationIPRule(cidr_range='127.0.0.0/24') 35 | packet = FakePacket(dst_ip='128.0.0.0') 36 | self.assertFalse(rule.filter_condition(packet)) 37 | 38 | def test_without_cidr_range(self): 39 | rule = ip_rules.DestinationIPRule(cidr_range='127.0.0.0') 40 | packet1 = FakePacket(dst_ip='127.0.0.0') 41 | packet2 = FakePacket(dst_ip='127.0.0.1') 42 | self.assertTrue(rule.filter_condition(packet1)) 43 | self.assertFalse(rule.filter_condition(packet2)) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | 49 | -------------------------------------------------------------------------------- /test/unit/test_port_filter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | from rules import port_filter 4 | from test.unit.fake_packet import FakePacket 5 | 6 | 7 | class TestPortRule(unittest.TestCase): 8 | 9 | def test_udp_filter_with_tcp_packet(self): 10 | rule = port_filter.PortRule(protocol='UDP', src_port=0) 11 | packet = FakePacket(protocol=socket.IPPROTO_TCP) 12 | self.assertFalse(rule.filter_condition(packet)) 13 | 14 | def test_udp_filter_with_udp_matching_src_port(self): 15 | rule = port_filter.PortRule(protocol='UDP', src_port=0) 16 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0) 17 | self.assertTrue(rule.filter_condition(packet)) 18 | 19 | def test_upd_filter_udp_packet_wrong_src_port(self): 20 | rule = port_filter.PortRule(protocol='UDP', src_port=0) 21 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=1) 22 | self.assertFalse(rule.filter_condition(packet)) 23 | 24 | def test_udp_filter_udp_packet_matching_dst_port(self): 25 | rule = port_filter.PortRule(protocol='UDP', dst_port=0) 26 | packet = FakePacket(protocol=socket.IPPROTO_UDP, dst_port=0) 27 | self.assertTrue(rule.filter_condition(packet)) 28 | 29 | def test_udp_filter_udp_packet_wrong_dst_port(self): 30 | rule = port_filter.PortRule(protocol='UDP', dst_port=0) 31 | packet = FakePacket(protocol=socket.IPPROTO_UDP, dst_port=1) 32 | self.assertFalse(rule.filter_condition(packet)) 33 | 34 | def test_udp_filter_udp_packet_matching_src_and_dst(self): 35 | rule = port_filter.PortRule(protocol='UDP', src_port=0, dst_port=0) 36 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_port=0) 37 | self.assertTrue(rule.filter_condition(packet)) 38 | 39 | def test_udp_filter_udp_packet_right_src_wrong_dst(self): 40 | rule = port_filter.PortRule(protocol='UDP', src_port=0, dst_port=0) 41 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_port=1) 42 | self.assertFalse(rule.filter_condition(packet)) 43 | 44 | def test_udp_filter_udp_packet_wrong_src_right_dst(self): 45 | rule = port_filter.PortRule(protocol='UDP', src_port=0, dst_port=0) 46 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=1, dst_port=1) 47 | self.assertFalse(rule.filter_condition(packet)) 48 | 49 | def test_udp_filter_udp_packet_wrong_src_wrong_dst(self): 50 | rule = port_filter.PortRule(protocol='UDP', src_port=0, dst_port=0) 51 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=1, dst_port=1) 52 | self.assertFalse(rule.filter_condition(packet)) 53 | 54 | def test_tcp_filter_udp_packet(self): 55 | rule = port_filter.PortRule(protocol='TCP', src_port=0) 56 | packet = FakePacket(protocol=socket.IPPROTO_UDP) 57 | self.assertFalse(rule.filter_condition(packet)) 58 | 59 | def test_tcp_filter_tcp_packet_matching_src_port(self): 60 | rule = port_filter.PortRule(protocol='TCP', src_port=0) 61 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0) 62 | self.assertTrue(rule.filter_condition(packet)) 63 | 64 | def test_tcd_filter_tcp_packet_wrong_src_port(self): 65 | rule = port_filter.PortRule(protocol='TCP', src_port=0) 66 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=1) 67 | self.assertFalse(rule.filter_condition(packet)) 68 | 69 | def test_tcp_filter_tcp_packet_matching_dst_port(self): 70 | rule = port_filter.PortRule(protocol='TCP', dst_port=0) 71 | packet = FakePacket(protocol=socket.IPPROTO_TCP, dst_port=0) 72 | self.assertTrue(rule.filter_condition(packet)) 73 | 74 | def test_tcp_filter_tcp_packet_wrong_dst_port(self): 75 | rule = port_filter.PortRule(protocol='TCP', dst_port=0) 76 | packet = FakePacket(protocol=socket.IPPROTO_TCP, dst_port=1) 77 | self.assertFalse(rule.filter_condition(packet)) 78 | 79 | def test_tcp_filter_tcp_packet_matching_src_and_dst(self): 80 | rule = port_filter.PortRule(protocol='TCP', src_port=0, dst_port=0) 81 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, dst_port=0) 82 | self.assertTrue(rule.filter_condition(packet)) 83 | 84 | def test_tcp_filter_tcp_packet_right_src_wrong_dst(self): 85 | rule = port_filter.PortRule(protocol='TCP', src_port=0, dst_port=0) 86 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, dst_port=1) 87 | self.assertFalse(rule.filter_condition(packet)) 88 | 89 | def test_tcp_filter_tcp_packet_wrong_src_right_dst(self): 90 | rule = port_filter.PortRule(protocol='TCP', src_port=0, dst_port=0) 91 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=1, dst_port=1) 92 | self.assertFalse(rule.filter_condition(packet)) 93 | 94 | def test_tcp_filter_tcp_packet_wrong_src_wrong_dst(self): 95 | rule = port_filter.PortRule(protocol='TCP', src_port=0, dst_port=0) 96 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=1, dst_port=1) 97 | self.assertFalse(rule.filter_condition(packet)) 98 | 99 | class TestPortRangeRule(unittest.TestCase): 100 | 101 | def test_udp_filter_tcp_packet(self): 102 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1) 103 | packet = FakePacket(protocol=socket.IPPROTO_TCP) 104 | self.assertFalse(rule.filter_condition(packet)) 105 | 106 | def test_udp_filter_udp_packet_in_src_range(self): 107 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1) 108 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0) 109 | self.assertTrue(rule.filter_condition(packet)) 110 | 111 | def test_udp_filter_udp_packet_out_src_range(self): 112 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1) 113 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5) 114 | self.assertFalse(rule.filter_condition(packet)) 115 | 116 | def test_udp_filter_udp_packet_in_dst_range(self): 117 | rule = port_filter.PortRangeRule(protocol='UDP', dst_lo=0, dst_hi=1) 118 | packet = FakePacket(protocol=socket.IPPROTO_UDP, dst_port=0) 119 | self.assertTrue(rule.filter_condition(packet)) 120 | 121 | def test_udp_filter_udp_packet_out_dst_range(self): 122 | rule = port_filter.PortRangeRule(protocol='UDP', dst_lo=0, dst_hi=1) 123 | packet = FakePacket(protocol=socket.IPPROTO_UDP, dst_port=5) 124 | self.assertFalse(rule.filter_condition(packet)) 125 | 126 | def test_udp_filter_udp_packet_in_src_range_and_dst_range(self): 127 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 128 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_port=0) 129 | self.assertTrue(rule.filter_condition(packet)) 130 | 131 | def test_udp_filter_udp_packet_in_src_range_not_dst_range(self): 132 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 133 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_port=5) 134 | self.assertFalse(rule.filter_condition(packet)) 135 | 136 | def test_udp_filter_udp_packet_in_dst_range_not_src_range(self): 137 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 138 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, dst_port=0) 139 | self.assertFalse(rule.filter_condition(packet)) 140 | 141 | def test_udp_filter_udp_packet_not_in_src_range_or_dst_range(self): 142 | rule = port_filter.PortRangeRule(protocol='UDP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 143 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, dst_port=5) 144 | self.assertFalse(rule.filter_condition(packet)) 145 | 146 | def test_tcp_filter_udp_packet(self): 147 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1) 148 | packet = FakePacket(protocol=socket.IPPROTO_UDP) 149 | self.assertFalse(rule.filter_condition(packet)) 150 | 151 | def test_tcp_filter_tcp_packet_in_src_range(self): 152 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1) 153 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0) 154 | self.assertTrue(rule.filter_condition(packet)) 155 | 156 | def test_tcp_filter_tcp_packet_out_src_range(self): 157 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1) 158 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=5) 159 | self.assertFalse(rule.filter_condition(packet)) 160 | 161 | def test_tcp_filter_tcp_packet_in_dst_range(self): 162 | rule = port_filter.PortRangeRule(protocol='TCP', dst_lo=0, dst_hi=1) 163 | packet = FakePacket(protocol=socket.IPPROTO_TCP, dst_port=0) 164 | self.assertTrue(rule.filter_condition(packet)) 165 | 166 | def test_tcp_filter_tcp_packet_out_dst_range(self): 167 | rule = port_filter.PortRangeRule(protocol='TCP', dst_lo=0, dst_hi=1) 168 | packet = FakePacket(protocol=socket.IPPROTO_TCP, dst_port=5) 169 | self.assertFalse(rule.filter_condition(packet)) 170 | 171 | def test_tcp_filter_tcp_packet_in_src_range_and_dst_range(self): 172 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 173 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, dst_port=0) 174 | self.assertTrue(rule.filter_condition(packet)) 175 | 176 | def test_tcp_filter_tcp_packet_in_src_range_not_dst_range(self): 177 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 178 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, dst_port=5) 179 | self.assertFalse(rule.filter_condition(packet)) 180 | 181 | def test_tcp_filter_tcp_packet_in_dst_range_not_src_range(self): 182 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 183 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=5, dst_port=0) 184 | self.assertFalse(rule.filter_condition(packet)) 185 | 186 | def test_tcp_filter_tcp_packet_not_in_src_range_or_dst_range(self): 187 | rule = port_filter.PortRangeRule(protocol='TCP', src_lo=0, src_hi=1, dst_lo=0, dst_hi=1) 188 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=5, dst_port=5) 189 | self.assertFalse(rule.filter_condition(packet)) 190 | 191 | 192 | if __name__ == '__main__': 193 | unittest.main() 194 | 195 | -------------------------------------------------------------------------------- /test/unit/test_port_ip_rule.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from rules.port_ip_rule import IPPortRule 3 | from test.unit.fake_packet import FakePacket 4 | import socket 5 | 6 | 7 | class TestIPPortRule(unittest.TestCase): 8 | 9 | def test_port_rule_only_match(self): 10 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1) 11 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0) 12 | self.assertTrue(rule.filter_condition(packet)) 13 | 14 | def test_port_rule_only_mismatch(self): 15 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1) 16 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5) 17 | self.assertFalse(rule.filter_condition(packet)) 18 | 19 | def test_port_and_src_rule_both_match(self): 20 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24') 21 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='127.0.0.0') 22 | self.assertTrue(rule.filter_condition(packet)) 23 | 24 | def test_port_and_src_rule_port_fails(self): 25 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24') 26 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, src_ip='127.0.0.0') 27 | self.assertFalse(rule.filter_condition(packet)) 28 | 29 | def test_port_and_src_rule_src_fails(self): 30 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24') 31 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='128.0.0.0') 32 | self.assertFalse(rule.filter_condition(packet)) 33 | 34 | def test_port_and_src_rule_both_fail(self): 35 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24') 36 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, src_ip='128.0.0.0') 37 | self.assertFalse(rule.filter_condition(packet)) 38 | 39 | def test_port_and_dst_rule_both_match(self): 40 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, dst_ip='127.0.0.0/24') 41 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_ip='127.0.0.0') 42 | self.assertTrue(rule.filter_condition(packet)) 43 | 44 | def test_port_and_dst_rule_port_fails(self): 45 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, dst_ip='127.0.0.0/24') 46 | packet = FakePacket(protocol=socket.IPPROTO_TCP, src_port=0, dst_ip='127.0.0.0') 47 | self.assertFalse(rule.filter_condition(packet)) 48 | 49 | def test_port_and_dst_rule_dst_fails(self): 50 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, dst_ip='127.0.0.0/24') 51 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, dst_ip='128.0.0.0') 52 | self.assertFalse(rule.filter_condition(packet)) 53 | 54 | def test_port_and_dst_rule_both_fail(self): 55 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, dst_ip='127.0.0.0/24') 56 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, dst_ip='128.0.0.0') 57 | self.assertFalse(rule.filter_condition(packet)) 58 | 59 | def test_port_src_and_dst_rule_all_match(self): 60 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 61 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='127.0.0.0', dst_ip='127.0.0.0') 62 | self.assertTrue(rule.filter_condition(packet)) 63 | 64 | def test_port_src_and_dst_rule_port_fails(self): 65 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 66 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, src_ip='127.0.0.0', dst_ip='127.0.0.0') 67 | self.assertFalse(rule.filter_condition(packet)) 68 | 69 | def test_port_src_and_dst_rule_src_fails(self): 70 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 71 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='128.0.0.0', dst_ip='127.0.0.0') 72 | self.assertFalse(rule.filter_condition(packet)) 73 | 74 | def test_port_src_and_dst_rule_dst_fails(self): 75 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 76 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='127.0.0.0', dst_ip='128.0.0.0') 77 | self.assertFalse(rule.filter_condition(packet)) 78 | 79 | def test_port_src_and_dst_rule_port_and_src_fail(self): 80 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 81 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, src_ip='128.0.0.0', dst_ip='127.0.0.0') 82 | self.assertFalse(rule.filter_condition(packet)) 83 | 84 | def test_port_src_and_dst_rule_port_and_dst_fail(self): 85 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 86 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, src_ip='127.0.0.0', dst_ip='128.0.0.0') 87 | self.assertFalse(rule.filter_condition(packet)) 88 | 89 | def test_port_src_and_dst_rule_src_and_dst_fail(self): 90 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 91 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=0, src_ip='128.0.0.0', dst_ip='128.0.0.0') 92 | self.assertFalse(rule.filter_condition(packet)) 93 | 94 | def test_port_src_and_dst_rule_all_fail(self): 95 | rule = IPPortRule(protocol='UDP', src_lo=0, src_hi=1, src_ip='127.0.0.0/24', dst_ip='127.0.0.0/24') 96 | packet = FakePacket(protocol=socket.IPPROTO_UDP, src_port=5, src_ip='128.0.0.0', dst_ip='128.0.0.0') 97 | self.assertFalse(rule.filter_condition(packet)) 98 | 99 | 100 | if __name__ == '__main__': 101 | unittest.main() 102 | 103 | -------------------------------------------------------------------------------- /test/unit/test_tcp_rules.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from rules import tcp_rules 3 | from test.unit.fake_packet import FakePacket 4 | import socket 5 | 6 | 7 | class TestTCPRule(unittest.TestCase): 8 | 9 | def test_udp_packet(self): 10 | rule = tcp_rules.TCPRule() 11 | packet = FakePacket(protocol=socket.IPPROTO_UDP) 12 | self.assertFalse(rule.filter_condition(packet)) 13 | 14 | def test_tcp_packet(self): 15 | rule = tcp_rules.TCPRule() 16 | packet = FakePacket(protocol=socket.IPPROTO_TCP) 17 | self.assertTrue(rule.filter_condition(packet)) 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | 22 | -------------------------------------------------------------------------------- /unit_test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | python -m unittest discover test.unit 3 | 4 | --------------------------------------------------------------------------------