Some services are not on the public internet so much as on a public internet. Local SMS and push gateways are a common example: the operator only accepts API calls originating from inside the country, either by whitelisting a national address range or by requiring you to be on a domestic network at all.
That is fine until your application server is not there. Ours ran on a Hetzner box in Germany — better price, better uptime, better network than anything we could get locally — and it needed to call an Algerian notification provider that hangs up on foreign addresses.
The two obvious answers are both bad
Move everything to a local VPS. You inherit the provider's uptime, their bandwidth, their hardware, and you throw away the reason you were hosting abroad in the first place.
Route all outbound traffic through a local box. Now every database call, every image fetch, every package install goes through a small machine you chose for its postcode rather than its capacity. It becomes your bottleneck and your single point of failure, and you will not notice until it falls over at the worst moment.
What you actually want is much narrower: one destination should leave from an Algerian address. Everything else should carry on exactly as before.
A tunnel and an egress box
The shape that works is a very small VPS in the country, joined to the app server by a WireGuard tunnel, and acting as an egress proxy for exactly one upstream.
[ app server, Hetzner DE ]
│ wg0 10.8.0.1
│
│ WireGuard (UDP 51820)
│
▼ wg0 10.8.0.2
[ tiny VPS, Algiers ] ──── public Algerian IP ────▶ [ notification provider ]
WireGuard is the right tool here because it is small enough to reason about. Two key pairs, two config files, no daemon soup, no certificate machinery.
On the Algerian box (/etc/wireguard/wg0.conf):
[Interface]
Address = 10.8.0.2/24
ListenPort = 51820
PrivateKey = <algiers-private-key>
[Peer]
# The app server
PublicKey = <hetzner-public-key>
AllowedIPs = 10.8.0.1/32On the app server:
[Interface]
Address = 10.8.0.1/24
PrivateKey = <hetzner-private-key>
MTU = 1420
[Peer]
PublicKey = <algiers-public-key>
Endpoint = <algiers-public-ip>:51820
# Only the tunnel subnet. NOT 0.0.0.0/0.
AllowedIPs = 10.8.0.2/32
PersistentKeepalive = 25Three lines there matter more than the rest.
AllowedIPs = 10.8.0.2/32 is the whole design. In WireGuard, AllowedIPs doubles as the routing table: listing 0.0.0.0/0 is what turns this into a full VPN and sends your entire internet through Algiers. Listing a single host keeps the tunnel to exactly what it is for.
PersistentKeepalive = 25 exists because the small box is usually behind NAT with a dynamic address. Without a keepalive the NAT mapping expires after a minute or two of silence and the tunnel goes quiet in one direction — the worst failure mode, because both ends still think they are up.
MTU = 1420 avoids the classic symptom where the handshake succeeds, small requests work, and anything with a real payload hangs forever. WireGuard adds its own header; if the resulting packet exceeds the path MTU and fragmentation is blocked, large POSTs simply disappear.
Proxy, do not route
You could now add a policy route so packets addressed to the provider leave through wg0. It works, and it is fragile: it depends on the provider's address never changing, and a stray ip route on a reboot silently reverts you to sending foreign-looking traffic.
It is sturdier to make the Algerian box a plain reverse proxy and have the application call it by name. The box then makes the upstream request from its own address, which is the whole point.
server {
listen 10.8.0.2:8443 ssl;
server_name notify-egress.internal;
# Only reachable from inside the tunnel.
allow 10.8.0.1;
deny all;
location / {
proxy_pass https://api.provider.example;
proxy_set_header Host api.provider.example;
proxy_ssl_server_name on;
# Fail fast: a notification is not worth a hung worker.
proxy_connect_timeout 5s;
proxy_read_timeout 10s;
}
}The application now calls https://notify-egress.internal:8443/..., which is a one-line change in configuration rather than anything structural. The provider whitelists one address — the Algerian VPS — and never sees Germany.
Make it fail soft
Anything you add to the path between your app and a third party is a new way for the app to break. A tunnel is no exception: the small VPS will reboot, the datacentre will have an incident, the operator will change something.
So the notification call must be allowed to fail without taking a request down with it. Queue it, give it a short timeout, retry with backoff, and alert on the rate of failure rather than on individual ones.
A cheap liveness check, run on a schedule, is worth the ten minutes it takes to write:
#!/usr/bin/env bash
# Is the tunnel actually carrying traffic, not just configured?
if ! ping -c1 -W2 10.8.0.2 >/dev/null 2>&1; then
systemctl restart wg-quick@wg0
logger -t egress "wg0 unreachable — restarted"
fiwg show is the other thing to watch. A peer whose latest handshake is more than a couple of minutes old is a peer that is gone, whatever the interface status says.
What this is really about
The instinct when a provider blocks you by geography is to move the whole application to where it will be accepted. Almost always the correct move is the opposite: keep the application where it belongs and move one specific call to where it has to originate from.
A five-euro VPS, forty lines of configuration and a tunnel are enough to buy that separation. And when the provider eventually opens up, or you change vendor, you delete a hostname instead of migrating a platform.