pitkley/dfw

wider_world_to_container not applying when external_network_interface is 'lo'

Opened this issue · 2 comments

xepa commented

I am currently using wider_world_to_container rules to "bind" containers to localhost. If there is another way of doing this please let me know.

[[wider_world_to_container.rules]]
network = "network"
dst_container = "container"
expose_port = [9999]
external_network_interface = "lo"

With the above config I would expect a connection from the host running the dockers to 127.0.0.1 port 9999 to be routed to the container container on the docker network network

I notice that the dnat rules this config creates are placed in the following (other rules are removed)

table ip dfw {
        chain prerouting {
                type nat hook prerouting priority dstnat - 5; policy accept;
                tcp dport 9999 iifname "lo" meta mark set 0x000000df dnat to 172.29.0.3:9999
        }
}


table inet dfw {
        chain forward {
                type filter hook forward priority filter - 5; policy accept;
                ....
                tcp dport 9999 ip daddr 172.29.0.3 iifname "lo" oifname "br-network" meta mark set 0x000000df accept
        }
}

this might work for external traffic but localhost traffic does not pass the prerouting nat chain but instead uses the output nat chain with the following I can allow this.

nft 'add chain inet dfw output { type nat hook output priority -5; }'
nft 'insert rule inet dfw output meta mark set 0x000000df oifname "lo" tcp dport 9999 dnat ip to 172.29.0.3:9999'
nft 'insert rule ip dfw postrouting ip saddr 127.0.0.1 oifname "br-network" tcp dport 9999 masquerade'  

Hi @xepa, thank you for reaching out. I had never really thought about the use case of NATting traffic from the host itself to a container, mostly because I probably always used port-publishing for this, but I understand how NATting it could still be preferable.

I have not thought a potential solution for this through entirely, so I'd like to get input on what you think would be a good solution from a usability standpoint:

  1. Keep the configuration as is and introduce a special case for when the external_network_interface contains/is lo, generating the matching output nat chain rules.
  2. Allow specifying a complex type for external_network_interface like { name = "lo", is_local_interface = true } for situations where a local interface is not called lo (while retaining backwards-compatibility to the simple string-type, of course).
  3. Extend the container_dnat implementation to not only allow cross-Docker-network NATting, but allow the source (and maybe destination) network to be a non-Docker network, too.
  4. Add a new host_to_container configuration category that supports this use-case.

I personally think that the last one would probably be the most consistent, although I also don't dislike the other options. WDYT?

(Please note that I can't give you any kind of timeline on when I would get around to implementing this feature, although I'm of course happy to receive and review PRs.)

xepa commented

Hi @pitkley thanks for the response, I am glad that this is listed as an enhancement and I might be able to take a look some implementation myself, hopefully I can brush up my rust knowledge, and find some time somewhere to actually dive in.

Please see my thoughts on the following:

  1. Some change will have to be done if (4) is implemented a warning (or a documentation gotcha) will have to be added as adding lo for the external_network_interface will not lead to expected results (but this would be documented). I have not tested this but this might also trigger if using a non loopback device.
  2. I do not know if there are loopback interfaces not called lo 🤷 I noticed that the rust code for pnet has the is_loopback maybe that can be useful, but I would not change this (maybe a future major version).
  3. this might also introduce more complexity and more edge cases (it would also prevent using them at the same time).
  4. host_to_container is also my personal best fit, although I would optionally supply the interface, this would then also cover none loopback interfaces and loopback interfaces not called lo