<?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>Pfsense on inetshell</title><link>https://inet.sh/en/tags/pfsense/</link><description>Recent content in Pfsense on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/pfsense/index.xml" rel="self" type="application/rss+xml"/><item><title>DHCP option 121: classless static routes (RFC 3442), how to encode them in hex and set them on ISC dhcpd, Kea, pfSense and MikroTik</title><link>https://inet.sh/en/posts/network/dhcp-option-121-classless-static-routes/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/network/dhcp-option-121-classless-static-routes/</guid><description>To let DHCP clients reach another network through a router other than the gateway (a site-to-site VPN, a second link) without touching every machine, the DHCP server hands out routes with option 121. The format is binary: one octet with the prefix length, only the significant octets of the destination and the four of the router, all concatenated. A Python script that produces it in the four formats ISC dhcpd, Kea, pfSense and MikroTik expect, and the rule that breaks the network if you forget it: when you send 121, the client ignores option 3 and you must include the default route.</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><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;Encode classless routes (RFC 3442) for DHCP option 121.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">ipaddress</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">encode</span><span class="p">(</span><span class="n">routes</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">dest</span><span class="p">,</span> <span class="n">via</span> <span class="ow">in</span> <span class="n">routes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">net</span> <span class="o">=</span> <span class="n">ipaddress</span><span class="o">.</span><span class="n">ip_network</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="n">strict</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">n</span> <span class="o">=</span> <span class="p">(</span><span class="n">net</span><span class="o">.</span><span class="n">prefixlen</span> <span class="o">+</span> <span class="mi">7</span><span class="p">)</span> <span class="o">//</span> <span class="mi">8</span>                 <span class="c1"># significant destination octets</span>
</span></span><span class="line"><span class="cl">        <span class="n">out</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">net</span><span class="o">.</span><span class="n">prefixlen</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">out</span> <span class="o">+=</span> <span class="n">net</span><span class="o">.</span><span class="n">network_address</span><span class="o">.</span><span class="n">packed</span><span class="p">[:</span><span class="n">n</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">out</span> <span class="o">+=</span> <span class="n">ipaddress</span><span class="o">.</span><span class="n">ip_address</span><span class="p">(</span><span class="n">via</span><span class="p">)</span><span class="o">.</span><span class="n">packed</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">routes</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;192.0.2.0/24&#34;</span><span class="p">,</span> <span class="s2">&#34;192.168.1.254&#34;</span><span class="p">),</span>   <span class="c1"># the other site, through the VPN router</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;0.0.0.0/0&#34;</span><span class="p">,</span>    <span class="s2">&#34;192.168.1.1&#34;</span><span class="p">),</span>     <span class="c1"># the default route: mandatory when you send 121</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">raw</span> <span class="o">=</span> <span class="n">encode</span><span class="p">(</span><span class="n">routes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;hex      &#34;</span><span class="p">,</span> <span class="n">raw</span><span class="o">.</span><span class="n">hex</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;mikrotik &#34;</span><span class="p">,</span> <span class="s2">&#34;0x&#34;</span> <span class="o">+</span> <span class="n">raw</span><span class="o">.</span><span class="n">hex</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;isc      &#34;</span><span class="p">,</span> <span class="s2">&#34;, &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">raw</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;pfsense  &#34;</span><span class="p">,</span> <span class="s2">&#34;:&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">b</span><span class="si">:</span><span class="s2">02x</span><span class="si">}</span><span class="s2">&#34;</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">raw</span><span class="p">))</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><span class="lnt">3
</span><span class="lnt">4
</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">hex       18c00002c0a801fe00c0a80101
</span></span><span class="line"><span class="cl">mikrotik  0x18c00002c0a801fe00c0a80101
</span></span><span class="line"><span class="cl">isc       24, 192, 0, 2, 192, 168, 1, 254, 0, 192, 168, 1, 1
</span></span><span class="line"><span class="cl">pfsense   18:c0:00:02:c0:a8:01:fe:00:c0:a8:01:01
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="the-format">The format</h2>
<p>Every route in option 121 is three fields glued together, no separators:</p>
<table>
  <thead>
      <tr>
          <th>Field</th>
          <th>Size</th>
          <th>Example <code>192.0.2.0/24 via 192.168.1.254</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prefix length</td>
          <td>1 octet</td>
          <td><code>18</code> (24)</td>
      </tr>
      <tr>
          <td>Destination, <strong>significant octets only</strong>: ⌈prefix / 8⌉</td>
          <td>0 to 4 octets</td>
          <td><code>c0 00 02</code> (192.0.2)</td>
      </tr>
      <tr>
          <td>Router</td>
          <td>4 octets</td>
          <td><code>c0 a8 01 fe</code></td>
      </tr>
  </tbody>
</table>
<p>A <code>/24</code> carries three destination octets, a <code>/16</code> two, a <code>/32</code> four, and the default route <code>/0</code> <strong>none</strong>: <code>00</code> followed directly by the router. Several routes are concatenated; the whole option fits in 255 octets (about 28 <code>/24</code> routes).</p>
<h2 id="the-rule-that-breaks-the-network">The rule that breaks the network</h2>
<p>RFC 3442, section 3: <em>&ldquo;If the DHCP server returns both a Classless Static Routes option and a Router option, the DHCP client MUST ignore the Router option.&rdquo;</em> If you start sending 121 with a single route to the other site, clients that understand it <strong>lose the gateway</strong> from option 3 and are left with no way out to the Internet. The default route goes inside 121, always, as in the example. Clients that do not understand 121 keep using option 3, so leave that configured too.</p>
<h2 id="where-it-goes">Where it goes</h2>
<p><strong>ISC dhcpd</strong> (<code>dhcpd.conf</code>): the option has to be declared, and it is worth sending it as 249 as well, which is what older Windows ask for:</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><span class="lnt">8
</span><span class="lnt">9
</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">option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
</span></span><span class="line"><span class="cl">option ms-classless-static-routes code 249 = array of unsigned integer 8;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">subnet 192.168.1.0 netmask 255.255.255.0 {
</span></span><span class="line"><span class="cl">  range 192.168.1.100 192.168.1.199;
</span></span><span class="line"><span class="cl">  option routers 192.168.1.1;
</span></span><span class="line"><span class="cl">  option rfc3442-classless-static-routes 24, 192,0,2, 192,168,1,254, 0, 192,168,1,1;
</span></span><span class="line"><span class="cl">  option ms-classless-static-routes     24, 192,0,2, 192,168,1,254, 0, 192,168,1,1;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>Kea</strong> (2.4 or later) understands the option by name with a readable notation, no hex:</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-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;option-data&#34;</span><span class="err">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;classless-static-route&#34;</span><span class="p">,</span> <span class="nt">&#34;code&#34;</span><span class="p">:</span> <span class="mi">121</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="s2">&#34;192.0.2.0/24 - 192.168.1.254, 0.0.0.0/0 - 192.168.1.1&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>pfSense / OPNsense</strong>: with the ISC backend, <em>Services → DHCP Server → Additional BOOTP/DHCP Options</em>, number <code>121</code>, type <em>String</em>, value in colon-separated hex (<code>18:c0:00:02:…</code>); add another with <code>249</code> for Windows. With the <strong>Kea</strong> backend, option 121 is not in the UI yet (as of this post there are open requests in both projects); if your version allows custom Kea options, use the JSON above; if not, that is a reason to stay on ISC.</p>
<p><strong>MikroTik RouterOS</strong>:</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-text" data-lang="text"><span class="line"><span class="cl">/ip dhcp-server option add name=classless code=121 value=0x18c00002c0a801fe00c0a80101
</span></span><span class="line"><span class="cl">/ip dhcp-server option add name=classless-ms code=249 value=0x18c00002c0a801fe00c0a80101
</span></span><span class="line"><span class="cl">/ip dhcp-server option sets add name=routes options=classless,classless-ms
</span></span><span class="line"><span class="cl">/ip dhcp-server network set [find address=192.168.1.0/24] dhcp-option-set=routes
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="which-clients-understand-it">Which clients understand it</h2>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>Option 121</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Windows (Vista and later)</td>
          <td>Yes; also accepts 249</td>
      </tr>
      <tr>
          <td>Linux with NetworkManager or systemd-networkd</td>
          <td>Yes, nothing to configure</td>
      </tr>
      <tr>
          <td>Linux with <code>dhclient</code></td>
          <td>Only if <code>rfc3442-classless-static-routes</code> is in the <code>request</code> list of <code>dhclient.conf</code> and a hook installs the routes (Debian and Ubuntu ship one in <code>/etc/dhcp/dhclient-exit-hooks.d/</code>)</td>
      </tr>
      <tr>
          <td>macOS and iOS</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Android</td>
          <td>Historically ignores it; do not count on it</td>
      </tr>
      <tr>
          <td>FreeBSD <code>dhclient</code></td>
          <td>Not without a custom <code>dhclient-script</code>; <code>dhcpcd</code> does</td>
      </tr>
  </tbody>
</table>
<p>Check on the client with <code>ip route</code> (Linux), <code>route print</code> (Windows) or <code>netstat -rn</code> (macOS) after renewing the lease.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>A <code>/32</code> with a router in another subnet</strong>: valid in the encoding, but the client needs a prior route to that router; order routes from most to least specific and put the local segment&rsquo;s first if needed.</li>
<li><strong>Routes to networks the DHCP server does not control</strong> (a DHCP server on one VLAN advertising routes through another) are accepted anyway: 121 is a list of bytes, nobody validates it. A one-octet mistake becomes a route to a made-up network.</li>
<li><strong>Clients do not revisit routes until they renew</strong> the lease; after changing the option, <code>T1</code> (half the lease) is how long they take to see it, or force a renewal.</li>
<li><strong>Windows and 249</strong>: if you only send 121, modern Windows uses it; 249 is for compatibility and carries exactly the same bytes.</li>
<li><strong><code>/0</code> advertisements from a rogue DHCP</strong>: any DHCP server on the LAN can rewrite every client&rsquo;s default route with 121; DHCP snooping on the switch is the only real defense.</li>
</ul>
<p>Reference: <a href="https://datatracker.ietf.org/doc/rfc3442/">RFC 3442</a>.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/gcp/cloud-vpn-not-up-ike-logs/">GCP Cloud VPN will not come up</a> — the VPN these routes usually point at.</li>
<li><a href="/en/posts/pfsense/nat-reflection-port-forward-from-lan/">pfSense: NAT reflection and split DNS</a> — the other LAN setting clients receive without noticing.</li>
</ul>
]]></content:encoded></item><item><title>GCP Cloud VPN will not come up: reading the IKE logs (peer not responding, traffic selectors narrowed) and BGP with pfSense</title><link>https://inet.sh/en/posts/gcp/cloud-vpn-not-up-ike-logs/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/gcp/cloud-vpn-not-up-ike-logs/</guid><description>When a Cloud VPN tunnel sits on &amp;#39;No incoming packets&amp;#39; or flaps, the answer is in Cloud Logging: the queries per phase (IKE_SA, CHILD_SA, rekey, DELETE) and what each message means: peer not responding, traffic selectors narrowed, received DELETE. Plus the HA VPN with BGP against pfSense/FRR case, where no routes come in until you add route maps.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>In <strong>Logs Explorer</strong>, every tunnel phase has its query:</p>
<table>
  <thead>
      <tr>
          <th>What you are after</th>
          <th>Query</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloud VPN cannot reach the peer</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; &quot;establishing IKE_SA failed, peer not responding&quot;</code></td>
      </tr>
      <tr>
          <td>Phase 1 starts</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;initiating IKE_SA&quot; OR &quot;generating IKE_SA_INIT request&quot;)</code></td>
      </tr>
      <tr>
          <td>IKE authentication</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;generating IKE_AUTH request&quot; OR &quot;parsed IKE_AUTH response&quot;)</code></td>
      </tr>
      <tr>
          <td>PSK accepted</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;authentication of&quot; AND &quot;with pre-shared key successful&quot;)</code></td>
      </tr>
      <tr>
          <td>Phase 1 established</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;IKE_SA&quot; AND &quot;established between&quot;)</code></td>
      </tr>
      <tr>
          <td>Everything phase 2 (rekeys included)</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; &quot;CHILD_SA&quot;</code></td>
      </tr>
      <tr>
          <td>Subnets do not match</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;Remote traffic selectors narrowed&quot; OR &quot;Local traffic selectors narrowed&quot;)</code></td>
      </tr>
      <tr>
          <td>Someone closes phase 2</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;received DELETE for ESP CHILD_SA&quot; OR &quot;sending DELETE for ESP CHILD_SA&quot;)</code></td>
      </tr>
  </tbody>
</table>
<p>Cloud VPN runs strongSwan underneath, so the messages are strongSwan&rsquo;s: if you know <code>charon</code>, you already know how to read them.</p>
<h2 id="what-each-message-means">What each message means</h2>
<p><strong><code>establishing IKE_SA failed, peer not responding</code>.</strong> Cloud VPN sends IKE_SA_INIT and nobody answers. In order of likelihood: the peer is on IKEv1 and the tunnel on IKEv2 (or the reverse), the configured peer IP is not the real public one (NAT in front of the firewall), UDP 500 and 4500 do not reach the peer, or the peer does not even have the tunnel enabled. The PSK does <strong>not</strong> produce this message: with a wrong PSK, phase 1 starts and fails at <code>IKE_AUTH</code>.</p>
<p><strong><code>IKE_SA … established between</code> and then nothing.</strong> Phase 1 fine, phase 2 not. Look for <code>CHILD_SA</code> and <code>traffic selectors</code>.</p>
<p><strong><code>Remote traffic selectors narrowed</code> / <code>Local traffic selectors narrowed</code>.</strong> The phase 2 networks do not match on both sides. On HA VPN (and route-based Classic VPN) Cloud VPN proposes <strong><code>0.0.0.0/0</code> on both sides</strong> and expects the same; a <em>policy-based</em> peer with specific subnets makes strongSwan narrow the selectors and pass traffic only between what matched, or nothing. On the peer: phase 2 with <code>0.0.0.0/0</code> local and remote (on pfSense, <em>Routed</em> mode, which creates an <code>ipsecN</code> tunnel interface), and routing handled by BGP or static routes.</p>
<p><strong><code>received DELETE for ESP CHILD_SA</code> in a loop.</strong> The peer tears phase 2 down every few minutes: mismatched phase 2 lifetimes with badly negotiated rekeys, or aggressive DPD. Match lifetimes (Cloud VPN uses 3 hours for phase 2 and 10 for phase 1) and let only one side initiate rekeys.</p>
<p><strong>Comes up, passes traffic for a while, dies with NAT-T.</strong> A peer behind NAT on UDP 4500 and a middlebox that &ldquo;helps&rdquo; (IPsec ALG on the router) drops encapsulated ESP packets. Disable the ALG or move the firewall onto a direct public IP.</p>
<h2 id="ha-vpn-with-bgp-against-pfsense-frr">HA VPN with BGP against pfSense (FRR)</h2>
<p>The tunnel comes up, <code>bgp</code> establishes, and <strong>no route comes in or goes out</strong>. FRR on pfSense starts with a policy that rejects everything not explicitly permitted:</p>
<ol>
<li><strong>Services → FRR → BGP → Route Maps</strong>: create <code>ALLOW-ALL</code>, <em>Action</em> Permit, <em>Sequence</em> 100, no criteria.</li>
<li><strong>BGP → Neighbors</strong> → the Cloud Router peer: <em>Remote AS</em> the Cloud Router&rsquo;s ASN, and under <em>Peer Filtering</em> assign <code>ALLOW-ALL</code> as route map <strong>in</strong> and <strong>out</strong>.</li>
<li><strong>BGP → Network Distribution</strong>: list the local networks you want to advertise (or <em>Default Originate</em> if everything goes through the tunnel).</li>
<li>On GCP, the Cloud Router with <code>--advertisement-mode custom</code> if you want to control which prefixes go out; by default it advertises the VPC subnets.</li>
</ol>
<p>Without step 2, <code>vtysh -c &quot;show ip bgp summary&quot;</code> shows the session <em>Established</em> with 0 prefixes in both directions, which is exactly the symptom.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>IKEv2</strong> unless the peer cannot: IKEv1 is still supported, but without IPv6 and with fewer ciphers. Classic VPN still exists (99.9%), but part of its functionality is retired; new work goes on HA VPN (99.99%).</li>
<li><strong>Two tunnels per gateway</strong> on HA VPN for the 99.99%: if you only bring one up, the gateway status stays in warning even though it works.</li>
<li><strong>MTU</strong>: 1460 by default on the GCP VPC; with IPsec on top, let TCP do MSS clamping on the peer (pfSense does it on the tunnel&rsquo;s <code>ipsecN</code> interface) or you will see transfers hanging after the handshake.</li>
<li><strong>Logs lag</strong>: a minute or two between the event and Logs Explorer. Refresh before changing the configuration again.</li>
<li><strong>A PSK with special characters</strong> copied from a password manager: check on the peer that nothing got eaten; the symptom is <code>IKE_AUTH</code> failing, not <em>peer not responding</em>.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/pfsense/nat-reflection-port-forward-from-lan/">pfSense: NAT reflection and split DNS</a> — the other classic of &ldquo;the tunnel is fine and I still cannot get there&rdquo;.</li>
<li><a href="https://docs.cloud.google.com/network-connectivity/docs/vpn/support/troubleshooting">Cloud VPN troubleshooting</a> in the Google Cloud documentation.</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>pfSense: ld-elf.so.1: Shared object "libssl.so.30" not found, required by "pkg"</title><link>https://inet.sh/en/posts/pfsense/libssl-so-30-not-found-pkg/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/libssl-so-30-not-found-pkg/</guid><description>pkg update and the pfSense Package Manager die with &amp;#39;Shared object libssl.so.30 not found, required by pkg&amp;#39;: the pkg binary got linked against an OpenSSL the base system does not have yet. Recovery with pkg-static (certctl rehash, reinstall pkg, pfSense-repo and pfSense-upgrade) and how to finish the upgrade properly.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>From the console (option 8) or <strong>Diagnostics → Command Prompt</strong>:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">certctl rehash
</span></span><span class="line"><span class="cl">pkg-static clean -ay
</span></span><span class="line"><span class="cl">pkg-static install -fy pkg
</span></span><span class="line"><span class="cl">pkg-static install -xfy pfSense-repo pfSense-upgrade
</span></span><span class="line"><span class="cl">pkg-static update -f
</span></span><span class="line"><span class="cl">pfSense-upgrade -d -c        <span class="c1"># shows which version the configured branch offers</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>With <code>pkg</code> alive again, finish the pending upgrade through <strong>System → Update</strong> (or <code>pfSense-upgrade -d</code>), not with <code>pkg upgrade</code>.</p>
<h2 id="the-error">The error</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ld-elf.so.1: Shared object &#34;libssl.so.30&#34; not found, required by &#34;pkg&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p>You see it on <code>pkg update</code>, when installing a package, or indirectly: <strong>System → Package Manager</strong> shows up empty and <strong>System → Update</strong> cannot check versions. Sometimes the binary complaining is <code>pfSense-repoc</code> instead of <code>pkg</code>; it is the same problem.</p>
<h2 id="why-it-happens">Why it happens</h2>
<p>pfSense is FreeBSD, and <code>pkg</code> is a binary dynamically linked against the system&rsquo;s <code>libssl</code>. When the configured repository belongs to a pfSense release built on a newer FreeBSD, its packages come linked against a newer OpenSSL (<code>libssl.so.30</code> = OpenSSL 3). If <code>pkg</code>, or <code>pfSense-repoc</code>, gets updated from that repo <strong>before</strong> the base system, the new binary asks for a library that does not exist on your install yet.</p>
<p>How you get there:</p>
<ul>
<li>A manual <code>pkg upgrade</code> from the shell. On pfSense you never do that.</li>
<li>Installing or reinstalling a package after switching the <strong>Branch</strong> in System → Update to a higher version, without upgrading.</li>
<li>An upgrade interrupted halfway (typical in the 2.7.0 → 2.7.1/2.7.2 jump, when the base moved to OpenSSL 3).</li>
<li>On pfSense Plus, rolling back to an older boot environment with the repo pointing at the new one (Redmine #15070).</li>
</ul>
<p>The library name will change across releases; the mechanics and the fix stay the same.</p>
<h2 id="fix">Fix</h2>
<p><code>pkg-static</code> is the statically linked copy of <code>pkg</code>: it needs no <code>libssl</code>, which is why it works when <code>pkg</code> does not.</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><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</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"># 1. Let pkg-static talk TLS to the repo (holds until the next reboot)</span>
</span></span><span class="line"><span class="cl">certctl rehash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Drop the cache and reinstall pkg and the repo configuration</span>
</span></span><span class="line"><span class="cl">pkg-static clean -ay
</span></span><span class="line"><span class="cl">pkg-static install -fy pkg
</span></span><span class="line"><span class="cl">pkg-static install -xfy pfSense-repo pfSense-upgrade   <span class="c1"># -x: regex, so pfSense-repoc comes along</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Refresh metadata and check</span>
</span></span><span class="line"><span class="cl">pkg-static update -f
</span></span><span class="line"><span class="cl">pfSense-upgrade -d -c
</span></span></code></pre></td></tr></table>
</div>
</div><p>If <code>pkg-static update</code> still fails, <code>pkg-static -d update</code> prints the full trace (certificates, DNS, wrong branch).</p>
<p>Before step 3 check the <strong>Branch</strong> under System → Update: it must be the version you want to reach, not an intermediate one. And finish the job through the GUI or with <code>pfSense-upgrade -d</code>: a stray <code>pkg upgrade</code> leaves you halfway again.</p>
<p>If the base system is so mixed up that even this does not fly, Netgate&rsquo;s documented last resort is <code>pkg-static upgrade -f</code> (reinstalls everything), followed by a reboot.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>pkg upgrade</code> does not exist on pfSense.</strong> The only supported path is System → Update / <code>pfSense-upgrade</code>. This whole situation is born from skipping it.</li>
<li><strong>Backup first</strong>: Diagnostics → Backup &amp; Restore. On ZFS, also a boot environment (System → Boot Environments) lets you roll back in one reboot.</li>
<li><strong>Do not copy <code>libssl.so.30</code> from another FreeBSD.</strong> It circulates as a trick; it leaves the system with libraries from two releases and breaks the next upgrade.</li>
<li><strong>Packages first, no.</strong> With an upgrade pending, upgrade the system and then install packages, not the other way around.</li>
<li>The repo and the upgrade use HTTPS: if <code>certctl rehash</code> is not enough, check the firewall&rsquo;s system time and DNS.</li>
</ul>
<p>Reference: <a href="https://docs.netgate.com/pfsense/en/latest/troubleshooting/upgrades.html">Upgrade Troubleshooting</a> in the Netgate documentation.</p>
]]></content:encoded></item><item><title>pfSense: Let's Encrypt certificate with ACME and Cloudflare DNS, then using it in HAProxy</title><link>https://inet.sh/en/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</guid><description>Issue a Let&amp;#39;s Encrypt certificate on pfSense with the ACME package and DNS-01 validation on Cloudflare (API token, Account ID and Zone ID only; no Global API Key), wildcard included, and hang it off the HAProxy frontend with an automatic restart on every renewal. With the token IP filter and DNS Sleep traps.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<ol>
<li><strong>Cloudflare</strong> → My Profile → API Tokens → <em>Create Token</em> → <em>Edit zone DNS</em> template: permissions <strong>Zone → Zone → Read</strong> and <strong>Zone → DNS → Edit</strong>, <em>Zone Resources</em> limited to your zone. Copy the token; the <strong>Account ID</strong> and <strong>Zone ID</strong> are on the zone&rsquo;s <em>Overview</em> page.</li>
<li><strong>pfSense</strong> → System → Package Manager → install <strong>acme</strong> and <strong>haproxy</strong>.</li>
<li>Services → ACME → <em>Account keys</em> → Add: <em>ACME Server</em> <strong>Let&rsquo;s Encrypt Production ACME v2</strong> (or <em>Staging</em> to test) → <em>Create new account key</em> → <em>Register</em> → Save.</li>
<li>Services → ACME → <em>Certificates</em> → Add: name, the account, <em>Domain SAN list</em>: <code>app.example.com</code> (and <code>*.example.com</code> if you want a wildcard) with method <strong>DNS-Cloudflare</strong>, and in its fields only <strong>Token</strong>, <strong>Account ID</strong> and <strong>Zone ID</strong>. <em>Actions list</em>: <strong>Restart Local Service</strong> → <code>haproxy</code>. Save → <strong>Issue</strong>.</li>
<li>Services → HAProxy → <em>Frontend</em> → edit → <em>SSL Offloading</em> ✔ on the listen address → <em>Certificate</em>: the one ACME just created → Save → <em>Apply Changes</em>.</li>
</ol>
<p>Automatic renewal (the package&rsquo;s daily cron) and HAProxy restarted on every renewal.</p>
<h2 id="cloudflare-the-token">Cloudflare: the token</h2>
<p>The <em>Global API Key</em> old guides ask for gives access to the whole account; an <strong>API token</strong> with minimal permissions is the right thing and is what the package&rsquo;s <em>DNS-Cloudflare</em> method uses when you fill in <code>Token</code>:</p>
<table>
  <thead>
      <tr>
          <th>Package field</th>
          <th>Value</th>
          <th>Where</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Token</td>
          <td>The API token</td>
          <td>My Profile → API Tokens</td>
      </tr>
      <tr>
          <td>Account ID</td>
          <td>Account ID</td>
          <td>Zone → Overview → <em>API</em> (right column)</td>
      </tr>
      <tr>
          <td>Zone ID</td>
          <td>Zone ID</td>
          <td>Same place</td>
      </tr>
      <tr>
          <td>Key / Email</td>
          <td>Empty</td>
          <td>Only for the Global API Key (do not use it)</td>
      </tr>
  </tbody>
</table>
<p>With <code>Token</code> one of the two IDs is enough; setting both does no harm. Token permissions: <strong>Zone:Read</strong> and <strong>DNS:Edit</strong> on the specific zone (<em>Include → Specific zone</em>).</p>
<p>If you enable <em>Client IP Address Filtering</em> on the token, it must include the firewall&rsquo;s public <strong>IPv4 and also IPv6</strong>: pfSense goes out through whichever it has, and a token limited to v4 fails as soon as the API call goes over v6.</p>
<h2 id="acme-account-and-certificate">ACME: account and certificate</h2>
<p><strong>Account.</strong> Create two: one against <em>Staging</em> to test and one against <em>Production</em>. Staging has no issuance limits but its certificates are not trusted; once everything works, switch the certificate&rsquo;s account to production and issue again.</p>
<p><strong>Certificate.</strong> Under Services → ACME → <em>Certificates</em> → Add:</p>
<ul>
<li><em>Domain SAN list</em>: one entry per name. For a <code>*.example.com</code> wildcard the validation has to be DNS-01 (HTTP-01 does not allow it). A wildcard does not cover the apex: add <code>example.com</code> as a second entry.</li>
<li><em>Method</em>: <strong>DNS-Cloudflare</strong> on each entry, with the three fields.</li>
<li><em>DNS Sleep</em>: Cloudflare propagates in seconds; the default is plenty. If issuance fails with <code>TXT not found</code>, raise it to <code>120</code>.</li>
<li><em>Key</em>: RSA 2048 or ECDSA 256; both work fine with HAProxy.</li>
<li><em>Actions list</em>: <strong>Restart Local Service</strong> → <strong>haproxy</strong>. Without it, HAProxy keeps serving the old certificate until someone restarts it, even though ACME has already renewed.</li>
</ul>
<p><strong>Issue</strong> from the certificate list. The result shows up under System → Certificates with the name you gave it; that is where HAProxy will see it.</p>
<h2 id="haproxy-using-the-certificate">HAProxy: using the certificate</h2>
<p>Services → HAProxy:</p>
<ol>
<li><em>Settings</em>: enable HAProxy, and leave <em>Max SSL Diffie-Hellman size</em> at 2048 or more.</li>
<li><em>Frontend</em> → your HTTPS frontend → <em>External address</em>: port <code>443</code>, <strong>SSL Offloading</strong> ✔ → <em>SSL Offloading</em> section → <strong>Certificate</strong>: the ACME one. For several domains on the same frontend, <em>Additional certificates</em> with the rest; HAProxy picks by SNI.</li>
<li>Save → <strong>Apply Changes</strong>.</li>
</ol>
<p>Test from outside:</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">openssl s_client -connect app.example.com:443 -servername app.example.com &lt;/dev/null 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> openssl x509 -noout -issuer -dates
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>issuer=C = US, O = Let's Encrypt</code> and <code>notAfter</code> 90 days out.</p>
<p>As a bonus, the pfSense GUI with the same certificate: System → Advanced → <em>Admin Access</em> → <em>SSL/TLS Certificate</em>, and a second action on the ACME certificate: <strong>Shell Command</strong> <code>/etc/rc.restart_webgui</code>.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Cloudflare in proxy mode (orange cloud)</strong> does not affect DNS-01, but it does affect how traffic arrives: if the <code>A</code> record is proxied, HAProxy sees Cloudflare&rsquo;s IP as the source. For an internal service behind pfSense, grey cloud.</li>
<li><strong><code>Issue</code> fails with <code>Invalid request headers</code> or <code>Authentication error</code></strong>: token without permission on that zone, token IP filter, or the Zone ID of another zone.</li>
<li><strong>Production rate limits</strong>: five certificates per set of names per week. Test on staging.</li>
<li><strong>The certificate renews; HAProxy does not reload it by itself.</strong> The restart action is mandatory. Check under Services → ACME that the last renewal has a date and in <code>openssl s_client</code> that <code>notAfter</code> moved.</li>
<li><strong>ACME certificates renew at 2/3 of their lifetime</strong> (about 60 days). If you see the cron running and nothing changes before then, that is normal.</li>
</ul>
<p>Reference: <a href="https://docs.netgate.com/pfsense/en/latest/packages/acme/index.html">ACME package</a> in the Netgate documentation and acme.sh&rsquo;s <a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi">DNS provider list</a> (<code>dns_cf</code> section).</p>
]]></content:encoded></item><item><title>pfSense: OpenVPN stops accepting clients with 'CRL has expired'</title><link>https://inet.sh/en/posts/pfsense/openvpn-crl-has-expired/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/openvpn-crl-has-expired/</guid><description>Every OpenVPN client on a pfSense fails at once with &amp;#39;VERIFY ERROR: depth=0, error=CRL has expired&amp;#39;. The internal CRL expired, or was born expired because of the 9999-day bug (Redmine #13424, a date past 2050 encoded as 1950). How to regenerate it from Cert Manager, the patch for old versions, and how to check it with openssl.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p><strong>System → Certificates → Certificate Revocation</strong> → pencil on the CRL the OpenVPN server uses → <strong>Lifetime (Days)</strong> = <code>730</code> → <strong>Save</strong>. Saving regenerates the CRL with valid dates. Then <strong>Status → Services → OpenVPN → Restart</strong>. Clients get back in without changing anything on their side.</p>
<p>If the GUI will not let you set a sane lifetime (versions before 2.7 / 23.01), first <strong>System → Patches</strong> (the <em>System Patches</em> package) → apply <em>Fix for CRL expiration lifetime default and maximum values</em> (<code>a3c1589086ea67d25a28ec14ab95d7fd9ab25fa2</code>), then the step above. Or upgrade pfSense: current versions already carry it.</p>
<h2 id="the-error">The error</h2>
<p>In the server log (Status → System Logs → OpenVPN) and on the client:</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-text" data-lang="text"><span class="line"><span class="cl">VERIFY ERROR: depth=0, error=CRL has expired: CN=client1, serial=3
</span></span><span class="line"><span class="cl">OpenSSL: error:0A000086:SSL routines::certificate verify failed
</span></span><span class="line"><span class="cl">TLS_ERROR: BIO read tls_read_plaintext error
</span></span><span class="line"><span class="cl">TLS Error: TLS handshake failed
</span></span></code></pre></td></tr></table>
</div>
</div><p>The clue is that <strong>every client fails at once</strong>. An expired certificate takes down one client; an expired CRL takes down the whole server, because it is checked on every handshake.</p>
<h2 id="why-it-happens">Why it happens</h2>
<p>Two roads lead here:</p>
<ol>
<li><strong>The CRL really expired.</strong> Every CRL has a <code>nextUpdate</code>; past that date OpenSSL treats it as invalid and OpenVPN rejects everyone. A CRL created years ago with a short lifetime and never touched ends up like this.</li>
<li><strong>The CRL was born expired.</strong> pfSense generated CRLs with a <strong>9999-day</strong> default, which pushed them past <strong>2050</strong>. X.509 encodes dates up to 2049 as <code>UTCTime</code> with a two-digit year, and pfSense wrote <code>50</code> where <code>GeneralizedTime</code> was due: OpenSSL reads it as <strong>1950</strong> and the CRL &ldquo;expired&rdquo; 75 years ago. That is Redmine <a href="https://redmine.pfsense.org/issues/13424">bug #13424</a>, and it explains why healthy installs started failing in 2022–2023 with nobody touching anything: CRLs regenerated by any change crossed 2050.</li>
</ol>
<p>Netgate&rsquo;s fix lowered the default lifetime to <strong>730 days</strong>, capped the maximum (2050 on 64-bit, 2038 on 32-bit), and ships in current versions; for old ones it remained as a patch in the <em>System Patches</em> package.</p>
<h2 id="checking-it">Checking it</h2>
<p>pfSense writes each OpenVPN server&rsquo;s CRL into its configuration directory:</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">ls /var/etc/openvpn/server1/
</span></span><span class="line"><span class="cl">openssl crl -in /var/etc/openvpn/server1/crl-verify -noout -lastupdate -nextupdate
</span></span></code></pre></td></tr></table>
</div>
</div><p>A <code>nextUpdate</code> in the past, or a year of 1950, is the whole diagnosis.</p>
<h2 id="fix">Fix</h2>
<ol>
<li><strong>Identify which CRL the server uses</strong>: VPN → OpenVPN → Servers → edit → <em>Peer Certificate Revocation list</em>.</li>
<li><strong>Regenerate it</strong>: System → Certificates → Certificate Revocation → pencil on that CRL → <strong>Lifetime (Days)</strong> <code>730</code> → Save. Any save rewrites it with <code>lastUpdate</code> = now.</li>
<li><strong>Restart OpenVPN</strong>: Status → Services. OpenVPN re-reads <code>crl-verify</code> when it notices the change, but the restart saves you the wait.</li>
<li><strong>Check again</strong> with the <code>openssl crl</code> above.</li>
</ol>
<p>On a version without the fix, between steps 1 and 2: System → Package Manager → install <em>System_Patches</em> → System → Patches → the patch shows up in the recommended list → <strong>Apply</strong>. Without it, the GUI writes 9999 again and the problem repeats as soon as you save.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Do not delete the CRL to &ldquo;fix it&rdquo;</strong>: without a CRL, the certificates you revoked become valid again. Edit it.</li>
<li><strong>One CRL per CA</strong>: if you have several OpenVPN servers with different CAs, check each one.</li>
<li><strong>Imported CRLs</strong> (from an external CA) cannot be regenerated from pfSense; request a new one from your CA and import it again.</li>
<li><strong>730 days run out too.</strong> Any revocation, CRL edit, or change to the OpenVPN server regenerates it; if you touch nothing for two years, you are back here. A calendar reminder is cheap.</li>
<li><strong>Past 2038 on 32-bit hardware</strong> the other limit applies; in practice none of that gear should still be your firewall.</li>
</ul>
<p>References: <a href="https://docs.netgate.com/pfsense/en/latest/certificates/crl.html">Certificate Revocation List Management</a> in the Netgate documentation and <a href="https://blog.pasqualefiorillo.it/how-to-fix-crl-has-expired-openvpn-error-on-pfsense/">Pasquale Fiorillo&rsquo;s write-up</a>.</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>