Skip to content

Commit 0756765

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

File tree

2 files changed

+153
-8
lines changed

2 files changed

+153
-8
lines changed

cmd/nerdctl/network/network_inspect_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,64 @@ func TestNetworkInspect(t *testing.T) {
397397
}
398398
},
399399
},
400+
{
401+
Description: "Test container network details",
402+
Setup: func(data test.Data, helpers test.Helpers) {
403+
helpers.Ensure("network", "create", data.Identifier("test-network"))
404+
405+
// See https://github.com/containerd/nerdctl/issues/4322
406+
if runtime.GOOS == "windows" {
407+
time.Sleep(time.Second)
408+
}
409+
410+
// Create and start a container on this network
411+
helpers.Ensure("run", "-d", "--name", data.Identifier("test-container"),
412+
"--network", data.Identifier("test-network"),
413+
testutil.CommonImage, "sleep", nerdtest.Infinity)
414+
415+
// Get container ID for later use
416+
containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n")
417+
data.Labels().Set("containerID", containerID)
418+
},
419+
Cleanup: func(data test.Data, helpers test.Helpers) {
420+
helpers.Anyhow("rm", "-f", data.Identifier("test-container"))
421+
helpers.Anyhow("network", "remove", data.Identifier("test-network"))
422+
},
423+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
424+
return helpers.Command("network", "inspect", data.Identifier("test-network"))
425+
},
426+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
427+
return &test.Expected{
428+
Output: func(stdout string, t tig.T) {
429+
var dc []dockercompat.Network
430+
err := json.Unmarshal([]byte(stdout), &dc)
431+
assert.NilError(t, err, "Unable to unmarshal output")
432+
assert.Equal(t, 1, len(dc), "Expected exactly one network")
433+
434+
network := dc[0]
435+
assert.Equal(t, network.Name, data.Identifier("test-network"))
436+
assert.Equal(t, 1, len(network.Containers), "Expected exactly one container")
437+
438+
// Get the container details
439+
containerID := data.Labels().Get("containerID")
440+
container := network.Containers[containerID]
441+
442+
// Test container name
443+
assert.Equal(t, container.Name, data.Identifier("test-container"))
444+
445+
// Test IPv4Address has CIDR notation (not empty and contains '/')
446+
assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty")
447+
assert.Assert(t, strings.Contains(container.IPv4Address, "/"), "IPv4Address should contain CIDR notation with /")
448+
449+
// Test MacAddress is present and has valid format
450+
assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty")
451+
452+
// Test IPv6Address is empty for IPv4-only network
453+
assert.Equal(t, "", container.IPv6Address, "IPv6Address should be empty for IPv4-only network")
454+
},
455+
}
456+
},
457+
},
400458
}
401459

402460
testCase.Run(t)

pkg/inspecttypes/dockercompat/dockercompat.go

Lines changed: 95 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,88 @@ 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 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+
for _, addr := range iface.Addrs {
994+
ip, _, err := net.ParseCIDR(addr)
995+
if err != nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
996+
continue
997+
}
998+
999+
for _, subnet := range subnets {
1000+
if subnet.Contains(ip) {
1001+
endpoint.MacAddress = iface.HardwareAddr
1002+
setIPAddresses(endpoint, addr)
1003+
return true
1004+
}
1005+
}
1006+
}
1007+
return false
1008+
}
1009+
1010+
// populateEndpointFromNetNS finds and populates endpoint info from network namespace interfaces
1011+
func populateEndpointFromNetNS(endpoint *EndpointResource, interfaces []native.NetInterface, subnets []*net.IPNet) {
1012+
for _, iface := range interfaces {
1013+
if !isUsableInterface(&iface) {
1014+
continue
1015+
}
1016+
1017+
if len(subnets) > 0 {
1018+
if matchInterfaceToSubnets(endpoint, &iface, subnets) {
1019+
return // Found matching interface
1020+
}
1021+
// Continue to next interface if this one doesn't match any subnets
1022+
continue
1023+
}
1024+
1025+
// Fallback: use first usable interface (for networks without explicit subnets)
1026+
endpoint.MacAddress = iface.HardwareAddr
1027+
for _, addr := range iface.Addrs {
1028+
setIPAddresses(endpoint, addr)
1029+
}
1030+
return
1031+
}
1032+
}
1033+
9521034
func NetworkFromNative(n *native.Network) (*Network, error) {
9531035
var res Network
9541036

@@ -973,15 +1055,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
9731055
res.Labels = *n.NerdctlLabels
9741056
}
9751057

1058+
// Parse network subnets for interface matching
1059+
networkSubnets := parseNetworkSubnets(res.IPAM.Config)
1060+
9761061
res.Containers = make(map[string]EndpointResource)
9771062
for _, container := range n.Containers {
978-
res.Containers[container.ID] = EndpointResource{
1063+
endpoint := EndpointResource{
9791064
Name: container.Labels[labels.Name],
980-
// EndpointID: container.EndpointID,
981-
// MacAddress: container.MacAddress,
982-
// IPv4Address: container.IPv4Address,
983-
// IPv6Address: container.IPv6Address,
9841065
}
1066+
1067+
if container.Process != nil && container.Process.NetNS != nil {
1068+
populateEndpointFromNetNS(&endpoint, container.Process.NetNS.Interfaces, networkSubnets)
1069+
}
1070+
1071+
res.Containers[container.ID] = endpoint
9851072
}
9861073

9871074
return &res, nil

0 commit comments

Comments
 (0)