banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Windows

banshee/windows-collection/windows/ssh/

interfaces

The state, traffic, errors and IPv4 addresses of each network adapter of a Windows, by SSH and PowerShell

  • windows
  • ssh
  • powershell
  • network
  • interfaces

by banshee1 installation2 versions

v2Latest

Published on October 28, 2026. One naming convention across collections; documentation completed.

Keen 1.0.0 · 153 lines
// The network adapters of a Windows, by SSH: the state, the traffic and the errors of each link, and its
// IPv4 addresses. An adapter is named as in "Network Connections" ("Ethernet", "vEthernet (Default
// Switch)"); the hidden adapters of the network stack (WAN Miniport, filters, tunnels) are not listed.
// Runs one PowerShell command that only reads Get-NetAdapter, Get-NetAdapterStatistics and
// Get-NetIPAddress (the CIM classes MSFT_NetAdapter, MSFT_NetAdapterStatisticsSettingData and
// MSFT_NetIPAddress). Neither the duplex nor the link changes are served there.
need('protocol:ssh');

per('interface', 'Interfaces');
output('link', 'text', array('label' => 'Link state', 'per' => 'interface'));
output('speed', 'number', array('label' => 'Negotiated speed', 'unit' => 'Mbit/s', 'per' => 'interface'));
output('mtu', 'number', array('label' => 'MTU', 'per' => 'interface'));
output('traffic-in', 'number', array('label' => 'Inbound traffic', 'unit' => 'bit/s', 'per' => 'interface'));
output('traffic-out', 'number', array('label' => 'Outbound traffic', 'unit' => 'bit/s', 'per' => 'interface'));
output('traffic', 'number', array('label' => 'Total traffic', 'unit' => 'bit/s', 'per' => 'interface'));
output('in-percent', 'number', array('label' => 'Inbound usage', 'unit' => '%', 'per' => 'interface'));
output('out-percent', 'number', array('label' => 'Outbound usage', 'unit' => '%', 'per' => 'interface'));
output('packets', 'number', array('label' => 'Packets', 'unit' => 'packets/s', 'per' => 'interface'));
output('errors', 'number', array('label' => 'Errors', 'unit' => 'errors/s', 'per' => 'interface'));
output('drops', 'number', array('label' => 'Drops', 'unit' => 'packets/s', 'per' => 'interface'));
output('drop-percent', 'number', array('label' => 'Share of packets dropped', 'unit' => '%', 'per' => 'interface'));
output('addresses', 'number', array('label' => 'IPv4 addresses', 'per' => 'interface'));

watch.state('link-down', 'Link down', 'link', array('critical' => array('down', 'lowerlayerdown')));
watch.custom('speed-degraded', 'Negotiated speed degraded', array(
    'help'     => 'Warning when the negotiated speed is below the expected speed.',
    'settings' => array(
        'expected' => array('label' => 'Expected speed', 'form' => 'number', 'unit' => 'Mbit/s', 'default' => 1000),
    ),
    'enabled'  => false,
));
watch.above('saturation-in', 'Inbound saturation', 'in-percent', array('warning' => 70, 'critical' => 90, 'for' => '5 min'));
watch.above('saturation-out', 'Outbound saturation', 'out-percent', array('warning' => 70, 'critical' => 90, 'for' => '5 min'));
watch.custom('errors', 'Errors', array(
    'help'     => 'Warning above a number of errors per minute; critical above a share of the traffic.',
    'settings' => array(
        'per-minute' => array('label' => 'Warning above', 'form' => 'number', 'unit' => 'errors/min', 'default' => 1),
        'share'      => array('label' => 'Critical above', 'form' => 'number', 'unit' => '% of traffic', 'default' => 0.1),
    ),
));
watch.above('drops', 'Packets dropped', 'drop-percent', array('warning' => 0.5, 'critical' => 2));
watch.habit('traffic-unusual', 'Unusual traffic', 'traffic', array('floor' => 10000000, 'enabled' => false));
watch.silence('mute', 'Silent interface', 'packets', array('warning' => '10 min', 'enabled' => false));
watch.missing('missing', 'Interface missing', 'link', array('enabled' => false));
watch.expect('mtu', 'Unexpected MTU', 'mtu', array('warning' => array(1500), 'enabled' => false));
watch.below('no-address', 'No IPv4 address left', 'addresses', array('critical' => 1, 'enabled' => false));

// Three lists, each joined to the adapter by its name. adapters: State (3 when the adapter is disabled),
// InterfaceOperationalStatus (the ifOperStatus of IF-MIB), ReceiveLinkSpeed and TransmitLinkSpeed in bit/s,
// MtuSize. statistics: the counters since the adapter started, bytes and packets in each direction.
// addresses: one per IPv4 address, with the name of its adapter. An adapter name may hold characters
// outside ASCII: the output is written in UTF-8, without the byte order mark of the default UTF-8 of .NET.
$read = ssh.run("powershell -NoProfile -NonInteractive -Command \""
    . "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding; "
    . "ConvertTo-Json -Compress -Depth 4 -InputObject @{"
    . "adapters = @(Get-NetAdapter | Select-Object Name, State, InterfaceOperationalStatus,"
    . " ReceiveLinkSpeed, TransmitLinkSpeed, MtuSize); "
    . "statistics = @(Get-NetAdapterStatistics | Select-Object Name, ReceivedBytes, SentBytes,"
    . " ReceivedUnicastPackets, ReceivedMulticastPackets, ReceivedBroadcastPackets,"
    . " SentUnicastPackets, SentMulticastPackets, SentBroadcastPackets,"
    . " ReceivedPacketErrors, OutboundPacketErrors, ReceivedDiscardedPackets, OutboundDiscardedPackets); "
    . "addresses = @(Get-NetIPAddress -AddressFamily IPv4 | Select-Object InterfaceAlias)}\"");
$answer = text.trim($read['output']);
if (list.count(text.match($answer, '^\{')) == 0) {
    fail('misconfigured', 'PowerShell did not return the network adapters', $answer . $read['errors']);
}
$network = json.read($answer);
if (list.count($network['adapters']) == 0) {
    fail('misconfigured', 'Get-NetAdapter lists no network adapter', $answer);
}

$statistics = array();
foreach ($network['statistics'] as $row) {
    $statistics[$row['Name']] = $row;
}
$address_count = array();
foreach ($network['addresses'] as $address) {
    $address_count[$address['InterfaceAlias']] = list.get($address_count, $address['InterfaceAlias'], 0) + 1;
}

// InterfaceOperationalStatus in the words of IF-MIB; a disabled adapter is said "disabled", so that it
// does not pass for a link down.
$states = array(1 => 'up', 2 => 'down', 3 => 'testing', 4 => 'unknown', 5 => 'dormant', 6 => 'notpresent', 7 => 'lowerlayerdown');

$expected_speed = watch.setting('speed-degraded', 'expected');
$errors_per_minute = watch.setting('errors', 'per-minute');
$errors_share = watch.setting('errors', 'share');

foreach ($network['adapters'] as $adapter) {
    $name = $adapter['Name'];
    $link = list.get($states, $adapter['InterfaceOperationalStatus'], 'unknown');
    if ($adapter['State'] == 3) {
        $link = 'disabled';
    }
    value('link', $link, $name);
    value('addresses', list.get($address_count, $name, 0), $name);
    value('mtu', $adapter['MtuSize'], $name);

    // An adapter that is down has no negotiated speed (0). The slower direction is the one that saturates.
    $speed = $adapter['ReceiveLinkSpeed'];
    if ($adapter['TransmitLinkSpeed'] < $speed) {
        $speed = $adapter['TransmitLinkSpeed'];
    }
    $speed = $speed / 1000000;
    if ($speed > 0) {
        value('speed', $speed, $name);
        if ($expected_speed != '' && $speed < $expected_speed) {
            watch.raise('speed-degraded', 'warning', $name . ' negotiated at ' . $speed . ' Mbit/s', $name);
        }
    }

    // The traffic, from the counters of this run and those of the previous one; nothing at the first run,
    // nor for an adapter without statistics.
    if (!list.has($statistics, $name)) {
        continue;
    }
    $row = $statistics[$name];
    $in_packets = $row['ReceivedUnicastPackets'] + $row['ReceivedMulticastPackets'] + $row['ReceivedBroadcastPackets'];
    $out_packets = $row['SentUnicastPackets'] + $row['SentMulticastPackets'] + $row['SentBroadcastPackets'];
    $in = counter.rate('in', $row['ReceivedBytes'] * 8, $name);
    $out = counter.rate('out', $row['SentBytes'] * 8, $name);
    $packets = counter.rate('packets', $in_packets + $out_packets, $name);
    $errors = counter.rate('errors', $row['ReceivedPacketErrors'] + $row['OutboundPacketErrors'], $name);
    $drops = counter.rate('drops', $row['ReceivedDiscardedPackets'] + $row['OutboundDiscardedPackets'], $name);
    if ($in == '' || $out == '' || $packets == '' || $errors == '' || $drops == '') {
        continue;
    }
    value('traffic-in', number.round($in), $name);
    value('traffic-out', number.round($out), $name);
    value('traffic', number.round($in + $out), $name);
    value('packets', number.round($packets, 2), $name);
    value('errors', number.round($errors, 3), $name);
    value('drops', number.round($drops, 3), $name);
    if ($packets + $drops > 0) {
        value('drop-percent', number.round(100 * $drops / ($packets + $drops), 2), $name);
    }
    if ($speed > 0) {
        // The negotiated speed is in Mbit/s: one percent of it is 10,000 bit/s.
        value('in-percent', number.round($in / ($speed * 10000), 1), $name);
        value('out-percent', number.round($out / ($speed * 10000), 1), $name);
    }

    // The errors: per minute for the warning, as a share of the traffic for the critical.
    if ($errors_per_minute != '' && $errors * 60 > $errors_per_minute) {
        watch.raise('errors', 'warning', $name . ': ' . number.round($errors * 60, 1) . ' errors per minute', $name);
    }
    if ($errors_share != '' && $packets + $errors > 0) {
        $share = 100 * $errors / ($packets + $errors);
        if ($share > $errors_share) {
            watch.raise('errors', 'critical', $name . ': ' . number.round($share, 2) . ' % of the traffic in error', $name);
        }
    }
}