An open API service indexing awesome lists of open source software.

https://github.com/bitwire-it/opnsense-dnsfilter


https://github.com/bitwire-it/opnsense-dnsfilter

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

README

          

# DNSFilter OPNsense Plugin

[![Release](https://github.com/bitwire-it/opnsense-dnsfilter/actions/workflows/release.yml/badge.svg)](https://github.com/bitwire-it/opnsense-dnsfilter/actions/workflows/release.yml)

Go-based DNS proxy for OPNsense that fixes per-device attribution in DNSFilter
by injecting RFC 7871 EDNS0 Client Subnet (ECS) into every forwarded query.

## The Problem

OPNsense's Unbound forwards DNS to DNSFilter using its **own IP address**.
Every device on your LAN looks identical to DNSFilter — you cannot enforce
per-device policies, and query logs show only the firewall's IP.

The root cause: OPNsense builds Unbound **without `--enable-subnet`**
([issue #363](https://github.com/opnsense/tools/issues/363), open since 2023,
PR unmerged). Even if you configure `send-client-subnet`, it silently does nothing.

## The Solution

This plugin runs a Go daemon that **binds port 53 directly**, sees each client's
real IP at the socket level, injects it as an ECS option, then forwards to
DNSFilter. Local domains (`.local`, `.lan`, AD, PTR) are routed to Unbound on
port 5335 (split DNS).

```
Without plugin:
192.168.1.50 → Unbound → DNSFilter sees: 192.168.1.1 (firewall) ❌

With plugin:
192.168.1.50 → proxy → DNSFilter sees: 192.168.1.50 (real client) ✅
192.168.1.50 → proxy → Unbound:5335 for .local / AD / PTR
```

## Install

### Via pkg repository (recommended — enables `pkg upgrade`)

```sh
# On OPNsense as root:
fetch -o /usr/local/etc/pkg/repos/dnsfilter.conf https://bitwire-it.github.io/opnsense-dnsfilter/dnsfilter.conf

pkg update
pkg install os-dnsfilter
```

### Direct install from GitHub Releases

```sh
VERSION=1.0.0 # check releases page for latest

fetch https://github.com/bitwire-it/opnsense-dnsfilter/releases/download/v${VERSION}/dnsfilter-proxy-${VERSION}.pkg
fetch https://github.com/bitwire-it/opnsense-dnsfilter/releases/download/v${VERSION}/os-dnsfilter-${VERSION}_1.pkg

pkg add dnsfilter-proxy-${VERSION}.pkg
pkg add os-dnsfilter-${VERSION}_1.pkg
```

### After installing

1. **Services → DNSFilter → Settings**
- Enter your DNSFilter API key (from DNSFilter Dashboard → Account → API Keys)
- Select your Site
- Click **Apply**

2. **Firewall → NAT → Port Forward** — add a rule:
- Interface: LAN · Protocol: TCP/UDP
- Destination port: 53
- Redirect target: `127.0.0.1` port `53`
- This forces devices with hard-coded DNS (e.g. `8.8.8.8`) through the proxy

3. **Services → DNSFilter → Dashboard** — verify devices appear with real LAN IPs

## Upgrade

```sh
pkg upgrade os-dnsfilter dnsfilter-proxy
```

Or via OPNsense GUI: **System → Firmware → Plugins** (if using the repo).

## Uninstall

```sh
pkg delete os-dnsfilter dnsfilter-proxy
```

The post-deinstall script restores Unbound to port 53 automatically.
Config (`/usr/local/etc/dnsfilter/proxy.conf`) and query database
(`/var/db/dnsfilter_queries.sqlite`) are preserved — remove manually if needed.

---

## Debugging

### Service status

```sh
service dnsfilter_proxy status
service dnsfilter_proxy check # live DNS resolution test through proxy
```

### Live logs

```sh
tail -f /var/log/dnsfilter_proxy.log

# errors only
grep -i "error\|failed\|upstream" /var/log/dnsfilter_proxy.log
```

### Runtime metrics

```sh
fetch -q -o - http://127.0.0.1:8953/status | python3 -m json.tool
```

Returns JSON with `queries_total`, `queries_blocked`, `errors_upstream`, `last_latency_ms`, etc.

### Query database

```sh
# recent queries
sqlite3 /var/db/dnsfilter_queries.sqlite \
"SELECT datetime(timestamp,'unixepoch'), client_ip, domain, action, response_ms \
FROM queries ORDER BY id DESC LIMIT 20;"

# top blocked domains in last hour
sqlite3 /var/db/dnsfilter_queries.sqlite \
"SELECT domain, COUNT(*) AS cnt FROM queries \
WHERE action='blocked' AND timestamp > strftime('%s','now','-1 hour') \
GROUP BY domain ORDER BY cnt DESC LIMIT 20;"
```

### Common issues

| Symptom | Cause | Fix |
|---|---|---|
| All devices show firewall IP in DNSFilter | ECS disabled or not reaching proxy | Check `ecs_enabled=true` in `proxy.conf`; verify NAT rule redirects port 53 |
| `upstream error` in log | DNSFilter resolvers unreachable | Confirm `103.247.36.36` reachable from firewall |
| Local domains going to DNSFilter | Missing from split-DNS list | Add to `local_domains` in `proxy.conf`, then `service dnsfilter_proxy reload` |
| `client_mac` always empty in DB | `arp -an` returns nothing | Run `/usr/sbin/arp -an` manually; check proxy has permission to exec it |
| Port 53 bind fails on start | Unbound still on port 53 | Re-run `+POST_INSTALL` or manually set `port: 5335` in Unbound config |

### Config reload (no restart)

```sh
service dnsfilter_proxy reload # sends SIGHUP — reloads local_domains only
```

Note: changes to `listen_addr`/`listen_port` require a full restart.

---

## Build from source

### Requirements

- Go 1.22+
- A FreeBSD 14 machine **or** Linux/macOS (cross-compile works for the binary;
use `scripts/build-pkg-linux.sh` for the `.pkg` files)

### Quick build

```sh
# Cross-compile + build .pkg files (Linux/macOS):
make pkg

# Deploy directly to OPNsense:
make deploy HOST=192.168.1.1

# Build + push a release (triggers CI):
git tag v1.0.0 && git push origin v1.0.0
```

### CI/CD

Pushing a `v*.*.*` tag triggers the GitHub Actions workflow which:

1. Cross-compiles `dnsfilter-proxy` for `FreeBSD/amd64`
2. Builds `dnsfilter-proxy-VERSION.pkg` and `os-dnsfilter-VERSION_1.pkg`
3. Creates a GitHub Release and uploads the `.pkg` files as assets
4. Generates `packagesite.yaml` with absolute GitHub Release URLs
5. Pushes repo metadata to the `gh-pages` branch (served via GitHub Pages)

## Architecture

```
opnsense-dnsfilter/
├── go/
│ ├── cmd/dnsfilter-proxy/main.go SIGHUP reload, HTTP status, PID
│ └── internal/
│ ├── config/ config file parsing + validation
│ ├── ecs/ RFC 7871 EDNS0 ECS injection/stripping
│ ├── classifier/ split DNS — local vs DNSFilter routing
│ ├── proxy/ UDP+TCP server, failover, block detection
│ ├── store/ SQLite async logger, ARP/DHCP enrichment
│ └── api/ DNSFilter REST API client
├── opnsense/
│ ├── mvc/app/
│ │ ├── controllers/OPNsense/DNSFilter/Api/
│ │ │ ├── ServiceController.php start/stop/status/configure
│ │ │ ├── SettingsController.php model CRUD + API sync
│ │ │ └── DevicesController.php per-device stats from SQLite
│ │ ├── models/OPNsense/DNSFilter/
│ │ │ ├── DNSFilter.xml config schema (config.xml)
│ │ │ ├── DNSFilter.php model + generateConfig()
│ │ │ └── DNSFilterApiClient.php PHP REST client
│ │ └── views/OPNsense/DNSFilter/
│ │ ├── dashboard.volt live status + top devices/blocked
│ │ ├── devices.volt per-device drill-down + policy assign
│ │ └── settings.volt general/split-DNS/policies/advanced
│ └── service/conf/actions.d/
│ └── actions_dnsfilter.conf configd actions
├── packages/
│ ├── dnsfilter-proxy/+MANIFEST binary package metadata
│ └── os-dnsfilter/ plugin package metadata + scripts
│ ├── +MANIFEST
│ ├── +POST_INSTALL moves Unbound→5335, enables service
│ ├── +PRE_DEINSTALL stops daemon
│ └── +POST_DEINSTALL restores Unbound→53
├── .github/workflows/
│ ├── release.yml build + release + Pages deploy
│ └── pages.yml GitHub Pages deployment
├── scripts/
│ ├── build.sh build on FreeBSD (native pkg create)
│ └── build-pkg-linux.sh cross-build .pkg on Linux/macOS
└── Makefile
```

## License

MIT