Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions cmd/nerdctl/container/container_inspect_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

"github.com/containerd/continuity/testutil/loopback"
"github.com/containerd/nerdctl/mod/tigron/expect"
"github.com/containerd/nerdctl/mod/tigron/require"
"github.com/containerd/nerdctl/mod/tigron/test"
"github.com/containerd/nerdctl/mod/tigron/tig"

Expand Down Expand Up @@ -586,6 +587,61 @@ USER test
testCase.Run(t)
}

func TestContainerInspectGateway(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you already checked CI failures related to this fix?

testCase := nerdtest.Setup()

// This test validates nerdctl's inspect conversion path
// Running this against Docker would not use this code path
testCase.Require = require.All(
require.Not(require.Windows),
require.Not(nerdtest.Docker),
nerdtest.Rootful,
)

testCase.NoParallel = true

testCase.Setup = func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier("net"))

helpers.Ensure("run", "-d",
"--name", data.Identifier("ctr"),
"--network", data.Identifier("net"),
testutil.CommonImage, "sleep", nerdtest.Infinity)

nerdtest.EnsureContainerStarted(helpers, data.Identifier("ctr"))
}

testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", data.Identifier("ctr"))
helpers.Anyhow("network", "rm", data.Identifier("net"))
}

testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("container", "inspect", data.Identifier("ctr"))
}

testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: expect.ExitCodeSuccess,
Output: func(stdout string, t tig.T) {
var containers []dockercompat.Container
err := json.Unmarshal([]byte(stdout), &containers)
assert.NilError(t, err, "Unable to unmarshal output\n")
assert.Equal(t, 1, len(containers), "Unexpectedly got multiple results\n")

assert.Assert(t, containers[0].NetworkSettings != nil)
assert.Assert(t, containers[0].NetworkSettings.Gateway != "")

network := nerdtest.InspectNetwork(helpers, data.Identifier("net"))
assert.Assert(t, len(network.IPAM.Config) > 0)
assert.Equal(t, network.IPAM.Config[0].Gateway, containers[0].NetworkSettings.Gateway)
},
}
}

testCase.Run(t)
}

type hostConfigValues struct {
Driver string
ShmSize int64
Expand Down
34 changes: 34 additions & 0 deletions pkg/containerinspector/containerinspector_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/containernetworking/plugins/pkg/ns"
"github.com/vishvananda/netlink"

"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
Expand Down Expand Up @@ -55,6 +56,10 @@ func InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error) {
res.Interfaces[i] = x
}
res.PrimaryInterface = determinePrimaryInterface(res.Interfaces)
routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
if err == nil {
res.Gateway = selectDefaultGateway(routes, res.PrimaryInterface)
}
return nil
}
if err := ns.WithNetNSPath(nsPath, fn); err != nil {
Expand All @@ -73,3 +78,32 @@ func determinePrimaryInterface(interfaces []native.NetInterface) int {
}
return 0
}

// isDefaultRoute reports whether route is a default route.
// netlink synthesizes a 0.0.0.0/0 Dst for default routes rather than leaving it
// nil, so the prefix length is what distinguishes them.
func isDefaultRoute(route netlink.Route) bool {
if route.Dst == nil {
return true
}
ones, _ := route.Dst.Mask.Size()
return ones == 0
}

// selectDefaultGateway returns the IPv4 default gateway, preferring the route
// that leaves through the primary interface. Returns "" when there is none.
func selectDefaultGateway(routes []netlink.Route, primaryIfIndex int) string {
fallback := ""
for _, route := range routes {
if !isDefaultRoute(route) || route.Gw == nil || route.Gw.To4() == nil {
continue
}
if route.LinkIndex == primaryIfIndex {
return route.Gw.String()
}
if fallback == "" {
fallback = route.Gw.String()
}
}
return fallback
}
101 changes: 101 additions & 0 deletions pkg/containerinspector/containerinspector_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package containerinspector

import (
"net"
"testing"

"github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
)

// defaultRoute mimics what netlink returns for a default route: Dst is not nil,
// it is synthesized as 0.0.0.0/0.
func defaultRoute(linkIndex int, gw string) netlink.Route {
return netlink.Route{
LinkIndex: linkIndex,
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
Gw: net.ParseIP(gw),
}
}

func subnetRoute(linkIndex int, cidr string) netlink.Route {
_, dst, _ := net.ParseCIDR(cidr)
return netlink.Route{LinkIndex: linkIndex, Dst: dst}
}

func TestSelectDefaultGateway(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
routes []netlink.Route
primary int
expected string
}{
{
name: "default route on the primary interface",
routes: []netlink.Route{subnetRoute(2, "10.4.1.0/24"), defaultRoute(2, "10.4.1.1")},
primary: 2,
expected: "10.4.1.1",
},
{
name: "prefers the primary interface over another default route",
routes: []netlink.Route{defaultRoute(3, "192.168.0.1"), defaultRoute(2, "10.4.1.1")},
primary: 2,
expected: "10.4.1.1",
},
{
name: "falls back when no default route matches the primary interface",
routes: []netlink.Route{defaultRoute(3, "192.168.0.1")},
primary: 2,
expected: "192.168.0.1",
},
{
name: "nil Dst is still treated as a default route",
routes: []netlink.Route{{LinkIndex: 2, Gw: net.ParseIP("10.4.1.1")}},
primary: 2,
expected: "10.4.1.1",
},
{
name: "no default route",
routes: []netlink.Route{subnetRoute(2, "10.4.1.0/24")},
primary: 2,
expected: "",
},
{
name: "onlink default route without a gateway is skipped",
routes: []netlink.Route{{LinkIndex: 2, Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}}},
primary: 2,
expected: "",
},
{
name: "IPv6 gateway is ignored",
routes: []netlink.Route{{LinkIndex: 2, Gw: net.ParseIP("fe80::1")}},
primary: 2,
expected: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.expected, selectDefaultGateway(tc.routes, tc.primary))
})
}
}
3 changes: 2 additions & 1 deletion pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ type CPUSettings struct {
// DefaultNetworkSettings is from https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L405-L414
type DefaultNetworkSettings struct {
// TODO EndpointID string // EndpointID uniquely represents a service endpoint in a Sandbox
// TODO Gateway string // Gateway holds the gateway address for the network
Gateway string // Gateway holds the gateway address for the network
GlobalIPv6Address string // GlobalIPv6Address holds network's global IPv6 address
GlobalIPv6PrefixLen int // GlobalIPv6PrefixLen represents mask length of network's global IPv6 address
IPAddress string // IPAddress holds the IPv4 address for the network
Expand Down Expand Up @@ -743,6 +743,7 @@ func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings
}

}
res.DefaultNetworkSettings.Gateway = n.Gateway
if primary != nil {
res.DefaultNetworkSettings.MacAddress = primary.MacAddress
res.DefaultNetworkSettings.IPAddress = primary.IPAddress
Expand Down
8 changes: 8 additions & 0 deletions pkg/inspecttypes/dockercompat/dockercompat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ func TestNetworkSettingsFromNative(t *testing.T) {
{
name: "Given NetNS with single Interface with Port Annotation, Return populated NetworkSettings",
n: &native.NetNS{
Gateway: "10.0.4.1",
Interfaces: []native.NetInterface{
{
Interface: net.Interface{
Expand All @@ -427,6 +428,9 @@ func TestNetworkSettingsFromNative(t *testing.T) {
Annotations: map[string]string{},
},
expected: &NetworkSettings{
DefaultNetworkSettings: DefaultNetworkSettings{
Gateway: "10.0.4.1",
},
Ports: &nat.PortMap{
nat.Port("77/tcp"): []nat.PortBinding{
{
Expand All @@ -449,6 +453,7 @@ func TestNetworkSettingsFromNative(t *testing.T) {
{
name: "Given NetNS with single Interface without Port Annotations, Return valid NetworkSettings w/ empty Ports",
n: &native.NetNS{
Gateway: "10.0.4.1",
Interfaces: []native.NetInterface{
{
Interface: net.Interface{
Expand All @@ -467,6 +472,9 @@ func TestNetworkSettingsFromNative(t *testing.T) {
Annotations: map[string]string{},
},
expected: &NetworkSettings{
DefaultNetworkSettings: DefaultNetworkSettings{
Gateway: "10.0.4.1",
},
Ports: &nat.PortMap{},
Networks: map[string]*NetworkEndpointSettings{
"unknown-eth0.100": {
Expand Down
1 change: 1 addition & 0 deletions pkg/inspecttypes/native/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type NetNS struct {
// Zero means unset.
PrimaryInterface int `json:"PrimaryInterface,omitempty"`
Interfaces []NetInterface `json:"Interfaces,omitempty"`
Gateway string `json:"Gateway,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Gateway string `json:"Gateway,omitempty"`
Gateway net.IP `json:"Gateway,omitempty"`

PortMappings []cni.PortMapping
}

Expand Down
Loading