node-uplink links answers each link's mac, master and parent (issue 1965) #669
@@ -19,7 +19,7 @@ func tools(m Machine) []stdio.Tool {
|
||||
"— its header, a backup a VPN client left beside it, a link in its place, a known writer running. (r)",
|
||||
Run: func(map[string]any) (any, error) { return m.ReadResolvers() }},
|
||||
{Name: seat + ".links",
|
||||
Description: "Every network link on the machine: its state, its addresses, whether the default route " +
|
||||
Description: "Every network link on the machine: its state, its addresses, its mac, the bridge or bond it is enslaved to (master), the link it rides on (parent: its name, or an index and namespace when that link is in another network namespace), whether the default route " +
|
||||
"leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's " +
|
||||
"tunnel included. (r)",
|
||||
Run: func(map[string]any) (any, error) { return m.Links(context.Background()) }},
|
||||
|
||||
@@ -221,12 +221,24 @@ type Link struct {
|
||||
State string `json:"state"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
// MAC is the link-layer address as ip says it (lo's all-zero included); left out where the link has
|
||||
// none. Master is the bridge or bond the link is enslaved to. Parent is the link it rides on: its
|
||||
// name where that link is in this machine's namespace, else a ParentPeer; both left out where none.
|
||||
MAC string `json:"mac,omitempty"`
|
||||
Master string `json:"master,omitempty"`
|
||||
Parent any `json:"parent,omitempty"`
|
||||
// Default says the default route leaves through it, and Metric that route's metric.
|
||||
Default bool `json:"default_route,omitempty"`
|
||||
Metric *int `json:"metric,omitempty"`
|
||||
DNS *LinkDNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// ParentPeer is a parent in another network namespace, which has no name here: ip's index and namespace id.
|
||||
type ParentPeer struct {
|
||||
Index int `json:"index"`
|
||||
Namespace int `json:"namespace"`
|
||||
}
|
||||
|
||||
// Links answers the links verb: every link from the kernel, its default route, and what the manager
|
||||
// knows of its names. A manager that cannot be asked is said, never read as no resolvers.
|
||||
func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
@@ -238,6 +250,11 @@ func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
Name string `json:"ifname"`
|
||||
State string `json:"operstate"`
|
||||
Kind string `json:"link_type"`
|
||||
MAC string `json:"address"`
|
||||
Master string `json:"master"`
|
||||
Link string `json:"link"`
|
||||
LinkIdx *int `json:"link_index"`
|
||||
NetNSID *int `json:"link_netnsid"`
|
||||
AddrInfo []struct {
|
||||
Local string `json:"local"`
|
||||
Prefix int `json:"prefixlen"`
|
||||
@@ -276,7 +293,13 @@ func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
}
|
||||
links := []Link{}
|
||||
for _, a := range addrs {
|
||||
l := Link{Name: a.Name, State: a.State, Kind: a.Kind}
|
||||
l := Link{Name: a.Name, State: a.State, Kind: a.Kind, MAC: a.MAC, Master: a.Master}
|
||||
switch {
|
||||
case a.Link != "":
|
||||
l.Parent = a.Link
|
||||
case a.LinkIdx != nil && a.NetNSID != nil:
|
||||
l.Parent = ParentPeer{Index: *a.LinkIdx, Namespace: *a.NetNSID}
|
||||
}
|
||||
for _, ai := range a.AddrInfo {
|
||||
l.Addresses = append(l.Addresses, a2s(ai.Local, ai.Prefix))
|
||||
}
|
||||
|
||||
@@ -103,8 +103,10 @@ func TestTheLinksCarryTheirDefaultRouteAndTheirNames(t *testing.T) {
|
||||
joined := strings.Join(args, " ")
|
||||
switch {
|
||||
case joined == "-j address show":
|
||||
return `[{"ifname":"lo","operstate":"UNKNOWN","link_type":"loopback","addr_info":[{"local":"127.0.0.1","prefixlen":8}]},
|
||||
{"ifname":"wlan0","operstate":"UP","link_type":"ether","addr_info":[{"local":"192.168.1.20","prefixlen":24}]},
|
||||
return `[{"ifname":"lo","operstate":"UNKNOWN","link_type":"loopback","address":"00:00:00:00:00:00","addr_info":[{"local":"127.0.0.1","prefixlen":8}]},
|
||||
{"ifname":"wlan0","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:01","addr_info":[{"local":"192.168.1.20","prefixlen":24}]},
|
||||
{"ifname":"veth1","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:02","master":"br-1","link":"veth2","addr_info":[]},
|
||||
{"ifname":"veth3","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:03","master":"br-1","link_index":7,"link_netnsid":2,"addr_info":[]},
|
||||
{"ifname":"vpn0","operstate":"UNKNOWN","link_type":"none","addr_info":[{"local":"172.16.9.9","prefixlen":32}]}]`, nil
|
||||
case joined == "-j -4 route show default":
|
||||
return `[{"dst":"default","dev":"wlan0","metric":600}]`, nil
|
||||
@@ -122,10 +124,20 @@ func TestTheLinksCarryTheirDefaultRouteAndTheirNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
links := answer["links"].([]Link)
|
||||
if len(links) != 3 || links[0].Name != "wlan0" || !links[0].Default || *links[0].Metric != 600 ||
|
||||
if len(links) != 5 || links[0].Name != "wlan0" || !links[0].Default || *links[0].Metric != 600 ||
|
||||
links[0].DNS == nil || links[0].DNS.Servers[0] != "192.168.1.1" {
|
||||
t.Fatalf("the links are %+v", links)
|
||||
}
|
||||
byName := map[string]Link{}
|
||||
for _, l := range links {
|
||||
byName[l.Name] = l
|
||||
}
|
||||
if byName["wlan0"].MAC != "aa:bb:cc:00:00:01" || byName["lo"].MAC != "00:00:00:00:00:00" ||
|
||||
byName["veth1"].Master != "br-1" || byName["veth1"].Parent != "veth2" ||
|
||||
byName["veth3"].Parent != (ParentPeer{Index: 7, Namespace: 2}) ||
|
||||
byName["vpn0"].MAC != "" || byName["vpn0"].Master != "" || byName["vpn0"].Parent != nil {
|
||||
t.Fatalf("mac, master and parent are %+v", byName)
|
||||
}
|
||||
for _, l := range links[1:] {
|
||||
if l.Default || (l.Name == "vpn0" && l.DNS != nil) {
|
||||
t.Fatalf("%+v", l)
|
||||
|
||||
@@ -19,7 +19,7 @@ func tools(m Machine) []stdio.Tool {
|
||||
"— its header, a backup a VPN client left beside it, a link in its place, a known writer running. (r)",
|
||||
Run: func(map[string]any) (any, error) { return m.ReadResolvers() }},
|
||||
{Name: seat + ".links",
|
||||
Description: "Every network link on the machine: its state, its addresses, whether the default route " +
|
||||
Description: "Every network link on the machine: its state, its addresses, its mac, the bridge or bond it is enslaved to (master), the link it rides on (parent: its name, or an index and namespace when that link is in another network namespace), whether the default route " +
|
||||
"leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's " +
|
||||
"tunnel included. (r)",
|
||||
Run: func(map[string]any) (any, error) { return m.Links(context.Background()) }},
|
||||
|
||||
@@ -221,12 +221,24 @@ type Link struct {
|
||||
State string `json:"state"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
// MAC is the link-layer address as ip says it (lo's all-zero included); left out where the link has
|
||||
// none. Master is the bridge or bond the link is enslaved to. Parent is the link it rides on: its
|
||||
// name where that link is in this machine's namespace, else a ParentPeer; both left out where none.
|
||||
MAC string `json:"mac,omitempty"`
|
||||
Master string `json:"master,omitempty"`
|
||||
Parent any `json:"parent,omitempty"`
|
||||
// Default says the default route leaves through it, and Metric that route's metric.
|
||||
Default bool `json:"default_route,omitempty"`
|
||||
Metric *int `json:"metric,omitempty"`
|
||||
DNS *LinkDNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// ParentPeer is a parent in another network namespace, which has no name here: ip's index and namespace id.
|
||||
type ParentPeer struct {
|
||||
Index int `json:"index"`
|
||||
Namespace int `json:"namespace"`
|
||||
}
|
||||
|
||||
// Links answers the links verb: every link from the kernel, its default route, and what the manager
|
||||
// knows of its names. A manager that cannot be asked is said, never read as no resolvers.
|
||||
func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
@@ -238,6 +250,11 @@ func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
Name string `json:"ifname"`
|
||||
State string `json:"operstate"`
|
||||
Kind string `json:"link_type"`
|
||||
MAC string `json:"address"`
|
||||
Master string `json:"master"`
|
||||
Link string `json:"link"`
|
||||
LinkIdx *int `json:"link_index"`
|
||||
NetNSID *int `json:"link_netnsid"`
|
||||
AddrInfo []struct {
|
||||
Local string `json:"local"`
|
||||
Prefix int `json:"prefixlen"`
|
||||
@@ -276,7 +293,13 @@ func (m Machine) Links(ctx context.Context) (map[string]any, error) {
|
||||
}
|
||||
links := []Link{}
|
||||
for _, a := range addrs {
|
||||
l := Link{Name: a.Name, State: a.State, Kind: a.Kind}
|
||||
l := Link{Name: a.Name, State: a.State, Kind: a.Kind, MAC: a.MAC, Master: a.Master}
|
||||
switch {
|
||||
case a.Link != "":
|
||||
l.Parent = a.Link
|
||||
case a.LinkIdx != nil && a.NetNSID != nil:
|
||||
l.Parent = ParentPeer{Index: *a.LinkIdx, Namespace: *a.NetNSID}
|
||||
}
|
||||
for _, ai := range a.AddrInfo {
|
||||
l.Addresses = append(l.Addresses, a2s(ai.Local, ai.Prefix))
|
||||
}
|
||||
|
||||
@@ -103,8 +103,10 @@ func TestTheLinksCarryTheirDefaultRouteAndTheirNames(t *testing.T) {
|
||||
joined := strings.Join(args, " ")
|
||||
switch {
|
||||
case joined == "-j address show":
|
||||
return `[{"ifname":"lo","operstate":"UNKNOWN","link_type":"loopback","addr_info":[{"local":"127.0.0.1","prefixlen":8}]},
|
||||
{"ifname":"wlan0","operstate":"UP","link_type":"ether","addr_info":[{"local":"192.168.1.20","prefixlen":24}]},
|
||||
return `[{"ifname":"lo","operstate":"UNKNOWN","link_type":"loopback","address":"00:00:00:00:00:00","addr_info":[{"local":"127.0.0.1","prefixlen":8}]},
|
||||
{"ifname":"wlan0","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:01","addr_info":[{"local":"192.168.1.20","prefixlen":24}]},
|
||||
{"ifname":"veth1","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:02","master":"br-1","link":"veth2","addr_info":[]},
|
||||
{"ifname":"veth3","operstate":"UP","link_type":"ether","address":"aa:bb:cc:00:00:03","master":"br-1","link_index":7,"link_netnsid":2,"addr_info":[]},
|
||||
{"ifname":"vpn0","operstate":"UNKNOWN","link_type":"none","addr_info":[{"local":"172.16.9.9","prefixlen":32}]}]`, nil
|
||||
case joined == "-j -4 route show default":
|
||||
return `[{"dst":"default","dev":"wlan0","metric":600}]`, nil
|
||||
@@ -122,10 +124,20 @@ func TestTheLinksCarryTheirDefaultRouteAndTheirNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
links := answer["links"].([]Link)
|
||||
if len(links) != 3 || links[0].Name != "wlan0" || !links[0].Default || *links[0].Metric != 600 ||
|
||||
if len(links) != 5 || links[0].Name != "wlan0" || !links[0].Default || *links[0].Metric != 600 ||
|
||||
links[0].DNS == nil || links[0].DNS.Servers[0] != "192.168.1.1" {
|
||||
t.Fatalf("the links are %+v", links)
|
||||
}
|
||||
byName := map[string]Link{}
|
||||
for _, l := range links {
|
||||
byName[l.Name] = l
|
||||
}
|
||||
if byName["wlan0"].MAC != "aa:bb:cc:00:00:01" || byName["lo"].MAC != "00:00:00:00:00:00" ||
|
||||
byName["veth1"].Master != "br-1" || byName["veth1"].Parent != "veth2" ||
|
||||
byName["veth3"].Parent != (ParentPeer{Index: 7, Namespace: 2}) ||
|
||||
byName["vpn0"].MAC != "" || byName["vpn0"].Master != "" || byName["vpn0"].Parent != nil {
|
||||
t.Fatalf("mac, master and parent are %+v", byName)
|
||||
}
|
||||
for _, l := range links[1:] {
|
||||
if l.Default || (l.Name == "vpn0" && l.DNS != nil) {
|
||||
t.Fatalf("%+v", l)
|
||||
|
||||
Reference in New Issue
Block a user