Modern consumer internet access is increasingly affected by the shortage of public IPv4 addresses. Today, many Internet service providers (ISPs) mask customer IPv4 addresses behind Carrier-Grade Network Address Translation (CGNAT), typically using the private address range 100.64.0.0/10 based on RFC 6598 as an addition to RFC 1918.
This creates a practical problem for my home lab. I use IoT and infrastructure devices such as air conditioning, photovoltaic and energy management systems, surveillance cameras, home automation and other services that I want to manage through VPN access.

One common workaround is to use IPv6.
IPv6 is different. My home network receives public IPv6 addresses, with up to 256 public /64 segments. Every system, or only selected systems, can therefore be reached directly over IPv6, if firewalling and routing allow it.
That solves the technical reachability problem for IPv6-capable clients, but not every network I use has reliable IPv6. Some guest Wi-Fi networks, mobile providers, corporate networks and hotel networks still behave like IPv4-only environments.
The practical workaround is a small dual-stack relay in the cloud.
Prerequisites
- Oracle OCI (Free Tier) account – any other hosted VM with dual stack support will work, as well.
- IPv6 connectivity at home
- Linux knowledge
- WireGuard basics
- Router with IPv6 support
- DynDNS for IPv6
The Basic Idea
I run an Ubuntu VM in the Oracle Cloud Free Tier. Oracle offers free-tier resources, including small VM instances and publicly reachable IPv4 and IPv6 addresses. This makes it possible to set up an IPv4-to-IPv6 relay that forwards selected traffic to my home network, without additional costs.
In my Oracle Cloud Infrastructure environment, the relay host is named ocigate. The VM has a public endpoint with IPv4 and IPv6 connectivity. A small machine-in-the-middle or relay VM!
The intent is to forward IPv4 traffic for two use cases:
- SSH access to a Raspberry Pi in my home network, reachable through a dynamic DNS AAAA record for IPv6 such as
pi.dyn.foo.bar - WireGuard reachability toward my core router, reachable through a dynamic DNS AAAA record such as
corerouter.dyn.foo.bar
The delegated public IPv6 prefixes at home are dynamic and may change from time to time. Therefore, the services are covered by dynamic DNS records instead of hardcoded IPv6 addresses.
Instead of exposing broad network access over IPv6, I only relay the services I actually need. In my case, that is SSH and WireGuard.
The two active relays are implemented with socat on ocigate:
Public listener on ocigate | Target over IPv6 | Purpose |
|---|---|---|
TCP4-LISTEN:443 with -T 120 | TCP6:pi.dyn.foo.bar:22 | SSH to the Raspberry Pi |
UDP4-LISTEN:51820 with -T 120 | UDP6:corerouter.dyn.foo.bar:51820 | WireGuard to the core router |
The flow looks like this:
flowchart LR
Client[IPv4 Client]
OCI[Oracle Cloud Free Tier<br/>Relay VM]
Pi[Raspberry Pi<br/>SSH TCP/22]
WG[Core Router<br/>WireGuard UDP/51820]
Client -->|TCP 443| OCI
Client -->|UDP 51820| OCI
OCI -->|IPv6 TCP 22| Pi
OCI -->|IPv6 UDP 51820| WGThis is not a full VPN concentrator, and it is not a replacement for firewall policy. It is a narrow Layer 4 relay: listen on one side, connect to the service on the other side, and let systemd supervise the process.
What This Solves
This does not make CGNAT disappear. It works around the part that matters for selected services:
- IPv4-only clients can reach a public cloud endpoint.
- The cloud endpoint can reach and forward my home network via IPv6.
- Only explicit services are exposed.
- The setup remains understandable and easy to inspect.
Why Not Just Use Port Forwarding?

With a normal public IPv4 address at home, I could forward a TCP or UDP port on the (client) edge router. With CGNAT, the public IPv4 address belongs to the ISP-side NAT layer, not to my router. I cannot create an inbound IPv4 rule there.
IPv6 does not have that problem in the same way, because my hosts can have globally routable addresses. But IPv6-only exposure is inconvenient when the client side is IPv4-only. A dual-stack cloud VM gives me a stable public meeting point.
Why systemd and socat?
There are many ways to build a relay. I chose socat because it is small, explicit, and supports both TCP and UDP forwarding. I use systemd because it gives me a benefit of service management:
- automatic restart after failure
- logs through
journalctl - predictable startup after reboot
- one unit per relay
- no large reverse proxy stack for two simple services
For SSH, this is straightforward TCP forwarding. For WireGuard, the relay is UDP, so it deserves more testing: UDP relays are less forgiving than TCP because there is no long-lived stream session. In practice, WireGuard handshakes make failures visible quickly.
SSH Relay: Public IPv4 Port 443 to IPv6 SSH at Home
The SSH relay is managed by the systemd unit file 443_socat.service (just create it):
# /etc/systemd/system/443_socat.service
[Unit]
Description=socat Service 443
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
DynamicUser=yes
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ExecStart=/etc/socat/443_socat
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
The wrapper script behind it is deliberately small:
#!/bin/bash
# /etc/socat/443_socat
# Socat script: TCP port 443 for SSH
/usr/bin/socat -T 120 TCP4-LISTEN:443,fork,reuseaddr TCP6:pi.dyn.foo.bar:22
The important part is the address-family boundary. ocigate listens on public IPv4 port 443, but the outgoing connection goes to the home Raspberry Pi over IPv6 on port 22.
Using port 443 for SSH is not magic security. It is mainly practical: many restrictive networks still allow outbound TCP 443, while arbitrary high ports are sometimes blocked. SSH security still comes from key-based authentication and careful server configuration.
The -T 120 option gives idle relay sessions a timeout. That keeps old socat child processes from lingering indefinitely after client traffic has stopped.
WireGuard Relay: Public IPv4 UDP 51820 to IPv6 WireGuard at Home
The WireGuard relay is managed by systemd unit file 51820_socat_wireguard.service:
# /etc/systemd/system/51820_socat_wireguard.service
[Unit]
Description=socat Service 51820 WireGuard VPN Clients
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
DynamicUser=yes
ExecStart=/etc/socat/51820_socat_wireguard
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
The wrapper script is equally explicit:
#!/bin/bash
# /etc/socat/51820_socat_wireguard
# Socat script: UDP port 51820 for WireGuard
/usr/bin/socat -T 120 UDP4-LISTEN:51820,fork,reuseaddr UDP6:corerouter.dyn.foo.bar:51820
This unit is intentionally narrow. It does not expose the entire home network. It only forwards one UDP service to one known endpoint. WireGuard still authenticates peers with its own cryptographic keys; socat only moves UDP packets between address families.
The UDP relay also uses -T 120. With fork, active clients can create child processes. The timeout keeps that behavior bounded instead of leaving old UDP relay processes around after they become idle.
Firewall Rules Still Matter
The cloud VM (machine-in-the-middle) should only allow the relay ports that are needed. Everything else should stay closed. The same applies at home: the target systems should only accept traffic from expected sources where practical. Please do not forget to enable inbound traffic in Oracle Cloud with Security Lists, Network Security Groups and the related Host Firewall.
-A INPUT -p udp --dport 51820 -j ACCEPT
-A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
For SSH, key-only authentication is mandatory in my view. Password login on a public relay is not worth the risk. In this setup, the public SSH relay listens on TCP 443, which can reduce connectivity problems from restrictive client networks. It should not be treated as a security control.
For WireGuard, the real protection is still the WireGuard key material and peer configuration. The relay only makes packets arrive; it should not be treated as an authentication layer.
Operational Notes
The setup is small enough that normal Linux tooling is sufficient – enable the services with:
sudo systemctl daemon-reload
sudo systemctl enable --now 443_socat.service
sudo systemctl enable --now 51820_socat_wireguard.service
Check service status with:
#ssh
systemctl status 443_socat.service
journalctl -u 443_socat.service -f
#wireguard
systemctl status 51820_socat_wireguard.service
journalctl -u 51820_socat_wireguard.service -f
ss -lnptu | grep -E "443|51820"
After changing a unit file, reload systemd and then restart the affected service:
sudo systemctl daemon-reload
sudo systemctl restart 443_socat.service
sudo systemctl restart 51820_socat_wireguard.service
After changing only a wrapper script, a restart of the affected service is enough:
sudo systemctl restart 443_socat.service
sudo systemctl restart 51820_socat_wireguard.service
If a relay process aborts, systemd restarts it according to Restart=always. If routing or DNS changes, the logs usually make the failure mode visible. I keep the units separate because it makes testing and rollback easier: SSH and WireGuard can be restarted independently.
Final Check
Validating connectivity to the relay nmap to ocigate.dyn.foo.bar:
sudo nmap -p 443 -Pn ocigate.dyn.foo.bar
PORT STATE SERVICE
443/tcp open https
sudo nmap -sU -p 51820 -Pn ocigate.dyn.foo.bar
PORT STATE SERVICE
51820/udp open|filtered unknown
To validate that packets arrive – check with tcpdump
#On the Relay
sudo tcpdump -nni any 'ip and (tcp dst port 443 or udp dst port 51820) and not src net 10.0.0.0/8 and not src net 172.16.0.0/12 and not src net 192.168.0.0/16 and not src net 100.64.0.0/10 and not src net 127.0.0.0/8 and not src net 169.254.0.0/16 and not src net 224.0.0.0/4'
#On the Pi
sudo tcpdump -nni any 'ip6 and tcp dst port 22 and not src net fe80::/10 and not src net fc00::/7 and not src net ::1/128'
#On the CoreRouter
sudo tcpdump -nni any 'ip6 and udp dst port 51820 and not src net fe80::/10 and not src net fc00::/7 and not src net ::1/128'
Finally, now it is time to check your approach under real conditions!
Things I Would Not Use This For
- You have native dual-stack connectivity.
- You need enterprise-grade HA.
- You require guaranteed bandwidth.
The value of this setup is its narrowness: two services, two relays, supervised by systemd, with the cloud VM acting as a small dual-stack bridge between the public internet and the parts of my home network that I intentionally expose.
Find other interesting network topic: Networking