Skip to main content
Using WireGuard for Transparent Proxying in OPNsense (Advanced)

Using WireGuard for Transparent Proxying in OPNsense (Advanced)

·2465 words·12 mins·
Kre³
Author
Kre³
Doing code and art with ❤
Table of Contents
The English version is translated by AI (Gemini 2.5 Pro Preview). If you want to view the original content, please switch to Chinese version.

Note This article involves a lot of advanced networking knowledge (some of which I only have a partial understanding of myself), so please have a certain degree of self-problem-solving ability when following this article!

Why and why you shouldn’t
#

This article is a follow-up and advanced version of “Installing tun2socks service on OPNsense - Kreee’s Blog” (meaning I procrastinated for 3 years and then squeezed out an article), mainly focusing on some thoughts after using the tun2socks solution for 3 years (+ my personal mental cleanliness). Here are the reasons why I want to use this method and my personal advice on when not to use it:

Why?:

  • Found that tun2socks has certain performance issues: when there are a huge number of TCP connections, tun2socks will have a higher CPU usage;
  • Security considerations: I don’t want to use the method of hacking OPNsense to use tun2socks (meaning using root privileges), and I have a mental aversion to modifying OPNsense in a terminal environment;
  • Security considerations: tun2socks has hardly been updated in recent years (although the function is stable and there is no need for updates unless a major vulnerability is disclosed);
  • WireGuard has become a first-class citizen of OPNsense, why not?

Why not:

  • You don’t know what you’re doing: This method involves a lot of in-depth knowledge related to networking, some of which I haven’t figured out myself. For the parts that I haven’t figured out myself (I will clearly mark them in the article), I will not try to answer related questions;
  • You don’t know what you’re doing: This article will not explain how to perform some simple operations in other systems (such as installing WireGuard, configuring WireGuard, setting up WireGuard to start automatically, etc.), but will only write out the configuration method in my own environment;
  • I have not tested the performance, resource usage, and stability of this method for a long time.

My Environment
#

  1. EndPoint: As a server with proxy tools and WireGuard exit installed, I chose Alpine Linux (the subsequent tutorial operations will also be based on this system);
  2. OPNsense: 2.5.17_2 (some operations in the official documentation tutorial have not been updated);
  3. Proxy software: mihomo

EndPoint: Configuring WireGuard
#

Install WireGuard (apk add wireguard-tools), generate public and private keys:

wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey

Write the WireGuard configuration file (/etc/wireguard/tunnel.conf):

[Interface]
Address = 10.0.0.1/24, fd00::1/64
ListenPort = 51820
PrivateKey = <The EndPoint private key just generated>

[Peer]
PublicKey = <The public key generated by OPNsense, to be filled in later>
AllowedIPs = 10.0.0.2/32, fd00::2/128

Create a link file to create a service file:

ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.tunnel

Use service wg-quick.tunnel start to start, service wg-quick.tunnel stop to stop, and rc-update add wg-quick.tunnel default to add to the default runlevel to achieve auto-start.


OPNsense: Configuring WireGuard
#

Reference: WireGuard Selective Routing to External VPN Endpoint - OPNsense Documentation

In VPN ‣ WireGuard ‣ Instances, create a WireGuard instance:

SettingValue
NameProxyTunnel
Public key(Automatically generated and filled into the configuration of the previous step)
Private key(Automatically generated)
Listen port(Can be left blank if you don’t want it to choose a random port itself)
Tunnel address10.0.0.2/32 fd00::2/128
PeersTunnel (to be configured later)
Disable routesTrue

Note!: In the original official documentation, there was a step to configure Gateways. The UI of the new version of the software has been changed and removed, so I filled the IP into the Tunnel address. Although I don’t know if it’s useful, I can run it anyway (escapes).

In VPN ‣ WireGuard ‣ Peers, create a WireGuard node:

SettingValue
NameTunnel
Public key(EndPoint’s public key, generated in the previous step)
Allowed IPs0.0.0.0/0 ::/0
Endpoint addressEndpoint.example.com
Endpoint port51820
InstancesProxyTunnel
Keepalive interval25

Enable this instance and restart WireGuard.


OPNsense: Configuring WireGuard Gateway
#

In Interfaces ‣ Assignments, create a new port for the WireGuard tunnel device you just created and save the settings.

In Interfaces ‣ [the newly added network port], make the following settings and save and apply:

SettingValue
EnableEnable Interface
DescriptionProxyTunnel
IPv4 Configuration TypeNone
IPv6 Configuration TypeNone
Dynamic gateway policyFalse

Do not select Dynamic gateway policy!

In System ‣ Gateways ‣ Configuration, add two gateways (corresponding to IPv4 and IPv6 respectively):

SettingValue
NameTUNPROXY
InterfaceProxyTunnel
Address FamilyIPv4
IP address10.0.0.3
Far GatewayTrue
Disable Gateway MonitoringTrue
SettingValue
NameTUNPROXY_IPv6
InterfaceProxyTunnel
Address FamilyIPv6
IP addressfd00::3
Far GatewayTrue
Disable Gateway MonitoringTrue

Note!: The IP address configured here should not conflict with existing IPs, that is, this IP cannot appear on any other device in this WireGuard network.


OPNsense: Configuring Gateway Firewall Rules
#

(Next is the part of configuring Aliases and configuring transparent proxy, which were mentioned in my previous article “Installing tun2socks service on OPNsense - Kreee’s Blog”. Here, to pad the word count for your convenience so you don’t have to jump back and forth, I will directly copy the content I wrote at that time below)

Make good use of Aliases

The configuration of aliases is in Firewall ‣ Aliases. You can quickly select one or more objects by customizing aliases.

The alias types I commonly use are Host(s), Port(s), MAC Address, and Network Group. The Host(s) type is used to store websites that I don’t want to be proxied, the Port(s) type is used to store ports that I want to be proxied, the MAC Address type is used to store individual devices that I want to be proxied, and the Network Group type is used to store a collection of proxy devices and a collection of non-proxy targets.

Therefore, the aliases to be used next are:

NameTypeDescription
NoProxyGroupNetwork groupIncludes non-proxy domains and local area network addresses
ProxyDevicesNetwork groupIncludes the MAC addresses of all devices to be proxied
ProxyPortPort(s)Port 80 and port 443

If you want, you can also use GeoIP rules, which require registering a MaxMind account: MaxMind GeoIP’s Setup - OPNsense Documentation.

Configuring Firewall Rules In Firewall ‣ Rules ‣ LAN, add a rule, and this rule should be before the default Default allow LAN to any rule and Default allow LAN IPv6 to any rule:

SettingValue
TCP/IP VersionIPv4
ProtocolTCP/UDP
SourceProxyDevices
Destination / InvertTrue
DestinationNoProxyGroup
Destination port rangeProxyPort to ProxyPort
GatewayTUNPROXY

For IPv6, you can also create a similar rule as above, using IPv6 for TCP/IP Version and the corresponding TUNPROXY_IPv6 for Gateway.

Save and apply.

(End of copy)


Configuring More Firewall Rules and NAT Outbound Rules
#

I’m not very clear about the principles of many of the following settings (these are operations from the official documentation). I can only provide all the information I have collected.

(Configure forwarding of traffic generated by OPNsense) In Firewall ‣ Rules ‣ Floating, add a rule:

SettingValue
ActionPass
QuickFalse
InterfaceDo not select any
Directionout
TCP/IP VersionIPv4
Protocolany
Source / InvertFalse
SourceTUNPROXY address
Destination / InvertTrue
DestinationTUNPROXY net
GatewayTUNPROXY
Advanced features
Allow OptionsTrue

An IPv6 version can also be configured in this way, selecting IPv6 for TCP/IP Version and the corresponding gateway for Source and Destination.

(I also found some posts discussing whether this rule is necessary: Wireguard Selective Routing - Why Step 9? - OPNsense Forum, and in this post a long Github Issues discussion thread about this step (I haven’t read it), you can try to read and think about it, the choice of whether this configuration is necessary is up to you, anyway, this is how I configured it)

(Create outbound NAT rule) In Firewall ‣ NAT ‣ Outbound, first select Hybrid outbound NAT rule generation to enable custom rules + automatically generated rules, and add the following rule:

SettingValue
InterfaceTUNPROXY
TCP/IP VersionIPv4
Protocolany
Source invertFalse
Source addressProxyDevices
Source portany
Destination invertFalse
Destination addressany
Destination portany
Translation / targetInterface address

An IPv6 version can also be configured in this way, selecting IPv6 for TCP/IP Version.

Save and apply.


EndPoint: Configuring Transparent Proxy
#

Reference: My Home Network Design Ideas, Starting the Debian Side-Router Journey (Part 4) - Evine’s Personal Blog

(Thanks to Evine for this article. Although I don’t quite understand the nftables rules written in it, each line has comments that allow me to roughly understand the ideas, and I have rewritten the rules with inet to support dual-stack)

In Alpine Linux, install nftables: apk add nftables; create an nftables script at /etc/wireguard/tproxy.nft:

#!/usr/sbin/nft -f

## Clear old rules
flush ruleset

## Only process traffic from the specified network card, which must be consistent with the interface in the IP rules
define interface = eth0

## mihomo's transparent proxy port
define tproxy_port = 7893

## mihomo's mark (routing-mark)
define mihomo_mark = 233

## Regular traffic mark, the mark added in the ip rule, must be consistent with the ip rule, corresponding to "1" in "ip rule add fwmark 1 lookup 100"
define default_mark = 1

## TCP ports for services running on this machine that need to be accessed from the public network (ports opened on the public network on this machine), service ports that are only accessed by the local LAN do not need to be in this variable, separated by half-width commas
define local_tcp_port = {
    22,        # ssh, set as needed
    443        # webui, set as needed
}

## Destination ports for TCP traffic in the LAN to be bypassed when accessed through this machine, that is, to allow other hosts in the LAN to actively set the DNS server to other servers, rather than the side router
define lan_2_dport_tcp = {
    53     # dns query
}

## Destination ports for UDP traffic in the LAN to be bypassed when accessed through this machine, that is, to allow other hosts in the LAN to actively set the DNS server to other servers, rather than the side router; in addition, it also allows other hosts in the LAN to access remote NTP servers
define lan_2_dport_udp = {
    53,    # dns query
    123    # ntp port
}

table inet mihomo {

    ## Reserved IPv4/IPv6 set
    set private_address4_set {
        type ipv4_addr
        flags interval
        elements = {
            127.0.0.0/8,
            100.64.0.0/10,
            169.254.0.0/16,
            224.0.0.0/4,
            240.0.0.0/4,
            10.0.0.0/8,
            172.16.0.0/12,
            192.168.0.0/16
        }
    }
    set private_address6_set {
        type ipv6_addr
        flags interval
        elements = {
            ::1/128,            # localhost
            fc00::/7,           # Unique Local Address (ULA, similar to IPv4 private addresses)
            fe80::/10,          # Link-local address
            ff00::/8,           # Multicast address
            64:ff9b::/96        # IPv4-IPv6 translation address (NAT64)
        }
    }


    ## prerouting chain
    chain prerouting {
        type filter hook prerouting priority filter; policy accept;
        meta l4proto { tcp, udp } socket transparent 1 meta mark set $default_mark accept # Bypass established connections
        meta mark $default_mark goto mihomo_tproxy                                        # Traffic already marked with default_mar belongs to local traffic and is directly sent to the transparent proxy
        fib daddr type { local, broadcast, anycast, multicast } accept                    # Bypass local, unicast, multicast, and anycast addresses
        tcp dport $lan_2_dport_tcp accept                                                 # Bypass TCP traffic from this machine to the destination port
        udp dport $lan_2_dport_udp accept                                                 # Bypass UDP traffic from this machine to the destination port
        meta nfproto ipv4 ip daddr @private_address4_set accept                           # Bypass destination addresses that are reserved IP addresses (IPv4)
        meta nfproto ipv6 ip6 daddr @private_address6_set accept                          # Bypass destination addresses that are reserved IP addresses (IPv6)
        goto mihomo_tproxy                                                                # Other traffic is transparently proxied to mihomo
    }

    ## Transparent proxy
    chain mihomo_tproxy {
        meta l4proto { tcp, udp } tproxy to :$tproxy_port meta mark set $default_mark
    }

    ## output chain
    chain output {
        type route hook output priority filter; policy accept;
        oifname != $interface accept                                                      # Bypass traffic for internal communication on this machine (interface lo)
        meta mark $mihomo_mark accept                                                     # Bypass traffic sent by mihomo on this machine
        fib daddr type { local, broadcast, anycast, multicast } accept                    # Bypass local, unicast, multicast, and anycast addresses
        udp dport { 53, 123 } accept                                                      # Bypass local DNS queries and NTP traffic
        tcp sport $local_tcp_port accept                                                  # Bypass TCP ports for services running locally, if you do not need to access these ports from the public network, you can comment out this line
        meta nfproto ipv4 ip daddr @private_address4_set accept                           # Bypass destination addresses that are reserved IP addresses (IPv4)
        meta nfproto ipv6 ip6 daddr @private_address6_set accept                          # Bypass destination addresses that are reserved IP addresses (IPv6)
        meta l4proto { tcp, udp } meta mark set $default_mark                             # Other traffic is rerouted to prerouting
    }
}

Pay attention to the following changes you need to make yourself:

  1. define interface = eth0 The network port needs to be changed to your own network port, and it must also match in the mihomo configuration file;
  2. define tproxy_port = 7893 The transparent proxy port must also match mihomo;
  3. define mihomo_mark = 233 The packet identifier must match mihomo’s routing-mark.

Save and grant execution permission chmod +x /etc/wireguard/tproxy.nft. At the same time, add the following two lines to the WireGuard configuration file /etc/wireguard/tunnel.conf to automatically configure the firewall rules:

[Interface]
...

PostUp = ip route add local default dev eth0 table 100 ; ip rule add fwmark 1 lookup 100 ; /etc/wireguard/tproxy.nft
PostDown = nft flush ruleset ; ip route del local default dev eth0 table 100 ; ip rule del fwmark 1 lookup 100

...
[Peer]
...

where eth0 needs to be changed to your own network port, same as above.

At this point, the transparent proxy rules have been configured. Note that this rule will directly release the private addresses of IPv4 and IPv6 without going through the transparent proxy, and the forwarding of these traffic may not be configured in the system configuration, so the traffic accessing these private addresses may be lost in the system. But because we have already eliminated the intranet traffic when configuring the transparent proxy in OPNsense, the impact is not significant, but it needs to be noted.

Regarding the IPv6 issue, many special service providers do not provide services for IPv6, so some people may encounter the situation where a special application spins for a long time before it finally loads. This is likely because the application starts in IPv6 mode, but falls back to IPv4 mode after finding that it is not connected. Therefore, my personal recommendation is to directly Reject the traffic of the proxy device to foreign countries (matched by GeoIP) in the firewall rules of OPNsense.

(Secretly make an irrelevant recommendation: the layer 4 proxy mode of the os-caddy plugin is really easy to use, those who know will know, it supports UDP, you can try it)