xref: /aosp_15_r20/external/bcc/examples/networking/tcp_mon_block/src/tcp_mon_block.py (revision 387f9dfdfa2baef462e92476d413c7bc2470293e)
1#!/usr/bin/python
2# author: https://github.com/agentzex
3# Licensed under the Apache License, Version 2.0 (the "License")
4
5# tcp_mon_block.py - uses netlink TC, kernel tracepoints and kprobes to monitor outgoing connections from given PIDs
6# and block connections to all addresses initiated from them (acting like an in-process firewall), unless they are listed in allow_list
7
8# outputs blocked connections attempts from monitored processes
9# Usage:
10#   python3 tcp_mon_block.py -i network_interface_name
11#   python3 tcp_mon_block.py -v -i network_interface_name (-v --verbose - will output all connections attempts, including allowed ones)
12#
13
14
15from bcc import BPF
16import pyroute2
17import socket
18import struct
19import json
20import argparse
21from urllib.parse import urlparse
22
23
24# TCP flags
25FIN = 0x01
26SYN = 0x02
27RST = 0x04
28PSH = 0x08
29ACK = 0x10
30URG = 0x20
31ECE = 0x40
32CWR = 0x80
33
34
35verbose_states = {
36    1: "Connection not allowed detected - forwarding to block",
37    2: "Connection allowed",
38    3: "Connection destroyed",
39}
40
41
42def get_verbose_message(state):
43    if state not in verbose_states:
44        return ""
45
46    return verbose_states[state]
47
48
49def parse_tcp_flags(flags):
50    found_flags = ""
51    if flags & FIN:
52        found_flags += "FIN; "
53    if flags & SYN:
54        found_flags += "SYN; "
55    if flags & RST:
56        found_flags += "RST; "
57    if flags & PSH:
58        found_flags += "PSH; "
59    if flags & ACK:
60        found_flags += "ACK; "
61    if flags & URG:
62        found_flags += "URG; "
63    if flags & ECE:
64        found_flags += "ECE; "
65    if flags & CWR:
66        found_flags += "CWR;"
67
68    return found_flags
69
70
71def ip_to_network_address(ip):
72    return struct.unpack("I", socket.inet_aton(ip))[0]
73
74
75def network_address_to_ip(ip):
76    return socket.inet_ntop(socket.AF_INET, struct.pack("I", ip))
77
78
79def parse_address(url_or_ip):
80    is_ipv4 = True
81    domain = ""
82
83    #first check if valid ipv4
84    try:
85        socket.inet_aton(url_or_ip)
86    except socket.error:
87        is_ipv4 = False
88
89    if is_ipv4:
90        return [url_or_ip]
91
92    # if not check if valid URL, parse and get its domain, resolve it to IPv4 and return it
93    try:
94        domain = urlparse(url_or_ip).netloc
95    except:
96        print(f"[-] {url_or_ip} is invalid IPv4 or URL")
97        return False
98
99    # should get a list of IPv4 addresses resolved from the domain
100    try:
101        hostname, aliaslist, ipaddrlist = socket.gethostbyname_ex(domain)
102    except:
103        print(f"[-] Failed to resolve {url_or_ip} to Ipv4")
104        return False
105
106    return ipaddrlist
107
108
109def create_bpf_allow_list(bpf):
110    bpf_allow_list = bpf.get_table("allow_list")
111    bpf_pid_list = bpf.get_table("pid_list")
112    with open("allow_list.json", "r") as f:
113        pids_to_list = json.loads(f.read())
114
115    print("[+] Reading and parsing allow_list.json")
116    for pid_to_list in pids_to_list:
117        try:
118            pid = int(pid_to_list["pid"])
119        except ValueError:
120            print(f"[-] invalid PID: {pid_to_list['pid']}")
121            continue
122
123        print(f"[+] Adding {pid} to monitored processes")
124        bpf_pid_list[bpf_pid_list.Key(pid)] = bpf_pid_list.Leaf(pid)
125
126        for url_or_ip in pid_to_list["allow_list"]:
127            ips = parse_address(url_or_ip)
128            if not ips:
129                continue
130            for ip in ips:
131                print(f"[+] Adding {ip} to allowed IPs")
132                ip = ip_to_network_address(ip)
133                bpf_allow_list[bpf_allow_list.Key(ip)] = bpf_allow_list.Leaf(ip)
134
135
136def create_tc(interface):
137    ip = pyroute2.IPRoute()
138    ipdb = pyroute2.IPDB(nl=ip)
139    try:
140        idx = ipdb.interfaces[interface].index
141    except:
142        print(f"[-] {interface} interface not found")
143        return False, False, False
144
145    try:
146        # deleting if exists from previous run
147        ip.tc("del", "clsact", idx)
148    except:
149        pass
150    ip.tc("add", "clsact", idx)
151    return ip, ipdb, idx
152
153
154def parse_blocked_event(cpu, data, size):
155    event = bpf["blocked_events"].event(data)
156    src_ip = network_address_to_ip(event.src_ip)
157    dst_ip = network_address_to_ip(event.dst_ip)
158    flags = parse_tcp_flags(event.tcp_flags)
159    print(f"{event.pid}: {event.comm.decode()} - {src_ip}:{event.src_port} -> {dst_ip}:{event.dst_port} Flags: {flags} was blocked!")
160
161
162def parse_verbose_event(cpu, data, size):
163    event = bpf["verbose_events"].event(data)
164    src_ip = network_address_to_ip(event.src_ip)
165    dst_ip = network_address_to_ip(event.dst_ip)
166    verbose_message = get_verbose_message(event.state)
167    print(f"{event.pid}: {event.comm.decode()} - {src_ip}:{event.src_port} -> {dst_ip}:{event.dst_port} - {verbose_message}")
168
169
170
171parser = argparse.ArgumentParser(description="Monitor given PIDs and block outgoing connections to all addresses initiated from them, unless they are listed in allow_list.json")
172parser.add_argument("-i", "--interface", help="Network interface name to monitor traffic on", required=True, type=str)
173parser.add_argument("-v", "--verbose", action="store_true", help="Set verbose output")
174args = parser.parse_args()
175print(f"[+] Monitoring {args.interface} interface")
176
177
178with open("tcp_mon_block.c", "r") as f:
179    bpf_text = f.read()
180
181if args.verbose:
182    print("[+] Verbose output is ON!")
183    bpf_text = bpf_text.replace("static bool VERBOSE_OUTPUT = false", "static bool VERBOSE_OUTPUT = true")
184
185
186ip, ipdb, idx = create_tc(args.interface)
187if not ip:
188    exit(-1)
189
190bpf = BPF(text=bpf_text)
191create_bpf_allow_list(bpf)
192
193# loading kprobe
194bpf.attach_kprobe(event="tcp_connect", fn_name="trace_connect_entry")
195
196# loading TC
197fn = bpf.load_func("handle_egress", BPF.SCHED_CLS)
198
199#default parent handlers:
200#https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/pkt_sched.h?id=1f211a1b929c804100e138c5d3d656992cfd5622
201#define TC_H_MIN_INGRESS	0xFFF2U
202#define TC_H_MIN_EGRESS		0xFFF3U
203
204ip.tc("add-filter", "bpf", idx, ":1", fd=fn.fd, name=fn.name, parent="ffff:fff3", classid=1, direct_action=True)
205bpf["blocked_events"].open_perf_buffer(parse_blocked_event)
206bpf["verbose_events"].open_perf_buffer(parse_verbose_event)
207
208
209print("[+] Monitoring started\n")
210while True:
211    try:
212        bpf.perf_buffer_poll()
213    except KeyboardInterrupt:
214        break
215
216ip.tc("del", "clsact", idx)
217ipdb.release()
218
219
220
221
222
223
224
225
226
227
228