Skip to content

Commit 20fc978

Browse files
committed
Add tnc.sh
1 parent a54c33a commit 20fc978

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

sh/tnc.sh

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# ─── Colors ───────────────────────────────────────────────────────────────────
6+
RED='\033[0;31m'
7+
GRN='\033[0;32m'
8+
YEL='\033[1;33m'
9+
CYN='\033[0;36m'
10+
RST='\033[0m'
11+
12+
# ─── Usage ────────────────────────────────────────────────────────────────────
13+
usage() {
14+
echo -e "\
15+
${YEL}Usage:${RST}
16+
$(basename "$0") <TIMEOUT> <IP> <PORT> [RESOLVE_FLAG] [THREADS]
17+
18+
${CYN}Arguments:${RST}
19+
TIMEOUT Connection timeout in seconds (integer)
20+
IP Target host(s) — accepts:
21+
• Single IP/hostname 192.168.1.1 / host.example.com
22+
• Comma-separated list 192.168.1.1,192.168.1.2
23+
• CIDR notation 192.168.1.0/24
24+
• IP range (dash) 10.0.0.1-10.0.0.20
25+
• Short range 192.168.1.1-20
26+
• File (one target per line) /path/to/hosts.txt
27+
PORT Target port(s) — accepts:
28+
• Single port 80
29+
• Comma-separated 22,80,443
30+
• Range (dash) 20-25
31+
RESOLVE (optional) 'r' or 'resolve' — omit -n from nc to enable DNS resolution
32+
THREADS (optional) Number of parallel jobs (requires GNU parallel)
33+
34+
${CYN}Examples:${RST}
35+
$(basename "$0") 3 192.168.1.1 22
36+
$(basename "$0") 3 192.168.1.0/24 80,443 r 50
37+
$(basename "$0") 3 targets.txt 22-25 resolve 20"
38+
exit 1
39+
}
40+
41+
# ─── Argument validation ─────────────────────────────────────────────────────
42+
[[ $# -lt 3 ]] && usage
43+
44+
TIMEOUT="$1"
45+
IP_ARG="$2"
46+
PORT_ARG="$3"
47+
RESOLVE_FLAG="${4:-}"
48+
THREADS="${5:-}"
49+
50+
# Validate timeout
51+
if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
52+
echo -e "${RED}[!] Error: TIMEOUT must be a positive integer.${RST}" >&2
53+
exit 1
54+
fi
55+
56+
# Validate resolve flag
57+
RESOLVE=false
58+
if [[ "${RESOLVE_FLAG,,}" == "r" || "${RESOLVE_FLAG,,}" == "resolve" ]]; then
59+
RESOLVE=true
60+
elif [[ -n "$RESOLVE_FLAG" && "$RESOLVE_FLAG" =~ ^[0-9]+$ && -z "$THREADS" ]]; then
61+
# User skipped resolve flag and passed threads as 4th arg
62+
THREADS="$RESOLVE_FLAG"
63+
elif [[ -n "$RESOLVE_FLAG" && ! "$RESOLVE_FLAG" =~ ^[0-9]+$ ]]; then
64+
echo -e "${RED}[!] Error: Unrecognised RESOLVE flag '${RESOLVE_FLAG}'. Use 'r', 'resolve', or leave empty.${RST}" >&2
65+
exit 1
66+
fi
67+
68+
# Validate threads
69+
if [[ -n "$THREADS" ]]; then
70+
if ! [[ "$THREADS" =~ ^[0-9]+$ ]] || [[ "$THREADS" -eq 0 ]]; then
71+
echo -e "${RED}[!] Error: THREADS must be a positive integer.${RST}" >&2
72+
exit 1
73+
fi
74+
if ! command -v parallel &>/dev/null; then
75+
echo -e "${RED}[!] Error: GNU parallel is required when THREADS is specified.${RST}" >&2
76+
echo -e "${YEL} Install: sudo apt install parallel / brew install parallel${RST}" >&2
77+
exit 1
78+
fi
79+
fi
80+
81+
# ─── Dependency check ────────────────────────────────────────────────────────
82+
if ! command -v nc &>/dev/null; then
83+
echo -e "${RED}[!] Missing dependency: nc (netcat)${RST}" >&2
84+
echo -e "${YEL} Install: sudo apt install netcat-openbsd${RST}" >&2
85+
exit 1
86+
fi
87+
88+
if ! command -v prips &>/dev/null; then
89+
echo -e "${RED}[!] Missing dependency: prips${RST}" >&2
90+
echo -e "${YEL} Install: sudo apt install prips${RST}" >&2
91+
exit 1
92+
fi
93+
94+
# ─── IP arithmetic helpers ───────────────────────────────────────────────────
95+
_ip_to_int() {
96+
local IFS='.'
97+
read -r a b c d <<< "$1"
98+
echo $(( (a << 24) + (b << 16) + (c << 8) + d ))
99+
}
100+
101+
_int_to_ip() {
102+
local ip="$1"
103+
printf '%d.%d.%d.%d\n' \
104+
$(( (ip >> 24) & 255 )) \
105+
$(( (ip >> 16) & 255 )) \
106+
$(( (ip >> 8) & 255 )) \
107+
$(( ip & 255 ))
108+
}
109+
110+
# ─── Expand IP targets ───────────────────────────────────────────────────────
111+
expand_ips() {
112+
local input="$1"
113+
local items=()
114+
115+
# Split on commas
116+
IFS=',' read -ra items <<< "$input"
117+
118+
for item in "${items[@]}"; do
119+
item="$(echo "$item" | xargs)" # trim whitespace
120+
[[ -z "$item" ]] && continue
121+
122+
if [[ -f "$item" ]]; then
123+
# ── File: read each line and recursively expand
124+
while IFS= read -r line || [[ -n "$line" ]]; do
125+
line="$(echo "$line" | xargs)"
126+
[[ -z "$line" || "$line" == \#* ]] && continue
127+
expand_ips "$line"
128+
done < "$item"
129+
130+
elif [[ "$item" == *"/"* ]]; then
131+
# ── CIDR notation
132+
prips "$item"
133+
134+
elif [[ "$item" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)-([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
135+
# ── Full IP range: 10.0.0.1-10.0.0.20
136+
local start_int end_int
137+
start_int=$(_ip_to_int "${BASH_REMATCH[1]}")
138+
end_int=$(_ip_to_int "${BASH_REMATCH[2]}")
139+
for (( ip = start_int; ip <= end_int; ip++ )); do
140+
_int_to_ip "$ip"
141+
done
142+
143+
elif [[ "$item" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.)([0-9]+)-([0-9]+)$ ]]; then
144+
# ── Short range: 192.168.1.1-20
145+
local prefix="${BASH_REMATCH[1]}"
146+
local lo="${BASH_REMATCH[2]}"
147+
local hi="${BASH_REMATCH[3]}"
148+
for (( i = lo; i <= hi; i++ )); do
149+
echo "${prefix}${i}"
150+
done
151+
152+
else
153+
# ── Single IP or hostname
154+
echo "$item"
155+
fi
156+
done
157+
}
158+
159+
# ─── Expand ports ────────────────────────────────────────────────────────────
160+
expand_ports() {
161+
local input="$1"
162+
local items=()
163+
IFS=',' read -ra items <<< "$input"
164+
165+
for item in "${items[@]}"; do
166+
item="$(echo "$item" | xargs)"
167+
if [[ "$item" =~ ^([0-9]+)-([0-9]+)$ ]]; then
168+
local lo="${BASH_REMATCH[1]}"
169+
local hi="${BASH_REMATCH[2]}"
170+
for (( p = lo; p <= hi; p++ )); do
171+
echo "$p"
172+
done
173+
elif [[ "$item" =~ ^[0-9]+$ ]]; then
174+
echo "$item"
175+
else
176+
echo -e "${RED}[!] Invalid port: $item${RST}" >&2
177+
exit 1
178+
fi
179+
done
180+
}
181+
182+
# ─── Core check function ────────────────────────────────────────────────────
183+
check_port() {
184+
local timeout="$1"
185+
local host="$2"
186+
local port="$3"
187+
local do_resolve="$4"
188+
189+
# With resolve: nc -vzw<T> (lets nc do DNS resolution / shows resolved name)
190+
# Without resolve: nc -nvzw<T> (-n skips DNS)
191+
local nc_flags
192+
if [[ "$do_resolve" == "true" ]]; then
193+
nc_flags="-vzw${timeout}"
194+
else
195+
nc_flags="-nvzw${timeout}"
196+
fi
197+
198+
local output
199+
# nc writes connection info to stderr
200+
if output=$(nc $nc_flags "$host" "$port" 2>&1); then
201+
echo -e "${GRN}[OPEN]${RST} ${host}:${port}"
202+
else
203+
echo -e "${RED}[CLOSED]${RST} ${host}:${port}" >&2
204+
fi
205+
}
206+
207+
# Export for GNU parallel
208+
export -f check_port 2>/dev/null || true
209+
export RED GRN YEL CYN RST 2>/dev/null || true
210+
211+
# ─── Build target list ───────────────────────────────────────────────────────
212+
mapfile -t IP_LIST < <(expand_ips "$IP_ARG" | sort -u -t'.' -k1,1n -k2,2n -k3,3n -k4,4n)
213+
mapfile -t PORT_LIST < <(expand_ports "$PORT_ARG" | sort -nu)
214+
215+
TOTAL_IPS=${#IP_LIST[@]}
216+
TOTAL_PORTS=${#PORT_LIST[@]}
217+
TOTAL_CHECKS=$(( TOTAL_IPS * TOTAL_PORTS ))
218+
219+
if [[ "$TOTAL_IPS" -eq 0 ]]; then
220+
echo -e "${RED}[!] No valid targets resolved from '${IP_ARG}'.${RST}" >&2
221+
exit 1
222+
fi
223+
224+
# echo -e "${YEL}──────────────────────────────────────────────────${RST}"
225+
# echo -e "${CYN} tnc.sh — TCP Port Checker${RST}"
226+
# echo -e "${YEL}──────────────────────────────────────────────────${RST}"
227+
# echo -e " Targets : ${TOTAL_IPS} host(s)"
228+
# echo -e " Ports : ${TOTAL_PORTS} port(s) [${PORT_ARG}]"
229+
# echo -e " Checks : ${TOTAL_CHECKS} total"
230+
# echo -e " Timeout : ${TIMEOUT}s"
231+
# echo -e " Resolve : ${RESOLVE}"
232+
# [[ -n "$THREADS" ]] && echo -e " Threads : ${THREADS} (GNU parallel)"
233+
# echo -e "${YEL}──────────────────────────────────────────────────${RST}"
234+
# echo ""
235+
236+
# ─── Execute checks ─────────────────────────────────────────────────────────
237+
if [[ -n "$THREADS" ]]; then
238+
# ── Parallel mode ─────────────────────────────────────────────────────
239+
job_list=$(mktemp)
240+
for host in "${IP_LIST[@]}"; do
241+
for port in "${PORT_LIST[@]}"; do
242+
echo "$TIMEOUT $host $port $RESOLVE"
243+
done
244+
done > "$job_list"
245+
246+
parallel --will-cite -j "$THREADS" --colsep ' ' \
247+
check_port {1} {2} {3} {4} < "$job_list"
248+
249+
rm -f "$job_list"
250+
else
251+
# ── Sequential mode ───────────────────────────────────────────────────
252+
for host in "${IP_LIST[@]}"; do
253+
for port in "${PORT_LIST[@]}"; do
254+
check_port "$TIMEOUT" "$host" "$port" "$RESOLVE"
255+
done
256+
done
257+
fi
258+
259+
# echo ""
260+
# echo -e "${YEL}──────────────────────────────────────────────────${RST}"
261+
# echo -e "${GRN} Done. ${TOTAL_CHECKS} check(s) completed.${RST}"
262+
# echo -e "${YEL}──────────────────────────────────────────────────${RST}"

0 commit comments

Comments
 (0)