PF pf blocks access by php curl

I'm having a problem with a curl access in PHP being blocked by my pf firewall. While there are also some curl settings problems, that'd for another post another day. for now, in curl I've turned off the SSL_VERIFYPEE and SSL_VERIFYHOST to help narrow down the problems.
I made a test program that just does the curl access to the U.S Department of Interior server to get the elevation for a passed latitude and longitude. I had to block IPv6 because that site now tries to connect to an IPv6 server 1st, then the V4 if those fail. Regrettably, the two ISPs in my area (Specturm and Brigthspeed) only provide IPv4. No IPv6. It's the dark ages in my neck of the woods.
When I run my test program with the PF on, I see:
Code:
# php testUsgs.php
     Going to net for lat=36.0576, long=-79.119
* Host epqs.nationalmap.gov:443 was resolved.
* IPv6: (none)
* IPv4: 3.167.112.15, 3.167.112.8, 3.167.112.103, 3.167.112.59
*   Trying 3.167.112.15:443...

It never connects, hangs, then times out. If I turn off the packet filter service pf stop, I can connect and see this:

Code:
# php testUsgs.php
     Going to net for lat=36.0576, long=-79.119
* Host epqs.nationalmap.gov:443 was resolved.
* IPv6: (none)
* IPv4: 3.167.112.59, 3.167.112.103, 3.167.112.15, 3.167.112.8
*   Trying 3.167.112.59:443...
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: C=US; ST=Virginia; L=Reston; O=U.S. Geological Survey; CN=*.nationalmap.gov
*  start date: Jun 21 00:00:00 2025 GMT
*  expire date: Jul 16 23:59:59 2026 GMT
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to epqs.nationalmap.gov (3.167.112.59) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://epqs.nationalmap.gov/v1/json?x=-79.119&y=36.0576&units=Meters/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: epqs.nationalmap.gov]
* [HTTP/2] [1] [:path: /v1/json?x=-79.119&y=36.0576&units=Meters/]
* [HTTP/2] [1] [user-agent: curl/8.14.1 (FreeBSD 14.1)]
* [HTTP/2] [1] [accept: application/json]
* [HTTP/2] [1] [accept-language: en-US,en;q=0.9]
* [HTTP/2] [1] [cache-control: max-age=0]
* [HTTP/2] [1] [upgrade-insecure-requests: 1]
> GET /v1/json?x=-79.119&y=36.0576&units=Meters/ HTTP/2
Host: epqs.nationalmap.gov
User-Agent: curl/8.14.1 (FreeBSD 14.1)
Accept: application/json
Accept-Language: en-US,en;q=0.9
cache-control: max-age=0
upgrade-insecure-requests: 1

* Request completely sent off
< HTTP/2 200
< content-type: application/json
< content-length: 181
< date: Sat, 06 Sep 2025 01:15:07 GMT
< x-amzn-trace-id: Root=1-68bb8b11-1d642e544520101f497e9fa5;Parent=0c24129099fd1bb6;Sampled=0;Lineage=1:47f15767:0
< x-amzn-requestid: 065156c8-7361-4b29-b3f8-6e714adb4e82
< access-control-allow-origin: *
< x-amz-apigw-id: QdKqvFRAPHcEF-g=
< x-cache: Miss from cloudfront
< via: 1.1 ddba66e53ff633c34296b8e866a481e2.cloudfront.net (CloudFront)  <-- at 172.67.74.2
< x-amz-cf-pop: IAD55-P8
< x-amz-cf-id: F3H3HKCpJJJrPvnW3BuxRbW2a1BKH96yUGFGxucZD0_iNeJ8X5UMEg==
<
* shutting down connection #0
  Good Reply!  Dry run, elevation is 216, connect time=10.316 seconds

Of course there was this little error, which is why SSL stuff is turned off for debugging:
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
But the request went through and I got an elevation. It looks like it directed to a Amazon cloud site at 172.67.74.2 I can ping that address.

In my pf.conf I have this line:
Code:
table <deptofinterior> = "{137.227.0.0/16, 3.128.0.0/9, 3.167.112.0/24, 18.238.4.0/24 }"
and near the bottom I have:
Code:
pass in quick on $INET from <deptofinterior> to any
Note also that 172.67.74.2 if not in the pf.conf

The router connected to the modem for the ISP has all the ephemeral TCP ports as pass through to the server. I can ping the 3.167.112.15 address which never seems to connect if the PF is on. I'm wondering if a connect to that sites moves the TCP connection to some other IP address that the PF doesn't like. I've looked at pftop but can't see anything unusual. I need some way to "catch the pf in the act"

Any suggestions on what to look at next would be appreciated. Below I also port the curl part, that's a problem for another day, but it may also be a clue. So it's there just in case. The URL that curl is running is:
https://epqs.nationalmap.gov/v1/json?x=-79.1186&y=36.0581&units=Meters
It always works from a chrome browser on windoze and Mac.

Freebsd 14.1 curl 8.15.0

- pete

The php curl code:
PHP:
    $url = "https://epqs.nationalmap.gov/v1/json?x=-79.1186&y=36.0581&units=Meters/";
    $ch = curl_init();
    ///// Setting options with an array for simple/common/standard settings  ////
    $opt = array(CURLOPT_URL => $url,
        CURLOPT_HTTPGET => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,     // force only IpV4 connections
        CURLOPT_FORBID_REUSE => true,               // don't keep the TCP link open
        CURLOPT_TCP_KEEPALIVE => false,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_CONNECTTIMEOUT => 6,
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_CAPATH => "/usr/local/etc/apache24/ssl/",
        CURLOPT_CAINFO => "/usr/local/etc/apache24/ssl/cacert.pem",
        CURLOPT_USERAGENT => "curl/8.14.1 (FreeBSD 14.1)",
        CURLOPT_SSL_VERIFYPEER => 0,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_VERBOSE => true
    );
    // now add custom headers that some sites may want to see //
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json',
        'Accept-Language: en-US,en;q=0.9',
        'cache-control: max-age=0',
        'upgrade-insecure-requests: 1'
    ]);

    curl_setopt_array($ch, $opt);
 
I had to block IPv6 because that site now tries to connect to an IPv6 server 1st, then the V4 if those fail. Regrettably, the two ISPs in my area (Specturm and Brigthspeed) only provide IPv4. No IPv6.
If you don't have IPv6 then your system isn't going to even try to connect on IPv6, that's simply not possible. So there's no reason to block IPv6 connections. If you have a small, initial delay on the connection it's probably the first DNS server request that's timing out before trying the secondary DNS server.

Code:
pass in quick on $INET from <deptofinterior> to any
Your curl(1) is an outgoing connection, not an incoming one.

Of course there was this little error, which is why SSL stuff is turned off for debugging:
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
You need security/ca_root_nss, which contains the CA root certificates to actually be able to verify the server's certificate chain.
 
The block on IPv6 was to avoid waiting for the delay. That delay seems much smaller this morning that yesterday though. Here's what I see when IPv6 is not blocked. I behaves as you mentioned:

Code:
* Host epqs.nationalmap.gov:443 was resolved.
* IPv6: 2600:9000:27d1:2200:7:9dcc:5680:93a1, 2600:9000:27d1:9600:7:9dcc:5680:93a1, 2600:9000:27d1:b800:7:9dcc:5680:93a1, 2600:9000:27d1:dc00:7:9dcc:5680:93a1, 2600:9000:27d1:da00:7:9dcc:5680:93a1, 2600:9000:27d1:6a00:7:9dcc:5680:93a1, 2600:9000:27d1:6200:7:9dcc:5680:93a1, 2600:9000:27d1:200:7:9dcc:5680:93a1
* IPv4: 3.167.112.59, 3.167.112.103, 3.167.112.8, 3.167.112.15
*   Trying [2600:9000:27d1:2200:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:2200:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:9600:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:9600:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:b800:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:b800:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:dc00:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:dc00:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:da00:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:da00:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:6a00:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:6a00:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:6200:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:6200:7:9dcc:5680:93a1: No route to host
*   Trying [2600:9000:27d1:200:7:9dcc:5680:93a1]:443...
* Immediate connect fail for 2600:9000:27d1:200:7:9dcc:5680:93a1: No route to host
*   Trying 3.167.112.59:443...
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
The site provides the IPv6 addresses first. This order had changed recently, perhaps to help push the world towards IPv6 ? If only the ISP providers followed suite... but at any rate, it's turned off so a bit of time saved perhaps.

W/R/T the packet filter, accesses now work and the start looks like:
Code:
* Host epqs.nationalmap.gov:443 was resolved.
* IPv6: (none)
* IPv4: 3.167.112.8, 3.167.112.59, 3.167.112.103, 3.167.112.15
*   Trying 3.167.112.8:443...
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted h2

curl was able to connect to the 3.167.112.8 site provided. I had two entries in the pf.conf table. The 1st was used to allow access:
deptofinterior = "{137.227.0.0/16, 3.128.0.0/9, 3.167.112.0/24, 18.238.4.0/24 }"
A second later entry was blocking an amazon server site:
amazon = "{ 3.0.0.0/8, 13.24.0.0/13, 13.48.0.0/13, 13.32.0.0/12, 13.56.0.0/14,\
At the end of the pf.conf:
That first entry was a "pass in quick on $INET ( from that deptofinterior list(and others) to any
After that there was a "block in log quick on $INET from (that amazon entry and others) to any

I thought that once there was a "pass in quick" match, the firewall allowed access and further parsing/checks stopped. However I had to remove the IP address 3.0.0.0/8 in order to be able to access the site.

to be more clear, here is the exact code after all the tables have been defined:
Code:
set skip on lo0
pass in quick on $INET from <mysites> to any
pass in quick on $INET from <my_whitelist> to any
pass in quick on $INET from <validemails> to any
pass in quick on $INET from <responder_whitelist> to any
block in quick on $INET from <country_block> to any
block in log quick on $INET from <countries> to any
block in log quick on $INET from <permaban> to any

The <my_whitelist> had the deptofinterior entry in it, and the <permaban> had the amazon entry in it.

Is this a clash when you have overlap between IP addresses? Perhaps the searching is optimized such that the entries with the largest number of IP addresses get looked at 1st ? So for now, all of amazon in the 3.0.0.0/8 is being passed through.

As more sites move to big cloud providers (and is happening with government sites here) it gets harder to block out other companies that host in the same space.

Still having "* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway." problems, but that I've got to work on just where to point the php curl code to. I've loaded the certificates you mentioned. Seems like it's always a challenge to figure out where various software looks for certs.

thanks for you reply, it nudged me in the right direction.
 
One closing comment and then this post is done:

After the firewall issue was cleaned up (and I still need to re-read the documents on pf to avoid a "Keep guessing until it works then move on"), there was still the curl problem:

* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway

The reason this was happening was when I was testing/trying, I set:
Code:
    CURLOPT_SSL_VERIFYPEER => 0,
    CURLOPT_SSL_VERIFYHOST => 0,
to try to wonk on just one problem at a time. security/ca_root_nss was installed and happy.

It turns out that if you have the above parameters, they tell curl to Not check the server certificates.

Changing the code to:
Code:
    CURLOPT_SSL_VERIFYPEER => 2,
    CURLOPT_SSL_VERIFYHOST => 1,
lets the checking occur and there is no error. And of course, it's what you really want too.
It would be nice if the error was something like " SSL certificate verify result: verifying server certificate turned off with CURLOPT_SLL_VERIFY settings"
it would be a hint as to what to do."

I hope this helps someone else down the road.
 
Back
Top