Ghost IP 👻: Make a Remote Server Appear on Your Network via WireGuard and NDP Proxy

I have a home server (Server B) with both IPv4 and IPv6 addresses, but it isn’t publicly accessible. I also have a school server (Server A) with a publicly accessible, static IPv6 /64
subnet and a private IPv4 address on the school’s LAN. A /64
IPv6 subnet means my school server can assign any IPv6 address within that block to itself, and it will be globally reachable. So, why not give one to my home server?
To assign a IPv6 address to Server A itself, simply pick an unused address in the prefix and run ip -6 addr add NEW_ADDRESS/64 def $PUBLIC_INTERFACE
. The address will be available immediately.
After some research, I succeeded in doing just that using WireGuard and NDP Proxy. I also gave my home server a private IPv4 address from the school’s LAN, allowing it to access school resources.
To be clear, here’s what this setup achieves:
- Server B gets its own IP addresses in Server A’s public
/64
IPv6 subnet and private IPv4 subnet. I call these “Ghost IPs.” - Full network access is established between Server B and all devices on Server A’s network, and vice-versa. Server B’s assigned public IPv6 address is reachable from anywhere in the world.
- Split-tunneling by default: Server B’s general internet traffic goes directly out to the internet, not through Server A.
- Devices on Server B’s original home network are still able to access Server B using its original local IP address.
Essentially, this is functionally equivalent to connecting Server B to the same switch as Server A with a virtual network cable. This equivalence is limited to Layer 3 (the Network Layer), as I’ll explain later.
Differences from FRP
It’s worth noting that in many cases, a tool like FRP is a much simpler solution. FRP can forward a single port or a range of ports from Server A to Server B, which is often enough for accessing simple services like SSH or a web server.
However, this WireGuard solution offers more isolation and flexibility:
- It forwards all ports (TCP, UDP, ICMP, etc.) by default, eliminating the need to reconfigure anything when you add a new service.
- Server B knows the actual IP addresses of connecting clients. This makes logging, firewalls, and IP-based access controls work correctly without extra configuration. It also improves compatibility with complex protocols that use random ports or initiate connections back to clients.
- Independent IP addresses for Server A and Server B mean services will never have port conflicts. An application on Server A listening on
::
or0.0.0.0
won’t interfere with Server B.
Fundamentally, WireGuard operates one layer down the networking stack from FRP, from Layer 4 (Transport) to Layer 3 (Network). This is why protocols like TCP, UDP, and ICMP all work seamlessly. An ICMP ping packet sent to Server B’s Ghost IP actually travels through the WireGuard tunnel to Server B’s kernel and back—something impossible with a Layer 4 tool.
The caveat is that you must manually assign static Ghost IPs for Server B. This is fine for IPv6 but can lead to IP conflicts on a typical IPv4 LAN that uses DHCP. Dynamic IP assignment protocols like DHCP (for IPv4) and parts of NDP (for IPv6) operate on Layer 2 (the Data Link Layer), so they cannot travel through WireGuard’s Layer 3 tunnel.
This article will focus on the simpler IPv6 setup first, then briefly cover the more complex IPv4 case. This is also the official solution to SJTU CS1952 Networking Lab Advanced 2.2.
Prerequisites
- Two servers running Ubuntu 22.04+.
- Server A: The server with a public static IPv6
/64
subnet (the school server). - Server B: The remote server that can initiate an outbound connection to Server A (the home server).
- Server A: The server with a public static IPv6
- Root or
sudo
access on both servers. - Physical access to both servers. If you mess things up, you may lose
ssh
connection to your server!
Step 1: Install WireGuard
Run this on both Server A and Server B.
1 | sudo apt update |
Step 2: Generate WireGuard Keys
Each server needs a key pair to establish a secure tunnel.
On Server A:
1 | sudo -i |
On Server B:
1 | sudo -i |
Step 3: Configure WireGuard on Server A (The School Server)
This configuration is the core of the setup. The script below automatically detects your network configuration, but you may manually override some values to adapt to your setup.
1 | sudo -i |
Step 4: Configure WireGuard on Server B (The Home Server)
1 | sudo -i |
Step 5: Modify Kernel Parameters on Server A
We need to enable IPv6 forwarding and NDP proxying on Server A’s kernel.
1 | sudo -i |
Step 6: Start the Services and Verify
Now, let’s bring the tunnel up and check that everything is working.
On Server A:
1 | # Enable and start the WireGuard service |
On Server B:
1 | # Enable and start the WireGuard service |
Step 7: Test the Connection
From any third device on the internet (like your laptop), you should now be able to reach Server B using its new public Ghost IP.
1 | # From a third machine (e.g., your laptop) |
How It Works: A Brief Explanation
Our setup relies on three key components:
Neighbor Discovery Proxy: Server A announces to its network that a server with IP address
SERVER_B_GHOST_IPV6
exists and can be reached through Server A. In IPv6, this is done using Neighbor Discovery Protocol (NDP) proxying. The commandip -6 neigh add proxy ...
does just that. This is only required when Server A’s IPv6 address is acquired through Stateless Address Autoconfiguration (SLAAC). There’s another case that the entire /64 prefix is given exclusively to the server, through Prefix Delegation (PD), which is typical for a cloud VM.WireGuard Tunnel: A standard, secure Layer 3 tunnel is established between the two servers.
Customized Routing:
- On Server A, we need to forward packets destined for
SERVER_B_GHOST_IPV6
from the public interface to the wireguard interface. TheFORWARD
chain is created in the nftables firewall, if not already created bydocker
orufw
.nft add rule ip6 filter
adds the forwarding rule. - On Server B, since we want the entire Internet to be able to connect to Server B via its ghost IP, but Server B should connect to the Internet directly by default (unless responding to a client or manually specified), we must turn Wireguard’s automatic routing off by
Table = off
and setup the rules manually, which is no additional rules in IPv6’s case.
- On Server A, we need to forward packets destined for
Troubleshooting
If things aren’t working, here’s a checklist to debug the issue.
Check
wg show
on both servers. Is there a successful handshake? If not, check your public keys and Server A’s firewall (ensure port 51820 UDP is open).Check
ip addr show dev wg0
on Server B. Does it have the public Ghost IP?Check
ip -6 neigh show proxy
on Server A. Is the proxy entry present for the Ghost IP?Use
tcpdump
to trace packets. This is the most powerful tool.- Start a continuous ping to
$SERVER_B_GHOST_IPV6
from a third machine (Server C). - On Server A, public interface:
sudo tcpdump -i $PUBLIC_INTERFACE -n icmp6
. Do you see the incoming Echo Request packets from Server C? - On Server A, WireGuard interface:
sudo tcpdump -i wg0 -n icmp6
. Do you see the Echo Request packets being forwarded into the tunnel? If not, check the forwarding rule (nft list chain inet filter FORWARD
). - On Server B, WireGuard interface:
sudo tcpdump -i wg0 -n icmp6
. Do you see the Echo Request packets arriving? Do you see Echo Reply packets being sent back? If not, check Server B’s configuration.
- Start a continuous ping to
Managing
nftables
rules:- View rules with handles:
sudo nft -a list chain ip6 filter FORWARD
- Delete a specific rule:
sudo nft delete rule ip6 filter FORWARD handle <number>
- Warning: Do not run
nft flush ruleset
! Other applications like Docker rely onnftables
. If you mess things up, restarting the Docker service (sudo systemctl restart docker
) can often restore its rules.
- View rules with handles:
Selecting interface manually: Server B can connect to the general internet via Server A by manually selecting the
wg0
interface, for exampleping -I wg0
. This also helps when the interface selection rules does not work as expected.
Extending to the IPv4 case
The IPv4 setup is more complex. In our first key component of the setup, the Address Resolution Protocol (ARP) proxy is used in IPv4 instead of the IPv6-specific NDP. The configuration interface is similar, though. In the third component, additional routing rules and kernel parameters need to be set. I won’t dive into a full explaination here and will simply provide an example configuration.
IPv4 & IPv6 Full Setup
Note:
192.168.10.2/16
andfd00:1234:5678::1/64
are private subnets within the Wireguard scope.10.100.100.10/28
is Server B’s ghost IPv4, in Server A’s LAN CIDR.2001:db8:1::2
is Server B’s ghost IPv6.10.20.20.10/16
is Server B’s original IPv4.
Warning: Make sure the three IPv4 ranges above do not overlap with each other! Or your devices may lose connection to the Internet.
Server A:
1 | [Interface] |
Server B:
1 | [Interface] |
- Title: Ghost IP 👻: Make a Remote Server Appear on Your Network via WireGuard and NDP Proxy
- Author: Stargazer ZJ
- Created at : 2025-07-23 19:44:18
- Updated at : 2025-07-26 17:33:27
- Link: https://ji-z.net/2025/07/23/Ghost-IP-Make-a-Remote-Server-Appear-on-Your-Network-via-WireGuard-and-NDP-Proxy/
- License: This work is licensed under CC BY-NC-SA 4.0.