├── LICENSE ├── README.md ├── build-packages.sh ├── debian └── control └── nft-stats.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 azlux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nft-stats 2 | Get the nftables counters easier to read 3 | 4 | It kind of hard to read the output of `nft list ruleset` so there is a small program parcising the output to make counter et stats easier to read. 5 | 6 | I make the ouput look like `iptables -nvL` 7 | 8 | ## Usage 9 | 10 | ``` 11 | nft-stats 12 | ``` 13 | 14 | ## TODO 15 | - Color 16 | - Counter when nft named counters into the config 17 | 18 | ## Install 19 | ### With APT (recommended) 20 | echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ bullseye main" | sudo tee /etc/apt/sources.list.d/azlux.list 21 | sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg 22 | sudo apt update 23 | sudo apt install nft-stats 24 | 25 | ### Manually 26 | You can just clone the project, it's a one-script (`nft-stats.py`) project. But you will have not auto-update. 27 | 28 | ## Example 29 | ### Without nft-stats 30 | command : `nft list ruleset` 31 |
32 | Click to expand! 33 | 34 | ``` 35 | root@AZLUX-PC:~# nft list ruleset 36 | table ip filter { 37 | set router { 38 | type ipv4_addr 39 | comment "Azlux routers" 40 | elements = { xxxx/32,xxxx/28,xxxx/32 } 41 | } 42 | set ip_source_users { 43 | type ipv4_addr 44 | flags interval 45 | elements = { xxxx,xxxx,xxxx,xxxx } 46 | } 47 | chain OUTPUT { 48 | type filter hook output priority filter; policy accept; 49 | oif "eth0" ip daddr @router tcp dport 179 counter packets 8345 bytes 410788 accept 50 | oif "eth0" tcp dport 179 counter packets 0 bytes 0 drop 51 | } 52 | 53 | chain INPUT { 54 | type filter hook input priority filter; policy accept; 55 | ct state established accept 56 | iif "lo" accept 57 | iif "eth0" ip saddr @ip_source_users tcp dport { 22, 80, 443 } counter packets 2361 bytes 141660 accept 58 | counter packets 8742 bytes 454622 drop 59 | } 60 | } 61 | table ip6 filter { 62 | set ip6_source_users { 63 | type ipv6_addr 64 | flags interval 65 | elements = { xx:xx:xx:xx::xx, 66 | xx:xx:xx:xx::xx } 67 | } 68 | 69 | chain INPUT { 70 | type filter hook input priority filter; policy accept; 71 | ct state established accept 72 | iif "lo" accept 73 | icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report } accept comment "Accept ICMPv6" 74 | iif "eth0" ip6 saddr @ip6_source_users tcp dport { 22, 80, 443 } counter packets 0 bytes 0 accept 75 | counter packets 4 bytes 321 drop 76 | } 77 | } 78 | table inet filter { 79 | chain FORWARD { 80 | type filter hook forward priority filter; policy drop; 81 | counter packets 0 bytes 0 drop 82 | } 83 | } 84 | 85 | ``` 86 |
87 | 88 | ### With nft-stats 89 | command : `nft-stats` 90 |
91 | Click to expand! 92 | 93 | ``` 94 | root@AZLUX-PC:~# nft-stats 95 | 96 | OUTPUT IP (policy ACCEPT) 97 | pkts bytes action 98 | 8240 396.13K ACCEPT oif "eth0" ip daddr @router tcp dport 179 99 | 0 0 DROP oif "eth0" tcp dport 179 100 | 101 | INPUT IP (policy ACCEPT) 102 | pkts bytes action 103 | - - ACCEPT oif "eth0" tcp dport 179 104 | - - ACCEPT oif "eth0" tcp dport 179 105 | 2310 135.35K ACCEPT iif "eth0" ip saddr @ip_source_users tcp dport 22, 80, 443 106 | 8659 439.32K DROP 107 | 108 | INPUT IP6 (policy ACCEPT) 109 | pkts bytes action 110 | - - ACCEPT ct state established accept 111 | - - ACCEPT iif "lo" accept 112 | - - ACCEPT icmpv6 type destination-unreachable, packet-too-big, time-exceeded, parameter-problem, mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report accept comment "Accept ICMPv6" 113 | 0 0 ACCEPT iif "eth0" ip6 saddr @ip6_source_users tcp dport 22, 80, 443 114 | 4 321 DROP 115 | 116 | FORWARD INET (policy DROP) 117 | pkts bytes action 118 | 0 0 DROP 119 | ``` 120 |
121 | -------------------------------------------------------------------------------- /build-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit the script if any of the commands fail 4 | set -e 5 | set -u 6 | set -o pipefail 7 | 8 | # Set working directory to the location of this script 9 | cd "$(dirname "${BASH_SOURCE[0]}")" 10 | 11 | STARTDIR="$(pwd)" 12 | DESTDIR="$STARTDIR/pkg" 13 | OUTDIR="$STARTDIR/deb" 14 | # get version 15 | repo="azlux/nft-stats" 16 | api=$(curl --silent "https://api.github.com/repos/$repo/releases/latest") 17 | new=$(echo $api | grep -Po '"tag_name": "\K.*?(?=")') 18 | 19 | # Remove potential leftovers from a previous build 20 | rm -rf "$DESTDIR" "$OUTDIR" 21 | 22 | 23 | ## nft-stats 24 | install -Dm 755 "$STARTDIR/nft-stats.py" "$DESTDIR/usr/local/bin/nft-stats" 25 | 26 | # Build .deb 27 | mkdir "$DESTDIR/DEBIAN" "$OUTDIR" 28 | cp "$STARTDIR/debian/"* "$DESTDIR/DEBIAN/" 29 | 30 | # Set version 31 | sed -i "s/VERSION-TO-REPLACE/$new/" "$DESTDIR/DEBIAN/control" 32 | 33 | dpkg-deb --build "$DESTDIR" "$OUTDIR" 34 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Package: nft-stats 2 | Version: VERSION-TO-REPLACE 3 | Section: net 4 | Priority: optional 5 | Architecture: all 6 | Maintainer: Azlux 7 | Description: Get the nftables counters easier to read 8 | Homepage: https://github.com/azlux/nft-stats 9 | Bugs: https://github.com/azlux/nft-stats/issues 10 | Depends: nftables, python3 11 | -------------------------------------------------------------------------------- /nft-stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | import os 4 | import re 5 | import subprocess 6 | import argparse 7 | 8 | re_counter = r'counter packets (\d+) bytes (\d+)' 9 | size_name = ("", "K", "M", "G", "T") 10 | 11 | 12 | def tabulator(text, min_field=10): 13 | tabs_to_append = min_field - len(text) 14 | return_string = (text if (tabs_to_append <= 0) else text + " " * tabs_to_append) 15 | return return_string 16 | 17 | 18 | def convert_size(size_bytes, one_k=1024, minimal=0): 19 | try: 20 | size = int(size_bytes) 21 | except ValueError: 22 | return size_bytes 23 | if size == 0: 24 | return "0" 25 | if size < minimal: 26 | return str(size) 27 | tp_i = int(math.floor(math.log(size, one_k))) 28 | tp_p = math.pow(1024, tp_i) 29 | tp_s = round(size / tp_p, 2) 30 | tp_s = str(tp_s) 31 | return f"{tp_s.rstrip('0').rstrip('.') if '.' in tp_s else tp_s}{size_name[tp_i]}" 32 | 33 | 34 | def run_command(args): 35 | command = "nft list ruleset" 36 | if args.table: 37 | command = f"nft list table {args.table}" 38 | if args.chain: 39 | command = f"nft list chain filter {args.chain}" 40 | if args.debug: 41 | print(f"## Command used : {command}") 42 | nft_run = subprocess.run(command, shell=True, capture_output=True) 43 | res = nft_run.stdout.decode().split('\n') 44 | if args.debug: 45 | print("## OUTPUT :") 46 | print(res) 47 | return res 48 | 49 | 50 | def clear_line(line): 51 | line = line.replace('{', '') 52 | line = line.replace('}', '') 53 | line = line.replace(';', '') 54 | line = line.strip() 55 | return line 56 | 57 | def nft_stats(command_result): 58 | table = "" 59 | chain = "" 60 | table_first_line_printed = False 61 | iter_res = iter(command_result) 62 | for line in iter_res: 63 | line = clear_line(line) 64 | if line.startswith('table'): 65 | table = f"{line.split()[1].upper()} {line.split()[2].upper()}" 66 | elif line.startswith('chain'): 67 | chain = line.split()[1] 68 | next_line = clear_line(next(iter_res)); 69 | if next_line.startswith('type') and 'policy' in next_line: 70 | policy = next_line.split('policy')[1].strip().upper() 71 | print(f"\nchain {chain.upper()} {table} (policy {policy})") 72 | table_first_line_printed = False 73 | else: 74 | print(f"\nchain {chain}") 75 | table_first_line_printed = False 76 | elif line.startswith('set'): 77 | chain = "" 78 | else: 79 | if line and chain: 80 | counter_hit = "-" 81 | counter_bytes = "-" 82 | action = "" 83 | match = None 84 | res = re.search(re_counter, line) 85 | if res: 86 | counter_hit = convert_size(res.group(1), one_k=1000, minimal=500000) 87 | counter_bytes = convert_size(res.group(2)) 88 | match = re.sub(re_counter, '', line) 89 | 90 | if 'accept' in line: 91 | action = "ACCEPT" 92 | elif 'reject' in line: 93 | action = "REJECT" 94 | elif 'drop' in line: 95 | action = "DROP" 96 | if not table_first_line_printed: 97 | print(f"{tabulator('pkts')} {tabulator('bytes')} {tabulator('action', 7)}") 98 | table_first_line_printed = True 99 | print(f"{tabulator(counter_hit)} {tabulator(counter_bytes)} {tabulator(action, 7)} {match if match!=None else line}") 100 | 101 | 102 | def main(): 103 | parser = argparse.ArgumentParser(description='Show well formated statitics for NFT output') 104 | parser.add_argument('--chain', '-c', type=str, required=False, help="Show specific Chain") 105 | parser.add_argument('--table', '-t', type=str, required=False, help="Show specific Table") 106 | parser.add_argument('--debug', '-d', action="store_true", help="Debug mode") 107 | args = parser.parse_args() 108 | 109 | out = run_command(args) 110 | nft_stats(out) 111 | 112 | 113 | if __name__ == '__main__': 114 | if os.geteuid() != 0: 115 | exit("You need to have root privileges to run this script.") 116 | main() 117 | --------------------------------------------------------------------------------