<?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>Nat on inetshell</title><link>https://inet.sh/en/tags/nat/</link><description>Recent content in Nat 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/nat/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>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></channel></rss>