Skip to content

Commit e7599da

Browse files
authored
Merge pull request moby#49829 from robmry/block_direct_routing_per_container
iptables: Direct routing DROP rules per-container, not per-port
2 parents c333c0d + a0ff0a3 commit e7599da

File tree

8 files changed

+123
-14
lines changed

8 files changed

+123
-14
lines changed

integration/network/bridge/iptablesdoc/generated/usernet-portmap-lo.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ The filter and nat tables are identical to [nat mode][0]:
139139

140140
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
141141
num pkts bytes target prot opt in out source destination
142-
1 0 0 DROP 6 -- !lo * 0.0.0.0/0 127.0.0.1 tcp dpt:8080
143-
2 0 0 DROP 6 -- !bridge1 * 0.0.0.0/0 192.0.2.2 tcp dpt:80
142+
1 0 0 DROP 0 -- !bridge1 * 0.0.0.0/0 192.0.2.2
143+
2 0 0 DROP 6 -- !lo * 0.0.0.0/0 127.0.0.1 tcp dpt:8080
144144

145145
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
146146
num pkts bytes target prot opt in out source destination
@@ -151,8 +151,8 @@ The filter and nat tables are identical to [nat mode][0]:
151151

152152
-P PREROUTING ACCEPT
153153
-P OUTPUT ACCEPT
154+
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -j DROP
154155
-A PREROUTING -d 127.0.0.1/32 ! -i lo -p tcp -m tcp --dport 8080 -j DROP
155-
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -p tcp -m tcp --dport 80 -j DROP
156156

157157

158158
</details>

integration/network/bridge/iptablesdoc/generated/usernet-portmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ And the raw table:
163163

164164
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
165165
num pkts bytes target prot opt in out source destination
166-
1 0 0 DROP 6 -- !bridge1 * 0.0.0.0/0 192.0.2.2 tcp dpt:80
166+
1 0 0 DROP 0 -- !bridge1 * 0.0.0.0/0 192.0.2.2
167167

168168
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
169169
num pkts bytes target prot opt in out source destination
@@ -174,7 +174,7 @@ And the raw table:
174174

175175
-P PREROUTING ACCEPT
176176
-P OUTPUT ACCEPT
177-
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -p tcp -m tcp --dport 80 -j DROP
177+
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -j DROP
178178

179179

180180
</details>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
-P PREROUTING ACCEPT
22
-P OUTPUT ACCEPT
3+
-A PREROUTING -d 192.168.0.2/32 ! -i docker0 -j DROP
34
-A PREROUTING -d 127.0.0.1/32 ! -i lo -p tcp -m tcp --dport 8080 -j DROP
4-
-A PREROUTING -d 192.168.0.2/32 ! -i docker0 -p tcp -m tcp --dport 80 -j DROP
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
-P PREROUTING ACCEPT
22
-P OUTPUT ACCEPT
3-
-A PREROUTING -d fd30:1159:a755::2/128 ! -i docker0 -p tcp -m tcp --dport 80 -j DROP
3+
-A PREROUTING -d fd30:1159:a755::2/128 ! -i docker0 -j DROP

libnetwork/drivers/bridge/bridge_linux.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,11 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
11741174
}
11751175
}
11761176

1177+
netip4, netip6 := endpoint.netipAddrs()
1178+
if err := n.iptablesNetwork.AddEndpoint(ctx, netip4, netip6); err != nil {
1179+
return err
1180+
}
1181+
11771182
// Up the host interface after finishing all netlink configuration
11781183
if err = d.linkUp(ctx, host); err != nil {
11791184
return fmt.Errorf("could not set link up for host interface %s: %v", hostIfName, err)
@@ -1190,6 +1195,18 @@ func (d *driver) CreateEndpoint(ctx context.Context, nid, eid string, ifInfo dri
11901195
return nil
11911196
}
11921197

1198+
// netipAddrs converts ep.addr and ep.addrv6 from net.IPNet to netip.Addr. If an address
1199+
// is non-nil, it's assumed to be valid.
1200+
func (ep *bridgeEndpoint) netipAddrs() (v4, v6 netip.Addr) {
1201+
if ep.addr != nil {
1202+
v4, _ = netip.AddrFromSlice(ep.addr.IP)
1203+
}
1204+
if ep.addrv6 != nil {
1205+
v6, _ = netip.AddrFromSlice(ep.addrv6.IP)
1206+
}
1207+
return v4, v6
1208+
}
1209+
11931210
// createVeth creates a veth device with one end in the container's network namespace,
11941211
// if it can get hold of the netns path and open the handles. In that case, it returns
11951212
// a netlink handle in the container's namespace that must be closed by the caller.
@@ -1282,6 +1299,11 @@ func (d *driver) DeleteEndpoint(nid, eid string) error {
12821299
return endpointNotFoundError(eid)
12831300
}
12841301

1302+
netip4, netip6 := ep.netipAddrs()
1303+
if err := n.iptablesNetwork.DelEndpoint(context.TODO(), netip4, netip6); err != nil {
1304+
return err
1305+
}
1306+
12851307
// Remove it
12861308
n.Lock()
12871309
delete(n.endpoints, eid)

libnetwork/drivers/bridge/bridge_store.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ func (d *driver) populateEndpoints() error {
8787
continue
8888
}
8989
n.endpoints[ep.id] = ep
90+
netip4, netip6 := ep.netipAddrs()
91+
if err := n.iptablesNetwork.AddEndpoint(context.TODO(), netip4, netip6); err != nil {
92+
log.G(context.TODO()).WithFields(log.Fields{
93+
"error": err,
94+
"ep.id": ep.id,
95+
}).Warn("Failed to restore per-endpoint firewall rules")
96+
}
9097
n.restorePortAllocations(ep)
9198
log.G(context.TODO()).Debugf("Endpoint (%.7s) restored to network (%.7s)", ep.id, ep.nid)
9299
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build linux
2+
3+
package iptabler
4+
5+
import (
6+
"context"
7+
"net/netip"
8+
9+
"github.com/docker/docker/libnetwork/iptables"
10+
)
11+
12+
func (n *Network) AddEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr) error {
13+
return n.modEndpoint(ctx, epIPv4, epIPv6, true)
14+
}
15+
16+
func (n *Network) DelEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr) error {
17+
return n.modEndpoint(ctx, epIPv4, epIPv6, false)
18+
}
19+
20+
func (n *Network) modEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr, enable bool) error {
21+
if n.ipt.IPv4 && epIPv4.IsValid() {
22+
if err := n.filterDirectAccess(ctx, iptables.IPv4, n.Config4, epIPv4, enable); err != nil {
23+
return err
24+
}
25+
}
26+
if n.ipt.IPv6 && epIPv6.IsValid() {
27+
if err := n.filterDirectAccess(ctx, iptables.IPv6, n.Config6, epIPv6, enable); err != nil {
28+
return err
29+
}
30+
}
31+
return nil
32+
}
33+
34+
// filterDirectAccess drops packets addressed directly to the container's IP address,
35+
// when direct routing is not permitted by network configuration.
36+
//
37+
// It is a no-op if:
38+
// - the network is internal
39+
// - gateway mode is "nat-unprotected" or "routed".
40+
// - "raw" rules are disabled (possibly because the host doesn't have the necessary
41+
// kernel support).
42+
//
43+
// Packets originating on the bridge's own interface and addressed directly to the
44+
// container are allowed - the host always has direct access to its own containers
45+
// (it doesn't need to use the port mapped to its own addresses, although it can).
46+
func (n *Network) filterDirectAccess(ctx context.Context, ipv iptables.IPVersion, config NetworkConfigFam, epIP netip.Addr, enable bool) error {
47+
if n.Internal || config.Unprotected || config.Routed {
48+
return nil
49+
}
50+
// For config that may change between daemon restarts, make sure rules are
51+
// removed - if the container was left running when the daemon stopped, and
52+
// direct routing has since been disabled, the rules need to be deleted when
53+
// cleanup happens on restart. This also means a change in config over a
54+
// live-restore restart will take effect.
55+
if rawRulesDisabled(ctx) {
56+
enable = false
57+
}
58+
accept := iptables.Rule{IPVer: ipv, Table: iptables.Raw, Chain: "PREROUTING", Args: []string{
59+
"-d", epIP.String(),
60+
"!", "-i", n.IfName,
61+
"-j", "DROP",
62+
}}
63+
return appendOrDelChainRule(accept, "DIRECT ACCESS FILTERING - DROP", enable)
64+
}

libnetwork/drivers/bridge/internal/iptabler/port.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (n *Network) setPerPortIptables(ctx context.Context, b types.PortBinding, e
5151
return err
5252
}
5353

54-
if err := n.filterDirectAccess(ctx, b, enable); err != nil {
54+
if err := n.dropLegacyFilterDirectAccess(ctx, b); err != nil {
5555
return err
5656
}
5757

@@ -203,12 +203,28 @@ func filterPortMappedOnLoopback(ctx context.Context, b types.PortBinding, hostIP
203203
return nil
204204
}
205205

206-
// filterDirectAccess adds an iptables rule that drops 'direct' remote
207-
// connections made to the container's IP address, when the network gateway
208-
// mode is "nat".
206+
// dropLegacyFilterDirectAccess deletes a rule that was introduced in 28.0.0 to
207+
// drop 'direct' remote connections made to the container's IP address - for
208+
// each published port on the container.
209209
//
210-
// This is a no-op if the gw_mode is "nat-unprotected" or "routed".
211-
func (n *Network) filterDirectAccess(ctx context.Context, b types.PortBinding, enable bool) error {
210+
// The normal filter-FORWARD rules would then drop packets sent directly to
211+
// unpublished ports. This rule was only created along with the rest of port
212+
// publishing (when a container's endpoint was selected as its gateway). Until
213+
// then, all packets addressed directly to the container's ports were dropped
214+
// by the filter-FORWARD rules.
215+
//
216+
// Since 28.0.2, direct routed packets sent to a container's address are all
217+
// dropped in a raw-PREROUTING rule - it doesn't need to be per-port (so, fewer
218+
// rules), and it can be created along with the endpoint (so directly-routed
219+
// packets are dropped at the same point whether or not the endpoint is currently
220+
// the gateway - so, very slightly earlier when it's not the gateway).
221+
//
222+
// This function was a no-op if the gw_mode was "nat-unprotected" or "routed".
223+
// It still is. but now always deletes the rule if it might have been created
224+
// by an older version of the daemon.
225+
//
226+
// TODO(robmry) - remove this once there's no upgrade path from 28.0.x or 28.1.x.
227+
func (n *Network) dropLegacyFilterDirectAccess(ctx context.Context, b types.PortBinding) error {
212228
if rawRulesDisabled(ctx) {
213229
return nil
214230
}
@@ -232,7 +248,7 @@ func (n *Network) filterDirectAccess(ctx context.Context, b types.PortBinding, e
232248
"!", "-i", n.IfName,
233249
"-j", "DROP",
234250
}}
235-
if err := appendOrDelChainRule(drop, "DIRECT ACCESS FILTERING - DROP", enable); err != nil {
251+
if err := appendOrDelChainRule(drop, "LEGACY DIRECT ACCESS FILTERING - DROP", false); err != nil {
236252
return err
237253
}
238254

0 commit comments

Comments
 (0)