Xray-core VPN package for pfSense CE — native GUI integration for VLESS+Reality tunnels with selective routing via pfSense Aliases and Firewall Rules.
Ported from os-xray (OPNsense plugin). All core logic (config generation, VLESS parser, process management, watchdog) is preserved; only the framework layer is rewritten for pfSense.
xray-core (VLESS+Reality outbound)
↓ SOCKS5 (127.0.0.1:10808, configurable)
hev-socks5-tunnel (amd64) / tun2socks (aarch64 fallback)
↓ TUN interface (e.g. tunproxy0)
pfSense Gateway → Firewall Rules → Selective routing
Traffic you want to tunnel is sent to the Xray gateway via pfSense policy-based routing — no changes to xray-core routing config are needed. Aliases and firewall rules work natively.
On amd64, hev-socks5-tunnel is used as the TUN bridge (~311 KB C binary). On aarch64 (no hev release available), tun2socks is used as a fallback. The backend can be forced with --backend regardless of architecture.
- Multi-instance — run several independent VPN tunnels simultaneously, each with its own UUID, TUN interface, SOCKS5 port, and config
- Connection groups — organize connections into manual or subscription-based groups; each instance binds to a group
- Subscription support — fetch
vless://links from a URL, auto-parse and sync connections (add / update / remove); auto-update every 30 minutes via cron; optional Happ device headers (X-Hwid, User-Agent, X-Device-Os, etc.) for providers that require device authentication - Connection rotation — on start or watchdog trigger, URL-tests all connections in the group and activates the first working one
- URL test — per-connection reachability test via a temporary xray-core SOCKS5 instance; stores ping RTT result in the GUI
- Wizard mode — VLESS+Reality fields in the GUI (UUID, SNI, PublicKey, ShortID, Fingerprint, flow)
- Custom JSON mode — paste any xray-core
config.jsondirectly; supports all protocols and transports (xhttp, ws, grpc, h2, kcp, tcp) - VLESS link import — paste a
vless://link to auto-fill wizard fields; non-Reality transports automatically fall back to Custom JSON mode - Per-instance start / stop / restart — without page reload, via AJAX
- Live status badges — xray-core + tun2socks status polled every 10 s
- Config validation — dry-run via
xray -testbefore start, without touching the running service - Test Connection — verifies the tunnel is actually proxying (HTTP check through SOCKS5)
- Diagnostics page — TUN IP, MTU, bytes/packets in/out, process uptime, ping RTT to VPN server
- Log viewer — last 200 lines of xray-core log and last 100 lines of watchdog log in the GUI
- Watchdog — cron-based crash recovery (per minute); respects manual stop (stopped flag)
- Auto-start on boot — FreeBSD rc.d script (
/usr/local/etc/rc.d/xray.sh) - Bypass Networks — configurable CIDR list routed directly, not through Xray
- Webhook notifications — per-instance and global webhook called when rotation finds no working connection
| Component | Version |
|---|---|
| pfSense CE | 2.7.x / 2.8.x |
| FreeBSD | 14.x / 15.x amd64 / aarch64 |
| PHP | 8.2 / 8.3 |
| xray-core | 24.x or later (recommended) |
| hev-socks5-tunnel | 2.x (amd64) |
| tun2socks | 2.x (aarch64 fallback) |
Architecture note:
install.shauto-detectsamd64andaarch64. On amd64,hev-socks5-tunnelis used by default; on aarch64,tun2socksis used. Use--backendto override.
SSH into pfSense and run:
fetch -o /tmp/install.sh https://raw.githubusercontent.com/pdazcom/pfSense-pkg-xray/main/install.sh && sh /tmp/install.shWhy
fetchinstead ofcurl? pfSense/FreeBSD shipsfetchby default.curlmay not be available without installing it separately.
The script will:
- Download
xray-coreandhev-socks5-tunnel(amd64) ortun2socks(aarch64) from GitHub Releases - Install binaries to
/usr/local/bin/xray-coreand/usr/local/tun2socks/ - Copy all package files to the correct pfSense filesystem locations
- Load the
if_tunkernel module - Configure log rotation (
/etc/newsyslog.conf.d/xray.conf) - Register the package in pfSense (adds VPN → Xray menu)
Open pfSense web UI → VPN → Xray → Settings → check Enable Xray → Save.
If you prefer to have the full source available (e.g. for development or customization):
cd /tmp
git clone https://github.com/pdazcom/pfSense-pkg-xray.git
cd pfSense-pkg-xray
sh install.sh
gitis not installed on pfSense by default. Install it first: System → Package Manager → Available Packages → searchgit→ Install.
# Full install (default)
sh install.sh
# Update files after git pull, restart instances
sh install.sh update
# Update files only, skip binary download
sh install.sh update --no-binaries
# Download/update binaries only
sh install.sh download-binaries
# Pin specific versions
sh install.sh --xray-version 25.4.30 --hev-version 2.14.4 --t2s-version 2.5.2
# Force a specific tunnel backend (overrides arch detection)
sh install.sh download-binaries --backend tun2socks
sh install.sh download-binaries --backend hev
# Full uninstall (stops services, removes files, cleans pfSense config)
sh install.sh uninstallVPN → Xray → Connections
Connections are organized into groups. There is always a Default group for manually managed connections.
- Select a group (or use Default) → Add Connection
- Expand Import from VLESS Link → paste link → Parse & Fill, or fill fields manually
- Save
- Add Group → set type to Subscription, enter the subscription URL
- Optionally enable Auto-update (updates every 30 minutes via cron)
- Optionally enable Happ Headers — if your provider uses Happ device authentication:
- Enter an HWID manually or click Generate to create a random one
- Adjust User-Agent, Device OS, Locale, OS Version as needed
- Save → Update Now to fetch and sync connections immediately
After updating, the connections list shows a ping result badge for each entry (run URL Test to populate it).
VPN → Xray → Instances → Add Instance
- Select a Connection Group — the instance will use connections from this group
- Set TUN Interface name (e.g.
tunproxy0) — must be unique per instance - Set SOCKS5 Port — must be unique per instance (default:
10808) - Save → Start
On start, the instance runs connection rotation: it URL-tests each connection in the group and activates the first working one. The active connection is shown in the instance list.
After starting an instance, configure pfSense to route selected traffic through Xray.
System → Routing → Gateways → Add:
- Interface: select the Xray TUN interface (appears as OPTx)
- Gateway IP: same as TUN IP (shown in Diagnostics → TUN IP)
(e.g. if TUN IP is
10.100.66.46, gateway is10.100.66.46) - Name:
XRAY_GW - Monitor IP: leave blank — the tunnel bridge does not forward ICMP, so any monitor IP will cause the gateway to be marked down; disable monitoring entirely
Firewall → Aliases → Add:
- Type: Network(s), Host(s), or URL Table
- Add the IPs, subnets, or domains you want to route through Xray
- Example URL Table:
https://antifilter.download/list/allyouneed.lst
Firewall → Rules → LAN → Add (place above the default allow rule):
- Action: Pass
- Protocol: TCP/UDP
- Source: LAN net (or specific hosts)
- Destination: your Alias
- Advanced Options → Gateway:
XRAY_GW - Save → Apply Changes
The tunnel bridge (hev-socks5-tunnel / tun2socks) forwards TCP and UDP traffic only. ICMP is not tunneled — ping packets do not travel through the Xray proxy.
As a result, if you route traffic toward an Xray gateway, ping will not work for that traffic even when HTTP/HTTPS works fine. This is expected.
To allow ping for routed hosts, add a separate firewall rule above the Xray rule:
- Action: Pass
- Protocol: ICMP
- Source: LAN net (or the same source as your main rule)
- Destination: the same Alias
- Advanced Options → Gateway: default (no gateway override — route directly)
ICMP traffic will then bypass the tunnel and go out through the normal WAN. Ping replies will carry your router's public IP, not the Xray server IP.
Outbound NAT required. Make sure Firewall → NAT → Outbound covers the source subnet for this ICMP traffic on the WAN interface. Without an outbound NAT mapping, echo requests leave but replies may not return correctly.
VPN → Xray → Diagnostics
- Select instance from dropdown
- Refresh — loads TUN interface stats (IP, MTU, bytes in/out, process uptime)
- Test Connection — sends HTTP request through SOCKS5 proxy, shows HTTP status code
- xray-core Log — last 200 lines of
/var/log/xray-core.log - Watchdog Log — last 100 lines of
/var/log/xray-watchdog.log
fetch -o /tmp/install.sh https://raw.githubusercontent.com/pdazcom/pfSense-pkg-xray/main/install.sh && sh /tmp/install.sh updateOr, if installed via git clone:
cd /tmp/pfSense-pkg-xray
git pull
sh install.sh updatefetch -o /tmp/install.sh https://raw.githubusercontent.com/pdazcom/pfSense-pkg-xray/main/install.sh && sh /tmp/install.sh uninstallOr, if installed via git clone:
cd /tmp/pfSense-pkg-xray
sh install.sh uninstallThen manually remove in pfSense UI:
- System → Routing → Gateways — delete
XRAY_GW - Firewall → Rules — delete rules that used
XRAY_GW
pfSense-pkg-xray/
├── pkg/
│ └── xray.xml # Package manifest (menus, hooks, cron)
├── files/
│ ├── usr/local/www/xray/
│ │ ├── xray_connections.php # Connection groups + connections list
│ │ ├── xray_connection_edit.php # Create/edit connection + VLESS import
│ │ ├── xray_group_edit.php # Create/edit connection group (manual/subscription)
│ │ ├── xray_instances.php # Instance list + live status (AJAX polling)
│ │ ├── xray_edit.php # Create/edit instance
│ │ ├── xray_settings.php # Global settings (enable, watchdog, test URL, webhook)
│ │ ├── xray_diagnostics.php # TUN stats, logs, connection test
│ │ └── xray_ajax.php # AJAX dispatcher + VLESS parser
│ ├── usr/local/pkg/
│ │ └── xray/includes/
│ │ ├── xray.inc # Config R/W, TUN registration, hooks, group/connection helpers
│ │ ├── xray_connections.inc # Connection CRUD backed by JSON file (CLI-safe)
│ │ ├── xray_vless.inc # VLESS link parser
│ │ ├── xray_validate.inc # Input validation (instances, connections, groups)
│ │ └── xray_foot.inc # Footer include
│ ├── usr/local/scripts/xray/
│ │ ├── xray-service-control.php # Process management (start/stop/status/validate)
│ │ ├── xray-watchdog.php # Crash recovery daemon
│ │ ├── xray-rotation.php # Connection rotation (URL-test group, pick winner)
│ │ ├── xray-urltest.php # URL-test a single connection via temporary xray-core
│ │ ├── xray-urltest-group.php # Async group URL-test (one xray-core, writes progress JSON)
│ │ ├── xray-subscription-update.php # Fetch & sync subscription group connections
│ │ ├── xray-subscription-autoupdate.php # Cron wrapper: auto-update all subscription groups
│ │ ├── xray-ifstats.php # TUN interface statistics + ping
│ │ └── xray-testconnect.php # SOCKS5 connectivity test
│ └── usr/local/etc/rc.d/
│ └── xray.sh # FreeBSD rc.d boot script
└── install.sh # Install / update / uninstall script
All runtime files are named by instance UUID to avoid conflicts between instances:
| File | Purpose |
|---|---|
/usr/local/etc/xray-core/config-{uuid}.json |
xray-core config |
/usr/local/etc/xray-core/connections.json |
Connections store (all groups) |
/usr/local/tun2socks/config-{uuid}.yaml |
tunnel bridge config (hev or tun2socks format) |
/usr/local/tun2socks/backend.txt |
active backend: hev or tun2socks |
/var/run/xray_core_{uuid}.pid |
xray-core PID |
/var/run/tunnel_{uuid}.pid |
tunnel bridge PID (hev-socks5-tunnel or tun2socks) |
/var/run/xray_start_{uuid}.lock |
Per-instance startup lock (flock) |
/var/run/xray_stopped_{uuid}.flag |
Manual stop marker (watchdog skips) |
/var/log/xray-core.log |
xray-core + tunnel bridge stderr output |
/var/log/xray-watchdog.log |
Watchdog restart events + subscription autoupdate log |
Service won't start
# Check xray-core config syntax
php /usr/local/scripts/xray/xray-service-control.php validate <uuid>
# Check xray-core log
tail -50 /var/log/xray-core.log
# Manual start
php /usr/local/scripts/xray/xray-service-control.php start <uuid>TUN interface doesn't appear
# Check if if_tun module is loaded
kldstat | grep if_tun
kldload if_tun
# Check which tunnel backend is active
cat /usr/local/tun2socks/backend.txt
# Check tunnel process is running
ps aux | grep -E 'hev-socks5|tun2socks'
# Check interface
ifconfig tunproxy0Gateway is marked down
The tunnel bridge (hev-socks5-tunnel / tun2socks) does not forward ICMP, so pfSense gateway monitoring will always fail — the monitor ping never gets a reply, and the gateway is marked down.
Disable monitoring on the Xray gateway: System → Routing → Gateways → Edit → set Monitor IP to blank (or uncheck Monitor Gateway). With monitoring disabled, pfSense keeps the gateway up regardless of ping results, and policy-based routing continues to work normally.
Traffic not routing through tunnel
# Test SOCKS5 directly
curl --socks5 127.0.0.1:10808 -s -o /dev/null -w '%{http_code}' https://1.1.1.1
# Check firewall rule has the correct gateway
# Check the alias contains the destination IPs
pfctl -t <alias_name> -T show | head -20Watchdog keeps restarting
tail -f /var/log/xray-watchdog.log
tail -100 /var/log/xray-core.log- Xray-core — the proxy engine
- hev-socks5-tunnel — lightweight SOCKS5 to TUN bridge (amd64)
- tun2socks — SOCKS5 to TUN bridge (aarch64 fallback)
- os-xray — OPNsense plugin this is ported from
BSD 2-Clause License.
This package is a derivative work of os-xray by Pavel, licensed under the BSD 2-Clause License. The original copyright notice is retained in the LICENSE file as required by the license terms.
Core logic ported from os-xray:
xray-service-control.php— config generation, process management, VLESS+Reality config builderxray-watchdog.php— crash recovery daemonxray-ifstats.php— TUN interface statisticsxray-testconnect.php— SOCKS5 connectivity test- VLESS link parser (
xray_ajax.php) — ported fromImportController.php
The pfSense framework layer (package manifest, GUI pages, xray.inc, xray_validate.inc, xray.sh) is original work.
