Files
.dotfile/script/scan-ip.sh
T
2026-07-21 23:35:28 +08:00

301 lines
9.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "扫描完成"