mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
confd: add support for static ARP and neighbor cache
Add support for static ARP (IPv4) and neighbor cache (IPv6) entries per interface. Static entries are installed as permanent kernel neighbor table entries that are never evicted by normal ARP/NDP aging. Fixes #819 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -435,6 +435,66 @@ admin@example:/config/interface/eth0/> <b>leave</b>
|
||||
admin@example:/>
|
||||
</code></pre>
|
||||
|
||||
## ARP and Neighbor Cache
|
||||
|
||||
Static ARP entries (IPv4) and neighbor cache entries (IPv6) can be
|
||||
configured per interface. The most common reasons to do so are:
|
||||
|
||||
- **Security** — prevent ARP/NDP spoofing by locking critical hosts
|
||||
(e.g., a default gateway) to their known MAC addresses
|
||||
- **Reliability** — ensure reachability of a host even when ARP/NDP
|
||||
traffic is suppressed or filtered (e.g., across a strict firewall)
|
||||
|
||||
Dynamic entries are learned automatically by the kernel using ARP and
|
||||
Neighbor Discovery Protocol (NDP), and are visible as read-only
|
||||
operational state alongside the static ones.
|
||||
|
||||
### Static IPv4 ARP Entry
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit interface eth0 ipv4</b>
|
||||
admin@example:/config/interface/eth0/ipv4/> <b>set neighbor 192.168.1.100 link-layer-address 00:11:22:33:44:55</b>
|
||||
admin@example:/config/interface/eth0/ipv4/> <b>diff</b>
|
||||
+interfaces {
|
||||
+ interface eth0 {
|
||||
+ ipv4 {
|
||||
+ neighbor 192.168.1.100 {
|
||||
+ link-layer-address 00:11:22:33:44:55;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
admin@example:/config/interface/eth0/ipv4/> <b>leave</b>
|
||||
admin@example:/>
|
||||
</code></pre>
|
||||
|
||||
### Static IPv6 Neighbor Entry
|
||||
|
||||
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||
admin@example:/config/> <b>edit interface eth0 ipv6</b>
|
||||
admin@example:/config/interface/eth0/ipv6/> <b>set neighbor 2001:db8::100 link-layer-address 00:11:22:33:44:55</b>
|
||||
admin@example:/config/interface/eth0/ipv6/> <b>diff</b>
|
||||
+interfaces {
|
||||
+ interface eth0 {
|
||||
+ ipv6 {
|
||||
+ neighbor 2001:db8::100 {
|
||||
+ link-layer-address 00:11:22:33:44:55;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
admin@example:/config/interface/eth0/ipv6/> <b>leave</b>
|
||||
admin@example:/>
|
||||
</code></pre>
|
||||
|
||||
The full neighbor table — including dynamically learned entries (origin:
|
||||
*dynamic*) — is available as operational state via NETCONF or RESTCONF.
|
||||
|
||||
> [!NOTE]
|
||||
> Static neighbor entries take effect immediately on `leave` (commit).
|
||||
> They are installed as *permanent* entries in the kernel neighbor table,
|
||||
> which means they are never evicted by the normal ARP/NDP aging process.
|
||||
|
||||
[1]: https://www.rfc-editor.org/rfc/rfc3442
|
||||
[2]: https://www.rfc-editor.org/rfc/rfc8344
|
||||
[3]: https://www.rfc-editor.org/rfc/rfc8981
|
||||
|
||||
@@ -681,6 +681,8 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
|
||||
err = err ? : netdag_gen_link_addr(ip, cif, dif);
|
||||
err = err ? : netdag_gen_ip_addrs(net, ip, "ipv4", cif, dif);
|
||||
err = err ? : netdag_gen_ip_addrs(net, ip, "ipv6", cif, dif);
|
||||
err = err ? : netdag_gen_ip_neighs(net, ip, "ipv4", cif, dif);
|
||||
err = err ? : netdag_gen_ip_neighs(net, ip, "ipv6", cif, dif);
|
||||
if (err)
|
||||
goto err_close_ip;
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ int netdag_gen_ipv4_autoconf(struct dagger *net, struct lyd_node *cif,
|
||||
struct lyd_node *dif);
|
||||
int netdag_gen_ip_addrs(struct dagger *net, FILE *ip, const char *proto,
|
||||
struct lyd_node *cif, struct lyd_node *dif);
|
||||
int netdag_gen_ip_neighs(struct dagger *net, FILE *ip, const char *proto,
|
||||
struct lyd_node *cif, struct lyd_node *dif);
|
||||
|
||||
/* if-bridge.c */
|
||||
int bridge_mstpd_gen(struct lyd_node *cifs);
|
||||
|
||||
@@ -242,6 +242,79 @@ static int netdag_set_conf_addrs(FILE *ip, const char *ifname,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int netdag_gen_diff_neigh(FILE *ip, const char *ifname,
|
||||
struct lyd_node *neigh)
|
||||
{
|
||||
enum lydx_op op = lydx_get_op(neigh);
|
||||
struct lyd_node *addr, *lladdr;
|
||||
struct lydx_diff addrd, lladrd;
|
||||
|
||||
addr = lydx_get_child(neigh, "ip");
|
||||
if (!addr)
|
||||
return -EINVAL;
|
||||
|
||||
lydx_get_diff(addr, &addrd);
|
||||
|
||||
if (op == LYDX_OP_DELETE) {
|
||||
fprintf(ip, "neigh del %s dev %s\n", addrd.old, ifname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
lladdr = lydx_get_child(neigh, "link-layer-address");
|
||||
if (!lladdr)
|
||||
return -EINVAL;
|
||||
|
||||
lydx_get_diff(lladdr, &lladrd);
|
||||
fprintf(ip, "neigh replace %s lladdr %s dev %s nud permanent\n",
|
||||
addrd.new, lladrd.new, ifname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int netdag_set_conf_neighs(FILE *ip, const char *ifname,
|
||||
struct lyd_node *ipvx)
|
||||
{
|
||||
struct lyd_node *neigh;
|
||||
|
||||
LYX_LIST_FOR_EACH(lyd_child(ipvx), neigh, "neighbor") {
|
||||
fprintf(ip, "neigh replace %s lladdr %s dev %s nud permanent\n",
|
||||
lydx_get_cattr(neigh, "ip"),
|
||||
lydx_get_cattr(neigh, "link-layer-address"),
|
||||
ifname);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int netdag_gen_ip_neighs(struct dagger *net, FILE *ip, const char *proto,
|
||||
struct lyd_node *cif, struct lyd_node *dif)
|
||||
{
|
||||
struct lyd_node *ipconf = lydx_get_child(cif, proto);
|
||||
struct lyd_node *ipdiff = lydx_get_child(dif, proto);
|
||||
const char *ifname = lydx_get_cattr(dif, "name");
|
||||
struct lyd_node *neigh;
|
||||
int err = 0;
|
||||
|
||||
if (!ipconf || !lydx_is_enabled(ipconf, "enabled")) {
|
||||
FILE *fp = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "flush-neigh.sh");
|
||||
if (fp) {
|
||||
fprintf(fp, "ip -%c neigh flush dev %s nud permanent\n", proto[3], ifname);
|
||||
fclose(fp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lydx_get_op(lydx_get_child(ipdiff, "enabled")) == LYDX_OP_REPLACE)
|
||||
return netdag_set_conf_neighs(ip, ifname, ipconf);
|
||||
|
||||
LYX_LIST_FOR_EACH(lyd_child(ipdiff), neigh, "neighbor") {
|
||||
err = netdag_gen_diff_neigh(ip, ifname, neigh);
|
||||
if (err)
|
||||
break;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int netdag_gen_ip_addrs(struct dagger *net, FILE *ip, const char *proto,
|
||||
struct lyd_node *cif, struct lyd_node *dif)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ MODULES=(
|
||||
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
|
||||
"infix-hardware@2026-02-08.yang"
|
||||
"ieee802-dot1q-types@2022-10-29.yang"
|
||||
"infix-ip@2025-11-02.yang"
|
||||
"infix-ip@2026-04-28.yang"
|
||||
"infix-if-type@2026-01-07.yang"
|
||||
"infix-routing@2026-03-11.yang"
|
||||
"ieee802-dot1ab-lldp@2022-03-15.yang"
|
||||
|
||||
@@ -18,6 +18,10 @@ module infix-ip {
|
||||
|
||||
description "This module augments ietf-ip with Infix extensions and deviations.";
|
||||
|
||||
revision 2026-04-28 {
|
||||
description "Add support for ARP and neighbor cache (ietf-ip neighbor lists).";
|
||||
reference "Internal, issue #819.";
|
||||
}
|
||||
revision 2025-11-02 {
|
||||
description "Change autoconf to presence container, removing enabled leaf.";
|
||||
reference "Internal, issue #1109.";
|
||||
@@ -65,15 +69,8 @@ module infix-ip {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation "/if:interfaces/if:interface/ip:ipv4/ip:neighbor" {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation "/if:interfaces/if:interface/ip:ipv6/ip:address/ip:status" {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation "/if:interfaces/if:interface/ip:ipv6/ip:neighbor" {
|
||||
deviate not-supported;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,17 @@ def ipaddrs(ifname=None, netns=None):
|
||||
return HOST.run_json(pre + ["ip", "-j", "addr", "show"] + filt)
|
||||
|
||||
return { addr["ifname"]: addr for addr in _ipaddrs(ifname, netns) }
|
||||
|
||||
|
||||
@cache
|
||||
def ipneighs(ifname=None, netns=None):
|
||||
def _ipneighs(ifname, netns):
|
||||
pre = ["ip", "netns", "exec", netns] if netns else []
|
||||
filt = ["dev", ifname] if ifname else []
|
||||
return HOST.run_json(pre + ["ip", "-j", "neigh", "show"] + filt, [])
|
||||
|
||||
result = {}
|
||||
for e in _ipneighs(ifname, netns):
|
||||
if dev := e.get("dev"):
|
||||
result.setdefault(dev, []).append(e)
|
||||
return result
|
||||
|
||||
@@ -1,4 +1,55 @@
|
||||
import ipaddress
|
||||
|
||||
from ..host import HOST
|
||||
from . import common
|
||||
|
||||
|
||||
def neigh_state(states):
|
||||
xlate = {
|
||||
"REACHABLE": "reachable",
|
||||
"STALE": "stale",
|
||||
"DELAY": "delay",
|
||||
"PROBE": "probe",
|
||||
"INCOMPLETE": "incomplete",
|
||||
}
|
||||
return next((xlate[s] for s in states if s in xlate), None)
|
||||
|
||||
|
||||
def neighbors(ifname, family):
|
||||
result = []
|
||||
for entry in common.ipneighs().get(ifname, []):
|
||||
dst = entry.get("dst", "")
|
||||
try:
|
||||
version = ipaddress.ip_address(dst).version
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if version != (4 if family == "inet" else 6):
|
||||
continue
|
||||
|
||||
lladdr = entry.get("lladdr")
|
||||
states = entry.get("state", [])
|
||||
|
||||
if not lladdr:
|
||||
continue
|
||||
|
||||
origin = "static" if "PERMANENT" in states else "dynamic"
|
||||
neigh = {
|
||||
"ip": dst,
|
||||
"link-layer-address": lladdr,
|
||||
"origin": origin,
|
||||
}
|
||||
|
||||
if family == "inet6":
|
||||
if state := neigh_state(states):
|
||||
neigh["state"] = state
|
||||
if entry.get("router"):
|
||||
neigh["is-router"] = [None]
|
||||
|
||||
result.append(neigh)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def inet2yang_origin(inet):
|
||||
"""Translate kernel IP address origin to YANG"""
|
||||
@@ -34,23 +85,31 @@ def addresses(ipaddr, proto):
|
||||
|
||||
def ipv4(ipaddr):
|
||||
ipv4 = {}
|
||||
ifname = ipaddr.get("ifname")
|
||||
|
||||
mtu = ipaddr.get("mtu")
|
||||
if mtu and ipaddr.get("ifname") != "lo":
|
||||
if mtu and ifname != "lo":
|
||||
ipv4["mtu"] = mtu
|
||||
|
||||
if addrs := addresses(ipaddr, "inet"):
|
||||
ipv4["address"] = addrs
|
||||
|
||||
if neighs := neighbors(ifname, "inet"):
|
||||
ipv4["neighbor"] = neighs
|
||||
|
||||
return ipv4
|
||||
|
||||
def ipv6(ipaddr):
|
||||
ipv6 = {}
|
||||
ifname = ipaddr.get("ifname")
|
||||
|
||||
if mtu := HOST.read(f"/proc/sys/net/ipv6/conf/{ipaddr['ifname']}/mtu"):
|
||||
if mtu := HOST.read(f"/proc/sys/net/ipv6/conf/{ifname}/mtu"):
|
||||
ipv6["mtu"] = int(mtu.strip())
|
||||
|
||||
if addrs := addresses(ipaddr, "inet6"):
|
||||
ipv6["address"] = addrs
|
||||
|
||||
if neighs := neighbors(ifname, "inet6"):
|
||||
ipv6["neighbor"] = neighs
|
||||
|
||||
return ipv6
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
- name: Interface with IPv4
|
||||
case: ipv4_address/test.py
|
||||
|
||||
- name: ARP and Neighbor Cache
|
||||
case: neighbor_cache/test.py
|
||||
|
||||
- name: Interface IPv6 Autoconf for Bridges
|
||||
case: ipv6_address/test.py
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,25 @@
|
||||
=== ARP and Neighbor Cache
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/neighbor_cache]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify that static ARP entries (IPv4) and neighbor cache entries (IPv6)
|
||||
can be configured on an interface and are immediately visible in the
|
||||
operational datastore with origin "static". Also verify that removing
|
||||
the entries causes them to disappear from the operational datastore.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[ARP and Neighbor Cache topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Configure static IPv4 ARP and IPv6 neighbor entries on target:data
|
||||
. Verify static IPv4 ARP entry is visible in operational state
|
||||
. Verify static IPv6 neighbor entry is visible in operational state
|
||||
. Remove static neighbor entries by clearing IPv4 and IPv6 config
|
||||
. Verify static neighbor entries are no longer present
|
||||
|
||||
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ARP and Neighbor Cache
|
||||
|
||||
Verify that static ARP entries (IPv4) and neighbor cache entries (IPv6)
|
||||
can be configured on an interface and are immediately visible in the
|
||||
operational datastore with origin "static". Also verify that removing
|
||||
the entries causes them to disappear from the operational datastore.
|
||||
"""
|
||||
import infamy
|
||||
import infamy.iface as iface
|
||||
|
||||
from infamy.util import until
|
||||
|
||||
IPV4_NEIGH = "192.0.2.1"
|
||||
IPV4_LLADR = "de:ad:be:ef:ca:fe"
|
||||
IPV6_NEIGH = "2001:db8::1"
|
||||
IPV6_LLADR = "de:ad:be:ef:ca:ff"
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
_, tport = env.ltop.xlate("target", "data")
|
||||
|
||||
with test.step("Configure static IPv4 ARP and IPv6 neighbor entries on target:data"):
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": tport,
|
||||
"ipv4": {
|
||||
"neighbor": [{
|
||||
"ip": IPV4_NEIGH,
|
||||
"link-layer-address": IPV4_LLADR,
|
||||
}]
|
||||
},
|
||||
"ipv6": {
|
||||
"neighbor": [{
|
||||
"ip": IPV6_NEIGH,
|
||||
"link-layer-address": IPV6_LLADR,
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Verify static IPv4 ARP entry is visible in operational state"):
|
||||
until(lambda: iface.neighbor_exist(target, tport, IPV4_NEIGH, IPV4_LLADR, "static"))
|
||||
|
||||
with test.step("Verify static IPv6 neighbor entry is visible in operational state"):
|
||||
until(lambda: iface.neighbor_exist(target, tport, IPV6_NEIGH, IPV6_LLADR, "static"))
|
||||
|
||||
with test.step("Remove static neighbor entries by clearing IPv4 and IPv6 config"):
|
||||
target.delete_xpath(
|
||||
f"/ietf-interfaces:interfaces/interface[name='{tport}']/ietf-ip:ipv4")
|
||||
target.delete_xpath(
|
||||
f"/ietf-interfaces:interfaces/interface[name='{tport}']/ietf-ip:ipv6")
|
||||
|
||||
with test.step("Verify static neighbor entries are no longer present"):
|
||||
until(lambda: not iface.neighbor_exist(target, tport, IPV4_NEIGH))
|
||||
until(lambda: not iface.neighbor_exist(target, tport, IPV6_NEIGH))
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,24 @@
|
||||
graph "1x2" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+80";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt> mgmt | <data> data }",
|
||||
pos="0,12!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
target [
|
||||
label="{ <mgmt> mgmt | <data> data } | target",
|
||||
pos="10,12!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
|
||||
host:data -- target:data [color=black]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x2 Pages: 1 -->
|
||||
<svg width="424pt" height="55pt"
|
||||
viewBox="0.00 0.00 424.03 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
|
||||
<title>1x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 420.03,-51 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-46.5 416.03,-46.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-46.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 300.03,-35.5 300.03,-35.5"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data--target:data</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -108,6 +108,28 @@ def is_oper_up(target, iface):
|
||||
return get_oper_status(target, iface) == "up"
|
||||
|
||||
|
||||
def _get_neighbors(target, iface, proto):
|
||||
interface = target.get_iface(iface)
|
||||
if interface is None:
|
||||
return None
|
||||
ip = interface.get(proto) or interface.get(f"ietf-ip:{proto}")
|
||||
return ip.get("neighbor") if ip else None
|
||||
|
||||
|
||||
def neighbor_exist(target, iface, address, lladdr=None, origin=None):
|
||||
"""Check if neighbor 'address' exists on iface, optionally matching lladdr and origin"""
|
||||
for proto in ("ipv4", "ipv6"):
|
||||
for n in _get_neighbors(target, iface, proto) or []:
|
||||
if n.get("ip") != address:
|
||||
continue
|
||||
if lladdr and n.get("link-layer-address") != lladdr:
|
||||
continue
|
||||
if origin and n.get("origin") != origin:
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def exist_bridge_multicast_filter(target, group, iface, bridge):
|
||||
"""Check if a bridge has a multicast filter for group with iface"""
|
||||
# The interface array is different in restconf/netconf, netconf has
|
||||
|
||||
Reference in New Issue
Block a user