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
tun2sockshas certain performance issues: when there are a huge number of TCP connections,tun2sockswill 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:
tun2sockshas 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#
- 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); - OPNsense:
2.5.17_2(some operations in the official documentation tutorial have not been updated); - 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:
| Setting | Value |
|---|---|
| Name | ProxyTunnel |
| 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 address | 10.0.0.2/32 fd00::2/128 |
| Peers | Tunnel (to be configured later) |
| Disable routes | True |
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:
| Setting | Value |
|---|---|
| Name | Tunnel |
| Public key | (EndPoint’s public key, generated in the previous step) |
| Allowed IPs | 0.0.0.0/0 ::/0 |
| Endpoint address | Endpoint.example.com |
| Endpoint port | 51820 |
| Instances | ProxyTunnel |
| Keepalive interval | 25 |
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:
| Setting | Value |
|---|---|
| Enable | Enable Interface |
| Description | ProxyTunnel |
| IPv4 Configuration Type | None |
| IPv6 Configuration Type | None |
| Dynamic gateway policy | False |
Do not select Dynamic gateway policy!
In System ‣ Gateways ‣ Configuration, add two gateways (corresponding to IPv4 and IPv6 respectively):
| Setting | Value |
|---|---|
| Name | TUNPROXY |
| Interface | ProxyTunnel |
| Address Family | IPv4 |
| IP address | 10.0.0.3 |
| Far Gateway | True |
| Disable Gateway Monitoring | True |
| Setting | Value |
|---|---|
| Name | TUNPROXY_IPv6 |
| Interface | ProxyTunnel |
| Address Family | IPv6 |
| IP address | fd00::3 |
| Far Gateway | True |
| Disable Gateway Monitoring | True |
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:
| Name | Type | Description |
|---|---|---|
| NoProxyGroup | Network group | Includes non-proxy domains and local area network addresses |
| ProxyDevices | Network group | Includes the MAC addresses of all devices to be proxied |
| ProxyPort | Port(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:
| Setting | Value |
|---|---|
| TCP/IP Version | IPv4 |
| Protocol | TCP/UDP |
| Source | ProxyDevices |
| Destination / Invert | True |
| Destination | NoProxyGroup |
| Destination port range | ProxyPort to ProxyPort |
| Gateway | TUNPROXY |
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:
| Setting | Value |
|---|---|
| Action | Pass |
| Quick | False |
| Interface | Do not select any |
| Direction | out |
| TCP/IP Version | IPv4 |
| Protocol | any |
| Source / Invert | False |
| Source | TUNPROXY address |
| Destination / Invert | True |
| Destination | TUNPROXY net |
| Gateway | TUNPROXY |
| Advanced features | |
| Allow Options | True |
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:
| Setting | Value |
|---|---|
| Interface | TUNPROXY |
| TCP/IP Version | IPv4 |
| Protocol | any |
| Source invert | False |
| Source address | ProxyDevices |
| Source port | any |
| Destination invert | False |
| Destination address | any |
| Destination port | any |
| Translation / target | Interface address |
An IPv6 version can also be configured in this way, selecting IPv6 for TCP/IP Version.
Save and apply.
EndPoint: Configuring Transparent Proxy#
(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:
define interface = eth0The network port needs to be changed to your own network port, and it must also match in the mihomo configuration file;define tproxy_port = 7893The transparent proxy port must also match mihomo;define mihomo_mark = 233The packet identifier must match mihomo’srouting-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)

