iptables Processing Flowchart (Updated Often)

FW-IDS – iptables Flowchart v2019-04-30 (2MB)

Many years ago, I started work with iptables, the Linux-based firewall software.  At the time, documentation was sparse, and the details about what happens to a packet during processing were hard to figure out.

Since then, documentation has improved, but I always wished there was a visualization that I could quickly use to trace a packet (observed or theoretical) through the various tables and chains.  While creating content for SANS FOR572, Advanced Network Forensics: Threat Hunting, Analysis, and Incident Response, I decided to create a flowchart myself.  Since I find it most useful in color, I’ve provided the document here.


  • 2019-04-30: Added the three chains on the SELinux “security” table; added additional routing decision points for locally-generated packets; added reference to a great Linode post on newer functionality.  Thanks to commenter Diego for the suggestion on adding the security table!
  • 2018-11-14: Reflects that outbound interface is determined by routing decision, not iptables.
  • 2018-09-01: Reflects that localhost-sourced/destined packets will not traverse the nat table’s PREROUTING/POSTROUTING chains, respectively.  Thanks to commenter Binarus for the pointer.
  • 2017-03-30: Thanks to commenter Eike for noting that some terminology with the outbound interface selection was unclear.
  • 2017-02-01: Thanks to commenter arm for noting that newer kernels also provide a NAT|input chain.
  • 2016-11-18: Thanks to commenter Andrey for pointing out an error, which has been corrected.  I’ve also adjusted the arrangement and cleaned up the logic a bit in this version.

I hope you find the document useful.  If you have any input to make it better, please let me know.


  1. Do you happen to know how that routing decision “for this host?” (right before the mangle table’s input chain) determines if the destination is actually “this host”?

    For example, I have a machine which has two ethernet interfaces (say, A and B). Each interface has an IP address and these IP addresses belong to different networks. So, when a packet arrives on interface A, and that packet is destined for ip address B, does the decision logic (“for this host?”) recognize that ‘B’ is an address on the host? Or does the decision really only ask: is this packet destined for the ip address of the interface which received the packet?

    1. That’s a great question – and I don’t have a definitive answer. My best (but totally speculative) guess is that the kernel’s network stack knows what its interfaces and their IP addresses are, and handles the packet internally when the destination is one of those. For example, if I run “ping” from one interface’s IP address to another on the same home, that packet never leaves the system. (e.g. If you have a tap/port mirror on the ethernet from the NIC, you won’t see that ping exchange.)
      One way to test part of this theory would be to put what I call “tracer” LOG rules on each table+chain that you expect the packet to pass, using a unique “–log-label” flag on each. Then you can check the syslog output to validate which chains processed a generated packet. If you do this, I recommend using command-line options on “ping” to send one only – the logs get REALLY chatty really fast. Plus, you can then craft the LOG rules to match only ICMP traffic, keeping things nice and clean.

      1. Thanks. I inserted LOG rules at top of all the chains in all tables, and indeed the packets described above only made it through the Filter input chain, and they only logged once. So, with that I think we can conclude that you are right that the kernel knows all the IP addresses it has. Thanks!

        1. Actually, I described that incorrectly: I didn’t add LOG rules to any pre-routing chains in any table no to Mangle input.

          1. great approach – and sounds like it operated as expected! Thank you for following up with the test results!!

  2. Thanks for preparing the diagram, it was *the* source I needed to debug an issue with iptables.

    Hint: the PNG file has transparent background which may render as black when opened in a browser. So if after opening it all you see are boxes on a solid black background, download it and “convert -flatten chart.png chart-fixed.png” to make the background white 🙂

    1. I’m glad you found the diagram useful!

      Thank you as well for the suggestion on re-coloring the PNG file – that will allow anyone’s preference on the file. Great idea!

    1. Loopback traffic would start at the “Locally-generated Packet” step, progress through the “Outgoing Packet” step – but the outgoing interface would be lo or equivalent. Then, the packet would be picked up at “Incoming Packet” and process through the “Local Processing” step.

      1. thanks.
        can you explain “Locally-generated Packet” to the outgoing host, why don’t progress through “PREROUTING” chain, maybe this packet need DNAT.

        1. Any packet generated by a local process – whether and originating session or a response – follows the pipeline starting at “Locally Generated Packet”. Therefore, you cannot adjust the destination IP address for these packets using the netfilter modules, as there is no table+chain that packet will traverse that allows the DNAT action. Since the routing decision is made first (by the IP stack), no further destination manipulation can be done.

          1. thanks for your reply.
            But, I check all the hook , the nat table + OUTPUT cahin allows the DNAT action.
            Maybe you can do a little test.

          2. Ah yes – nat/OUTPUT does allow that now. Hence the second routing decision. That table+chain is reflected in the table as well.

    1. It’s a bit difficult to compare them directly like that due to some substantial differences in their operation. For example, there are no pre-defined chains and no-predefined tables in nftables, yet it does have pre-defined processes that sound similar (e.g. PREROUTING). IMHO, it’s confusing at first for iptables veterans because the terminology does not carry over 1:1 per se.

      nftables seems to try and group commands by function versus by sequence (what iptables does). It seems the idea is to combine into nftables the functions that were called by multiple indpendent tools (e.g. iptables and ip6tables).

      You may want to checkout these (very high level) process flow maps here: https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks

      Continuing with my example above, there is still a PREROUTING process in some nftables charts, but it’s not a chain. So, as a long-time iptables user, one tends to think “What is that? A chain?” No, it’s just informing you of the current position of the packet as it is traversing IP packet processing.

      Another related example of their differences…. nftables uses table “types” or “families” which are ip, arp, ip6, bridge, inet, and netdev. Overall, it’s really comparing apples to cucumbers. The tools are quite different.

      Re-capping… the big diffs between iptables and nftables are nftables has:
      – no built-in or default chains or tables
      – all tool “families” combined into a single tool
      – new kernel sub-system that allows some new features (e.g. built-in data sets)
      – theoretically easier to scale (YMMV) vs. iptables

      No wonder nftables adoption has lagged since it was introduced. Those of us who have been writing iptables rules for a long time really don’t have a lot of good reasons to move, quite frankly.

    2. Great question and thanks very much to @David for the details. I agree it’s a confusing shift from those who know iptables at any level to get a good idea of what nftables does differently and how to translate from old iptables syntax to nftables.

      I’ve started the very preliminary stages of a diagram after you posted the comment, but after a few hours of research and testing, it’s going to be quite difficult to create a parallel diagram. The simple hook ASCII art that David linked is a start, but there are a lot more nuances than appear in the diagram.

      I can’t project when anything may be ready for release, but I’ll make a note of it here if/when it is.

  3. Why do the counters not seem to reflect chain traversals? I’ve been trying to figure this out for years, and the counter behavior is apparently not a well understood topic.

    Here’s my current output for the “incoming packet destined for local processing”. Everything was zeroed out at the same time. Why are the 8116 packets in “nat PREROUTING” not shown for “raw” and “mangle PREROUTING”? Also, why do the number of “new connection” packets in the “filter INPUT” chain (rules 2-4) not equal 8116? (They almost do, though). Note: “mangle FORWARD” and “filter FORWARD” both show “0 packets”.

    The script I used for visualization purposes is at the bottom.


    Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target prot opt in out source destination


    Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target prot opt in out source destination


    Chain PREROUTING (policy ACCEPT 8116 packets, 631K bytes)
    pkts bytes target prot opt in out source destination


    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
    pkts bytes target prot opt in out source destination


    Chain INPUT (policy DROP 2296 packets, 120K bytes)
    pkts bytes target prot opt in out source destination
    15899 2500K ACCEPT all -- * * state RELATED,ESTABLISHED
    4979 406K DROP udp -- * * multiport ports 137:138
    787 258K ACCEPT udp -- * * udp spt:67 dpt:68
    2296 120K LOG all -- * * LOG flags 0 level 4


    Chain INPUT (policy ACCEPT 6 packets, 1968 bytes)
    pkts bytes target prot opt in out source destination

    The bash script:

    # display iptables chains in order of packet traversal
    # see https://stuffphilwrites.com/2014/09/iptables-processing-flowchart/

    "raw PREROUTING"
    "mangle PREROUTING"
    "nat PREROUTING"
    "mangle INPUT"
    "filter INPUT"
    "nat INPUT"

    show_iptables() {
    local table=$1
    local chain=$2
    echo -e "${table}\n"
    iptables -t ${table} -L ${chain} -vn;

    for chain in "${CHAINS[@]}"; do
    show_iptables ${chain}

    1. (edited your post for formatting)

      You have a great question there and to be honest, I’m not sure what’s going on there. I never paid much attention to the packet counters for the default chain policy, as my configurations are typically set to ALLOW, with explicit LOG+DROP rules at the bottom to handle things. All I might suggest would be to add rules on each of the preceding chains that match the traffic you want to count to see if that triggers the increase as expected.

      I’m interested in hearing your results – this is a very curious observation indeed.

    1. I removed that line some time ago because not all received packets will result in a locally-generated response. The line was confusing for most people, as the received and sent packets are discrete events.
      Also, there are no sources for the image, just the image itself.

    The best datapath that I saw!

    But I have a question…
    After [mangle POSTROUTING], there is a . Why?
    This decision is not taken in (between PREROUTING and INPUT).

    Thank you.

    1. Truthfully, I don’t know why that is. You may want to contact the kernel developers that contributed this code to get better detail on that.

        1. Thanks for the suggestion and link. I rarely encounter systems with SELinux enabled, so I had left that out. But it’s a good suggestion and I’ve updated the document. I’ll replace that in the post in just a moment.

    1. This has been a problem for a while now – the site was taken down without explanation. However, I’ve just pulled a mirror from the Wayback Machine and hosted it myself. The short link has been updated (note the link is actually for572.com/iptables-structure). Thanks for the nudge to finally check this off the proverbial “to-do” list!

  5. I always thought you would design the flow chart first then make the software – not the other way round. Then again what do I know.

    Glad you finally got round to making this.

  6. Hi I try policy based routing and made one fwmark in filter FORWARD, after some ACCEPT and DENY in my firewall. Via ip rule matching this fwmark I try to make a routing decission. I can see that the fwmark is set and the rule is correct, but never used.
    So I think there is no routing decision after filter FORARD.
    If I set my fwmark in mangle PRERPOUTING it works.

    1. Interesting observation – possibly a semantic/clarity issue on my mart. Additionally, the source documentation is ambiguous here. (Their diagram shows the interface routing decision is done after the filter|OUTPUT and filter|FORWARD chains, but tables 6-2 and 6-3 do not reflect this.)

      I included that block and specified it was an “INTERFACE Routing Decision”, meaning the system determines what the output interface will be, but doesn’t make a full routing calculation. This interface determination is important, because the POSTROUTING chains will have access to the outgoing interface name for the “-o” flag in iptables.

      I’ll update this to reflect that it’s an interface determination, not a routing decision, though. Give me a few minutes to update and publish.

      Thanks for the observation and feedback!

      1. The ruleset filter.FORWARD permits tests of the outbound interface (-o … ) as well as the inbound interface. The choice of output interface cannot therefore follow filter.FORWARD.


        1. Ah – that’s an interesting nuance. The outbound interface is initially identified during the “Routing Decision” step. This is what’s used during the -o evaluation on the filter.FORWARD chain. However, since the packet is not released to an interface until after the various *.OUTPUT rules have been handled. I absolutely understand where the confusion comes from here and agree that perhaps the wording on “Outbound Interface Assignment” could be clearer – perhaps “Release to outbound interface”. I did try to mirror the upstream iptables documentation as closely as possible, though. I’ll flag this for clarification in the next update – thank you for pointing it out!

  7. A little problem with the chart:

    Right after the locally-generated-packet and before the raw-output, I think there should be a routing-decision.

    Am I right?

    1. I don’t believe so but I’m looking into a few resources on this. The routing decision is made down the processing pipeline in the “Interface Routing Decision” box. I’m quite sure that the iptables/netfilter processing is all done before that step.

    1. Also, you are not showing a path for local packets with local destinations to take, which traverse OUTPUT and then INPUT.

      1. Great catch on the NAT/INPUT chain. It seems this was not present in CentOS prior to version 7 (possibly 6 – don’t have a system to test at this moment). I’ll get all of the details on the placement within the flowchart nailed down and post an update here when it’s ready.
        Some details on this are here:

        With regard to the local destinations, that’s not something I’d want to address in the chart itself, as it’s just a path from “Outgoing packet” to “Incoming packet” that doesn’t happen to leave the system’s memory.

        Thanks for the great call on NAT/INPUT!

      2. Thank you very much for that useful chart which helped me a lot and is the most accurate and up-to-date one I could find so far.

        However, I think there is still an error. In response to zrm’s comment, you have stated that packets originating from and destined for the local machine (i.e. looped back packets) just travel the output chain first, then the input chain, and that you therefore don’t want to treat this subject explicitly in the graph.

        As far as I can tell, this is not correct. Many years ago, I had problems with understanding some test results and therefore posted a message at the netfilter mailing list. The answer from Pascal Hambourg clearly stated that locally generated packets do not pass the nat PREROUTING chain, for example.

        This means that packets which are generated locally and absorbed locally do *not* run through all input changes (and eventually not even through all output changes, but I don’t know for sure).

        Therefore, I would be very grateful if you could update the graph so that it correctly reflects how purely internal packets are flowing.

        The conversation at the netfilter mailing list is here:

        Please note that I might be wrong since it is 8 years later now. On the other hand, I strongly doubt that they have changed this because it just makes sense.

        1. Aha – I see what was meant before and I appreciate the input. I’ve updated this to reflect two differences when handling localhost-to-localhost packets. (Note that the nat/POSTROUTING chain is not used for packets destined to localhost either.)


  8. Well, do not use it as a generic flowchart. It has two mistakes when compared to the source it was derived from.

    1. Hello, Andrey – can you let me know what the errors are? This is based on a number of sources, and I believe it to be correct. However, if there are errors, I would very much like to fix them right away.

      1. 1. ‘filter FORWARD’ and ‘filter OUTPUT’ should merge not at ‘mangle POSTROUTING’, but at ‘routing decision’ (that is incorrectly placed between ‘nat OUTPUT’ and ‘filter OUTPUT’).
        2. branching ‘For this host?’ is also a routing decision (or routing decision should precede it) .

        1. Thanks for the update, Andrey. Great catch on the logic bug – fixed. I’ve also adjusted the logic flow a bit to (hopefully) be more coherent and straightforward.

Leave a comment

Your email address will not be published. Required fields are marked *