Page MenuHomeFreeBSD

ng_bridge(4): do not move a host on a reflected transmit
AcceptedPublic

Authored by dteske on Thu, Sep 3, 6:04 PM.
Referenced Files
F170548410: D59350.id185785.diff
Sat, Sep 5, 8:47 AM
F170547555: D59350.id185786.diff
Sat, Sep 5, 8:41 AM
F170547552: D59350.diff
Sat, Sep 5, 8:41 AM
F170420094: D59350.diff
Fri, Sep 4, 6:14 PM
Unknown Object (File)
Thu, Sep 3, 10:43 PM
Unknown Object (File)
Thu, Sep 3, 10:24 PM
Unknown Object (File)
Thu, Sep 3, 10:15 PM
Unknown Object (File)
Thu, Sep 3, 10:15 PM

Details

Summary

Bridging needs promiscuous receive, so ng_bridge(4) setups enable
it on ng_ether(4) lower (setpromisc 1; see ether.bridge in
share/examples/netgraph). A promiscuous interface can receive a
packet this node just sent out that hook. After minStableAge, that
looks like the host jumped, so the table entry moves onto the hook
we transmitted on. Inbound unicast to that host is then dropped
(destination already known on the incoming hook). Classic link
hooks (learnMac=1) see this as well as uplink.

Remember the last hook we sent each source out. A flooded frame
(broadcast, multicast, unknown unicast) is copied out every other
hook, so also remember when a source was last flooded. If the same
source arrives on the recorded hook, or on any hook after a flood,
within minStableAge, drop the packet without moving the host and
without muting the hook. A genuine move is delayed at most
minStableAge, the same tolerance the loop detector already imposes.
Userland NGM_BRIDGE_MOVE_HOST is unchanged. Independent of
learnMac=0 (D58902).

The LOOP DETECTION section of ng_bridge(4) promises that a host
reappearing on another link within the minimum stable time mutes
that link. This change carves out an exception, so document it
there: a reflected transmit is dropped without a move and without
a mute.

The loop test expected ELOOP once the loop closed; the returning
frame is now a reflected transmit, dropped with no error, so check
for that and for loopDrops without loopDetects on the returning
hooks. Add a reflect test for a single bridge: the echo is dropped
within minStableAge and becomes a genuine move after.

MFC after: 1 week

Test Plan

Tested on CURRENT (GENERIC, INVARIANTS+VIMAGE) with the patched ng_bridge.ko.
All hooks in this topology are classic link hooks (learnMac=1); no uplink hooks
are involved.

Grab a copy of jng from an older release (don't use the jng in CURRENT [9.x] which
uses uplink hooks), use something like jng from stable/15 which uses regular
links only. We'll name this jng1.1 for our tests, and put it in our homedir.

fetch -o ~/jng1.1 https://cgit.freebsd.org/src/plain/share/examples/jails/jng?h=stable/15
chmod +x ~/jng1.1

Setup:

vnet jail with an ng_eiface(4) on the same ng_bridge(4) as the host PHY
(ng_ether lower -> link0, upper -> link1, eiface -> link2):

sudo jail -c name=test host.hostname=test.home.arpa vnet persist
jls # shows jail with hostname test.home.arpa with path /
iface=ue0 # tailor to your hardware
sudo ~/jng1.1 bridge test $iface # classic linkN wiring
sudo ngctl ls -l # confirm wiring is lower->link0, upper->link1, eiface->link2
ifconfig -l # shows ng0_test
sudo ifconfig ng0_test vnet test # moves ng0_test into vnet jail
ifconfig -l # confirm ng0_test gone -- moved into jail
sudo jexec test ifconfig -l # shows: lo0 ng0_test
sudo jexec test ifconfig ng0_test inet 192.0.2.1/24
sudo jexec test ifconfig ng0_test # confirm UP with 192/24 IP address
# NB: Jot down the ether value

On my test rig, the jail MAC for ng0_test was 02:40:d5:88:1e:24.

NB: We're going to encode that MAC into a raw frame injection on a separate
link to trigger loopDrops.


Test 1:

Reflection within the window is dropped without a move or mute. A reflected
transmit is indistinguishable from an injected frame bearing the same source,
so it can be synthesized on any NIC. Keep source recently-flooded from jail:

In Terminal A:

sudo jexec test ping -i 0.2 192.0.2.255

Open another terminal. Set iface to the same value as before:

In Terminal B:

iface=ue0 # tailor to your hardware

Check broadcast packet receipt:

while :;do sudo ngctl msg ${iface}bridge: getstats 2; sleep 0.2; done

Confirm that recvBroadcast=<N> is increasing each time.

NB: Ctrl-C the above loop before continuing.

Now inject a frame with the jail MAC as source on a scratch hook.

First generate a string to represent your MAC hex to be used in the frame.

NB: MAC 02:40:d5:88:1e:24 becomes \x02\x40\xd5\x88\x1e\x24

sudo jexec test ifconfig ng0_test ether
hex_mac='\x11\x22\x33\x44\x55\x66' # NB: use single-quotes

Alternatively, you can:

hex_mac=$( sudo jexec test ifconfig ng0_test ether |
    awk '$1=="ether"&&gsub(/^|:/, "\\x", $2){print $2}' )

NB: Copy/paste whole block so ngctl msg statements run immediately after.

sudo -v
{ printf '\xff\xff\xff\xff\xff\xff'"$hex_mac"'\x08\x00'
  head -c 46 /dev/zero
  sleep 30; } | sudo nghook ${iface}bridge: link9 > /dev/null &
sleep 1
sudo ngctl msg ${iface}bridge: getstats 9
sudo ngctl msg ${iface}bridge: gettable

Result: getstats 9 shows recvPackets=1 recvBroadcast=1 loopDrops=1; gettable
still has the MAC on link2; link9 is not muted. On the unpatched module this
frame moves the host onto link9 (entry age was well past minStableAge) and
inbound unicast to the jail black-holes.

NB: Wait for bg nghook to exit, or kill %1, before re-injecting on same hook.


Test 2:

Genuine moves still work.

NB: Cancel the ping in Terminal A.

Stop all jail traffic, wait several seconds (> minStableAge [1s]), inject the
same frame on link8.

sudo -v
{ printf '\xff\xff\xff\xff\xff\xff'"$hex_mac"'\x08\x00'
  head -c 46 /dev/zero
  sleep 30; } | sudo nghook ${iface}bridge: link8 > /dev/null &
sleep 1
sudo ngctl msg ${iface}bridge: getstats 8
sudo ngctl msg ${iface}bridge: gettable

Result: getstats 8 shows recvPackets=1 recvBroadcast=1 and no loopDrops;
gettable shows the host moved onto link8 (legacy move semantics) with
age / staleness reset (absent fields = 0) on jail MAC entry.


Test 3:

Recovery: one transmit from the jail returns the entry to link2.

NB: Execute within Test 2's sleep 30 window (or bump that sleep) - when link8
disconnects, its hosts are deleted and the MAC returns to link2 via re-insert
rather than the move this test demonstrates.

NB: But wait >2s after the Test 2 injection: the accepted broadcast was itself
flooded, refreshing the reflection window.

sudo jexec test ping -c 1 192.0.2.2 # failure expected
sudo ngctl msg ${iface}bridge: gettable

Result: the ping fails (nothing at 192.0.2.2), but its ARP broadcast is a
genuine transmit from the jail: gettable shows the MAC back on link2 with
age / staleness reset.


Clean up:

sudo ifconfig ng0_test -vnet test
ifconfig -l # confirm ng_eiface reclaimed from jail
sudo ~/jng1.1 shutdown test
ifconfig -l # confirm ng_eiface gone
sudo jail -r test
jls # confirm jail removed
sudo ngctl shutdown ${iface}bridge:
sudo ngctl ls # confirm ng_bridge gone
sudo ngctl msg ${iface}: setpromisc 0
sudo ngctl msg ${iface}: setautosrc 1
sudo kldunload ng_bridge # optional

Module built with -Werror clean; ng_bridge.4 passes mandoc -Tlint.

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 76479
Build 73362: arc lint + arc unit

Event Timeline

dteske requested review of this revision.Thu, Sep 3, 6:04 PM
dteske added a project: Netgraph.

Excellent work on this. Thanks!

This revision is now accepted and ready to land.Fri, Sep 4, 12:21 AM

@kfv thank you so much for pointing me at the tests and working through the logic with me to make sure we do the right thing (updating the test now that we return 0 instead of ELOOP)