This guide details how to route traffic from a server abroad through a home network in mainland China. This allows you to use your home IP address for specific applications running on the foreign server.
Here is our setup:
Host A:
- Located in China.
- Behind a restrictive NAT (NAT traversal is not possible).
- WAN IP: 10.255.0.1(Internal IP from the ISP(me)).
- WireGuard IP: 10.10.10.1.
Host B:
- Publicly accessible IP address <Host B IP>.
- WireGuard IP: 10.10.10.2.
On both hosts, IP forwarding is enabled: net.ipv6.conf.all.forwarding and net.ipv4.ip_forward are set to 1.
The goal is to establish a tunnel to Host B. Traffic entering this tunnel will be routed to Host A and exit from its network, effectively using Host A's IP address.
Due to the Great Firewall (GFW), the overlay network (WireGuard) must be built on top of an anti-censorship underlay tunnel. For this purpose, we'll use Hysteria2, as its client supports the necessary port forwarding.
First, we need to create a secure and robust tunnel to bypass censorship.
Generate a CA and Sign the Server Certificate
We need to generate a self-signed certificate for the Hysteria2 server using Host B's public IP address.
On Host B, run the following script:
#!/bin/sh
# Replace with Host B's actual public IP
ip="<Host B IP>"
# Generate the Certificate Authority (CA)
openssl genrsa -out hysteria.ca.key 2048
openssl req -new -x509 -days 3650 -key hysteria.ca.key -subj "/CN=Hysteria Root CA" -out hysteria.ca.crt
# Generate the server key and certificate signing request (CSR)
openssl req -newkey rsa:2048 -nodes -keyout hysteria.server.key -subj "/CN=$ip" -out hysteria.server.csr
# Sign the server certificate with our CA, including the IP in the Subject Alternative Name
openssl x509 -req -extfile <(printf "subjectAltName=IP:$ip") -days 3650 -in hysteria.server.csr -CA hysteria.ca.crt -CAkey hysteria.ca.key -CAcreateserial -out hysteria.server.crtThe files hysteria.ca.crt, hysteria.server.crt, and hysteria.server.key will be needed later.
Set Up the Hysteria2 Server
On Host B, create the Hysteria2 server configuration:
# /etc/hysteria/config.yaml
listen: :4432
tls:
  cert: /home/debian/hysteria.server.crt # The server certificate generated in the previous step
  key: /home/debian/hysteria.server.key  # The server key generated in the previous step
auth:
  type: userpass
  userpass:
    <USERNAME>: <PASSWORD> # Replace with your own credentials
quic:
  maxIncomingStreams: 8192
ignoreClientBandwidth: trueImplement Port Hopping
To make the connection more resilient, we can use port hopping. Add the following nftables rule on Host B to redirect traffic from a wide range of UDP ports to the Hysteria2 server port.
define INGRESS_INTERFACE="eth0" # Your WAN interface
define PORT_RANGE=20000-50000
define HYSTERIA_SERVER_PORT=4432 # Hysteria's listening port
table inet hysteria_porthopping {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    iifname $INGRESS_INTERFACE udp dport $PORT_RANGE counter redirect to :$HYSTERIA_SERVER_PORT
  }
}
Configure the Hysteria2 Client
On Host A, create the Hysteria2 client configuration. This setup will forward the local port 127.0.0.1:51700 on Host A to the same port on Host B, which is where our WireGuard server will listen.
# /var/lib/hy/config.yml 
server: hysteria2://<USERNAME>:<PASSWORD>@<Host B IP>:20000-50000 # Use the credentials and port range from the server config
tls:
  sni: <Host B IP>
  ca: /var/lib/hy/hysteria.ca.crt # The CA certificate generated on Host B
fastOpen: true
lazy: false
transport:
  udp:
    hopInterval: 15s
udpForwarding:
- listen: 127.0.0.1:51700
  remote: 127.0.0.1:51700
  timeout: 120sNow, we'll configure the WireGuard tunnel that runs inside the Hysteria2 tunnel. We'll focus only on the specific settings for this scenario.
Configure WireGuard on Host B (Server)
On Host B, create the WireGuard configuration. Note the Table = off and MTU settings.
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.10.2/24
SaveConfig = false
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
ListenPort = 51700 # Listens on the port forwarded by Hysteria2
PrivateKey = <Host B Private Key>
MTU = 1380
Table = off # Important: Prevents wg-quick from creating routing rules
[Peer]
PublicKey = <Host A Public Key>
AllowedIPs = 0.0.0.0/0 # Allows all traffic from Host A to be routedConfigure WireGuard on Host A (Client)
On Host A, create the WireGuard configuration. The Endpoint will be the local port forwarded by Hysteria2.
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.10.1/24
PrivateKey = <Host A Private Key>
# No ListenPort is needed since Host A is behind NAT.
# No Table=off is needed.
# ...
[Peer]
PublicKey = <Host B Public Key>
Endpoint = 127.0.0.1:51700 # Connects to the local Hysteria2 forwarder
AllowedIPs = 10.10.10.2/32
PersistentKeepalive = 25Start the Hysteria2 tunnel first, then bring up the WireGuard interfaces on both hosts. The WireGuard connection should now be established. Host A should be able to ping 10.10.10.2. Host B will not be able to ping Host A, which is expected because Host B has no route to A.
To make traffic from Host B appear as if it originates from Host A, we need to apply Source NAT (SNAT) on Host A.
Add the following rule using nftables:
table inet nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    # NAT traffic from Host B (10.10.10.2) coming through wg0 and going out the main interface
    iifname { wg0 } oifname enp0s4 ip saddr 10.10.10.2 snat to 10.255.0.1
  }
}
Alternatively, you can use masquerade for a more dynamic SNAT rule.
Finally, on Host B, we'll use a tool like sing-box to direct application traffic into the WireGuard tunnel.
Create the following sing-box configuration on Host B:
{
  "log": {
    "disabled": false,
    "level": "debug",
    "timestamp": false
  },
  "dns": {
    "servers": [
      {
        "tag": "cmcc",
        "type": "udp",
        "server": "local ISP dns",
        "detour": "into"
      },
      {
        "tag": "google",
        "type": "udp",
        "server": "8.8.8.8"
      }
    ],
    "rules": [
      {
        "type": "logical",
        "mode": "or",
        "rules": [
          {
            "rule_set": "geosite-geolocation-!cn",
            "invert": true
          },
          {
            "domain": ["ip.sb"],
            "ip_cidr": ["104.26.13.31/32", "172.67.75.172/32"],
            "rule_set": ["geoip-cn", "geosite-cn"]
          }
        ],
        "server": "cmcc"
      }
    ],
    "strategy": "ipv4_only",
    "final": "google",
    "independent_cache": true
  },
  "inbounds": [
    {
      "type": "socks",
      "tag": "socks-in",
      // edit me
      "listen": "127.0.0.1",
      "listen_port": 1700
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    },
    {
      "type": "direct",
      "tag": "into",
      "bind_interface": "wg0",
      "inet4_bind_address": "10.10.10.2"
    }
  ],
  "route": {
    "auto_detect_interface": true,
    "final": "into",
    "default_domain_resolver": "cmcc",
    "rule_set": [
      {
        "type": "remote",
        "tag": "geoip-cn",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
      },
      {
        "type": "remote",
        "tag": "geosite-cn",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs"
      },
      {
        "type": "remote",
        "tag": "geosite-geolocation-!cn",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
      }
    ]
  }
}Now, any SOCKS5 traffic sent to localhost:1700 on Host B will be routed through the WireGuard interface (wg0), exit from Host A's network, and use its public IP. The SOCKS inbound can be replaced with any other protocol supported by sing-box. Setup is complete.