├── bin └── iptables-trace.py └── README /bin/iptables-trace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # pip install python-iptables 4 | import socket 5 | import select 6 | import signal 7 | import ctypes 8 | import struct 9 | 10 | import iptc 11 | from libnetfilter.log import nflog_handle 12 | from libnetfilter.netlink import nf_log 13 | 14 | 15 | # monkey patch as the std version breaks with inverted values 16 | def _get_all_parameters(self): 17 | import shlex 18 | params = {} 19 | ip = self.rule.get_ip() 20 | buf = self._get_saved_buf(ip) 21 | if buf is None: 22 | return params 23 | res = shlex.split(buf.decode('ascii')) 24 | res.reverse() 25 | values = [] 26 | key = None 27 | while len(res) > 0: 28 | x = res.pop() 29 | if x.startswith('--'): # This is a parameter name. 30 | values.append(x[2:]) 31 | key = " ".join(values) 32 | params[key] = [] 33 | continue 34 | if key: 35 | params[key].append(x) # This is a parameter value. 36 | else: 37 | values.append(x) 38 | return params 39 | 40 | iptc.ip4tc.IPTCModule.get_all_parameters = _get_all_parameters 41 | 42 | 43 | running = True 44 | def signal_handler(signal, frame): 45 | global running 46 | running = False 47 | signal.signal(signal.SIGINT, signal_handler) 48 | 49 | def nfbpf_compile(pattern): 50 | import ctypes.util 51 | 52 | class _bpf_insn(ctypes.Structure): 53 | _fields_ = [ 54 | ("code",ctypes.c_short), 55 | ("jt",ctypes.c_uint8), 56 | ("jf",ctypes.c_uint8), 57 | ("k",ctypes.c_uint32) 58 | ] 59 | 60 | class bpf_program(ctypes.Structure): 61 | _fields_ = [ 62 | ("bf_len", ctypes.c_int), 63 | ("bf_insns", ctypes.POINTER(_bpf_insn)) 64 | ] 65 | 66 | _LP_bpf_program = ctypes.POINTER(bpf_program) 67 | 68 | libpcap = ctypes.CDLL(ctypes.util.find_library("pcap")) 69 | 70 | def pcap_compile_nopcap_errcheck(result, function, args): 71 | if result == -1: 72 | raise ValueError(args) 73 | 74 | # int pcap_compile_nopcap(int snaplen, int linktype, struct bpf_program *fp, char *str, int optimize, bpf_uint32 netmask); 75 | libpcap.pcap_compile_nopcap.restype = ctypes.c_int 76 | libpcap.pcap_compile_nopcap.argtypes = [ctypes.c_int, ctypes.c_int, _LP_bpf_program, ctypes.c_char_p, ctypes.c_int, ctypes.c_uint32] 77 | libpcap.pcap_compile_nopcap.errcheck = pcap_compile_nopcap_errcheck 78 | 79 | 80 | buf = ctypes.c_char_p(pattern.encode('ascii')) 81 | optimize = ctypes.c_int(1) 82 | mask = ctypes.c_uint32(0xffffffff) 83 | program = bpf_program() 84 | DLT_RAW = 12 85 | libpcap.pcap_compile_nopcap(40, DLT_RAW, ctypes.byref(program), buf, optimize, mask) 86 | if program.bf_len > 64: # XT_BPF_MAX_NUM_INSTR 87 | raise ValueError("bpf: number of instructions exceeds maximum") 88 | r = "{:d}, ".format(program.bf_len) 89 | r += ", ".join(["{i.code} {i.jt} {i.jf} {i.k}".format(i=program.bf_insns[i]) for i in range(program.bf_len)]) 90 | return r 91 | 92 | 93 | class bcolors: 94 | HEADER = '\033[95m' 95 | OKBLUE = '\033[94m' 96 | OKGREEN = '\033[92m' 97 | WARNING = '\033[93m' 98 | FAIL = '\033[91m' 99 | ENDC = '\033[0m' 100 | 101 | @staticmethod 102 | def ok(data): 103 | return bcolors.OKGREEN + data + bcolors.ENDC 104 | @staticmethod 105 | def fail(data): 106 | return bcolors.FAIL + data + bcolors.ENDC 107 | @staticmethod 108 | def next(data): 109 | return bcolors.OKBLUE + data + bcolors.ENDC 110 | 111 | 112 | def format_parameters(args): 113 | r = {} 114 | for k,v in args.items(): 115 | if type(v) == str: 116 | r[k] = v 117 | else: 118 | r[k] = " ".join(v) 119 | return str(r) 120 | 121 | def trace_cb(gh, nfmsg, nfa, data): 122 | prefix = nfa.prefix.decode('ascii') 123 | if not prefix.startswith('TRACE: '): 124 | return 0 125 | 126 | # chainname may have :, therefore split and re-create chainname 127 | p = prefix[7:].split(":") 128 | tablename,chainname,type,rulenum = [p[0], ':'.join(p[1:-2]), p[-2], p[-1]] 129 | 130 | table = iptc.Table(tablename) 131 | chain = iptc.Chain(table, chainname) 132 | pkt = nfa.payload 133 | 134 | if tablename == 'raw' and chainname in ('PREROUTING','OUTPUT'): 135 | print(nf_log(pkt, nfa.indev, nfa.outdev)) 136 | 137 | r = "\t{} {} ".format(tablename,chainname) 138 | if type == 'policy': 139 | x = chain.get_policy().name 140 | if x == 'ACCEPT': 141 | x = bcolors.ok(x) 142 | else: 143 | x = bcolors.fail(x) 144 | elif type == 'rule': 145 | r += "(#{r}) ".format(r=rulenum.strip()) 146 | rule = chain.rules[int(rulenum)-1] 147 | x = "{r.protocol} {r.src} -> {r.dst} ".format(r=rule) 148 | for m in rule.matches: 149 | if m.name == 'comment': 150 | r += "/* {} */".format(m.get_all_parameters()['comment'][0]) 151 | else: 152 | x += "{}:{} ".format(m.name, format_parameters(m.get_all_parameters())) 153 | 154 | tp = rule.target.get_all_parameters() 155 | if len(tp) > 0: 156 | tp = format_parameters(tp) 157 | else: 158 | tp = "" 159 | 160 | if rule.target.name == 'ACCEPT': 161 | targetname = bcolors.ok(rule.target.name) 162 | elif rule.target.name in ('REJECT','DROP'): 163 | targetname = bcolors.fail(rule.target.name) 164 | else: 165 | targetname = bcolors.next(rule.target.name) 166 | x += "\n\t\t=> {} {}".format(targetname, tp) 167 | elif type == 'return': 168 | # unconditional rule having the default policy of the calling chain get named "return" 169 | # net/ipv4/netfilter/ip_tables.c 170 | # get_chainname_rulenum 171 | try: 172 | r += "(#{r}) ".format(r=rulenum.strip()) 173 | rule = chain.rules[int(rulenum)-1] 174 | if rule.target.name == 'ACCEPT': 175 | targetname = bcolors.ok(rule.target.name) 176 | elif rule.target.name in ('REJECT','DROP'): 177 | targetname = bcolors.fail(rule.target.name) 178 | else: 179 | targetname = bcolors.next(rule.target.name) 180 | x = "=> {}".format(targetname) 181 | except Exception as e: 182 | x = "return" 183 | 184 | r += "NFMARK=0x{:x} (0x{:x})".format(nfa.nfmark & data, nfa.nfmark) 185 | 186 | print("{}\n\t\t{}".format(r,x)) 187 | return 0 188 | 189 | 190 | def main(): 191 | global running 192 | import argparse 193 | import random 194 | 195 | parser = argparse.ArgumentParser(description='iptables-trace') 196 | 197 | parser.add_argument('--clear-chain', action='store_true', default=False, help="delete all rules in the chain") 198 | parser.add_argument('--chain','-c', type=str, nargs='*', choices=['OUTPUT','PREROUTING'], default=["OUTPUT",'PREROUTING'], help='chain') 199 | parser.add_argument('--source','-s', type=str, action='store', default=None, help='source') 200 | parser.add_argument('--destination','-d', type=str, action='store', default=None, help='destination') 201 | parser.add_argument('--protocol', '-p', type=str, action='store', default=None, help='protocol') 202 | parser.add_argument('--xmark-mask', '-M', type=str, action='store', default="0x800001ff", help='mark mask (bits to use) default is not to use lower 9 bits and the highest') 203 | parser.add_argument('--limit', action='store_true', default=False, help="limit rule matches to 1/second") 204 | parser.add_argument('bpf', type=str, action='store', default='', nargs='*') 205 | 206 | args = parser.parse_args() 207 | print(args) 208 | 209 | if not iptc.is_table_available(iptc.Table.RAW): 210 | raise ValueError("table raw does not exist") 211 | table = iptc.Table("raw") 212 | 213 | rules = [] 214 | for i in args.chain: 215 | chain = iptc.Chain(table, i) 216 | if args.clear_chain == True and len(chain.rules) != 0: 217 | while len(chain.rules) > 0: 218 | i = chain.rules[0] 219 | print("delete rule {}".format(i)) 220 | chain.delete_rule(i) 221 | 222 | mark = iptc.Rule() 223 | if args.protocol: 224 | mark.protocol = args.protocol 225 | if args.source: 226 | mark.src = args.source 227 | if args.destination: 228 | mark.dst = args.destination 229 | 230 | if args.bpf: 231 | filter = args.bpf if isinstance(args.bpf, str) else ' '.join(args.bpf) 232 | bpf = mark.create_match("bpf") 233 | bpf.bytecode = nfbpf_compile(filter) 234 | comment = mark.create_match("comment") 235 | comment.comment = 'bpf: "{}"'.format(filter) 236 | if args.limit: 237 | limit = mark.create_match('limit') 238 | limit.limit = "1/second" 239 | # limit.limit_burst = "1" 240 | 241 | mark.target = iptc.Target(mark, "MARK") 242 | m = 0 243 | while m == 0: 244 | _m = random.randint(0,2**32-1) 245 | _m &= ~int(args.xmark_mask, 16) 246 | m = "0x{:x}".format(_m) 247 | mark.target.set_mark = m 248 | chain.append_rule(mark) 249 | rules.append((chain,mark)) 250 | 251 | trace = iptc.Rule() 252 | match = trace.create_match("mark") 253 | match.mark = "{}/0x{:x}".format(m,0xffffffff & ~int(args.xmark_mask, 16)) 254 | trace.target = iptc.Target(trace, "TRACE") 255 | chain.append_rule(trace) 256 | rules.append((chain,trace)) 257 | 258 | n = nflog_handle.open() 259 | r = n.unbind_pf(socket.AF_INET) 260 | r = n.bind_pf(socket.AF_INET) 261 | qh = n.bind_group(0) 262 | qh.set_mode(0x02, 0xffff) 263 | 264 | qh.callback_register(trace_cb, int(args.xmark_mask, 16)); 265 | 266 | fd = n.fd 267 | 268 | while running: 269 | try: 270 | r,w,x = select.select([fd],[],[],1.) 271 | if len(r) == 0: 272 | # timeout 273 | # print("timeout") 274 | continue 275 | if fd in r: 276 | n.handle_io() 277 | except: 278 | pass 279 | 280 | for chain,rule in rules: 281 | chain.delete_rule(rule) 282 | 283 | if __name__ == '__main__': 284 | main() 285 | 286 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | iptables-trace 2 | 3 | This software allows tracing a packet through the linux kernels netfilter 4 | tables. 5 | It is meant to assist in debugging and writing iptables rules. 6 | 7 | 8 | THIS SOFTWARE DOES NOT WORK WITH iptables-over-nftables. 9 | USE xtables-monitor --trace for iptables-over-nftables. 10 | 11 | xtables-monitor --trace 12 | 13 | 1 TRACE: 2 fc475095 raw:PREROUTING:rule:0x3:CONTINUE -4 -t raw -A PREROUTING -p icmp -j TRACE 14 | 2 PACKET: 0 fc475095 IN=lo LL=0x3040000000000000000000000000800 SRC=127.0.0.1 DST=127.0.0.1 LEN=84 TOS=0x0 TTL=64 ID=38349DF 15 | 3 TRACE: 2 fc475095 raw:PREROUTING:return: 16 | 4 TRACE: 2 fc475095 raw:PREROUTING:policy:ACCEPT 17 | 5 TRACE: 2 fc475095 filter:INPUT:return: 18 | 6 TRACE: 2 fc475095 filter:INPUT:policy:DROP 19 | 7 TRACE: 2 0df9d3d8 raw:PREROUTING:rule:0x3:CONTINUE -4 -t raw -A PREROUTING -p icmp -j TRACE 20 | 21 | 22 | 23 | INSTALLATION 24 | 25 | Install 26 | * python-libnetfilter 27 | * libnetfilter-log 28 | * python-iptables 29 | and run iptables-trace 30 | 31 | OPERATION 32 | iptables-trace is rather limited in the arguments you can provide. 33 | If possible (supported by kernel) use --bpf, --bpf "host example.org" will 34 | trace packets in both directions, which is currently not possible using --source or --destination. 35 | --limit can be used to limit the amount of traced packets to 1 packet per second. 36 | 37 | The output looks like: 38 | 39 | IN=vif0 OUT= SRC=1.1.1.1 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=61 ID=58730 PROTO=TCP SPT=45331 DPT=80 # iptables LOG like formatting of the packet 40 | ... 41 | raw PREROUTING # table chain 42 | ACCEPT # policy 43 | mangle PREROUTING (#3) # table chain (rule number) 44 | ip 1.1.0.0/255.255.0.0 -> 0.0.0.0/0.0.0.0 # rule 45 | => MARK {'set-xmark': '0x100/0x100'} # action taken 46 | ... 47 | 48 | GUTS 49 | iptables-trace creates additional rules in the raw table. 50 | These rules are used to set a TRACE target on packets you are interested in. 51 | The logging of these TRACE messages is retrieved using libnetfilter_log. 52 | By parsing the TRACE messages, the table, chain and action is retrieved. 53 | python-iptables is used to lookup the iptables rule and format it for display. 54 | The rules in the raw table are deleted upon program exit. 55 | 56 | 57 | EXAMPLES 58 | On a router/firewall, a typical output will look like this: 59 | 60 | IN=vif2 OUT= SRC=1.1.1.1 DST=2.2.2.2 LEN=60 TOS=0x00 PREC=0x00 TTL=43 ID=4997 PROTO=TCP SPT=34419 DPT=22 61 | raw PREROUTING 62 | ACCEPT 63 | mangle PREROUTING 64 | ACCEPT 65 | nat PREROUTING 66 | ACCEPT 67 | mangle FORWARD 68 | ACCEPT 69 | filter FORWARD (#31) 70 | ip 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 set:{'match-set': 'profile:server-terminal dst'} 71 | => server-terminal:filter 72 | filter server-terminal:filter (#8) 73 | tcp 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 mark:{'! mark': '0x100/0x100'} tcp:{'dport': '22'} 74 | => ACCEPT 75 | mangle POSTROUTING 76 | ACCEPT 77 | nat POSTROUTING 78 | ACCEPT 79 | 80 | You can see all matching rules for the packet and the actions taken. 81 | In case of NAT, you'll see the modifications to the packet in nat/POSTROUTING. 82 | 83 | 84 | IN=vif0 OUT= SRC=10.5.1.7 DST=1.1.1.1 LEN=84 TOS=0x00 PREC=0x00 TTL=61 ID=48889 PROTO=ICMP TYPE=8 CODE=0 85 | raw PREROUTING 86 | ACCEPT 87 | mangle PREROUTING (#4) 88 | ip 10.0.0.0/255.0.0.0 -> 0.0.0.0/0.0.0.0 89 | => MARK {'set-xmark': '0x100/0x100'} 90 | mangle PREROUTING (#11) 91 | ip 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 mark:{'mark': '0x100/0x100'} set:{'match-set': 'profile:client-normal src'} 92 | => client-normal:mangle 93 | mangle client-normal:mangle (#1) 94 | return 95 | mangle PREROUTING 96 | ACCEPT 97 | nat PREROUTING 98 | ACCEPT 99 | mangle FORWARD 100 | ACCEPT 101 | filter FORWARD (#12) 102 | ip 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 set:{'match-set': 'profile:client-normal src'} 103 | => client-normal:filter 104 | filter client-normal:filter (#10) /* allow icmp */ 105 | icmp 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 mark:{'mark': '0x100/0x100'} 106 | => ACCEPT 107 | mangle POSTROUTING 108 | ACCEPT 109 | nat POSTROUTING (#6) 110 | ip 10.5.1.0/255.255.255.0 -> 0.0.0.0/0.0.0.0 111 | => SNAT {'to-source random persistent': '', 'to-source': '2.2.2.2-2.2.2.3', 'to-source random': ''} 112 | 113 | 114 | In case a fwmarks are used for routing decisions, you can see the iptables setting the mark: 115 | 116 | IN=vif0 OUT= SRC=1.1.1.1 DST=2.2.2.2 LEN=40 TOS=0x00 PREC=0x00 TTL=61 ID=58730 PROTO=TCP SPT=45331 DPT=80 117 | raw PREROUTING 118 | ACCEPT 119 | mangle PREROUTING (#3) 120 | ip 1.1.0.0/255.255.0.0 -> 0.0.0.0/0.0.0.0 121 | => MARK {'set-xmark': '0x100/0x100'} 122 | mangle PREROUTING (#10) 123 | ip 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 mark:{'mark': '0x100/0x100'} set:{'match-set': 'profile:client-restricted src'} 124 | => client-restricted:mangle 125 | mangle client-restricted:mangle (#1) /* proxy mark */ 126 | tcp 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 mark:{'mark': '0x100/0x100'} tcp:{'dport': '80'} 127 | => MARK {'set-xmark': '0x1/0x1'} 128 | mangle client-restricted:mangle (#2) 129 | return 130 | mangle PREROUTING 131 | ACCEPT 132 | mangle FORWARD 133 | ACCEPT 134 | filter FORWARD (#1) 135 | ip 0.0.0.0/0.0.0.0 -> 0.0.0.0/0.0.0.0 state:{'state': 'RELATED,ESTABLISHED'} 136 | => ACCEPT 137 | mangle POSTROUTING 138 | ACCEPT 139 | 140 | 141 | 142 | But it can assist in writing local rules as well. 143 | 144 | 145 | Not considering tunnels, a packet will traverse the filters/chains as outlined in the simplified diagram: 146 | 147 | +---------------------+ +-----------------------+ 148 | | NETWORK INTERFACE | | NETWORK INTERFACE | 149 | +----------+----------+ +-----------------------+ 150 | | ^ 151 | | | 152 | | | 153 | v | 154 | +---------------------+ | 155 | | PREROUTING | | 156 | +---------------------+ | 157 | | | | 158 | | +-----------------+ | | 159 | | | raw | | | 160 | | +--------+--------+ | | 161 | | v | | 162 | | +-----------------+ | +----------+------------+ 163 | | | conn. tracking | | | POSTROUTING | 164 | | +--------+--------+ | +-----------------------+ 165 | | v | | | 166 | | +-----------------+ | | +-------------------+ | 167 | | | mangle | | | | source NAT | | 168 | | +--------+--------+ | | +-------------------+ | 169 | | v | | ^ | 170 | | +-----------------+ | | +--------+----------+ | 171 | | | destination NAT | | | | mangle | | 172 | | +-----------------+ | | +-------------------+ | 173 | +----------+----------+ +------------------------+ +-----------------------+ 174 | | | FORWARD | ^ 175 | | +------------------------+ | 176 | v | | | 177 | +-------------+ | +--------+ +--------+ | | 178 | | QOS ingress +----->| | mangle +->| filter | |------------>+ 179 | +------+------+ | +--------+ +--------+ | | 180 | | | | | 181 | | +------------------------+ | 182 | | | 183 | | | 184 | v | 185 | +---------------------+ +----------+------------+ 186 | | INPUT | | OUTPUT | 187 | +---------------------+ +-----------------------+ 188 | | | | | 189 | | +---------------+ | | +-----------------+ | 190 | | | mangle | | | | filter | | 191 | | +-------+-------+ | | +-----------------+ | 192 | | v | | ^ | 193 | | +---------------+ | | +-------+---------+ | 194 | | | filter | | | | destination NAT | | 195 | | +---------------+ | | +-----------------+ | 196 | +----------+----------+ | ^ | 197 | | | +-------+---------+ | 198 | | | | mangle | | 199 | | | +-----------------+ | 200 | | | ^ | 201 | | | +-------+---------+ | 202 | | | | conn. tracking | | 203 | | | +-----------------+ | 204 | | | ^ | 205 | | | +-------+---------+ | 206 | | | | raw | | 207 | | | +-----------------+ | 208 | | +-----------------------+ 209 | v ^ 210 | +--------------------------------------------------------------- +------------+ 211 | | LOCAL PROCESS | 212 | +-----------------------------------------------------------------------------+ 213 | 214 | --------------------------------------------------------------------------------