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 @@
+
+
+
+
+
+ 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
+