Skip to content

Commit e37c5bd

Browse files
committed
add MAC, IPv4, IPv6 addresses to nework inspect
Signed-off-by: Arjun Raja Yogidas <arjunry@amazon.com>
1 parent d6f561e commit e37c5bd

File tree

2 files changed

+258
-8
lines changed

2 files changed

+258
-8
lines changed

cmd/nerdctl/network/network_inspect_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package network
1919
import (
2020
"encoding/json"
2121
"errors"
22+
"net"
2223
"os/exec"
2324
"runtime"
2425
"strings"
@@ -397,6 +398,164 @@ func TestNetworkInspect(t *testing.T) {
397398
}
398399
},
399400
},
401+
{
402+
Description: "Test container network details",
403+
Setup: func(data test.Data, helpers test.Helpers) {
404+
helpers.Ensure("network", "create", data.Identifier("test-network"))
405+
406+
// See https://github.com/containerd/nerdctl/issues/4322
407+
if runtime.GOOS == "windows" {
408+
time.Sleep(time.Second)
409+
}
410+
411+
// Create and start a container on this network
412+
helpers.Ensure("run", "-d", "--name", data.Identifier("test-container"),
413+
"--network", data.Identifier("test-network"),
414+
testutil.CommonImage, "sleep", nerdtest.Infinity)
415+
416+
// Get container ID for later use
417+
containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n")
418+
data.Labels().Set("containerID", containerID)
419+
},
420+
Cleanup: func(data test.Data, helpers test.Helpers) {
421+
helpers.Anyhow("rm", "-f", data.Identifier("test-container"))
422+
helpers.Anyhow("network", "remove", data.Identifier("test-network"))
423+
},
424+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
425+
return helpers.Command("network", "inspect", data.Identifier("test-network"))
426+
},
427+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
428+
return &test.Expected{
429+
Output: func(stdout string, t tig.T) {
430+
var dc []dockercompat.Network
431+
err := json.Unmarshal([]byte(stdout), &dc)
432+
assert.NilError(t, err, "Unable to unmarshal output")
433+
assert.Equal(t, 1, len(dc), "Expected exactly one network")
434+
435+
network := dc[0]
436+
assert.Equal(t, network.Name, data.Identifier("test-network"))
437+
assert.Equal(t, 1, len(network.Containers), "Expected exactly one container")
438+
439+
// Get the container details
440+
containerID := data.Labels().Get("containerID")
441+
container := network.Containers[containerID]
442+
443+
// Test container name
444+
assert.Equal(t, container.Name, data.Identifier("test-container"))
445+
446+
// Windows InspectNetNS is not implemented
447+
if runtime.GOOS != "windows" {
448+
// Verify IPv4Address is not empty and has CIDR notation
449+
assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty")
450+
assert.Assert(t, strings.Contains(container.IPv4Address, "/"), "IPv4Address should contain CIDR notation with /")
451+
452+
// Verify IPv4Address is within the network's subnet
453+
if len(network.IPAM.Config) > 0 && network.IPAM.Config[0].Subnet != "" {
454+
_, subnet, err := net.ParseCIDR(network.IPAM.Config[0].Subnet)
455+
assert.NilError(t, err, "Failed to parse network subnet")
456+
457+
containerIP, _, err := net.ParseCIDR(container.IPv4Address)
458+
assert.NilError(t, err, "Failed to parse container IPv4Address")
459+
assert.Assert(t, subnet.Contains(containerIP), "IPv4Address should be within the network's subnet")
460+
}
461+
462+
// Test MacAddress is present and has valid format
463+
assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty")
464+
465+
// Test IPv6Address is empty for IPv4-only network
466+
assert.Equal(t, "", container.IPv6Address, "IPv6Address should be empty for IPv4-only network")
467+
}
468+
},
469+
}
470+
},
471+
},
472+
{
473+
Description: "Test dual-stack network with both IPv4 and IPv6",
474+
Require: require.Not(require.Windows), // NetNS not implemented on Windows
475+
Setup: func(data test.Data, helpers test.Helpers) {
476+
helpers.Ensure("network", "create",
477+
"--ipv6",
478+
"--subnet", "10.1.0.0/24",
479+
"--subnet", "fd00::/64",
480+
data.Identifier("test-dual-stack"))
481+
482+
// See https://github.com/containerd/nerdctl/issues/4322
483+
if runtime.GOOS == "windows" {
484+
time.Sleep(time.Second)
485+
}
486+
487+
// Create and start a container on this dual-stack network
488+
helpers.Ensure("run", "-d",
489+
"--name", data.Identifier("test-container"),
490+
"--network", data.Identifier("test-dual-stack"),
491+
testutil.CommonImage, "sleep", nerdtest.Infinity)
492+
493+
// Get container ID for later use
494+
containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n")
495+
data.Labels().Set("containerID", containerID)
496+
},
497+
Cleanup: func(data test.Data, helpers test.Helpers) {
498+
helpers.Anyhow("rm", "-f", data.Identifier("test-container"))
499+
helpers.Anyhow("network", "remove", data.Identifier("test-dual-stack"))
500+
},
501+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
502+
return helpers.Command("network", "inspect", data.Identifier("test-dual-stack"))
503+
},
504+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
505+
return &test.Expected{
506+
Output: func(stdout string, t tig.T) {
507+
var dc []dockercompat.Network
508+
err := json.Unmarshal([]byte(stdout), &dc)
509+
assert.NilError(t, err, "Unable to unmarshal output")
510+
assert.Equal(t, 1, len(dc), "Expected exactly one network")
511+
512+
network := dc[0]
513+
assert.Equal(t, network.Name, data.Identifier("test-dual-stack"))
514+
assert.Equal(t, 2, len(network.IPAM.Config), "Expected two subnets (IPv4 and IPv6)")
515+
516+
// Get the container details
517+
containerID := data.Labels().Get("containerID")
518+
container := network.Containers[containerID]
519+
520+
// Test container name
521+
assert.Equal(t, container.Name, data.Identifier("test-container"))
522+
523+
// Parse both subnets
524+
var ipv4Subnet, ipv6Subnet *net.IPNet
525+
for _, config := range network.IPAM.Config {
526+
if config.Subnet != "" {
527+
_, subnet, err := net.ParseCIDR(config.Subnet)
528+
assert.NilError(t, err, "Failed to parse subnet")
529+
if subnet.IP.To4() != nil {
530+
ipv4Subnet = subnet
531+
} else {
532+
ipv6Subnet = subnet
533+
}
534+
}
535+
}
536+
537+
// Verify IPv4 address is present and within subnet
538+
assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty in dual-stack network")
539+
ipv4, _, err := net.ParseCIDR(container.IPv4Address)
540+
assert.NilError(t, err, "Failed to parse IPv4Address")
541+
if ipv4Subnet != nil {
542+
assert.Assert(t, ipv4Subnet.Contains(ipv4), "IPv4 address should be within the IPv4 subnet")
543+
}
544+
545+
// Verify IPv6 address is present and within subnet
546+
assert.Assert(t, container.IPv6Address != "", "IPv6Address should not be empty in dual-stack network")
547+
ipv6, _, err := net.ParseCIDR(container.IPv6Address)
548+
assert.NilError(t, err, "Failed to parse IPv6Address")
549+
if ipv6Subnet != nil {
550+
assert.Assert(t, ipv6Subnet.Contains(ipv6), "IPv6 address should be within the IPv6 subnet")
551+
}
552+
553+
// Verify MAC address is present
554+
assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty")
555+
},
556+
}
557+
},
558+
},
400559
}
401560

402561
testCase.Run(t)

pkg/inspecttypes/dockercompat/dockercompat.go

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -929,9 +929,9 @@ type Network struct {
929929
type EndpointResource struct {
930930
Name string `json:"Name"`
931931
// EndpointID string `json:"EndpointID"`
932-
// MacAddress string `json:"MacAddress"`
933-
// IPv4Address string `json:"IPv4Address"`
934-
// IPv6Address string `json:"IPv6Address"`
932+
MacAddress string `json:"MacAddress"`
933+
IPv4Address string `json:"IPv4Address"`
934+
IPv6Address string `json:"IPv6Address"`
935935
}
936936

937937
type structuredCNI struct {
@@ -949,6 +949,92 @@ type MemorySetting struct {
949949
DisableOOMKiller bool `json:"disableOOMKiller"`
950950
}
951951

952+
// parseNetworkSubnets extracts and parses subnet configurations from IPAM config
953+
func parseNetworkSubnets(ipamConfigs []IPAMConfig) []*net.IPNet {
954+
var subnets []*net.IPNet
955+
for _, config := range ipamConfigs {
956+
if config.Subnet != "" {
957+
_, subnet, err := net.ParseCIDR(config.Subnet)
958+
if err != nil {
959+
log.L.WithError(err).Warnf("failed to parse subnet %q", config.Subnet)
960+
continue
961+
}
962+
subnets = append(subnets, subnet)
963+
}
964+
}
965+
return subnets
966+
}
967+
968+
// isUsableInterface checks if a network interface is usable (not loopback and interface is up)
969+
func isUsableInterface(iface *native.NetInterface) bool {
970+
return iface.Interface.Flags&net.FlagLoopback == 0 &&
971+
iface.Interface.Flags&net.FlagUp != 0
972+
}
973+
974+
// setIPAddresses assigns IPv4 or IPv6 addresses from CIDR notation to the endpoint
975+
func setIPAddresses(endpoint *EndpointResource, cidr string) {
976+
ip, _, err := net.ParseCIDR(cidr)
977+
if err != nil {
978+
return
979+
}
980+
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
981+
return
982+
}
983+
984+
if ip.To4() != nil {
985+
endpoint.IPv4Address = cidr
986+
} else if ip.To16() != nil {
987+
endpoint.IPv6Address = cidr
988+
}
989+
}
990+
991+
// matchInterfaceToSubnets tries to match an interface to network subnets
992+
func matchInterfaceToSubnets(endpoint *EndpointResource, iface *native.NetInterface, subnets []*net.IPNet) bool {
993+
matched := false
994+
for _, addr := range iface.Addrs {
995+
ip, _, err := net.ParseCIDR(addr)
996+
if err != nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
997+
continue
998+
}
999+
1000+
for _, subnet := range subnets {
1001+
if subnet.Contains(ip) {
1002+
if !matched {
1003+
endpoint.MacAddress = iface.HardwareAddr
1004+
matched = true
1005+
}
1006+
setIPAddresses(endpoint, addr)
1007+
break // Break inner loop, continue checking other addresses
1008+
}
1009+
}
1010+
}
1011+
return matched
1012+
}
1013+
1014+
// populateEndpointFromNetNS finds and populates endpoint info from network namespace interfaces
1015+
func populateEndpointFromNetNS(endpoint *EndpointResource, interfaces []native.NetInterface, subnets []*net.IPNet) {
1016+
for _, iface := range interfaces {
1017+
if !isUsableInterface(&iface) {
1018+
continue
1019+
}
1020+
1021+
if len(subnets) > 0 {
1022+
if matchInterfaceToSubnets(endpoint, &iface, subnets) {
1023+
return // Found matching interface
1024+
}
1025+
// Continue to next interface if this one doesn't match any subnets
1026+
continue
1027+
}
1028+
1029+
// Fallback: use first usable interface (for networks without explicit subnets)
1030+
endpoint.MacAddress = iface.HardwareAddr
1031+
for _, addr := range iface.Addrs {
1032+
setIPAddresses(endpoint, addr)
1033+
}
1034+
return
1035+
}
1036+
}
1037+
9521038
func NetworkFromNative(n *native.Network) (*Network, error) {
9531039
var res Network
9541040

@@ -973,15 +1059,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
9731059
res.Labels = *n.NerdctlLabels
9741060
}
9751061

1062+
// Parse network subnets for interface matching
1063+
networkSubnets := parseNetworkSubnets(res.IPAM.Config)
1064+
9761065
res.Containers = make(map[string]EndpointResource)
9771066
for _, container := range n.Containers {
978-
res.Containers[container.ID] = EndpointResource{
1067+
endpoint := EndpointResource{
9791068
Name: container.Labels[labels.Name],
980-
// EndpointID: container.EndpointID,
981-
// MacAddress: container.MacAddress,
982-
// IPv4Address: container.IPv4Address,
983-
// IPv6Address: container.IPv6Address,
9841069
}
1070+
1071+
if container.Process != nil && container.Process.NetNS != nil {
1072+
populateEndpointFromNetNS(&endpoint, container.Process.NetNS.Interfaces, networkSubnets)
1073+
}
1074+
1075+
res.Containers[container.ID] = endpoint
9851076
}
9861077

9871078
return &res, nil

0 commit comments

Comments
 (0)