Layer-4-Port-Scanner is a command-line Layer 4 port scanner written in C. The project scans TCP and UDP ports on a target host and prints port states in text form.
.
├── includes/
├── src/
├── tests/
├── Makefile
└── README.md
.
├── includes/
│ ├── arg_parser.h
│ ├── dns_parser.h
│ ├── net_utils.h
│ ├── portlist.h
│ ├── scanner.h
│ ├── tcp_scanner.h
│ └── udp_scanner.h
└── src/
├── arg_parser.c
├── dns_parser.c
├── main.c
├── net_utils.c
├── portlist.c
├── scanner.c
├── tcp_scanner.c
└── udp_scanner.c
Required tools and libraries:
- Linux system
- GCC with C17 support
- make
- libpcap runtime and development headers
For integration tests that manipulate firewall rules, these tools are also needed:
sudo apt install -y iptablesBuild scanner binary:
makeor
make allClean binaries:
make cleanHelp:
./ipk-L4-scan -hList available interfaces:
./ipk-L4-scan -iRun scans (requires elevated privileges for raw sockets/capture):
sudo ./ipk-L4-scan -i lo -t 80 127.0.0.1
sudo ./ipk-L4-scan -i lo -u 80 127.0.0.1
sudo ./ipk-L4-scan -i lo -t 1,80,443 -u 80 localhost
sudo ./ipk-L4-scan -i lo -t 1-100 -w 500 localhostGeneral syntax:
sudo ./ipk-L4-scan -i INTERFACE [-u PORTS] [-t PORTS] HOST [-w TIMEOUT] [-h | --help]-
CLI argument parsing with validation:
- required: interface, host, and at least one protocol set (
-tor-u) - supports timeout override via
-w - supports
-h/--help - special
-ialone prints interfaces
- required: interface, host, and at least one protocol set (
-
Port set parsing:
- single port:
80 - comma list:
22,80,443 - range:
1-1024 - validates boundaries 1-65535
- rejects malformed and mixed unsupported format (
22,25-30,35)
- single port:
-
Host resolution:
- resolves hostnames to unique IP list
- supports IPv4 and IPv6 addresses from DNS
-
TCP scanning:
- IPv4: raw TCP SYN probe with manually built IPv4 + TCP header and checksum
- IPv6: raw TCP probe using kernel checksum option (
IPV6_CHECKSUM) - packet capture with BPF filter through libpcap
- interpretation:
- SYN+ACK -> open
- RST -> closed
- timeout/no valid reply -> filtered
-
UDP scanning:
- sends UDP probe to target port
- listens for ICMP/ICMPv6 port unreachable replies with libpcap
- interpretation:
- ICMP port unreachable -> closed
- no ICMP error before timeout -> open
-
Interruption:
- SIGINT/SIGTERM handled through shared stop flag
- scanner and test runner stop safely on interruption
- Raw sockets were used for TCP scanning to avoid full
connect()handshakes and to control packet-level behavior. - libpcap was used for response capture because it provides reliable filtering and cross-protocol packet inspection.
- Scanner orchestration is split into modules:
arg_parserfor user inputportlistfor port expression parsingdns_parserfor target resolutiontcp_scannerandudp_scannerfor protocol-specific logicscannerfor execution flow and result printing
- Tests are separated into unit-like parser checks and integration-like TCP/UDP behavior checks on loopback.
- UDP behavior intentionally follows assignment interpretation where no error response is treated as open.
For testing i decided to implement a custom test runner in C with a simple assertion framework. This allows for more realistic integration tests that can manipulate firewall rules and start temporary servers, which would be difficult to achieve with a shell script or external testing framework.
tests/
├── cases/
│ ├── arg_parser_tests.c
│ ├── arg_parser_tests.h
│ ├── dns_parser_tests.c
│ ├── dns_parser_tests.h
│ ├── portlist_tests.c
│ ├── portlist_tests.h
│ ├── tcp_tests.c
│ ├── tcp_tests.h
│ ├── udp_tests.c
│ └── udp_tests.h
├── helpers/
│ ├── test_env.c
│ └── test_env.h
├── runner/
│ ├── test_runner.c
│ └── test_runner.h
└── main.c
Build and run automated tests:
sudo make testWhat was tested:
- CLI option parsing (
-i,-t,-u,-w,-h,--help) - required argument combinations
- invalid option/value handling
Why:
- scanner behavior depends on valid inputs and safe early rejection of invalid usage.
How:
- tests call
parse_arguments()with handcraftedargc/argvarrays and assert return codes/fields.
Testing environment:
- same environment as listed in Reproducible Environment section.
Inputs, expected outputs, actual outputs:
- Normal input:
- input:
{"scanner","-w","3000","-u","53","-i","eth0","1.1.1.1"} - expected:
EX_OK, timeout3000, udp ports53 - actual: pass
- input:
- Edge input:
- input:
{"scanner","-i","eth0","-t","22","-w","abc","example.com"} - expected:
EX_USAGEand error text for invalid timeout - actual: pass, printed
Error: Invalid timeout value 'abc'.
- input:
Sample textual output:
===== ARG PARSER ======
help short... [PASS]
help long... [PASS]
default timeout... [PASS]
custom timeout... [PASS]
missing interface... Error: Interface (-i) is required for scanning.
[PASS]
invalid timeout... Error: Invalid timeout value 'abc'.
[PASS]
==============
What was tested:
- parser for single ports, lists, ranges, full range, invalid values, null inputs
Why:
- wrong port parsing would produce wrong scans or memory errors.
How:
- tests call
parse_ports()and assertEX_OK/EX_USAGE, counts, and content of parsed arrays.
Testing environment:
- same environment as listed in Reproducible Environment section.
Inputs, expected outputs, actual outputs:
- Normal input:
- input:
"22,80,443" - expected: count 3 and ports
[22,80,443] - actual: pass
- input:
- Edge input:
- input:
"30-20" - expected:
EX_USAGE(reverse range rejected) - actual: pass, printed
Error: Invalid port range '30-20'. End must be greater than start.
- input:
Sample textual output:
===== PORTLIST ======
single_port_valid... [PASS]
comma_list_valid... [PASS]
range_valid... [PASS]
invalid_zero_port... Error: Invalid port number '0'.
[PASS]
invalid_range_reverse_order... Error: Invalid port range '30-20'. End must be greater than start.
[PASS]
==============
What was tested:
- resolving valid literal/hostname
- handling invalid hostnames and invalid IPv4 literals
- safe free of null list
Why:
- scanning requires valid resolved addresses; failures must be explicit and safe.
How:
- tests call
get_host_ips()and verify list shape, uniqueness, address family validity, and return codes.
Testing environment:
- same environment as listed in Reproducible Environment section.
Inputs, expected outputs, actual outputs:
- Normal input:
- input:
"127.0.0.1"and"localhost" - expected:
EX_OKand at least one valid IP node - actual: pass
- input:
- Edge input:
- input:
"definitely-invalid-hostname.example.invalid" - expected:
EX_NOHOST - actual: pass with resolver error text
- input:
Sample textual output:
===== DNS PARSER ======
resolve_ipv4_literal... [PASS]
resolve_localhost_has_at_least_one_ip... [PASS]
invalid_hostname_returns_nohost... Error resolving host 'definitely-invalid-hostname.example.invalid': Name or service not known
[PASS]
==============
What was tested:
- integration behavior of
scan_tcp()andscan_udp()for open/closed/filtered-like scenarios - both IPv4 and IPv6 loopback paths
- repeated response consistency checks
Why:
- parser/unit tests are not enough; scanner state detection must be validated against real packets and local servers.
How:
- helper code starts temporary TCP/UDP servers on loopback ports, then scan functions are executed.
- filtered scenarios are emulated with temporary
iptables/ip6tablesDROP rules.
Testing environment:
- same environment as listed in Reproducible Environment section.
Inputs, expected outputs, actual outputs:
- Normal target examples:
- TCP open probe to local server port (e.g., 8080)
- UDP open probe to local server port (e.g., 8080)
- Edge target examples:
- closed port with no server
- filtered port using temporary firewall rule
Sample textual output:
===== TCP ======
tcp_open_ipv4... [PASS]
tcp_closed_ipv4... [PASS]
tcp_filtered_ipv4... [PASS]
tcp_open_closed_filtered_ipv4... [PASS]
tcp_multiple_responses_ipv4... [PASS]
tcp_open_ipv6... [PASS]
tcp_closed_ipv6... [PASS]
tcp_filtered_ipv6... [PASS]
tcp_open_closed_filtered_ipv6... [PASS]
tcp_multiple_responses_ipv6... [PASS]
==============
===== UDP ======
udp_open_ipv4... [PASS]
udp_closed_ipv4... [PASS]
udp_filtered_ipv4... [PASS]
udp_open_closed_filtered_ipv4... [PASS]
udp_multiple_responses_ipv4... [PASS]
udp_open_ipv6... [PASS]
udp_closed_ipv6... [PASS]
udp_filtered_ipv6... [PASS]
udp_open_closed_filtered_ipv6... [PASS]
udp_multiple_responses_ipv6... [PASS]
Note
Due to the repetitive nature of certain test cases, parts of the argument, DNS, and port list tests were initially generated using an LLM. These outputs were carefully reviewed, validated, and significantly refined. The generated content served only as a starting point for specific edge cases.
- Elevated privileges are required for real scanning.
- Integration tests can be unstable across restricted environments (setups without usable sudo for firewall commands).
- Linux only behavior
// SO_BINDTODEVICE is Linux-specific behavior.
if (interface != NULL) {
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, interface, strlen(interface)) < 0) {
perror("Error binding UDP socket to interface");
close(sock);
return EX_OSERR;
}
}