Oracle Cloud VM Docker No Route To Host

TLDR; Oracle cloud is pre-loaded with a bunch of iptable rules. You’ll have to hack at them to get Docker networking to behave in a typical fashion.

The Setup

We’re using an Oracle Cloud Ubuntu VM as our host. Assume a machine with LAN address 10.0.0.100 on the etho0 interface. We’re interested in running a Linuxserver.io Swag container as a reverse proxy on a user-defined bridge Docker network lsio that shows up in ifconfig as br-xyz. In the Swag container’s compose file, we port forward 10.0.0.100:80:80 and 10.0.0.100:443:443.

# This is your typical linuxserver.io swag reverse proxy setup.
docker ps --format "table \t\t\t\t"
CONTAINER ID   NAMES         IMAGE                         PORTS                                          NETWORKS
91b38a3164f2   swag          lscr.io/linuxserver/swag      10.0.0.100:80->80/tcp, 10.0.0.100:443->443/tcp lsio

If we’ve set this up correctly, we expect to be able to reach a webpage at http://10.0.0.100:80.

ubuntu@myhost:~$ curl http://10.0.0.100:80
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>

The Problem

If we exec into the swag container, we expect the following ip and port to be reachable. Unfortunately, it is not.

ubuntu@myhost:~$ docker exec -it swag /bin/bash
root@133d975caa2f:/# curl 10.0.0.100
curl: (7) Failed to connect to 10.0.0.100 port 80 after 2 ms: Couldn't connect to serve

The reason for this is that Oracle VMs are pre-configured with a bunch of iptable rules and with UFW disabled. I believe the reasoning for these rules is to ensure VMs are sufficiently hardened and also to manager their boot volume traffic, which is mounted via iSCSI(?). They document their odd setup here. Note that Oracle states that you need to explicitly open port 80 for web traffic to flow. However, we did not do this with our Ubuntu VM, so why is traffic available at 10.0.0.100:80? This behavior is driven by how Docker port forwards work. Documented here:

When you publish a container’s ports using Docker, traffic to and from that container gets diverted before it goes through the ufw firewall settings. Docker routes container traffic in the nat table, which means that packets are diverted before it reaches the INPUT and OUTPUT chains that ufw uses. Packets are routed before the firewall rules can be applied, effectively ignoring your firewall configuration.

If we take a look at the NAT table, we see that traffic is DNAT’d through to our docker container. When the packet hits the DNAT rule, the next chain that will be executed is the FORWARD chain where it hits out swag reverse proxy.

ubuntu@myhost:~$ sudo iptables -L -v -t nat
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 2846  181K DOCKER     all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  382 55570 DOCKER     all  --  any    any     anywhere            !localhost/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 [... TRUNCATED ... ]

Chain DOCKER (2 references)
 pkts bytes target     prot opt in      out     source               destination
    0     0 RETURN     all  --  docker0 any     anywhere             anywhere
    0     0 RETURN     all  --  br-xyz  any     anywhere             anywhere
    0     0 DNAT       tcp  --  !br-xyz any     anywhere             myhost.mysubnet.myvcn.oraclevcn.com  tcp dpt:https to:172.20.0.2:443
   20  1200 DNAT       tcp  --  !br-xyz any     anywhere             myhost.mysubnet.myvcn.oraclevcn.com  tcp dpt:http to:172.20.0.2:80

This means you don’t actually need to open up port 80 and 443 for the reverse proxy to work. Note however that traffic originating from br-xyz network will not get DNAT-ed. Instead the DOCKER chain returns and packets end up in the INPUT chain. Given Oracle’s restrictive iptable settings, our packet gets rejected as it only matches the last rule.

ubuntu@myhost:~$ sudo iptables -L -v
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 144K   20M ts-input   all  --  any    any     anywhere             anywhere             # This is because I installed tailscale w/ accept-routes
69380   12M ACCEPT     all  --  any    any     anywhere             anywhere             state RELATED,ESTABLISHED
    2   168 ACCEPT     icmp --  any    any     anywhere             anywhere
31629 2614K ACCEPT     all  --  lo     any     anywhere             anywhere
    0     0 ACCEPT     udp  --  any    any     anywhere             anywhere             udp spt:ntp
  501 29392 ACCEPT     tcp  --  any    any     anywhere             anywhere             state NEW tcp dpt:ssh
   32  4715 REJECT     all  --  any    any     anywhere             anywhere             reject-with icmp-host-prohibited

The Solution

For the typical reverse-proxy application, you probably won’t need to access the swag reverse-proxy from inside the user-defined bridge network through ip port 10.0.0.100:80. If the reverse-proxy redirects traffic from some public ip or domain, you should be able to get the same effect by going through http://mydomain.example.com:80. However, sometimes my reverse proxy is hosted on a Tailscale IP and it might need to “talk to itself” through its own Tailscale IP. An example would be if you ran an uptime-kuma service that needs to check if services are up behind your reverse proxy hosted on your host’s tailscale ip.

The solution is to allow traffic headed to port 80 and 443 in the INPUT chain.

# Add the following to your iptable, where br-xyz is the network your container is hosted
-A INPUT -i br-xyz -d 10.0.0.100 -p tcp -m state --state NEW -m multiport --dports 80,443 -j ACCEPT

I’m not aware of any way to automatically represent the IP addr of eth0 instead of hardcoding it like I did above. You could make the ip subnet range by replacing it with something like 10.0.0.100/24.

IPv6

Oracle doesn’t bork ip6tables, so you shouldn’t need to do anything extra to get this to work. You will need to configure docker to work with ipv6.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Custom TLD Over Tailscale
  • Running step-ca in docker w/ Yubikey
  • Notes on Arduino GPS Library
  • Mosh + Tmux + Copy Paste
  • Reverse Proxies With Custom ACME