diff --git a/share/man/man4/rtnetlink.4 b/share/man/man4/rtnetlink.4 --- a/share/man/man4/rtnetlink.4 +++ b/share/man/man4/rtnetlink.4 @@ -403,14 +403,27 @@ .Ss RTM_DELADDR Not supported .Ss RTM_GETADDR +Fetches interface addresses in the current VNET matching conditions. +Each address is reported as a +.Dv RTM_NEWADDR +message. +The following filters are recognised by the kernel: +.Pp +.Bd -literal -offset indent -compact +ifa_family required family or AF_UNSPEC +ifa_index matching interface index or 0 +.Ed .Ss TLVs .Bl -tag -width indent .It Dv IFA_ADDRESS (binary) masked interface address or destination address for p2p interfaces. .It Dv IFA_LOCAL -(binary) local interface address +(binary) local interface address. +Set for IPv4 and p2p addresses. +.It Dv IFA_LABEL +(string) interface name. .It Dv IFA_BROADCAST -(binary) broacast interface address +(binary) broacast interface address. .El .Ss Groups The following groups are defined: diff --git a/sys/netlink/route/iface.c b/sys/netlink/route/iface.c --- a/sys/netlink/route/iface.c +++ b/sys/netlink/route/iface.c @@ -672,6 +672,36 @@ return (modify_link(hdr, &attrs, &bm, nlp, npt)); } +struct nl_parsed_ifa { + uint8_t ifa_family; + uint8_t ifa_prefixlen; + uint8_t ifa_scope; + uint32_t ifa_index; + uint32_t ifa_flags; + struct sockaddr *ifa_address; + struct sockaddr *ifa_local; +}; + +#define _IN(_field) offsetof(struct ifaddrmsg, _field) +#define _OUT(_field) offsetof(struct nl_parsed_ifa, _field) +static const struct nlfield_parser nlf_p_ifa[] = { + { .off_in = _IN(ifa_family), .off_out = _OUT(ifa_family), .cb = nlf_get_u8 }, + { .off_in = _IN(ifa_prefixlen), .off_out = _OUT(ifa_prefixlen), .cb = nlf_get_u8 }, + { .off_in = _IN(ifa_scope), .off_out = _OUT(ifa_scope), .cb = nlf_get_u8 }, + { .off_in = _IN(ifa_flags), .off_out = _OUT(ifa_flags), .cb = nlf_get_u8_u32 }, + { .off_in = _IN(ifa_index), .off_out = _OUT(ifa_index), .cb = nlf_get_u32 }, +}; + +static const struct nlattr_parser nla_p_ifa[] = { + { .type = IFA_ADDRESS, .off = _OUT(ifa_address), .cb = nlattr_get_ip }, + { .type = IFA_LOCAL, .off = _OUT(ifa_local), .cb = nlattr_get_ip }, + { .type = IFA_FLAGS, .off = _OUT(ifa_flags), .cb = nlattr_get_uint32 }, +}; +#undef _IN +#undef _OUT +NL_DECLARE_PARSER(ifaddrmsg_parser, struct ifaddrmsg, nlf_p_ifa, nla_p_ifa); + + /* {ifa_family=AF_INET, ifa_prefixlen=8, ifa_flags=IFA_F_PERMANENT, ifa_scope=RT_SCOPE_HOST, ifa_index=if_nametoindex("lo")}, @@ -826,15 +856,39 @@ } static int -rtnl_handle_getaddr(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) +dump_iface_addrs(struct netlink_walkargs *wa, struct ifnet *ifp) { struct ifaddr *ifa; + + CK_STAILQ_FOREACH(ifa, &ifp->if_addrhead, ifa_link) { + if (wa->family != 0 && wa->family != ifa->ifa_addr->sa_family) + continue; + if (ifa->ifa_addr->sa_family == AF_LINK) + continue; + wa->count++; + if (!dump_iface_addr(wa->nw, ifp, ifa, &wa->hdr)) + return (ENOMEM); + wa->dumped++; + } + + return (0); +} + +static int +rtnl_handle_getaddr(struct nlmsghdr *hdr, struct nlpcb *nlp, struct nl_pstate *npt) +{ struct ifnet *ifp; int error = 0; + struct nl_parsed_ifa attrs = {}; + error = nl_parse_nlmsg(hdr, &ifaddrmsg_parser, npt, &attrs); + if (error != 0) + return (error); + struct netlink_walkargs wa = { .so = nlp, .nw = npt->nw, + .family = attrs.ifa_family, .hdr.nlmsg_pid = hdr->nlmsg_pid, .hdr.nlmsg_seq = hdr->nlmsg_seq, .hdr.nlmsg_flags = hdr->nlmsg_flags | NLM_F_MULTI, @@ -843,22 +897,19 @@ NL_LOG(LOG_DEBUG2, "Start dump"); - CK_STAILQ_FOREACH(ifp, &V_ifnet, if_link) { - CK_STAILQ_FOREACH(ifa, &ifp->if_addrhead, ifa_link) { - if (wa.family != 0 && wa.family != ifa->ifa_addr->sa_family) - continue; - if (ifa->ifa_addr->sa_family == AF_LINK) - continue; - wa.count++; - if (!dump_iface_addr(wa.nw, ifp, ifa, &wa.hdr)) { - error = ENOMEM; - break; - } - wa.dumped++; - } - if (error != 0) - break; - } + if (attrs.ifa_index != 0) { + ifp = ifnet_byindex(attrs.ifa_index); + if (ifp == NULL) + error = ENOENT; + else + error = dump_iface_addrs(&wa, ifp); + } else { + CK_STAILQ_FOREACH(ifp, &V_ifnet, if_link) { + error = dump_iface_addrs(&wa, ifp); + if (error != 0) + break; + } + } NL_LOG(LOG_DEBUG2, "End dump, iterated %d dumped %d", wa.count, wa.dumped); @@ -991,7 +1042,7 @@ }, }; -static const struct nlhdr_parser *all_parsers[] = { &ifmsg_parser }; +static const struct nlhdr_parser *all_parsers[] = { &ifmsg_parser, &ifaddrmsg_parser }; void rtnl_iface_add_cloner(struct nl_cloner *cloner) diff --git a/sys/netlink/route/ifaddrs.h b/sys/netlink/route/ifaddrs.h --- a/sys/netlink/route/ifaddrs.h +++ b/sys/netlink/route/ifaddrs.h @@ -52,7 +52,7 @@ IFA_UNSPEC, IFA_ADDRESS = 1, /* binary, prefix address (destination for p2p) */ IFA_LOCAL = 2, /* binary, interface address */ - IFA_LABEL = 3, /* not supported */ + IFA_LABEL = 3, /* string, interface name */ IFA_BROADCAST = 4, /* binary, broadcast ifa */ IFA_ANYCAST = 5, /* not supported */ IFA_CACHEINFO = 6, /* not supported */ diff --git a/tests/atf_python/sys/net/netlink.py b/tests/atf_python/sys/net/netlink.py --- a/tests/atf_python/sys/net/netlink.py +++ b/tests/atf_python/sys/net/netlink.py @@ -49,6 +49,12 @@ ] +class Nlmsgdone(Structure): + _fields_ = [ + ("error", c_int), + ] + + class Nlmsgerr(Structure): _fields_ = [ ("error", c_int), @@ -961,6 +967,8 @@ ), ] +nldone_attrs = [] + nlerr_attrs = [ AttrDescr(NlErrattrType.NLMSGERR_ATTR_MSG, NlAttrStr), AttrDescr(NlErrattrType.NLMSGERR_ATTR_OFFS, NlAttrU32), @@ -989,6 +997,7 @@ rtnl_ifa_attrs = [ AttrDescr(IfattrType.IFA_ADDRESS, NlAttrIp), AttrDescr(IfattrType.IFA_LOCAL, NlAttrIp), + AttrDescr(IfattrType.IFA_LABEL, NlAttrStr), AttrDescr(IfattrType.IFA_BROADCAST, NlAttrIp), AttrDescr(IfattrType.IFA_ANYCAST, NlAttrIp), AttrDescr(IfattrType.IFA_FLAGS, NlAttrU32), @@ -1167,6 +1176,25 @@ nla.print_attr(" ") +class NetlinkDoneMessage(StdNetlinkMessage): + messages = [NlMsgType.NLMSG_DONE.value] + nl_attrs_map = prepare_attrs_map(nldone_attrs) + + @property + def error_code(self): + return self.base_hdr.error + + def parse_base_header(self, data): + if len(data) < sizeof(Nlmsgdone): + raise ValueError("length less than nlmsgdone header") + done_hdr = Nlmsgdone.from_buffer_copy(data) + sz = sizeof(Nlmsgdone) + return (done_hdr, sz) + + def print_base_header(self, hdr, prepend=""): + print("{}error={}".format(prepend, hdr.error)) + + class NetlinkErrorMessage(StdNetlinkMessage): messages = [NlMsgType.NLMSG_ERROR.value] nl_attrs_map = prepare_attrs_map(nlerr_attrs) @@ -1340,6 +1368,7 @@ NetlinkRtMessage, NetlinkIflaMessage, NetlinkIfaMessage, + NetlinkDoneMessage, NetlinkErrorMessage, ] xmap = {} @@ -1476,20 +1505,63 @@ self.write_data(msg_bytes) -def main(): - helper = NlHelper() - if False: - nl = Nlsock(NlConst.NETLINK_GENERIC, helper) - nl.request_families() - else: - nl = Nlsock(NlConst.NETLINK_ROUTE, helper) - # nl.request_ifaddrs(socket.AF_INET) - # nl.request_raw() - nl.request_routes(0) - # nl.request_ifaces() - while True: - msg = nl.read_message() +class NetlinkMultipartIterator(object): + def __init__(self, obj, seq_number: int, msg_type): + self._obj = obj + self._seq = seq_number + self._msg_type = msg_type + + def __iter__(self): + return self + + def __next__(self): + msg = self._obj.read_message() + if self._seq != msg.nl_hdr.nlmsg_seq: + raise ValueError("bad sequence number") + if msg.is_type(NlMsgType.NLMSG_ERROR): + raise ValueError( + "error while handling multipart msg: {}".format(msg.error_code) + ) + elif msg.is_type(NlMsgType.NLMSG_DONE): + if msg.error_code == 0: + raise StopIteration + raise ValueError( + "error listing some parts of the multipart msg: {}".format( + msg.error_code + ) + ) + elif not msg.is_type(self._msg_type): + raise ValueError("bad message type: {}".format(msg)) + return msg + + +class NetlinkTestTemplate(object): + REQUIRED_MODULES = ["netlink"] + + def setup_netlink(self, netlink_family: NlConst): + self.helper = NlHelper() + self.nlsock = Nlsock(netlink_family, self.helper) + + def write_message(self, msg): print("") + print("============= >> TX MESSAGE =============") msg.print_message() - msg.print_as_bytes(msg._orig_data, "-- DATA --") - pass + self.nlsock.write_data(bytes(msg)) + msg.print_as_bytes(bytes(msg), "-- DATA --") + + def read_message(self): + msg = self.nlsock.read_message() + print("") + print("============= << RX MESSAGE =============") + msg.print_message() + return msg + + def get_reply(self, tx_msg): + self.write_message(tx_msg) + while True: + rx_msg = self.read_message() + if tx_msg.nl_hdr.nlmsg_seq == rx_msg.nl_hdr.nlmsg_seq: + return rx_msg + + def read_msg_list(self, seq, msg_type): + return list(NetlinkMultipartIterator(self, seq, msg_type)) diff --git a/tests/sys/netlink/Makefile b/tests/sys/netlink/Makefile --- a/tests/sys/netlink/Makefile +++ b/tests/sys/netlink/Makefile @@ -7,6 +7,7 @@ ATF_TESTS_C += test_snl ATF_TESTS_PYTEST += test_rtnl_iface.py +ATF_TESTS_PYTEST += test_rtnl_ifaddr.py CFLAGS+= -I${.CURDIR:H:H:H} diff --git a/tests/sys/netlink/test_rtnl_iface.py b/tests/sys/netlink/test_rtnl_iface.py --- a/tests/sys/netlink/test_rtnl_iface.py +++ b/tests/sys/netlink/test_rtnl_iface.py @@ -6,49 +6,24 @@ from atf_python.sys.net.netlink import IflinkInfo from atf_python.sys.net.netlink import IfLinkInfoDataVlan from atf_python.sys.net.netlink import NetlinkIflaMessage +from atf_python.sys.net.netlink import NetlinkTestTemplate from atf_python.sys.net.netlink import NlAttrNested from atf_python.sys.net.netlink import NlAttrStr from atf_python.sys.net.netlink import NlAttrStrn from atf_python.sys.net.netlink import NlAttrU16 from atf_python.sys.net.netlink import NlAttrU32 from atf_python.sys.net.netlink import NlConst -from atf_python.sys.net.netlink import NlHelper from atf_python.sys.net.netlink import NlmBaseFlags from atf_python.sys.net.netlink import NlmNewFlags from atf_python.sys.net.netlink import NlMsgType from atf_python.sys.net.netlink import NlRtMsgType -from atf_python.sys.net.netlink import Nlsock from atf_python.sys.net.vnet import SingleVnetTestTemplate -class TestRtNlIface(SingleVnetTestTemplate): - REQUIRED_MODULES = ["netlink"] - +class TestRtNlIface(SingleVnetTestTemplate, NetlinkTestTemplate): def setup_method(self, method): super().setup_method(method) - self.helper = NlHelper() - self.nlsock = Nlsock(NlConst.NETLINK_ROUTE, self.helper) - - def write_message(self, msg): - print("") - print("============= >> TX MESSAGE =============") - msg.print_message() - self.nlsock.write_data(bytes(msg)) - msg.print_as_bytes(bytes(msg), "-- DATA --") - - def read_message(self): - msg = self.nlsock.read_message() - print("") - print("============= << RX MESSAGE =============") - msg.print_message() - return msg - - def get_reply(self, tx_msg): - self.write_message(tx_msg) - while True: - rx_msg = self.read_message() - if tx_msg.nl_hdr.nlmsg_seq == rx_msg.nl_hdr.nlmsg_seq: - return rx_msg + self.setup_netlink(NlConst.NETLINK_ROUTE) def get_interface_byname(self, ifname): msg = NetlinkIflaMessage(self.helper, NlRtMsgType.RTM_GETLINK.value) @@ -236,13 +211,11 @@ # * # * {len=76, type=RTM_NEWLINK, flags=NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL|NLM_F_CREATE, seq=1662892737, pid=0}, # * {ifi_family=AF_UNSPEC, ifi_type=ARPHRD_NETROM, ifi_index=0, ifi_flags=0, ifi_change=0}, - # * [ # * {{nla_len=8, nla_type=IFLA_LINK}, 2}, # * {{nla_len=12, nla_type=IFLA_IFNAME}, "xvlan22"}, # * {{nla_len=24, nla_type=IFLA_LINKINFO}, - # * [ # * {{nla_len=8, nla_type=IFLA_INFO_KIND}, "vlan"...}, - # * {{nla_len=12, nla_type=IFLA_INFO_DATA}, "\x06\x00\x01\x00\x16\x00\x00\x00"}]}]}, iov_len=76}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 76 + # * {{nla_len=12, nla_type=IFLA_INFO_DATA}, "\x06\x00\x01\x00\x16\x00\x00\x00"} # */ @pytest.mark.skip(reason="vlan support needs more work") @pytest.mark.require_user("root") diff --git a/tests/sys/netlink/test_rtnl_ifaddr.py b/tests/sys/netlink/test_rtnl_ifaddr.py new file mode 100644 --- /dev/null +++ b/tests/sys/netlink/test_rtnl_ifaddr.py @@ -0,0 +1,144 @@ +import ipaddress +import socket +import struct + +from atf_python.sys.net.netlink import IfattrType +from atf_python.sys.net.netlink import NetlinkIfaMessage +from atf_python.sys.net.netlink import NetlinkTestTemplate +from atf_python.sys.net.netlink import NlConst +from atf_python.sys.net.netlink import NlHelper +from atf_python.sys.net.netlink import NlmBaseFlags +from atf_python.sys.net.netlink import NlMsgType +from atf_python.sys.net.netlink import NlRtMsgType +from atf_python.sys.net.netlink import Nlsock +from atf_python.sys.net.netlink import RtScope +from atf_python.sys.net.vnet import SingleVnetTestTemplate + + +class TestRtNlIfaddr(SingleVnetTestTemplate, NetlinkTestTemplate): + def setup_method(self, method): + method_name = method.__name__ + if "4" in method_name: + self.IPV4_PREFIXES = ["192.0.2.1/24"] + if "6" in method_name: + self.IPV6_PREFIXES = ["2001:db8::1/64"] + super().setup_method(method) + self.setup_netlink(NlConst.NETLINK_ROUTE) + + def test_46_nofilter(self): + """Tests that listing outputs both IPv4/IPv6 and interfaces""" + msg = NetlinkIfaMessage(self.helper, NlRtMsgType.RTM_GETADDR.value) + msg.nl_hdr.nlmsg_flags = ( + NlmBaseFlags.NLM_F_ACK.value | NlmBaseFlags.NLM_F_REQUEST.value + ) + self.write_message(msg) + + ret = [] + for rx_msg in self.read_msg_list(msg.nl_hdr.nlmsg_seq, NlRtMsgType.RTM_NEWADDR): + ifname = socket.if_indextoname(rx_msg.base_hdr.ifa_index) + family = rx_msg.base_hdr.ifa_family + ret.append((ifname, family, rx_msg)) + + ifname = "lo0" + assert len([r for r in ret if r[0] == ifname]) > 0 + + ifname = self.vnet.iface_alias_map["if1"].name + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6]) == 2 + + def test_46_filter_iface(self): + """Tests that listing outputs both IPv4/IPv6 for the specific interface""" + epair_ifname = self.vnet.iface_alias_map["if1"].name + + msg = NetlinkIfaMessage(self.helper, NlRtMsgType.RTM_GETADDR.value) + msg.nl_hdr.nlmsg_flags = ( + NlmBaseFlags.NLM_F_ACK.value | NlmBaseFlags.NLM_F_REQUEST.value + ) + msg.base_hdr.ifa_index = socket.if_nametoindex(epair_ifname) + self.write_message(msg) + + ret = [] + for rx_msg in self.read_msg_list(msg.nl_hdr.nlmsg_seq, NlRtMsgType.RTM_NEWADDR): + ifname = socket.if_indextoname(rx_msg.base_hdr.ifa_index) + family = rx_msg.base_hdr.ifa_family + ret.append((ifname, family, rx_msg)) + + ifname = epair_ifname + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6]) == 2 + assert len(ret) == 3 + + def filter_iface_family(self, family, num_items): + """Tests that listing outputs IPv4 for the specific interface""" + epair_ifname = self.vnet.iface_alias_map["if1"].name + + msg = NetlinkIfaMessage(self.helper, NlRtMsgType.RTM_GETADDR.value) + msg.nl_hdr.nlmsg_flags = ( + NlmBaseFlags.NLM_F_ACK.value | NlmBaseFlags.NLM_F_REQUEST.value + ) + msg.base_hdr.ifa_family = family + msg.base_hdr.ifa_index = socket.if_nametoindex(epair_ifname) + self.write_message(msg) + + ret = [] + for rx_msg in self.read_msg_list(msg.nl_hdr.nlmsg_seq, NlRtMsgType.RTM_NEWADDR): + assert family == rx_msg.base_hdr.ifa_family + assert epair_ifname == socket.if_indextoname(rx_msg.base_hdr.ifa_index) + ret.append(rx_msg) + assert len(ret) == num_items + return ret + + def test_4_broadcast(self): + """Tests header/attr output for listing IPv4 ifas on broadcast iface""" + ret = self.filter_iface_family(socket.AF_INET, 1) + # Should be 192.0.2.1/24 + msg = ret[0] + # Family and ifindex has been checked already + assert msg.base_hdr.ifa_prefixlen == 24 + # Ignore IFA_FLAGS for now + assert msg.base_hdr.ifa_scope == RtScope.RT_SCOPE_UNIVERSE.value + + assert msg.get_nla(IfattrType.IFA_ADDRESS).addr == "192.0.2.1" + assert msg.get_nla(IfattrType.IFA_LOCAL).addr == "192.0.2.1" + assert msg.get_nla(IfattrType.IFA_BROADCAST).addr == "192.0.2.255" + + epair_ifname = self.vnet.iface_alias_map["if1"].name + assert msg.get_nla(IfattrType.IFA_LABEL).text == epair_ifname + + def test_6_broadcast(self): + """Tests header/attr output for listing IPv6 ifas on broadcast iface""" + ret = self.filter_iface_family(socket.AF_INET6, 2) + # Should be 192.0.2.1/24 + if ret[0].base_hdr.ifa_scope == RtScope.RT_SCOPE_UNIVERSE.value: + (gmsg, lmsg) = ret + else: + (lmsg, gmsg) = ret + # Start with global ( 2001:db8::1/64 ) + msg = gmsg + # Family and ifindex has been checked already + assert msg.base_hdr.ifa_prefixlen == 64 + # Ignore IFA_FLAGS for now + assert msg.base_hdr.ifa_scope == RtScope.RT_SCOPE_UNIVERSE.value + + assert msg.get_nla(IfattrType.IFA_ADDRESS).addr == "2001:db8::1" + assert msg.get_nla(IfattrType.IFA_LOCAL) is None + assert msg.get_nla(IfattrType.IFA_BROADCAST) is None + + epair_ifname = self.vnet.iface_alias_map["if1"].name + assert msg.get_nla(IfattrType.IFA_LABEL).text == epair_ifname + + # Local: fe80::/64 + msg = lmsg + assert msg.base_hdr.ifa_prefixlen == 64 + # Ignore IFA_FLAGS for now + assert msg.base_hdr.ifa_scope == RtScope.RT_SCOPE_LINK.value + + addr = ipaddress.ip_address(msg.get_nla(IfattrType.IFA_ADDRESS).addr) + assert addr.is_link_local + # Verify that ifindex is not emmbedded + assert struct.unpack("!H", addr.packed[2:4])[0] == 0 + assert msg.get_nla(IfattrType.IFA_LOCAL) is None + assert msg.get_nla(IfattrType.IFA_BROADCAST) is None + + epair_ifname = self.vnet.iface_alias_map["if1"].name + assert msg.get_nla(IfattrType.IFA_LABEL).text == epair_ifname