# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Run ifconfig, parse the results, and gather them."""

if __name__ == "__main__":
    import os
    import sys
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))
    del sys
    del os

import string
import socket
import os
import re
import platform
import logging
try:
    import winreg as _winreg
except:
    pass

debug = False

logger = logging.getLogger("gather.gather_interfaces")

# In normal use, the file argument to gather_from_file will be the
# output from /sbin/ifconfig.  During testing, it will be test data.
# Typical input data for gather_from_file:
# eth0      Link encap:Ethernet  HWaddr 00:A0:CC:7C:0D:7A
#           inet addr:10.0.5.25  Bcast:192.168.1.255  Mask:255.255.255.0
#           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
#           RX packets:261957 errors:0 dropped:0 overruns:0 frame:0
#           TX packets:257886 errors:0 dropped:0 overruns:0 carrier:0
#           collisions:0 txqueuelen:1000
#           RX bytes:144004866 (137.3 MiB)  TX bytes:45949289 (43.8 MiB)
#           Interrupt:10 Base address:0x2000
#
# eth0:0    Link encap:Ethernet  HWaddr 00:A0:CC:7C:0D:7A
#           inet addr:10.0.5.23  Bcast:192.168.1.255  Mask:255.255.255.0
#           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
#           Interrupt:10 Base address:0x2000
#
# lo        Link encap:Local Loopback
#           inet addr:127.0.0.1  Mask:255.0.0.0
#           UP LOOPBACK RUNNING  MTU:16436  Metric:1
#           RX packets:804074 errors:0 dropped:0 overruns:0 frame:0
#           TX packets:804074 errors:0 dropped:0 overruns:0 carrier:0
#           collisions:0 txqueuelen:0
#           RX bytes:169172279 (161.3 MiB)  TX bytes:169172279 (161.3 MiB)

# This could have been done with one large regexp instead, but I find it
# difficult to debug my regexps when they are big.  Note that not all lines
# appear for all interfaces.  For example, packet counts are missing for eth0:0
# above.

# Redhad 6, 7 the output of ifconfig has changed.
#
#ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
#       inet 10.0.12.19  netmask 255.255.255.0  broadcast 10.0.12.255
#       inet6 fe80::20c:29ff:fe4b:4622  prefixlen 64  scopeid 0x20<link>
#       ether 00:0c:29:4b:46:22  txqueuelen 1000  (Ethernet)
#       RX packets 1299516208  bytes 237837942120 (221.5 GiB)
#       RX errors 0  dropped 9192  overruns 0  frame 0
#       TX packets 1568011211  bytes 195142720881 (181.7 GiB)
#       TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0


_linux_ifaceline = re.compile(r"^(?P<interface>[^\s]*)\s.*Link\sencap:[^\s]+\s*"
                              r"(HWaddr\s+(?P<mac>[0-9A-F:]+))?")

_match = _linux_ifaceline.search("eth0      Link encap:Ethernet  "
                         "HWaddr 00:A0:CC:7C:0D:7A  ")
assert _match, "_linux_ifaceline is broken"
assert _match.group("mac") == "00:A0:CC:7C:0D:7A", "Can't get mac's"

# Test a regex and return it.
def linecheck(regex, name, testpattern):
    assert regex.search(testpattern), name + " is broken"
    return regex

_linux_ifacelines_regexps = [
    linecheck(re.compile(r"^(?P<interface>[^\s]*)\s.*Link\sencap:[^\s]+\s*"
                              r"(HWaddr\s+(?P<mac>[0-9A-F:]+))?"),
              "eth0",
              "eth0      Link encap:Ethernet  ")    ,
    linecheck(re.compile(r"^(?P<interface>[^\s]*|[^\s]:\d+):\s"),
              "ens33:1",
              "ens33:1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500"),
    linecheck(re.compile(r"^(?P<interface>[^\s]*)\s.*Link encap:Ethernet\s+HWaddr\s+(?P<mac>[\d:a-fA-F]+).+$"),
              "ipline ether ubuntu 16.x ifconfig",
              "ens33     Link encap:Ethernet  HWaddr 00:0c:29:d5:22:3d"),
    ]

# _ipre matches ip addresses.
_ipre = r"\d+\." * 3 + r"\d+"
assert re.match(_ipre,"192.168.1.6"), "_ipre is broken"

_linux_regexps = [
    linecheck(re.compile(r"inet\saddr:\s*(?P<ip>" + _ipre + r")\s+"
                         r"(Bcast:(?P<broadcast>" + _ipre + r")\s+)?"
                         r"Mask:(?P<netmask>" + _ipre + r")"),
              "ipline",
              "         inet addr:192.168.1.6  Bcast:192.168.1.255  "
              "Mask:255.255.255.0"),
    linecheck(re.compile(r"inet\s*(?P<ip>" + _ipre + r")\s+"
                         r"(netmask\s*(?P<netmask>" + _ipre + r")\s+)?"
                         r"broadcast\s*(?P<broadcast>" + _ipre + r")"),
              "ipline",
              "         inet 192.168.1.6  netmask 255.255.255.0   "
              "broadcast 192.168.1.255"),
    linecheck(re.compile(r"inet\s*(?P<ip>" + _ipre + r")\s+"
                         r"(netmask\s*(?P<netmask>" + _ipre + r")\s+)?"
                         r"destination\s*(?P<broadcast>" + _ipre + r")"),
              "ipline tunnel",
              "         inet 192.168.1.6  netmask 255.255.255.0   "
              "destination 192.168.1.255"),
    linecheck(re.compile(r"inet\s*(?P<ip>" + _ipre + r")\s+"
                         r"(netmask\s*(?P<netmask>" + _ipre + r")\s+)?"),
              "ipline lo",
              "         inet 192.168.1.6  netmask 255.255.255.0   "
              "destination 192.168.1.255"),
    linecheck(re.compile(r"^\s+ether\s+(?P<mac>[\d:a-fA-F]+).+$"),
              "ipline ether ifconfig",
              "      ether 8:0:20:b9:46:72  txqueuelen 1000  (Ethernet)"),
    ]

_solaris_ifaceline = re.compile(r"^(?P<interface>[^\s:]*):")
_match = _solaris_ifaceline.search(\
"hme0: flags=1000863<UP,BROADCAST,NOTRAILERS,RUNNING,MULTICAST,IPv6> "
"mtu 1500 index 2")
assert _match, "_solaris_ifaceline is broken"

_solaris_regexps = [
    linecheck(re.compile(r"inet\s+(?P<ip>" + _ipre + r")\s+"
                         r"netmask\s+(?P<netmask_hex>[0-9A-Fa-f]+)\s+"
                         r"(broadcast\s+(?P<broadcast>" + _ipre + r"))?"),
              "inet line of solaris ifconfig",
              "        inet 10.0.5.201 netmask ffffff00 "
              "broadcast 10.255.255.255 "),
    linecheck(re.compile(r"ether\s+(?P<mac>[\d:a-fA-F]+)"),
              "ether line of solaris ifconfig",
              "        ether 8:0:20:b9:46:72")]

_aix_ifaceline = _solaris_ifaceline
_aix_regexps = [
    linecheck(re.compile(r"inet\s+(?P<ip>" + _ipre + r")\s+"
                         r"netmask\s+0x(?P<netmask_hex>[0-9A-Fa-f]+)\s+"
                         r"(broadcast\s+(?P<broadcast>" + _ipre + r"))?"),
              "inet line of aix",
"        inet 10.69.16.40 netmask 0xfffffc00 broadcast 10.69.19.255\n"),
]

_windows_ifaceline = re.compile(r"^Ethernet adapter (?P<interface_long>.*):")
_windows_regexps = [
    linecheck(re.compile(r"Physical Address.*: (?P<mac>[0-9A-Fa-f-]+)"),
              "physaddr",
              "Physical Address. . . . . . . . . : 00-04-23-64-11-43"),
    linecheck(re.compile(r"IP Address.*: (?P<ip>%s)" % _ipre),
              "ipaddr",
              "IP Address. . . . . . . . . . . . : 10.0.5.25"),
    linecheck(re.compile(r"Subnet Mask.*: (?P<netmask>%s)" % _ipre),
              "netmask",
              "Subnet Mask . . . . . . . . . . . : 255.255.255.0"),
    linecheck(re.compile(r"Default Gateway.*: (?P<gateway>%s)" % _ipre),
              "gateway",
              "Default Gateway . . . . . . . . . : 10.0.5.1"),
    linecheck(re.compile(r"DHCP Server.*: (?P<dhcpserver>%s)" % _ipre),
              "dhcpserver",
              "DHCP Server . . . . . . . . . . . : 10.0.5.3"),
    ]

# Convert a mac address into the form we want it to have in the
# database.
# In the database, all hex digits should be upper case, and all colons
# should be gone.
# Before we remove colons, we have to reinsert leading zeros when
# there's only one digit between the colons.  For example,
# "8:0:20:b9:46:72" has to become "080020B94672"
def massage_mac(mac):
    mac = mac.upper()
    while True:
        match = _tweak_mac_regexp.match(mac)
        if not match: break
        before, after = match.group("before", "after")
        mac = before + "0" + after
    mac = mac.replace(":", "")
    mac = mac.replace("-", "")
    return mac

_tweak_mac_regexp = re.compile(r"^(?P<before>.*)\b(?P<after>[0-9A-F]\b.*)$")

# Change a netmask like ff000000 to 255.0.0.0.
def fix_netmask_hex(netmask):
    if len(netmask) < 8:
        # It's a bad netmask that doesn't start with ff, but we
        # shouldn't die on misconfigurations...
        netmask = "0"*(8 - len(netmask)) + netmask
    assert len(netmask) == 8, "Bad netmask " + netmask
    result = ""
    for i in range(0, 8, 2):
        result = result + str(int(netmask[i:i+2], 16)) + "."
    # Now trim the excess "." we put on.
    return result[:-1]

# Map long Windows network names to something shorted.  Algorithm:
# Take the first (up to) two characters of each word, except if the
# word is all digits, then take the whole word (assuming this is some
# kind of sequence number).  Concatenate the resulting parts without
# spaces.  Example: "Local Network Connection 123" becomes
# "LoNeCo123".
def fix_long_interface(name):
    res = []
    for word in name.split():
        if word.isdigit():
            res.append(word)
        else:
            res.append(word[:2])
    return "".join(res)

columns = ["interface", "ip", "mac", "broadcast", "netmask",
           # The gateway and dhcpserver columns are only filled in
           # for Windows.  For Unix machines, these two values are
           # gathered in the route and dhcpserver gathers, respectively.
           "gateway", "dhcpserver",
           # Keep dnshostname last so the 'ouch' hack in the unit test
           # works.
           "dnshostname",]

def addToTable(table, row, isaix):
    # On Solaris, the local interface is spelled "lo0".  On Linux it's
    # just "lo".  But both have IP address 127.0.0.1.
    # If this interface is an ipv6 interface, ignore it.
    #for i in row:
    #    print "### " , i, row[i]
    if row and "127.0.0.1" != row.get("ip") and not row.get("ipv6"):
        # When we get the MAC, it has colons, such as 00:A0:CC:7C:0D:7A.
        # We should store the MAC without colons, such as 00A0CC7C0D7A.
        if row.get("mac"):
            row["mac"] = massage_mac(row["mac"])
        if row.get("ip"):
            try:
                (shorthostname, _, _) = socket.gethostbyaddr(row["ip"])
                row["dnshostname"] = socket.getfqdn(shorthostname)
            except socket.error:
                # No dnshostname if gethostbyaddr or getfqdn failed.
                pass
        if isaix and not row.get("mac"):
            # XXX What a delightful mess this is
            row["mac"] = aix_getmac(row.get("interface"))
        table.add([row.get(column) for column in columns])

def aix_getmac(ifname):
    ether = (6*r"[0-9a-f][0-9a-f]?\.")[:-2]
    regex = re.compile(re.escape(ifname) + r"\s+\S+\s+\S+\s+(" + ether + r")\s")
    pipe = os.popen("/usr/bin/netstat -I " + ifname, "r")
    try:
        for line in pipe:
            m = regex.match(line)
            if m:
                mac = m.group(1)
                bytes = mac.split(".")
                assert len(bytes) == 6, (mac, bytes)
                bytes = [int(b, 16) for b in bytes]
                return "%02x"*6 % tuple(bytes)
    finally:
        pipe.close()

# Assuming output form /sbin/ifconfig is in file, add the appropriate rows to
# table.
def gather_from_file(file, collector, ifaceline, otherlines, isaix=False):
    table = collector.table("interfaces", columns);
    row = {}
    for line in file:
        if debug: print("line = %r" % line)
        match = ifaceline.search(line)
        if match:
            if debug: print("---", match.groups())
            # Add the old row
            addToTable(table, row, isaix)
            # Start a new one
            row = {}
            if ",IPv6" in line:
                # Skip Solaris IPv6 interfaces.
                row["ipv6"] = "yes"
        for otherline in otherlines:
            match = match or otherline.search(line)
            if match:
                for key, value in match.groupdict().items():
                    if value is not None:
                        row[key] = value
                        if debug:
                            print("Updated " + key + " to " + repr(row[key]))
        if "netmask_hex" in row:
            row["netmask"] = fix_netmask_hex(row["netmask_hex"])
        if "interface_long" in row:
            row["interface"] = fix_long_interface(row["interface_long"])
    # Add the last row.
    addToTable(table, row, isaix)

# TODO: function belong only handles the first input definition. Will need to change when we support multi-homed systems


def gather_from_file_linux(file, collector, ifacelines, otherlines, isaix=False):
    table = collector.table("interfaces", columns);
    row = {}
    #debug = True
    for line in file:
        if len(line.strip()) == 0:
            if debug: print("--- len(row): ", len(row))
            addToTable(table, row, isaix)
            row = dict()
            continue

        if debug: print("line = %r" % line)
        #print "--- line = %r" % line
        for iface in ifacelines:
            match = iface.search(line)
            if match:
                if debug: print("--- ", match.groups())
                # Start a new one
                for key, value in match.groupdict().items():
                    if value is not None:
                        row[key] = value
                        if debug:
                            print("--- Updated " + key + " to " + repr(row[key]))

        for otherline in otherlines:
            match = match or otherline.search(line)
            if match:
                # Add the old row
                for key, value in match.groupdict().items():
                    if value is not None:
                        row[key] = value
                        if debug:
                            print("Updated " + key + " to " + repr(row[key]))
    # Add the last row.
    addToTable(table, row, isaix)


def solaris_gather_from_file(file, collector):
    gather_from_file(file, collector, _solaris_ifaceline, _solaris_regexps)

def linux_gather_from_file(file, collector):
    gather_from_file_linux(file, collector, _linux_ifacelines_regexps, _linux_regexps)

def aix_gather_from_file(file, collector):
    gather_from_file(file, collector, _aix_ifaceline, _aix_regexps, isaix=True)

def hpux_gather_from_file(file, collector):
    # The file argument is a pipe connected to the output of "netstat -iw"
    # e.g.:
    # Name              Mtu        Network         Address         Ipkts      Ierrs      Opkts      Oerrs      Coll
    # lan0              1500       10.69.16.0      hpux2.esi.sigmasrc.com 219917     0          165326     0          0
    # lo0               4136       loopback        localhost       46756      0          46756      0          0
    f = os.popen("/etc/lanscan -ia")
    # This output looks like this:
    # 0x0011855F189A lan0 snap0
    # 0x0011855F189B lan1 snap1
    map = {}
    for line in f:
        words = line.split()
        if len(words) == 3:
            mac, name = words[:2]
            if mac.startswith("0x"):
                mac = mac[2:]
            map[name] = mac
    table = collector.table("interfaces", columns)
    heading = file.readline()
    assert heading.startswith("Name ")
    for line in file:
        if line.startswith(" "):
            continue
        fields = line.split()
        if len(fields) < 4:
            continue
        name, mtu, network, address = fields[:4]
        if network == "loopback":
            continue
        mac = map.get(name)
        ip, netmask, broadcast = hpux_ifconfig(name)
        dnshostname = None
        if not re.match(r"\A\d+\.\d+\.\d+\.\d+\Z", address):
            dnshostname = address
        table.add([name, ip, mac, broadcast, netmask, None, None, dnshostname])

def hpux_ifconfig(name):
    pipe = os.popen("/etc/ifconfig %s" % name)
    # The output looks like this; we want the second line:
    # lan0: flags=1843<UP,BROADCAST,RUNNING,MULTICAST,CKO>
    #         inet 10.69.18.24 netmask fffffc00 broadcast 10.69.19.255
    try:
        for line in pipe:
            m = re.match(r"\s+inet (\d+\.\d+\.\d+\.\d+)"
                         r" netmask ([0-9a-f]+)"
                         r" broadcast (\d+\.\d+\.\d+\.\d+)",
                         line)
            if m:
                ip, netmask, broadcast = m.group(1, 2, 3)
                return ip, fix_netmask_hex(netmask), broadcast
    finally:
        pipe.close()
    return None, None, None # If unsuccessful

def osx_gather_from_file(file, collector):
    # Look for three types of lines:
    # en0: flags=...
    #      inet 1.2.3.4 netmask 0xffffff00 broadcast 1.2.3.255
    #      ether 00:11:22:33:aa:bb
    table = collector.table("interfaces", columns)
    name = inet = netmask = broadcast = ether = None
    for line in file:
        m = re.match(r"^(\w+): flags=", line)
        if m:
            name = m.group(1)
            inet = netmask = broadcast = ether = None
            continue
        m = re.match(r"\s+inet (\S+) netmask (?:0x)?(\S+) broadcast (\S+)",
                     line)
        if m:
            inet, netmask, broadcast = m.group(1, 2, 3)
            continue
        m = re.match(r"\s+ether (\S+)", line)
        if m:
            ether = m.group(1).replace(":", "")
        if name and inet and netmask and broadcast and ether:
            if not inet.startswith("127."):
                netmask = fix_netmask_hex(netmask)
                try:
                    shorthostname = socket.gethostname()
                    dnshostname = socket.getfqdn()
                except socket.error:
                    # No dnshostname if gethostbyaddr or getfqdn failed.
                    dnshostname = None
                table.add([name, inet, ether, broadcast, netmask, None, None, dnshostname])
                name = None
                inet = None
                netmask = None
                broadcast = None
                ether = None
                # TODO remove the return to support multi-homed support
                return

def get_interface_keys():
    valList = []
    try:
        x = _winreg.ConnectRegistry(None,  _winreg.HKEY_LOCAL_MACHINE)
        try:
            root = _winreg.OpenKey(x, "System\\CurrentControlSet\\Control\\Network")
            try:
                try:
                    nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(root)
                    for i in range(nsubkeys):
                        try:
                            subkeyname = _winreg.EnumKey(root, i)
                        except OSError as err:
                            continue
                        fullkey = "HKLM\\System\\CurrentControlSet\\Control\\Network\\%s" % subkeyname
                        valList.append(fullkey)
                finally:
                    _winreg.CloseKey(root)
            except:
                pass
        finally:
            _winreg.CloseKey(x)
    except:
        pass
    return valList

def get_conn_name(key, name):
    i = key.find("\\")
    if i < 0:
        return None
    key = key[i+1:]
    try:
        x = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
        try:
            y = _winreg.OpenKey(x, key)
            try:
                val = _winreg.QueryValueEx(y, name)
                if val != None:
                    return val[0]
                else:
                    return None
            finally:
                _winreg.CloseKey(y)
        finally:
            _winreg.CloseKey(x)
    except OSError:
        return None

def windows_gather_from_file(collector):
    from win32com.client import GetObject
    table = collector.table("interfaces", columns)
    try:
        objItems_na = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_NetworkAdapter")
        objItems = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_NetworkAdapterConfiguration")
        keys = get_interface_keys()
        for item in objItems:
            ip = mask = gateway = dns = ""
            enabled = item.Properties_("IPEnabled").Value
            if not enabled:
                enabled = item.Properties_("IPXEnabled").Value
            if not enabled:
                continue
            # We are now returning the network connection name as it appears
            # in network connections panel for the interface column instead of the
            # GUID of the interface because GUIDs are not user friendly.
            #interface = item.Properties_("SettingID").Value
            #if not interface or interface == "":
            interface = ""
            caption = item.Properties_("Caption").Value
            try:
                for item_na in objItems_na:
                    if item_na.Properties_("Caption").Value == caption:
                        interface = item_na.Properties_("NetConnectionID").Value
                        break
            except:
                # on 2000 NetConnectionID is not available
                pass

            # if interface name could not be found such as on windows 2000
            # check the registry
            if interface == "" or interface == None:
                interface = item.Properties_("SettingID").Value
                for akey in keys:
                    connkey = "%s\\%s\\connection" % (akey, interface)
                    name = get_conn_name(connkey, "Name")
                    if name != None:
                        interface = name
                        break

            # still did not get the interface, make it up based on card index
            if interface == "" or interface == None:
                a = caption.find("[")
                b = caption.find("]")
                if a >= 0 and b >= 0:
                    interface = "interface%s" % caption[a+1:b]
                else:
                    continue
            iplist = item.Properties_("IPAddress").Value
            if iplist:
                ip = iplist[0]
            if ip == "0.0.0.0" or ip == "":
                continue
            mac = item.Properties_("MACAddress").Value
            mac = mac.replace(':', '')
            masklist = item.Properties_("IPSubnet").Value
            if masklist:
                mask = masklist[0]
            gatewaylist = item.Properties_("DefaultIPGateway").Value
            if gatewaylist:
                gateway = gatewaylist[0]
            dhcp = item.Properties_("DHCPServer").Value
            hostname = item.Properties_("DNSHostName").Value
            if not hostname:
                hostname = ""
            domainname = item.Properties_("DNSDomain").Value
            if not domainname:
                domainname = ""
            dns = hostname + "." + domainname
            table.add([interface, ip, mac, "", mask, gateway, dhcp, dns])
    except:
        logger.exception("interfaces gather failed")

# --- $AVDATA/interfaces.txt generation ---------------------------------------
# interfaces.txt is normally produced by the running poliwall process and read
# by the Fuel runtime (host.getinterfaces / getinterfaceinfo).  When poliwall
# enforcement is disabled the poliwall process isn't running, so the file never
# appears and interface-aware rules get an empty set (and the runtime logs
# "Can't open .../interfaces.txt").  Since this gather already enumerates every
# interface from the OS, we emit the file ourselves as a fallback -- but only
# while poliwall is NOT running, so we never clobber poliwall's authoritative
# file (which carries the real 'W'/'L' wireless/wired classification we can't
# reliably determine here).

def _netmask_to_prefix(netmask):
    """Dotted-quad netmask -> CIDR prefix length, or None if unparseable."""
    if not netmask:
        return None
    try:
        octets = [int(x) for x in str(netmask).split(".")]
    except ValueError:
        return None
    if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
        return None
    return sum(bin(o).count("1") for o in octets)

def _poliwall_running(options):
    """True if the poliwall process is up (it then owns interfaces.txt)."""
    try:
        f = open(options.datapath("pwpid.txt"))
        try:
            pid = int(f.readline().strip())
        finally:
            f.close()
    except (EnvironmentError, ValueError):
        return False  # no / garbage pidfile -> poliwall isn't running
    try:
        from sigma.sched import process
        return bool(process.Process(pid).isalive())
    except Exception:
        return True   # can't tell -> assume running, don't clobber its file

def write_interfaces_file(rows):
    """Write $AVDATA/interfaces.txt from gathered interface rows so interface-
    aware Fuel rules work without a running poliwall.  Each row is
    (ip, broadcast, netmask, name).  No-op while poliwall is running.  Best
    effort -- logs but never raises."""
    from sigma.util import config
    options = config.Options()
    if _poliwall_running(options):
        return
    fn = options.datapath("interfaces.txt")
    lines = []
    for ip, broadcast, netmask, name in rows:
        # The name is the last whitespace-delimited field and both readers
        # (rtlib's 4-token regex and gathering.get_active_interfaces' words[-1])
        # assume it has no spaces -- true for poliwall's GUID names but not for
        # Windows friendly names like "Ethernet 2".  Collapse to one token.
        name = "_".join(str(name).split()) if name else ""
        if not ip or not name:
            continue
        prefix = _netmask_to_prefix(netmask)
        addr = broadcast or ip
        network = "%s/%d" % (addr, prefix) if prefix is not None else addr
        # iftype 'L' (land-line): the W/L distinction is poliwall's; when
        # poliwall runs it rewrites this file with the real classification.
        lines.append("%s L %s %s" % (ip, network, name))
    try:
        tmp = fn + ".tmp"
        f = open(tmp, "w")
        try:
            f.write("".join(line + "\n" for line in lines))
        finally:
            f.close()
        os.replace(tmp, fn)
        logger.info("Wrote %d interface(s) to %s", len(lines), fn)
    except EnvironmentError as err:
        logger.warning("Can't write %s: %s", fn, err)

class _IfaceRecorder(object):
    """Gather-Table proxy that also captures rows for interfaces.txt."""
    def __init__(self, table, columns, sink):
        self._table = table
        self._idx = dict((c, i) for i, c in enumerate(columns))
        self._sink = sink
    def add(self, record):
        self._table.add(record)
        try:
            def g(col):
                i = self._idx.get(col)
                return record[i] if i is not None and i < len(record) else None
            self._sink.append((g("ip"), g("broadcast"), g("netmask"),
                               g("interface")))
        except Exception:
            pass  # capture is best-effort; never disturb the gather

class _RecordingCollector(object):
    """Wraps the gather Collector so the interfaces table also feeds the file."""
    def __init__(self, real, sink):
        self._real = real
        self._sink = sink
    def table(self, name, columns):
        return _IfaceRecorder(self._real.table(name, columns), columns, self._sink)

def Gather(collector):
    # Capture interface rows as they're gathered (see write_interfaces_file).
    iface_rows = []
    rec = _RecordingCollector(collector, iface_rows)
    if os.name == "nt":
        # Windows
        #file = os.popen("ipconfig /all")
        windows_gather_from_file(rec)
    else:
        basicos = platform.uname()[0]
        if basicos == "AIX":
            file = os.popen("/etc/ifconfig -a")
        elif basicos == "Linux" and os.path.exists("/usr/sbin/ifconfig"):
            # RedHat 7 change the pathing
            file = os.popen("/usr/sbin/ifconfig -a")
        elif os.path.exists("/sbin/ifconfig"):
            # Solaris insists on an argument to /sbin/ifconfig; "-a" works for
            # both Linux and Solaris.  For Linux, -a is the default argument.
            file = os.popen("/sbin/ifconfig -a")
        elif os.path.exists("/usr/bin/netstat"): # HP-UX
            file = os.popen("/usr/bin/netstat -iw")
        else:
            raise RuntimeError("can't gather interfaces on this platform")
        try:
            if basicos == "SunOS":
                solaris_gather_from_file(file, rec)
            elif basicos == "Linux":
                linux_gather_from_file(file, rec)
            elif basicos == "HP-UX":
                hpux_gather_from_file(file, rec)
            elif basicos == "Darwin":
                file = os.popen('/sbin/ifconfig -a')
                osx_gather_from_file(file, rec)
            elif basicos == "AIX":
                aix_gather_from_file(file, rec)
            else:
                raise Exception("Can't gather interfaces on %s" % basicos)
        finally:
            file.close()
    # Best-effort: also emit interfaces.txt for the Fuel runtime.
    try:
        write_interfaces_file(iface_rows)
    except Exception:
        logger.exception("Couldn't write interfaces.txt")

def _test():
    from sigma.gather import gathering
    gathering.test(Gather)

if __name__ == "__main__":
    _test()
