<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pf on inetshell</title><link>https://inet.sh/en/tags/pf/</link><description>Recent content in Pf on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Mon, 05 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/pf/index.xml" rel="self" type="application/rss+xml"/><item><title>OPNsense: port-forward unreachable from the same WAN subnet (reply-to)</title><link>https://inet.sh/en/posts/opnsense/reply-to-same-wan-subnet/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/opnsense/reply-to-same-wan-subnet/</guid><description>Why an OPNsense port-forward works from the internet but times out from an IP in the same /24 as the WAN, and how “Disable reply-to on WAN rules” fixes it.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p><strong>Firewall → Settings → Advanced → Disable reply-to on WAN rules</strong> → check → <strong>Save</strong> → <strong>Apply</strong>.</p>
<p>Symptom: a port-forward or WAN rule answers from any internet IP, but from a host that lives in <strong>the same subnet as the firewall&rsquo;s WAN</strong> everything times out (SSH, HTTPS, even <code>ping</code>), even though the rule says <code>from any</code>.</p>
<p>With a single WAN this is safe: the firewall stops forcing replies through the gateway and goes back to its routing table.</p>
<h2 id="the-setup">The setup</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">                203.0.113.0/24  (same L2 segment)
</span></span><span class="line"><span class="cl">   ┌────────────────────┬────────────────────┬────────────────────┐
</span></span><span class="line"><span class="cl">   │                    │                    │                    │
</span></span><span class="line"><span class="cl"> gateway             fw01 (OPNsense)      neighbor             internet
</span></span><span class="line"><span class="cl"> 203.0.113.1         WAN 203.0.113.40     203.0.113.50         (via gateway)
</span></span><span class="line"><span class="cl">                        │ rdr :22 → 192.168.1.10
</span></span><span class="line"><span class="cl">                        ▼
</span></span><span class="line"><span class="cl">                     LAN server
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>From the internet: <code>ssh -p 22 203.0.113.40</code> reaches the LAN server. Fine.</li>
<li>From <code>neighbor</code> (203.0.113.50): every port times out.</li>
</ul>
<h2 id="what-reply-to-does">What <code>reply-to</code> does</h2>
<p>OPNsense generates the rules on WAN-type interfaces (those with a gateway) with <code>reply-to</code> pointing at that gateway:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pfctl -sr <span class="p">|</span> grep reply-to
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pass in quick on vtnet0 reply-to (vtnet0 203.0.113.1) inet proto tcp from any to 192.168.1.10 port = ssh flags S/SA keep state
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>reply-to</code> is a <strong>pf</strong> option. When a packet comes in and creates a state, pf records in that state that <strong>every reply</strong> must leave through <code>vtnet0</code> toward <code>203.0.113.1</code>. Those replies skip the routing table: pf picks the next hop on its own.</p>
<p>It exists for <strong>multi-WAN</strong>. If you have two providers and a client comes in on WAN2, the reply must leave through WAN2. If it leaves through the default route (WAN1), it carries a source IP that provider 1 does not recognize and gets lost. <code>reply-to</code> guarantees a symmetric return path without thinking about routes.</p>
<h2 id="why-it-breaks-neighbors">Why it breaks neighbors</h2>
<p>For an internet client, &ldquo;reply to the gateway&rdquo; is exactly what the routing table would do anyway, so nothing looks wrong.</p>
<p>For the neighbor it is different. Its IP (203.0.113.50) is on a <strong>connected network</strong> of the firewall. Without <code>reply-to</code>, the firewall would ARP for its MAC and answer directly. With <code>reply-to</code>, the SYN-ACK leaves with the <strong>gateway&rsquo;s MAC</strong> as destination:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># on the OPNsense box (csh shell: wrap in sh -c)</span>
</span></span><span class="line"><span class="cl">sh -c <span class="s2">&#34;tcpdump -lnei vtnet0 host 203.0.113.50 and tcp port 22&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;neighbor-mac&gt; &gt; &lt;fw01-mac&gt;          203.0.113.50.49612 &gt; 203.0.113.40.22: Flags [S]    ← arrives directly
</span></span><span class="line"><span class="cl">&lt;fw01-mac&gt;     &gt; 00:00:5e:00:01:&lt;vrid&gt; 203.0.113.40.22 &gt; 203.0.113.50.49612: Flags [S.]   ← leaves to the gateway
</span></span></code></pre></td></tr></table>
</div>
</div><p>(<code>00:00:5e:00:01:xx</code> is the VRRP virtual MAC: the gateway was a redundant pair.)</p>
<p>From there it depends on the gateway. To it, the packet arrives on the same interface it would have to leave through, and if it runs a stateful filter it sees a SYN-ACK for a connection whose SYN it never saw. In the lab, the gateway simply did not send it back onto the segment. On the neighbor you only see retried SYNs and no <code>[S.]</code>.</p>
<p>The fingerprint on the firewall is a half-open state:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pfctl -ss <span class="p">|</span> grep 203.0.113.50
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">all tcp 192.168.1.10:22 (203.0.113.40:22) &lt;- 203.0.113.50:57792   SYN_SENT:ESTABLISHED
</span></span></code></pre></td></tr></table>
</div>
</div><p>The server already answered (<code>ESTABLISHED</code>) and the client is still waiting (<code>SYN_SENT</code>): <strong>the reply is lost on the way back</strong>, not on the way in. The filter is not blocking anything.</p>
<h2 id="what-the-fix-changes">What the fix changes</h2>
<p>With <strong>Disable reply-to on WAN rules</strong> checked, OPNsense regenerates the rules without <code>reply-to</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pass in quick on vtnet0 inet proto tcp from any to 192.168.1.10 port = ssh flags S/SA keep state
</span></span></code></pre></td></tr></table>
</div>
</div><p>The state no longer pins the next hop, so replies go through the normal routing table:</p>
<table>
  <thead>
      <tr>
          <th>Reply destination</th>
          <th>With <code>reply-to</code></th>
          <th>Without <code>reply-to</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Internet client</td>
          <td>gateway</td>
          <td>gateway (default route)</td>
      </tr>
      <tr>
          <td>Neighbor in 203.0.113.0/24</td>
          <td>gateway ✗</td>
          <td>direct via ARP ✓</td>
      </tr>
      <tr>
          <td>Client that came in on another WAN (multi-WAN)</td>
          <td>its gateway ✓</td>
          <td>default route ✗</td>
      </tr>
  </tbody>
</table>
<p>The last row is the cost. With multi-WAN, do not disable it globally. Instead, drop <code>reply-to</code> only on the affected port-forward&rsquo;s rule: in the rule, <strong>Advanced → reply-to</strong>. Check which options your version offers.</p>
<h2 id="verify">Verify</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># on the OPNsense box</span>
</span></span><span class="line"><span class="cl">sh -c <span class="s2">&#34;pfctl -sr | grep -c reply-to; grep -n disablereplyto /conf/config.xml&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># expected: 0  and  &lt;disablereplyto&gt;yes&lt;/disablereplyto&gt;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># from the neighbor</span>
</span></span><span class="line"><span class="cl">nc -z -w5 203.0.113.40 <span class="m">22</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> open
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="traps">Traps</h2>
<ul>
<li><strong>&ldquo;Disable force gateway&rdquo; is not this.</strong> It sits on the same page with a similar name, but it only affects traffic <strong>originated by the firewall itself</strong> (DNS, NTP, updates). It does not touch replies to inbound traffic.</li>
<li><strong>A static route on the gateway does not help.</strong> The gateway already has the subnet as connected; the problem is that it will not send the packet back onto the same segment, not that it does not know the way.</li>
<li><strong>csh shell.</strong> Over SSH, OPNsense drops you into <code>csh</code>, and things like <code>2&gt;/dev/null</code> fail with <code>Ambiguous output redirect</code>. Use <code>sh -c &quot;...&quot;</code> or <code>ssh root@fw01 sh &lt;&lt;'EOF'</code>.</li>
<li><strong>Test every port, not one.</strong> If only one port fails it is a different problem (rule, NAT, service). If they all fail, <code>ping</code> included, and only from the same subnet, suspect <code>reply-to</code>.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li>OPNsense docs, firewall settings: <a href="https://docs.opnsense.org/manual/firewall_settings.html">https://docs.opnsense.org/manual/firewall_settings.html</a></li>
<li><code>pf.conf(5)</code>, <code>route-to</code> / <code>reply-to</code> options: <a href="https://man.freebsd.org/cgi/man.cgi?query=pf.conf">https://man.freebsd.org/cgi/man.cgi?query=pf.conf</a></li>
</ul>
]]></content:encoded></item><item><title>pfctl: table cheat sheet for pfSense, OPNsense and FreeBSD</title><link>https://inet.sh/en/posts/pfsense/pfctl-tables-cheat-sheet/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/pfctl-tables-cheat-sheet/</guid><description>pfctl -t/-T commands for PF tables: list, show, add, delete, test, flush, reload from a file and expire entries. With pfSense&amp;#39;s internal tables (sshguard, virusprot, snort2c, bogons, aliases and pfBlockerNG) and what gets lost on a filter reload.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pfctl -sT                                    <span class="c1"># loaded tables</span>
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T show                     <span class="c1"># contents</span>
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T add 192.168.1.10         <span class="c1"># add an IP or network (10.20.0.0/16 works too)</span>
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T delete 192.168.1.10
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T <span class="nb">test</span> 192.168.1.10        <span class="c1"># is it inside? (match / no match)</span>
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T flush                    <span class="c1"># empty the table</span>
</span></span><span class="line"><span class="cl">pfctl -t &lt;table&gt; -T replace -f /path/list    <span class="c1"># replace the whole contents from a file</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">pfctl -t sshguard -T flush                   <span class="c1"># pfSense: clear the failed-login lockout</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="what-a-table-is">What a table is</h2>
<p>A PF <strong>table</strong> is a set of addresses and networks that rules reference as <code>&lt;name&gt;</code>. It changes live, without reloading the ruleset, and holds thousands of entries with fast lookups; that is why PF uses tables for block lists, bogons, and anything that changes more often than the rules.</p>
<p>In <code>pf.conf</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">table &lt;blocklist&gt; persist file &#34;/etc/pf/blocklist.txt&#34;
</span></span><span class="line"><span class="cl">block in quick from &lt;blocklist&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>persist</code> keeps the table even when no rule uses it at the moment; <code>file</code> loads it from a file (one IP or network per line).</p>
<h2 id="commands">Commands</h2>
<p>All in the form <code>pfctl -t &lt;table&gt; -T &lt;command&gt;</code>:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>show</code></td>
          <td>Lists the entries. With <code>-v</code>, per-entry counters (packets/bytes in/out)</td>
      </tr>
      <tr>
          <td><code>add &lt;ip|net&gt; …</code></td>
          <td>Adds one or more entries</td>
      </tr>
      <tr>
          <td><code>delete &lt;ip|net&gt; …</code></td>
          <td>Removes entries</td>
      </tr>
      <tr>
          <td><code>test &lt;ip&gt; …</code></td>
          <td>Says whether the address falls inside the table (without touching it)</td>
      </tr>
      <tr>
          <td><code>flush</code></td>
          <td>Empties the table; the table keeps existing</td>
      </tr>
      <tr>
          <td><code>kill</code></td>
          <td>Deletes the whole table (if no rule uses it)</td>
      </tr>
      <tr>
          <td><code>replace -f &lt;file&gt;</code></td>
          <td>Swaps the entire contents for the file&rsquo;s, in a single operation</td>
      </tr>
      <tr>
          <td><code>expire &lt;seconds&gt;</code></td>
          <td>Deletes entries whose counters have been idle for more than N seconds</td>
      </tr>
      <tr>
          <td><code>zero</code></td>
          <td>Zeroes the table&rsquo;s counters (or one entry&rsquo;s, if you name it)</td>
      </tr>
  </tbody>
</table>
<p>And without <code>-t</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pfctl -sT                    <span class="c1"># names of all tables</span>
</span></span><span class="line"><span class="cl">pfctl -vvsT                  <span class="c1"># global info per table: entries, evaluations, references</span>
</span></span><span class="line"><span class="cl">pfctl -T load -f /etc/pf.conf   <span class="c1"># reload only the table definitions from pf.conf</span>
</span></span><span class="line"><span class="cl">pfctl -sr <span class="p">|</span> grep <span class="s1">&#39;&lt;name&gt;&#39;</span>    <span class="c1"># which rules use a table</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="tables-on-pfsense">Tables on pfSense</h2>
<ul>
<li><strong>Aliases are tables.</strong> A Host, Network, or URL Table alias called <code>Admins</code> exists in PF as <code>&lt;Admins&gt;</code>; <code>pfctl -t Admins -T show</code> shows what pfSense resolved, including the FQDNs it already turned into IPs.</li>
<li><strong><code>sshguard</code></strong>: where IPs blocked for failed SSH or webGUI logins land. If you locked yourself out, <code>pfctl -t sshguard -T flush</code> (documented by Netgate under <a href="https://docs.netgate.com/pfsense/en/latest/troubleshooting/locked-out.html">Locked out of the WebGUI</a>).</li>
<li><strong><code>virusprot</code></strong>: hosts blocked for exceeding a rule&rsquo;s <em>Max. src. conn. rate</em> or <em>Max. states</em> limits. <code>pfctl -t virusprot -T show</code> and <code>-T delete &lt;ip&gt;</code> to release one.</li>
<li><strong><code>snort2c</code></strong>: Snort/Suricata blocks in legacy mode. <code>-T flush</code> to unblock everything.</li>
<li><strong><code>bogons</code> / <code>bogonsv6</code></strong>: the lists that update themselves when you enable <em>Block bogon networks</em> on an interface.</li>
<li><strong>pfBlockerNG</strong>: every list is a <code>pfB_*</code> alias. To know whether an IP is blocked by one: <code>pfctl -t pfB_Top_v4 -T test 203.0.113.7</code>.</li>
</ul>
<p>On OPNsense the mechanics are the same (<code>pfctl -sT</code> to see the names); the login lockout table is called <code>sshlockout</code>.</p>
<h2 id="what-gets-lost-on-reload">What gets lost on reload</h2>
<p>Any <code>-T add</code>, <code>-T delete</code>, or <code>-T flush</code> lives until the next <strong>filter reload</strong>: saving almost anything in the GUI, <code>Filter Reload</code> under Status → Filter Reload, or the alias update cron. At that point pfSense regenerates every table from its alias and your manual changes are gone. To make something permanent, edit the alias; for quick tests and unblocking, <code>pfctl</code> is perfect.</p>
<h2 id="beyond-tables">Beyond tables</h2>
<p>What gets used most alongside the above:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pfctl -sr                    <span class="c1"># loaded rules (with -vv, counters and rule numbers)</span>
</span></span><span class="line"><span class="cl">pfctl -ss                    <span class="c1"># state table</span>
</span></span><span class="line"><span class="cl">pfctl -si                    <span class="c1"># global statistics</span>
</span></span><span class="line"><span class="cl">pfctl -k 192.168.1.10        <span class="c1"># kill a host&#39;s states</span>
</span></span><span class="line"><span class="cl">pfctl -F states              <span class="c1"># kill all states (cuts every connection)</span>
</span></span><span class="line"><span class="cl">pfctl -d / pfctl -e          <span class="c1"># disable / enable PF (no PF means no NAT: only to get out of a lockout)</span>
</span></span><span class="line"><span class="cl">pfctl -nf /etc/pf.conf       <span class="c1"># syntax check without loading</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Full reference: the OpenBSD Handbook&rsquo;s <a href="https://www.openbsdhandbook.com/pf/cheat_sheet/">pfctl cheat sheet</a> and <code>man pfctl</code> on FreeBSD.</p>
]]></content:encoded></item></channel></rss>