-
Notifications
You must be signed in to change notification settings - Fork 18.9k
Add gateway mode "nat-unprotected" #48597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
be62866 to
393d4a0
Compare
826284d to
a284fda
Compare
b11455a to
7d85aa8
Compare
| return n.config.GwModeIPv4.routed(), n.config.GwModeIPv6.routed() | ||
| } | ||
|
|
||
| func (n *bridgeNetwork) getUnprotected(v iptables.IPVersion) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure we'll find this name confusing some time in the future. Maybe isNATUnprotected would be more explicit?
| func (n *bridgeNetwork) getUnprotected(v iptables.IPVersion) bool { | |
| func (n *bridgeNetwork) isNATUnprotected(v iptables.IPVersion) bool { |
Or maybe you can introduce a more generic method: func (n *bridgeNetwork) isGwMode(gwm gwMode) bool 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yes - I see your point! I think I had routed-unprotected in-mind (which doesn't exist, but might at some point) ... so, this method would return true for *-unprotected.
I'll change it to isGwModeUnprotected.
| gwModeRouted gwMode = "routed" | ||
| gwModeDefault gwMode = "" | ||
| gwModeNAT gwMode = "nat" | ||
| gwModeNATUnprot gwMode = "nat-unprotected" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to make nat-unprotected the default for existing networks -- only new networks should use nat. At least, that's what it should be for Docker Desktop (and DD doesn't have a way to 'migrate' Docker objects during upgrades).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting our discussion here ...
Setting nat-unprotected for existing IPv4 networks would be right if the host had previously been running with the filter-FORWARD chain's policy set to ACCEPT ... which happens when sysctl ip_forward is enabled before the daemon starts.
But, when it's set to DROP, and for IPv6 - only published ports would be exposed. So, we'd silently be making things worse in many cases by defaulting to nat-unprotected.
So, the change to the default nat mode will break a few setups by blocking remote access to unpublished ports, and anyone who's relying on that will have to re-create their networks with the new mode - but, at least it's fail-safe, and this change will block remote access for users (possibly the majority) who hadn't realised those ports were open.
7d85aa8 to
594541d
Compare
akerouanton
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
594541d to
8d02388
Compare
|
Rebased - which got rid of the first commit, it was just |
|
Unrelated-looking test failure ... |
|
|
||
| docker network create \ | ||
| -o com.docker.network.bridge.name=bridge1 \ | ||
| -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we need an option to explicitly set this for IPv4/IPv6 is that we changed the default for IPv6 already (but not IPv4 for backward-compat), correct?
(Mostly trying to get my head around what's needed to make the overall experience more aligned in future, and possibly reduce some complexity in codepaths)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defaults haven't changed ... in 27.0 (#47871) we introduced the IPv4 and IPv6 gateway mode options - nat became a new name for the old behaviour, and routed mode was added. The default is still nat for both IPv4 and IPv6.
For IPv4, NAT is normal, so you might want to publish ports to the host. For IPv6, direct routing is preferred and, if you have the routing set up in your network, you might want to disable NAT. So, they're configurable separately.
In 28.0, we're changing the default behaviour (nat mode) by blocking access to unpublished ports from other hosts on the same layer-2 network that have set up a route to the container network ("direct routing"). Previously, these ports were only blocked if the filter-FORWARD chain's default policy was DROP. But, docker only sets to DROP if it enables IP forwarding itself.
The old behaviour for a host with filter-FORWARD policy ACCEPT is used as a feature, so we're adding nat-unprotected as a way to restore the old nat behaviour.
It's possible to use nat-unprotected for IPv6, but its filter-FORWARD policy was always set to DROP when ip6tables was enabled - so the new restrictions in nat mode are less likely to be breaking changes.
| n.Lock() | ||
| defer n.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly curious; we have so much locking everywhere; do we have good insight what exactly is it synchronising? (was contemplating if it's purely n.config, and if we should have an accessor for that to return it 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think it's protecting concurrent access to n.config ... an accessor to fetch a copy of the whole of config might be better - but most of the locking is probably unnecessary because the config won't change once created.
But, when adding new accessors, I'm sticking with the scheme others us - it needs proper review and tidy-up and, for me, the cost of acquiring the mutex is outweighed by the risk of making it crashy by missing something!
| if err := setPerPortForwarding(b, v, bridgeName, enable); err != nil { | ||
| return err | ||
|
|
||
| if !n.isGwModeUnprotected(v) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nit-picky, but I had to look 2 levels deep to see the exact details;
- we need
isGwModeUnprotectedto select the gateway mode for either IPv4 or IPv6 n.config.GwModeIPv6/n.config.GwModeIPv4are agwMode, which is effectively a glorified string, but we started to dig in to addinggwMode.something() boolaccessors, which just compare it
Wondering if it would make sense to have a n.gatewayMode(v iptables.IPVersion) gwMode method, which would allow comparing it where we need;
if n.gateWayMode(v) == gwModeNATUnprot {
// ...
}or for cases where there's multiple code-paths;
switch n.gateWayMode(v) {
case gwModeNAT:
// ....
case gwModeNATUnprot:
// ...
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we end up adding a routed-unprotected mode, this code will want to know whether the mode is nat-unprotected or routed-unprotected ... it could explicitly check for the mode being any one of the ones it's interested in - but I think collecting the logic into one place is better. Less danger of missing-a-bit when things change. But, can change it if you like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've unwrapped it a bit ... now there's a bridgeNetwork.gwMode() that returns a gwMode, and the caller checks unprotected() themselves.
(Similarly, could replace getNATDisabled() with gwMode(), although it'd mean acquiring the mutex an extra time to get both modes.)
| // that can route directly to the container's address (by setting up a | ||
| // route via the host's address). | ||
| action := "DROP" | ||
| if n.isGwModeUnprotected(ipVersion) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to my earlier pondering about locking; this takes a lock here (not really important, just .. thinking out loud I guess)
|
|
||
| // Append to the filter table's DOCKER chain (the default rule must follow | ||
| // per-port ACCEPT rules, which will be inserted at the top of the chain). | ||
| if err := appendOrDelChainRule(rule, "DEFAULT FWD", true); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But interestingly (regarding my "locking" pondering); n.registerIptCleanFunc actually mutates state of the network and does not have any synchronisation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right - it's a mess ... but, not this PR's mess!
| // Append to the filter table's DOCKER chain (the default rule must follow | ||
| // per-port ACCEPT rules, which will be inserted at the top of the chain). | ||
| if err := appendOrDelChainRule(rule, "DEFAULT FWD", true); err != nil { | ||
| return fmt.Errorf("failed to add default-drop rule: %s", err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ugh; I see a bunch more further up, and this was probably existing code, but we really shouldn't use err.Error() for these, and either wrap the error (if we need to be able to access the wrapped one) or just format;
| return fmt.Errorf("failed to add default-drop rule: %s", err.Error()) | |
| return fmt.Errorf("failed to add default-drop rule: %w", err) |
or (if we want to mask the underlying error)
| return fmt.Errorf("failed to add default-drop rule: %s", err.Error()) | |
| return fmt.Errorf("failed to add default-drop rule: %v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it was a copy and paste of an existing block - I'll change it.
Signed-off-by: Rob Murray <[email protected]>
8d02388 to
027a336
Compare
Same as "nat" mode, there's masquerading and port mapping from the host - but no port/protocol filtering for direct access to the container's address from remote hosts. This is the old default behaviour for IPv4 when the filter-FORWARD chain's default policy was "ACCEPT" (the daemon would only set it to "DROP" when it set sysctl "ip_forward" itself, but it didn't set up DROP rules for unpublished ports). Now, port filtering doesn't depend on the filter-FORWARD policy. So, this mode is added as a way to restore the old/surprising/insecure behaviour for anyone who's depending on it. Networks will need to be re-created with this new gateway mode. Signed-off-by: Rob Murray <[email protected]>
027a336 to
0aba672
Compare
laurazard
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
thaJeztah
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
## Description Updates for moby 28.0 networking. ## Related issues or tickets Series of commits ... - Fix description of 'inhibit_ipv4' - Not changed in moby 28.0, updated to clarify difference from (new) IPv6-only networks. - Updates to default bridge address config - moby/moby#48319 - Describe IPv6-only network config - moby/moby#48271 - docker/cli#5599 - Update description of gateway modes - moby/moby#48594 - moby/moby#48596 - moby/moby#48597 - Describe gateway selection in the networking overview. - docker/cli#5664 - Describe gateway mode `isolated` - moby/moby#49262 ## Reviews <!-- Notes for reviewers here --> <!-- List applicable reviews (optionally @tag reviewers) --> - [ ] Technical review - [ ] Editorial review - [ ] Product review --------- Signed-off-by: Rob Murray <[email protected]>
- What I did
Add gateway mode
nat-unprotectedfor use in optioncom.docker.network.bridge.gateway_mode_ipv[46].A
nat-unprotectednetwork:natgateway mode).nat).nat-unprotectedmode can be used to restore the old default behaviour for IPv4 when the filter-FORWARDchain's default policy was "ACCEPT". (Before #48594, the daemon would only set the policy to "DROP" when it set sysctl "ip_forward" itself. Now, it never touches the default policy.)https://docs.docker.com/engine/network/packet-filtering-firewalls/#direct-routing will need an update.
- How I did it
In the
DOCKERchain, wherenatmode appends a default-DROP rule for any packets not accepted by the per-port/protocol rules,nat-unprotectedappends a default-ACCEPT rule (needed in case the filter-FORWARD chain's default policy is DROP).Because the default is ACCEPT, no per-port/protocol rules to ACCEPT packets for the published ports are created. (If the userland proxy is enabled, it is still started.)
- How to verify it
New tests.
- Description for the changelog