diff --git a/README.md b/README.md index b42ddac..c9a8804 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,16 @@ To display only selected chains (supports regexp): To also render empty chains: -`awk -f iptables-vis.awk -v 'include_empty_chains=1 < iptables.txt > iptables.dia'` +`awk -f iptables-vis.awk -v 'include_empty_chains=1' < iptables.txt > iptables.dia` + +Mermaid Output +=============== +Same as above, but use `iptables-vis-mermaid.awk`, e.g. + +`awk -f iptables-vis-mermaid.awk -v 'include_empty_chains=1' < iptables.txt > iptables.mmd` + +To `mermaid.html` file can be used to view mermaid files in the browser. +It needs to be servied via HTTPS so it can load the mermaid failes. Legend ====== diff --git a/iptables-vis-mermaid.awk b/iptables-vis-mermaid.awk new file mode 100755 index 0000000..d97c58c --- /dev/null +++ b/iptables-vis-mermaid.awk @@ -0,0 +1,253 @@ +#!/usr/bin/awk -f +# +BEGIN { + print "flowchart TD" + counter = 0 + edge_counter = 0 +} + +function make_id(s) { + gsub(/-/, "_", s) + return s +} + +function sanitize_label(s) { + gsub(/"/, "'", s) + gsub(/ +/, " ", s) + gsub(/^ | $/, "", s) + return s +} + +# Begin of a chain +/^Chain/ { + chainname = $2 + in_chain=1 + is_a_chain[chainname] = 1 + if ( chainname !~ chain_selector && chain_selector != "") + next + in_relevant_chain=1 + if ( $3 == "(policy" ) + policy=$4 + else + policy=0 + last=chainname + delete chain_buffer; chain_buf_size = 0 + delete pending_edge_styles; chain_edge_counter = 0 + delete filter_nodes; delete targets; delete target_node_names; delete target_labels + num_targets = 0 + chain_buffer[chain_buf_size++] = " subgraph sg_" make_id(chainname) " [\"" chainname "\"]" + chain_buffer[chain_buf_size++] = " direction TB" +} + +# Filter in chain +in_chain && /^ *[0-9]/ { + filters_in_chain++ + if ( !in_relevant_chain ) + next + name="Node" counter++ + reject_with="" + label="" + if ( $4 != "all" ) + label=label $4 " " + if ( $5 != "--" ) + label=label $5 " " + if ( $6 != "any" ) + label=label "in:" $6 " " + if ( $7 != "any" ) + label=label "out:" $7 " " + if ( $8 != "anywhere" ) + label=label "src:" $8 " " + if ( $9 != "anywhere" ) + label=label "dst:" $9 " " + comment=0 + for (i=10; i<=NF; i++) { + if ( $i == "/*" ) + comment=1 + else if ( $i == "*/" ) + comment=0 + else if ( $i == "reject-with" ) { + i++ + reject_with = $i + i++ + } + else if ( ! comment ) + label=label " " $i + } + if ( label == "" ) + label = "*" + label = sanitize_label(label) + chain_buffer[chain_buf_size++] = " " name "{\"" label "\"}:::rule" + chain_buffer[chain_buf_size++] = " " make_id(last) " --> " name + chain_edge_counter++ + last=name + filter_nodes[num_targets++] = name + target_node_name = chainname "_" $3 + if ( reject_with ) + target_node_name = target_node_name "_" reject_with + target_label = $3 + if ( reject_with ) + target_label = target_label "
" reject_with + + targets[name] = $3 + all_targets[name] = $3 + target_node_names[name] = target_node_name + all_target_node_names[name] = target_node_name + target_labels[target_node_name] = target_label +} + +# End of chain +/^$/ { + in_chain=0 + filter_number[chainname] = filters_in_chain + if (in_relevant_chain) + finalize_chain() + filters_in_chain=0 + in_relevant_chain=0 +} + +function finalize_chain( i, head_class, pol_target_node, pol_target_id, idx, name, target, target_node_name, target_label, target_id, rhs, safe_lbl) { + if ( !filters_in_chain && !include_empty_chains ) { + delete chain_buffer; chain_buf_size = 0 + delete pending_edge_styles; chain_edge_counter = 0 + delete filter_nodes; delete targets + delete target_node_names; delete target_labels + num_targets = 0 + return + } + + # Determine chain head class + if ( !filters_in_chain ) + head_class = ":::chain_empty" + else if ( chainname ~ "^(INPUT|OUTPUT|FORWARD|PREROUTING|POSTROUTING)$" ) + head_class = ":::chain_head_builtin" + else + head_class = ":::chain_head" + + # Track chain order for top-alignment links emitted in END + chain_order[num_chains++] = make_id(chainname) + + # Chain head node definition (inside subgraph) + chain_buffer[chain_buf_size++] = " " make_id(chainname) "[\"" chainname "\"]" head_class + + # Policy edge + target node (inside subgraph) + if ( policy ) { + pol_target_node = chainname "_" policy + pol_target_id = make_id(pol_target_node) + chain_buffer[chain_buf_size++] = " " make_id(last) " --> " pol_target_id + if ( policy ~ "^ACCEPT$" ) + pending_edge_styles[chain_edge_counter] = "stroke:green" + else if ( policy ~ "^(DROP|REJECT)" ) + pending_edge_styles[chain_edge_counter] = "stroke:red" + chain_edge_counter++ + if ( !already_rendered[pol_target_node] ) { + already_rendered[pol_target_node] = 1 + if ( policy ~ "^ACCEPT$" ) + chain_buffer[chain_buf_size++] = " " pol_target_id "[\"ACCEPT\"]:::accept" + else if ( policy ~ "^DROP$" ) + chain_buffer[chain_buf_size++] = " " pol_target_id "[\"DROP\"]:::drop" + else if ( policy ~ "^REJECT($|_)" ) + chain_buffer[chain_buf_size++] = " " pol_target_id "[\"REJECT\"]:::reject" + else if ( policy ~ "^RETURN$" ) + chain_buffer[chain_buf_size++] = " " pol_target_id "[\"RETURN\"]:::return" + } + } + + # Rule-to-target edges + target node definitions (all inside subgraph) + for ( idx = 0; idx < num_targets; idx++ ) { + name = filter_nodes[idx] + target = targets[name] + target_node_name = target_node_names[name] + target_label = target_labels[target_node_name] + target_id = make_id(target_node_name) + + if ( target ~ "^ACCEPT$" ) + pending_edge_styles[chain_edge_counter] = "stroke:green" + else if ( target ~ "^REJECT($|_)|DROP$" ) + pending_edge_styles[chain_edge_counter] = "stroke:red" + else if ( target ~ "^RETURN$" ) + pending_edge_styles[chain_edge_counter] = "stroke:blue" + + if ( !already_rendered[target_node_name] ) { + already_rendered[target_node_name] = 1 + if ( target ~ "^ACCEPT$" ) + rhs = target_id "[\"ACCEPT\"]:::accept" + else if ( target ~ "^DROP$" ) + rhs = target_id "[\"DROP\"]:::drop" + else if ( target ~ "^REJECT($|_)" ) { + safe_lbl = sanitize_label(target_label) + rhs = target_id "[\"" safe_lbl "\"]:::reject" + } + else if ( target ~ "^RETURN$" ) + rhs = target_id "[\"RETURN\"]:::return" + else { + safe_lbl = sanitize_label(target_label) + rhs = target_id "[\"" safe_lbl "\"]:::chain_target" + } + } else { + rhs = target_id + } + + chain_buffer[chain_buf_size++] = " " name " --> " rhs + chain_edge_counter++ + } + + # Close subgraph + chain_buffer[chain_buf_size++] = " end" + + # Merge pending edge styles into global edge_styles at current offset + for ( i = 0; i < chain_edge_counter; i++ ) { + if ( i in pending_edge_styles ) + edge_styles[edge_counter + i] = pending_edge_styles[i] + } + + # Flush chain buffer + for ( i = 0; i < chain_buf_size; i++ ) + print chain_buffer[i] + edge_counter += chain_edge_counter + + delete chain_buffer; chain_buf_size = 0 + delete pending_edge_styles; chain_edge_counter = 0 + delete filter_nodes; delete targets + delete target_node_names; delete target_labels + num_targets = 0 + # Note: do NOT delete already_rendered — it is global across chains +} + +END { + filter_number[chainname] = filters_in_chain + if (in_relevant_chain) + finalize_chain() + + # Override style for custom chain targets that turned out to be empty chains + for ( name in all_targets ) { + target = all_targets[name] + if ( !is_a_chain[target] ) + continue + target_node_name = all_target_node_names[name] + if ( filter_number[target] == 0 && !style_overridden[target_node_name] ) { + style_overridden[target_node_name] = 1 + print " style " make_id(target_node_name) " fill:#eeeeee,stroke:#aaaaaa,stroke-dasharray:5 5" + } + } + + # linkStyle directives for colored edges + for ( i = 0; i < edge_counter; i++ ) { + if ( i in edge_styles ) + print " linkStyle " i " " edge_styles[i] + } + + # Invisible links from the first chain head to all others to force top-alignment + for ( i = 1; i < num_chains; i++ ) + print " " chain_order[0] " ~~~ " chain_order[i] + + # classDef declarations + print " classDef accept fill:lightgreen,stroke:green" + print " classDef drop fill:#ff6666,stroke:red" + print " classDef reject fill:#ff9999,stroke:red" + print " classDef return fill:#1ab3ff,stroke:blue" + print " classDef rule fill:white,stroke:black" + print " classDef chain_head fill:white,stroke:black" + print " classDef chain_head_builtin fill:#ffffcc,stroke:#999900" + print " classDef chain_target fill:white,stroke:#666" + print " classDef chain_empty fill:#eeeeee,stroke:#aaaaaa,stroke-dasharray:5 5" +} diff --git a/mermaid.html b/mermaid.html new file mode 100644 index 0000000..08686f5 --- /dev/null +++ b/mermaid.html @@ -0,0 +1,151 @@ + + + + + + + + Mermaid Viewer + + + + + + +

+ A very basic viewer for Mermaid files, generated by Copilot. +

+ +

+ Serve this file over http(s) for this to work, use something like +
+ python3 -m http.server 8000 or npx -y serve . or php -S 8000 +

+ +
+ + + + + + + + + +
+ + +
+ + +