├── 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 |
--------------------------------------------------------------------------------