301 lines
9.2 KiB
Bash
Executable File
301 lines
9.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# ICMP/TCP 扫描本网段或指定网段的在线主机,并显示主机名、MAC、厂商与开放端口
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck disable=SC1091
|
||
source "$SCRIPT_DIR/common/help.sh"
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法: scan-ip [端口或网段参数...]
|
||
|
||
扫描局域网主机:
|
||
无参数 ICMP 扫描本网段 .1-.254,并尽力显示名称、MAC 和厂商
|
||
10.0.0 扫描指定网段前缀
|
||
22 扫描本网段 TCP 22 端口
|
||
80,443,8080 扫描多个端口(任一开放即显示)
|
||
22 10.0.0 指定网段 + 端口(顺序不限)
|
||
|
||
选项:
|
||
-h, --help 显示此帮助
|
||
|
||
依赖: ping;端口模式需要 nc (netcat)。
|
||
名称: macOS 使用系统解析器;Linux 使用 NSS(getent),安装 avahi-utils 时也会查询 mDNS;均会回退反向 DNS。
|
||
MAC: 从同一二层网络的 ARP/邻居表读取;nmap 的 OUI 数据库存在时会离线显示厂商。
|
||
|
||
提示: IP 和 hostname 都可能变化或根本不公开;MAC + 厂商通常更适合辨认设备。
|
||
要永久、准确地对应“这台设备是谁”,请在路由器 DHCP 客户端列表中为其设置备注和 DHCP 保留地址。
|
||
EOF
|
||
}
|
||
|
||
dotfile_help_requested "${1:-}" && dotfile_show_help
|
||
|
||
TIMEOUT=1
|
||
|
||
find_mac_prefix_file() {
|
||
local nmap_bin prefix candidate
|
||
local candidates=(
|
||
/usr/local/share/nmap/nmap-mac-prefixes
|
||
/opt/homebrew/share/nmap/nmap-mac-prefixes
|
||
/usr/share/nmap/nmap-mac-prefixes
|
||
/usr/local/share/nmap-mac-prefixes
|
||
)
|
||
|
||
if nmap_bin=$(command -v nmap 2>/dev/null); then
|
||
prefix=$(cd "$(dirname "$nmap_bin")/.." 2>/dev/null && pwd)
|
||
candidates+=("$prefix/share/nmap/nmap-mac-prefixes")
|
||
fi
|
||
|
||
for candidate in "${candidates[@]}"; do
|
||
if [ -r "$candidate" ]; then
|
||
printf '%s' "$candidate"
|
||
return
|
||
fi
|
||
done
|
||
}
|
||
|
||
MAC_PREFIX_FILE=$(find_mac_prefix_file)
|
||
|
||
is_port_spec() {
|
||
[[ "$1" =~ ^[0-9]+$ || "$1" =~ ^[0-9]+-[0-9]+$ || "$1" =~ ^[0-9,]+$ ]]
|
||
}
|
||
|
||
is_ip_prefix() {
|
||
[[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){1,3}$ ]]
|
||
}
|
||
|
||
PORT_SPEC=""
|
||
PREFIX_ARG=""
|
||
|
||
if [ -n "$1" ] && is_ip_prefix "$1"; then
|
||
PREFIX_ARG="$1"
|
||
[ -n "$2" ] && is_port_spec "$2" && PORT_SPEC="$2"
|
||
elif [ -n "$1" ]; then
|
||
is_port_spec "$1" && PORT_SPEC="$1"
|
||
[ -n "$2" ] && is_ip_prefix "$2" && PREFIX_ARG="$2"
|
||
fi
|
||
|
||
if [ -n "$1" ] && [ -z "$PORT_SPEC" ] && [ -z "$PREFIX_ARG" ]; then
|
||
echo "错误: 无法识别参数: $1"
|
||
usage
|
||
exit 1
|
||
fi
|
||
|
||
if command -v ip &>/dev/null; then
|
||
LOCAL_IP=$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d/ -f1 | head -n 1)
|
||
elif command -v ifconfig &>/dev/null; then
|
||
LOCAL_IP=$(ifconfig | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -n 1)
|
||
else
|
||
echo "错误: 找不到 ip 或 ifconfig 命令" && exit 1
|
||
fi
|
||
|
||
[ -z "$LOCAL_IP" ] && echo "错误: 未能检测到本地 IP" && exit 1
|
||
|
||
if [ -n "$PREFIX_ARG" ]; then
|
||
PREFIX="$PREFIX_ARG"
|
||
else
|
||
PREFIX="${LOCAL_IP%.*}"
|
||
fi
|
||
|
||
PORTS=()
|
||
if [ -n "$PORT_SPEC" ]; then
|
||
if [[ "$PORT_SPEC" =~ ^[0-9]+$ ]]; then
|
||
PORTS=("$PORT_SPEC")
|
||
elif [[ "$PORT_SPEC" =~ ^[0-9]+-[0-9]+$ ]]; then
|
||
START_PORT=${PORT_SPEC%-*}
|
||
END_PORT=${PORT_SPEC#*-}
|
||
if [ "$START_PORT" -gt "$END_PORT" ]; then
|
||
echo "错误: 起始端口 ($START_PORT) 不能大于结束端口 ($END_PORT)" && exit 1
|
||
fi
|
||
for p in $(seq "$START_PORT" "$END_PORT"); do PORTS+=("$p"); done
|
||
elif [[ "$PORT_SPEC" =~ ^[0-9,]+$ ]]; then
|
||
IFS=',' read -ra PORTS <<<"$PORT_SPEC"
|
||
else
|
||
echo "错误: 无效的端口格式: $PORT_SPEC"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
echo "本机 IP: $LOCAL_IP,扫描网段: $PREFIX.1-254"
|
||
if [ ${#PORTS[@]} -gt 0 ]; then
|
||
echo "扫描端口: ${PORTS[*]} (TCP,超时 ${TIMEOUT}s)"
|
||
else
|
||
echo "扫描模式: ICMP (在线检测)"
|
||
fi
|
||
echo "------------------------------------------------------"
|
||
|
||
hostname_for_ip() {
|
||
local ip="$1"
|
||
local hostname=""
|
||
|
||
# macOS 的系统解析器包含本机 hosts、DNS 和可用的 Bonjour/mDNS 记录。
|
||
if [ "$(uname -s)" = "Darwin" ] && command -v dscacheutil &>/dev/null; then
|
||
hostname=$(dscacheutil -q host -a ip_address "$ip" 2>/dev/null \
|
||
| awk -F': ' '/^name: / {print $2; exit}')
|
||
fi
|
||
|
||
# Linux 的 NSS 配置可包含 /etc/hosts、DNS 与 mDNS(例如 libnss-mdns)。
|
||
if [ -z "$hostname" ] && command -v getent &>/dev/null; then
|
||
hostname=$(getent hosts "$ip" 2>/dev/null | awk 'NR == 1 {print $2}')
|
||
fi
|
||
|
||
# 未配置 NSS mDNS 时,avahi-utils 仍可直接查询局域网的 .local 名称。
|
||
if [ -z "$hostname" ] && command -v avahi-resolve-address &>/dev/null; then
|
||
if command -v timeout &>/dev/null; then
|
||
hostname=$(timeout 1 avahi-resolve-address -4 "$ip" 2>/dev/null \
|
||
| awk -F '\t' 'NR == 1 {print $2}')
|
||
else
|
||
hostname=$(avahi-resolve-address -4 "$ip" 2>/dev/null \
|
||
| awk -F '\t' 'NR == 1 {print $2}')
|
||
fi
|
||
fi
|
||
|
||
# 没有本地记录时再查询 PTR;限制超时,避免名称解析拖慢扫描。
|
||
if [ -z "$hostname" ] && command -v dig &>/dev/null; then
|
||
# macOS 的 dig 会在 DNS 超时时将 ";; connection timed out" 输出到 stdout。
|
||
# PTR 记录只接受由主机名字符组成的一行,避免将诊断信息显示为主机名。
|
||
hostname=$(dig +time=1 +tries=1 +short -x "$ip" 2>/dev/null \
|
||
| awk '/^[[:alnum:]_][[:alnum:]_.-]*\.?$/ {print; exit}')
|
||
fi
|
||
|
||
# PTR 记录通常以点结尾,显示时去掉以保持紧凑。
|
||
printf '%s' "${hostname%.}"
|
||
}
|
||
|
||
normalize_mac() {
|
||
local mac="$1"
|
||
local octet
|
||
local octets=()
|
||
local normalized=()
|
||
|
||
IFS=':' read -r -a octets <<<"$mac"
|
||
[ "${#octets[@]}" -eq 6 ] || {
|
||
printf '%s' "$mac"
|
||
return
|
||
}
|
||
|
||
for octet in "${octets[@]}"; do
|
||
[[ "$octet" =~ ^[[:xdigit:]]{1,2}$ ]] || {
|
||
printf '%s' "$mac"
|
||
return
|
||
}
|
||
printf -v octet '%02x' "0x$octet"
|
||
normalized+=("$octet")
|
||
done
|
||
(IFS=':'; printf '%s' "${normalized[*]}")
|
||
}
|
||
|
||
mac_for_ip() {
|
||
local ip="$1"
|
||
local mac=""
|
||
|
||
# 一次 ICMP/TCP 探测后,位于同一二层网络的设备一般会出现在 ARP/邻居表中。
|
||
if [ "$(uname -s)" = "Darwin" ] && command -v arp &>/dev/null; then
|
||
mac=$(arp -n "$ip" 2>/dev/null \
|
||
| awk '/ at ([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}/ {for (i = 1; i <= NF; i++) if ($i == "at") {print $(i + 1); exit}}')
|
||
elif command -v ip &>/dev/null; then
|
||
mac=$(ip neigh show to "$ip" 2>/dev/null \
|
||
| awk '/lladdr/ {for (i = 1; i <= NF; i++) if ($i == "lladdr") {print $(i + 1); exit}}')
|
||
elif command -v arp &>/dev/null; then
|
||
mac=$(arp -n "$ip" 2>/dev/null \
|
||
| awk '/ at ([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}/ {for (i = 1; i <= NF; i++) if ($i == "at") {print $(i + 1); exit}}')
|
||
fi
|
||
|
||
normalize_mac "$mac"
|
||
}
|
||
|
||
mac_vendor_for_mac() {
|
||
local mac="$1"
|
||
local first_octet prefix vendor
|
||
|
||
[ -z "$mac" ] && return
|
||
|
||
first_octet=${mac%%:*}
|
||
# 本地管理位为 1 的 MAC 常见于 iOS/Android/Windows 的 Wi-Fi 随机地址,不能据此推断厂商。
|
||
if (( (16#$first_octet) & 2 )); then
|
||
printf '%s' '随机/本地管理 MAC'
|
||
return
|
||
fi
|
||
|
||
[ -z "$MAC_PREFIX_FILE" ] && return
|
||
prefix=${mac//:/}
|
||
prefix=${prefix:0:6}
|
||
prefix=$(printf '%s' "$prefix" | tr '[:lower:]' '[:upper:]')
|
||
vendor=$(awk -v prefix="$prefix" '$1 == prefix {$1 = ""; sub(/^ /, ""); print; exit}' "$MAC_PREFIX_FILE")
|
||
printf '%s' "$vendor"
|
||
}
|
||
|
||
format_host() {
|
||
local ip="$1"
|
||
local hostname mac vendor
|
||
hostname=$(hostname_for_ip "$ip")
|
||
mac=$(mac_for_ip "$ip")
|
||
vendor=$(mac_vendor_for_mac "$mac")
|
||
|
||
printf '%s' "$ip"
|
||
if [ -n "$hostname" ]; then
|
||
printf ' name=%s' "$hostname"
|
||
fi
|
||
if [ -n "$mac" ]; then
|
||
printf ' mac=%s' "$mac"
|
||
fi
|
||
if [ -n "$vendor" ]; then
|
||
printf ' vendor=%s' "$vendor"
|
||
fi
|
||
}
|
||
|
||
if [ ${#PORTS[@]} -eq 0 ]; then
|
||
if [ "$(uname -s)" = "Darwin" ]; then
|
||
for i in $(seq 1 254); do
|
||
(
|
||
ip="$PREFIX.$i"
|
||
ping -c 1 -W 1000 -S "$LOCAL_IP" "$ip" >/dev/null 2>&1 \
|
||
&& echo "$(format_host "$ip") 在线"
|
||
) &
|
||
done
|
||
else
|
||
for i in $(seq 1 254); do
|
||
(
|
||
ip="$PREFIX.$i"
|
||
ping -c 1 -W 1 -I "$LOCAL_IP" "$ip" >/dev/null 2>&1 \
|
||
&& echo "$(format_host "$ip") 在线"
|
||
) &
|
||
done
|
||
fi
|
||
wait
|
||
echo "------------------------------------------------------"
|
||
echo "扫描完成"
|
||
exit 0
|
||
fi
|
||
|
||
if ! command -v nc &>/dev/null; then
|
||
echo "错误: 找不到 nc (netcat)"
|
||
exit 1
|
||
fi
|
||
|
||
NC_OPTS=(-z -w "$TIMEOUT")
|
||
if [ "$(uname -s)" = "Darwin" ]; then
|
||
NC_OPTS+=(-G "$TIMEOUT")
|
||
fi
|
||
|
||
check_host() {
|
||
local ip="$1"
|
||
local hit=()
|
||
for port in "${PORTS[@]}"; do
|
||
if nc "${NC_OPTS[@]}" "$ip" "$port" >/dev/null 2>&1; then
|
||
hit+=("$port")
|
||
fi
|
||
done
|
||
if [ ${#hit[@]} -gt 0 ]; then
|
||
echo "$(format_host "$ip") 开放: ${hit[*]}"
|
||
fi
|
||
}
|
||
|
||
for i in $(seq 1 254); do
|
||
check_host "$PREFIX.$i" &
|
||
done
|
||
wait
|
||
|
||
echo "------------------------------------------------------"
|
||
echo "扫描完成"
|