#! /usr/bin/env bash # ouilookup - lookup OUI of MAC address prefixes in Wireshark's "manuf" file # Copyright (C) 2024-2025 Erik Auerswald # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . set -eu PROG=ouilookup VERSION='2025-01-06-03' EXIT_CODE=0 DEF_MANUF='/usr/share/wireshark/manuf' print_copyright() { echo 'Copyright (C) 2024-2025 Erik Auerswald' } print_version() { printf -- '%s version %s\n' "${PROG}" "${VERSION}" } print_usage() { printf -- 'Usage: %s { -h | -V | -L }\n' "$PROG" printf -- 'Usage: %s [MAC_ADDR_PR...]\n' "$PROG" } print_license() { cat <. EOL } print_help() { print_version print_copyright echo print_usage cat <&2; print_usage; exit 2;; ':') echo "$PROG: error: argument required for option '-$OPTARG'" 1>&2; print_usage; exit 2;; *) echo "$PROG: error: getopts() failure" 1>&2; exit 2;; esac done shift $((OPTIND - 1)) test -r "$MANUF" || { echo "$PROG: error: cannot read manufacturer file" 1>&2 exit 2 } test "$((2**48 - 1))" -eq "$(printf -- '%d\n' '0xffffffffffff')" || { echo "$PROG: error: insufficient integer arithmetic support in shell" 1>&2 exit 2 } # keep only hexadecimal digits from a string keep_hex() { test "$#" -eq 1 || { printf -- '%s: %s(): error: wrong number of arguments ($# instead of %d)\n'\ "$PROG" 'keep_hex' '1' 1>&2 exit 2 } local M_OUI M_OUI="$1" printf -- '%s\n' "$M_OUI" | tr -dc '0-9a-fA-F\n' } # right pad MAC address prefix with zeros to 6 bytes zero_fill_mac_prefix() { test "$#" -eq 1 || { printf -- '%s: %s(): error: wrong number of arguments ($# instead of %d)\n'\ "$PROG" 'zero_fill_mac_prefix' '1' 1>&2 exit 2 } local MAC_PR MAC_PR="$1" MAC_PR_LEN="${#MAC_PR}" MAC_PR_HEX="0x${MAC_PR}" printf -- '0x%x\n' "$(( MAC_PR_HEX << (48 - MAC_PR_LEN * 4) ))" } # check if a MAC address prefix matches a OUI that is longer than 3 bytes # the MAC address prefix must comprise only hexadecimal digits # the OUI may contain separators, and must end with a prefix length in bits # separated with a slash ('/') # returns 0 for match, 1 for no match, 2 for errors prefix_match() { test "$#" -eq 2 || { printf -- '%s: %s(): error: wrong number of arguments ($# instead of %d)\n'\ "$PROG" 'prefix_match' '2' 1>&2 exit 2 } local MAC_PR_BY M_OUI PR_LEN M_OUI_BY BIT_MASK ALL_ONES ZERO_BITS MAC_PR_BY="$1" M_OUI="${2%/*}" PR_LEN="${2#*/}" M_OUI_BY="$(keep_hex "$M_OUI")" { test "${#MAC_PR_BY}" -ge 6 && test "${#MAC_PR_BY}" -le 12 ; } || { printf -- '%s: %s(): error: MAC address prefix length must be in [24,48]\n'\ "$PROG" 'prefix_match' 1>&2 return 2 } { test "$PR_LEN" -ge 24 && test "$PR_LEN" -le 48 ; } || { printf -- '%s: %s(): error: OUI prefix length must be in [24,48]\n' \ "$PROG" 'prefix_match' 1>&2 return 2 } test "$(( ${#MAC_PR_BY} * 4 ))" -ge "$PR_LEN" || { printf -- '%s: %s(): warning: MAC address prefix too short for OUI %s\n' \ "$PROG" 'prefix_match' "${M_OUI}/${PR_LEN}" 1>&2 return 2 } ALL_ONES=$((2**48 - 1)) ZERO_BITS=$((48 - PR_LEN)) # having the BIT_MASK in hexadecimal notation helps debugging BIT_MASK="$(printf -- '0x%x\n' "$(( (ALL_ONES << ZERO_BITS) & ALL_ONES))")" MAC_PR_BY="$(zero_fill_mac_prefix "$MAC_PR_BY")" M_OUI_BY="$(zero_fill_mac_prefix "$M_OUI_BY")" test "$((MAC_PR_BY & BIT_MASK))" -eq "$((M_OUI_BY & BIT_MASK))" } # look up a single MAC address prefix lookup() { test "$#" -eq 1 || { printf -- '%s: %s(): error: wrong number of arguments ($# instead of %d)\n'\ "$PROG" 'lookup' '1' 1>&2 exit 2 } local MAC_ADDR_PR MAC_ADDR_PR_BY OUI_PAT MAC_ADDR_PR="$1" MAC_ADDR_PR_BY="$(keep_hex "$MAC_ADDR_PR")" OUI_PAT="$(echo "$MAC_ADDR_PR_BY" | sed -En 's/^(..)(..)(..).*$/^\1.\2.\3/p')" test -n "$OUI_PAT" || { printf -- \ '%s: lookup(): error: cannot determine OUI for MAC address prefix "%s"\n'\ "$PROG" "$MAC_ADDR_PR" 1>&2 EXIT_CODE=2 return } printf -- '--- %s ---\n' "$MAC_ADDR_PR" if grep -i -q -- "$OUI_PAT" "$MANUF"; then grep -i -- "$OUI_PAT" "$MANUF" | cut -f 1,3 \ | while read -r M_OUI M_NAME; do case "$M_OUI" in */??) { prefix_match "$MAC_ADDR_PR_BY" "$M_OUI" && printf -- '%s\t%s\n' "$M_OUI" "$M_NAME" ; } || true;; *) printf -- '%s\t%s\n' "$M_OUI" "$M_NAME";; esac done else echo '(OUI not found)' EXIT_CODE=1 fi } if test "$#" -eq 0; then while read -r MAC; do lookup "$MAC" done else for MAC in "$@"; do lookup "$MAC" done fi exit "$EXIT_CODE"