<?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>Opnsense on inetshell</title><link>https://inet.sh/en/tags/opnsense/</link><description>Recent content in Opnsense 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/opnsense/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><item><title>pfSense and OPNsense: the port forward does not work from the LAN itself (NAT reflection and split DNS)</title><link>https://inet.sh/en/posts/pfsense/nat-reflection-port-forward-from-lan/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/nat-reflection-port-forward-from-lan/</guid><description>From outside the port forward works, but from the LAN the public IP or the domain lands on the firewall&amp;#39;s GUI or a certificate error. Two fixes: split DNS with host overrides in the DNS Resolver (the recommended one) or NAT reflection in Pure NAT mode with automatic outbound NAT. Includes the OPNsense equivalent and the DNS rebinding trap.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p><strong>Clean option (split DNS):</strong> Services → DNS Resolver → <em>Host Overrides</em> → <code>app.example.com</code> → the server&rsquo;s internal IP (<code>192.168.1.50</code>). LAN clients must use the firewall as their DNS.</p>
<p><strong>NAT reflection option:</strong> System → Advanced → <em>Firewall &amp; NAT</em>:</p>
<ul>
<li><em>NAT Reflection mode for port forwards</em>: <strong>Pure NAT</strong></li>
<li><em>Enable NAT Reflection for 1:1 NAT</em>: ✔</li>
<li><em>Enable automatic outbound NAT for Reflection</em>: ✔</li>
</ul>
<p>Save and test from a LAN client against the public IP.</p>
<h2 id="the-problem">The problem</h2>
<p>The port forward <code>WAN:443 → 192.168.1.50:443</code> works from the Internet. From a machine on the same LAN, <code>https://app.example.com</code> (which resolves to the public IP) answers with the pfSense GUI, a certificate error, or <code>Potential DNS Rebind attack detected</code>.</p>
<p>The packet leaves the client towards the public IP, reaches the firewall through the LAN interface, and stops there: the port forward rule listens on the WAN, not the LAN, and the destination IP is one of the firewall&rsquo;s own addresses. It never &ldquo;goes out and comes back in&rdquo;.</p>
<h2 id="option-1-split-dns">Option 1: split DNS</h2>
<p>This is what Netgate recommends: the name resolves to the internal IP inside and to the public one outside. Traffic goes direct, the server sees the client&rsquo;s real IP, and the firewall touches nothing.</p>
<ol>
<li>Services → DNS Resolver → <em>Host Overrides</em> → <strong>Add</strong>: host <code>app</code>, domain <code>example.com</code>, IP <code>192.168.1.50</code>. If the service has several names, add <em>Additional Names</em>.</li>
<li>Clients must use the firewall as DNS (the norm if DHCP hands it out). A client with <code>8.8.8.8</code> hardcoded will keep going to the public IP.</li>
<li><code>nslookup app.example.com</code> from the LAN → <code>192.168.1.50</code>.</li>
</ol>
<p>Requirement: the internal and external ports must be the same (if outside you forward <code>8443 → 443</code>, the internal client will have to use <code>443</code> directly).</p>
<p>If you run the DNS Forwarder (dnsmasq) instead of the Resolver, the equivalent setting is under Services → DNS Forwarder → <em>Host Overrides</em>.</p>
<h2 id="option-2-nat-reflection">Option 2: NAT reflection</h2>
<p>When you do not control the clients&rsquo; DNS, or there are lots of ports, the firewall can &ldquo;reflect&rdquo; the connection: it rewrites the destination to the internal IP and, with automatic outbound NAT, the source too, so the reply comes back through the firewall.</p>
<p>System → Advanced → <em>Firewall &amp; NAT</em>, <em>Network Address Translation</em> section:</p>
<table>
  <thead>
      <tr>
          <th>Setting</th>
          <th>Value</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NAT Reflection mode for port forwards</td>
          <td><strong>Pure NAT</strong></td>
          <td>PF rules only: every protocol, no port limit. <em>NAT + Proxy</em> uses a daemon, TCP only and at most 500 ports per rule</td>
      </tr>
      <tr>
          <td>Enable NAT Reflection for 1:1 NAT</td>
          <td>✔</td>
          <td>If you have 1:1 NAT besides port forwards</td>
      </tr>
      <tr>
          <td>Enable automatic outbound NAT for Reflection</td>
          <td>✔</td>
          <td>Without it, when client and server share a subnet the server replies straight to the client, which does not recognize the connection and drops it</td>
      </tr>
      <tr>
          <td>Reflection Timeout</td>
          <td>(NAT + Proxy only)</td>
          <td>Ignore it in Pure NAT</td>
      </tr>
  </tbody>
</table>
<p>It can also be enabled per rule: Firewall → NAT → Port Forward → edit → <em>NAT reflection</em> → <em>Enable (Pure NAT)</em>, leaving the global setting on <em>Disable</em>.</p>
<p>Costs: the server sees the firewall&rsquo;s LAN IP as the source (not the client&rsquo;s), and all that traffic crosses the firewall even when client and server sit on the same switch.</p>
<h2 id="opnsense">OPNsense</h2>
<p>Same idea, other menus:</p>
<ul>
<li>Split DNS: Services → Unbound DNS → <em>Overrides</em> → Host Overrides.</li>
<li>Reflection: Firewall → Settings → <em>Advanced</em> → <strong>Reflection for port forwards</strong>, <strong>Reflection for 1:1</strong>, <strong>Automatic outbound NAT for Reflection</strong>.</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>DNS rebinding.</strong> If instead of a host override you point the Resolver at an external DNS that returns private IPs (an internal DNS at another site, a provider with views), Unbound drops those answers as rebinding protection. Do not untick <em>Disable DNS Rebinding Checks</em> under System → Advanced → Admin Access: add <code>private-domain: &quot;example.com&quot;</code> under Services → DNS Resolver → <em>Custom options</em> and only that domain is exempt.</li>
<li><strong>A client with hardcoded DNS</strong> bypasses split DNS. Either force it through DHCP, catch it with a port 53 NAT rule, or use reflection.</li>
<li><strong>Pure NAT needs to know the interface and gateway</strong> when rules load; with dynamic WANs that change IP, a <em>Filter Reload</em> after the IP change fixes it.</li>
<li><strong>Different ports inside and outside</strong> only work with reflection; split DNS does not translate ports.</li>
<li><strong>Certificates</strong>: with split DNS the browser still sees <code>app.example.com</code>, so the server&rsquo;s certificate works as is. When reflecting through the public IP, too. The certificate error at the start was the firewall GUI&rsquo;s, not the server&rsquo;s.</li>
</ul>
<p>References: <a href="https://docs.netgate.com/pfsense/en/latest/recipes/port-forwards-from-local-networks.html">Port Forwards from Local Networks</a> and <a href="https://docs.netgate.com/pfsense/en/latest/nat/reflection.html">NAT Reflection</a> in the Netgate documentation.</p>
]]></content:encoded></item><item><title>Virtualized pfSense with VirtIO: no traffic until hardware checksum offload is disabled</title><link>https://inet.sh/en/posts/pfsense/virtio-disable-hardware-checksum-offload/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/virtio-disable-hardware-checksum-offload/</guid><description>pfSense or OPNsense on Proxmox, KVM or bhyve with VirtIO NICs (vtnet): the firewall boots but passes no traffic, or hosts behind it cannot be reached from the hypervisor and other VMs. It is vtnet checksum offload on FreeBSD. System &amp;gt; Advanced &amp;gt; Networking &amp;gt; Disable hardware checksum offload and a mandatory reboot.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p><strong>System → Advanced → Networking → Disable hardware checksum offload</strong> ✔ → <strong>Save</strong> → <strong>reboot the VM</strong>. The GUI does not say so, but the change does not apply to <code>vtnet</code> interfaces until the reboot.</p>
<p>To test without rebooting (lost at boot):</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">ifconfig vtnet0 -rxcsum -txcsum -tso -lro
</span></span><span class="line"><span class="cl">ifconfig vtnet1 -rxcsum -txcsum -tso -lro
</span></span></code></pre></td></tr></table>
</div>
</div><p>On OPNsense: <strong>Interfaces → Settings → Hardware CRC → Disable hardware checksum offload</strong>, and the same reboot.</p>
<h2 id="symptoms">Symptoms</h2>
<p>The VM boots, the interfaces are <code>UP</code>, LAN DHCP hands out addresses, and still:</p>
<ul>
<li>Hosts behind the firewall cannot reach the Internet, or connections stall halfway (ping passes, TCP hangs).</li>
<li>From the virtualization host itself or from other VMs on the same host nothing protected by pfSense is reachable, not even the GUI.</li>
<li>Switching the VM&rsquo;s NIC to <code>e1000</code> fixes it on the spot, which confirms the diagnosis.</li>
</ul>
<h2 id="why-it-happens">Why it happens</h2>
<p>FreeBSD&rsquo;s <code>vtnet</code> driver, with checksum offload enabled, hands the network stack packets whose checksum never got computed: between VMs on the same host, and between the host and the VM, traffic never crosses a physical NIC that would do it. FreeBSD sees invalid checksums and drops them (FreeBSD bug 165059). Netgate documents it under <a href="https://docs.netgate.com/pfsense/en/latest/virtualization/virtio-driver-support.html">VirtIO Driver Support</a>.</p>
<p>pfSense tries to tick the option only when it detects <code>vtnet</code> interfaces <strong>at install time</strong>. If the VirtIO NIC was added later, if you restored the <code>config.xml</code> from a physical firewall, or if you migrated the VM from another hypervisor with a different NIC type, the option stays off.</p>
<h2 id="procedure">Procedure</h2>
<ol>
<li>
<p><strong>System → Advanced → Networking</strong>, <em>Network Interfaces</em> section:</p>
<ul>
<li><strong>Disable hardware checksum offload</strong>: tick it.</li>
<li><strong>Disable hardware TCP segmentation offload</strong> and <strong>Disable hardware large receive offload</strong>: already ticked by default; leave them.</li>
</ul>
</li>
<li>
<p><strong>Save</strong>.</p>
</li>
<li>
<p><strong>Reboot</strong> from Diagnostics → Reboot. Without this step nothing changes on <code>vtnet</code>.</p>
</li>
<li>
<p>Check:</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">ifconfig vtnet0 <span class="p">|</span> grep options
</span></span></code></pre></td></tr></table>
</div>
</div><p>Neither <code>RXCSUM</code> nor <code>TXCSUM</code> (nor <code>TSO4</code>, <code>LRO</code>) should show up.</p>
</li>
</ol>
<h2 id="if-it-is-still-the-same">If it is still the same</h2>
<ul>
<li><strong>All of the VM&rsquo;s NICs must be the same kind.</strong> A <code>vtnet</code> with offload and an <code>e1000</code> behave differently; check every <code>ifconfig</code>.</li>
<li><strong>Offload on the host.</strong> Netgate notes that sometimes checksums and TSO must also be turned off on the hypervisor. On Proxmox/KVM, on the VM&rsquo;s <code>tap</code> interface: <code>ethtool -K tap100i0 tx off</code> (the number is the VMID and <code>i0</code> the NIC). Rarely needed; try it last.</li>
<li><strong>Plan B</strong>: <code>e1000</code> NIC model on the VM. Less throughput than VirtIO without offload, but none of this bug.</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>The reboot is not optional</strong> and the GUI does not ask for it. It is the number one cause of &ldquo;I ticked it and it still does not work&rdquo;.</li>
<li><strong>It is not a rules problem.</strong> With offload on, packets are dropped before reaching <code>pf</code>; the firewall log shows nothing.</li>
<li><strong><code>e1000</code> on OPNsense too</strong> as plan B; the equivalent setting lives under <em>Interfaces → Settings</em>.</li>
<li><strong>Multiqueue</strong> (<code>queues=N</code> on Proxmox) works fine with <code>vtnet</code> once offload is off; do not mistake it for the cause.</li>
</ul>
]]></content:encoded></item></channel></rss>