Skip to content

Instantly share code, notes, and snippets.

@taikedz
Last active January 26, 2026 11:05
Show Gist options
  • Select an option

  • Save taikedz/70cac60a1e418e9ee7998b44a9e5e857 to your computer and use it in GitHub Desktop.

Select an option

Save taikedz/70cac60a1e418e9ee7998b44a9e5e857 to your computer and use it in GitHub Desktop.
Find out NIC details

Read 'ip a' and show NIC details

Utility to read the output of ip a and print most pertinent details

Allows filtering on inet/inet6 scope values

By default, prints summarised information from the NIC entries

$> python3 read_ipa/read-ipa.py 
lo (loopback, UNKNOWN) : 00:00:00:00:00:00
  127.0.0.1/8 -> host lo
  ::1/128 -> host noprefixroute
enp1s0 (ether, UP) : 52:54:00:40:5b:e3
  10.113.211.83/24 -> global dynamic noprefixroute enp1s0
  fe80::5054:ff:fe40:5be3/64 -> link
docker0 (ether, DOWN) : 46:a9:35:5b:82:d1
  172.17.0.1/16 -> global docker0
  fe80::44a9:35ff:fe5b:82d1/64 -> link

Can also print one-line information for CLI parsing, for example

$> python3 read-ipa.py -S    # all info in oneline form
127.0.0.1/8 ::1/128 NAME=lo MAC=00:00:00:00:00:00 TYPE=loopback STATE=UNKNOWN SCOPES4='host lo' SCOPES6='host noprefixroute'
10.113.211.83/24 fe80::5054:ff:fe40:5be3/64 NAME=enp1s0 MAC=52:54:00:40:5b:e3 TYPE=ether STATE=UP SCOPES4='global dynamic noprefixroute enp1s0' SCOPES6='link'
172.17.0.1/16 fe80::44a9:35ff:fe5b:82d1/64 NAME=docker0 MAC=46:a9:35:5b:82:d1 TYPE=ether STATE=DOWN SCOPES4='global docker0' SCOPES6='link'

Find outward-facing IPs

To find LAN IPs of the box, run the script like so:

python3 read-ipa.py -M

This queries the IP routing via ip r and matches against detail NIC entries. All other options for output control remain available.

#!/usr/bin/env python3
import os
import re
import pathlib
import argparse
from subprocess import Popen, PIPE
THIS = pathlib.Path(os.path.realpath(__file__))
HEREDIR = pathlib.Path(os.path.dirname(THIS))
class ParseError(Exception):
pass
class NIC:
def __init__(self, name, state):
self.name = name
self.state = state
self.type = None
self.mac = None
self.ip4ip = None
self.ip4scopes = None
self.ip6ip = None
self.ip6scopes = None
def __str__(self):
detail = [f"{self.name} ({self.type}, {self.state}) : {self.mac}"]
if self.ip4ip:
detail.append(f" {self.ip4ip} -> {self.ip4scopes}")
if self.ip6ip:
detail.append(f" {self.ip6ip} -> {self.ip6scopes}")
return '\n'.join(detail)
def cli_args():
parser = argparse.ArgumentParser()
parser.add_argument("scopes", help="Scope filters - only display NICs with all specified scopes", nargs="*")
parser.add_argument("--ip4", "-4", help="Do not print full output, print IPv4 address", action="store_true")
parser.add_argument("--ip6", "-6", help="Do not print full output, print IPv6 address", action="store_true")
parser.add_argument("--name", "-n", help="Do not print full output, print NIC name", action="store_true")
parser.add_argument("--state", "-s", help="Do not print full output, print NIC state", action="store_true")
parser.add_argument("--mac", "-m", help="Do not print full output, print NIC MAC address", action="store_true")
parser.add_argument("--type", "-t", help="Do not print full output, print NIC type", action="store_true")
parser.add_argument("--scope4", "-c", help="Do not print full output, print IPv4 scopes", action="store_true")
parser.add_argument("--scope6", "-C", help="Do not print full output, print IPv6 scopes", action="store_true")
parser.add_argument("--force-ip-print", "-F", help="When printing IPs selectively, forcibly print a string where IP should be", action="store_true")
parser.add_argument("--my-ip", "-M", help="Display my LAN-reachable IP addresses", action="store_true")
parser.add_argument("--short-all", "-S", help="Print details in single-line", action="store_true")
args = parser.parse_args()
non_flattening_options = ["scopes", "force_ip_print", "my_ip"]
if args.short_all:
[setattr(args, prop, True) for prop in dir(args) if not prop.startswith("_") and not prop in ["scopes", "my_ip"] ]
booltypes = [getattr(args, prop) for prop in dir(args) if not prop.startswith("_") and not prop in non_flattening_options]
args.defaults = not any(booltypes)
return args
def parse_ipa(data):
lines = data.split("\n")
nics = []
current_nic = None
for L in lines:
if m := re.match(r'\d+: ([a-z0-9@]+):.*?state ([A-Z]+)', L):
if current_nic:
nics.append(current_nic)
current_nic = NIC(m.group(1), m.group(2) )
elif current_nic is None:
raise ParseError(f"Got a block inner line before a block was declared - line: {repr(L)}")
elif m := re.match(r"link/([a-z0-9]+)\s+([a-f0-9:]+)", L.strip()):
current_nic.type = m.group(1)
current_nic.mac = m.group(2)
elif m := re.match(r"inet\s+([0-9./]+).+?scope (.+)", L.strip()):
current_nic.ip4ip = m.group(1)
current_nic.ip4scopes = m.group(2)
elif m := re.match(r"inet6\s+([0-9a-f:/]+).+?scope (.+)", L.strip()):
current_nic.ip6ip = m.group(1)
current_nic.ip6scopes = m.group(2)
if current_nic:
nics.append(current_nic)
return nics
def find_default_route_devicenames():
proc = Popen(["ip", "r"], stdout=PIPE)
stdout, _ = proc.communicate()
defaults = []
for line in str(stdout, 'utf-8').split("\n"):
if "default via" in line:
m = re.match(".+?dev ([a-zA-Z0-9@]+)", line)
defaults.append(m.group(1))
return defaults
def main():
args = cli_args()
proc = Popen(["ip", "a"], stdout=PIPE)
stdout, _ = proc.communicate()
nics = parse_ipa(str(stdout, 'utf-8'))
if args.scopes:
nics = filter_scopes(nics, args.scopes)
if args.my_ip:
names = find_default_route_devicenames()
if not names:
print("No default route detected")
exit(1)
else:
nics = [nic for nic in nics if nic.name in names]
if args.defaults:
[print(nic) for nic in nics]
else:
for nic in nics:
details = []
if args.force_ip_print:
nic.ip4ip = _or('NO_IP4', nic.ip4ip)
nic.ip6ip = _or('NO_IP6', nic.ip6ip)
if args.ip4:
if nic.ip4ip:
details.append(nic.ip4ip)
if args.ip6:
if nic.ip6ip:
details.append(nic.ip6ip)
if args.name:
details.append("NAME="+nic.name)
if args.mac:
details.append("MAC="+nic.mac)
if args.type:
details.append("TYPE="+nic.type)
if args.state:
details.append("STATE="+nic.state)
if args.scope4:
details.append(f"SCOPES4={repr(nic.ip4scopes)}")
if args.scope6:
details.append(f"SCOPES6={repr(nic.ip6scopes)}")
print(' '.join(details) )
def _or(val, check):
return check if check else val
def filter_scopes(nics, scopes):
remain = []
for nic in nics:
retain = True
nic_scopes = _or('',nic.ip4scopes).split() + _or('',nic.ip6scopes).split()
for scope in scopes:
if scope not in nic_scopes:
retain = False
break
if retain:
remain.append(nic)
return remain
# Generic launch and catch assertions
if __name__ == "__main__":
try:
main()
except Exception as e:
# Catches all exceptions, but not KeyboardInterrupt
# Normally silence tracebacks. Run with PY_TRACEBACK=true to show them
if os.getenv("PY_TRACEBACK") == "true":
raise
else:
print(e)
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment