471 lines
21 KiB
Markdown
471 lines
21 KiB
Markdown
# System Architecture
|
|
|
|
This document breaks down how the port scanner is designed and why asynchronous I/O with concurrent workers provides both speed and clarity.
|
|
|
|
## High Level Architecture
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Command Line Interface │
|
|
│ (Boost.Program_Options Parser) │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────┐
|
|
│ PortScanner Object │
|
|
│ - Configuration Management │
|
|
│ - Work Queue (ports to scan) │
|
|
│ - Thread/Concurrency Control │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────┐
|
|
│ Boost.Asio io_context │
|
|
│ (Event Loop / Async Runtime) │
|
|
└──────────────┬──────────────────────┘
|
|
│
|
|
┌───────┴───────┐
|
|
▼ ▼
|
|
┌─────────────┐ ┌─────────────┐
|
|
│ Socket │ │ Timer │
|
|
│ (TCP │ │ (Timeout │
|
|
│ Connection) │ │ Detection) │
|
|
└─────────────┘ └─────────────┘
|
|
│ │
|
|
└───────┬───────┘
|
|
▼
|
|
┌───────────────┐
|
|
│ Target │
|
|
│ Host:Port │
|
|
└───────────────┘
|
|
```
|
|
|
|
### Component Breakdown
|
|
|
|
**Command Line Interface (main.cpp)**
|
|
- Purpose: Parse user input and initialize the scanner with configuration
|
|
- Responsibilities: Validate arguments, set defaults, display help text and usage examples
|
|
- Interfaces: Creates and configures a `PortScanner` object, then calls `start()` and `run()`
|
|
|
|
**PortScanner Controller (PortScanner class)**
|
|
- Purpose: Orchestrate the scanning process and manage concurrent operations
|
|
- Responsibilities: Maintain work queue of ports to scan, enforce thread limits, track statistics (open/closed/filtered counts), provide result formatting
|
|
- Interfaces: Exposes `set_options()`, `start()`, and `run()` methods; internally uses Boost.Asio primitives
|
|
|
|
**Boost.Asio io_context**
|
|
- Purpose: Event loop that drives all async operations
|
|
- Responsibilities: Schedule async socket operations and timer callbacks, dispatch completion handlers when I/O completes, manage execution strand for thread safety
|
|
- Interfaces: Provides async_connect, async_read_some, and async_wait operations that our scanner uses
|
|
|
|
**Socket and Timer Pair**
|
|
- Purpose: Each port scan uses one socket (for connection) and one timer (for timeout)
|
|
- Responsibilities: Socket attempts TCP connection; timer races against socket to detect filtered ports
|
|
- Interfaces: Completion handlers fire when either socket connects/fails or timer expires
|
|
|
|
## Data Flow
|
|
|
|
### Primary Scanning Flow
|
|
|
|
Step by step walkthrough of what happens when you run `./simplePortScanner -i 192.168.1.1 -p 80-443`:
|
|
```
|
|
1. main.cpp:12-23 → Parse command line arguments
|
|
Extracts IP (192.168.1.1), port range (80-443), thread count (default 100), timeout (default 2 sec)
|
|
|
|
2. main.cpp:37-40 → Initialize PortScanner
|
|
Calls set_options() which resolves DNS to IP address endpoint
|
|
|
|
3. PortScanner.cpp:77-82 → setup_queue()
|
|
Fills queue with ports 80, 81, 82, ... 443 (364 ports total)
|
|
|
|
4. PortScanner.cpp:109-115 → start()
|
|
Posts MAX_THREADS work items to io_context via strand
|
|
Each work item is a call to scan() function
|
|
|
|
5. main.cpp:41 → run()
|
|
Calls io.run() which blocks until all async operations complete
|
|
|
|
6. PortScanner.cpp:123-165 → scan() (called MAX_THREADS times concurrently)
|
|
Pops port from queue, creates socket and timer, races them
|
|
|
|
IF timeout expires first (line 130-136):
|
|
→ Port is FILTERED
|
|
→ Print result, decrement counter, recursively call scan() for next port
|
|
|
|
IF connection succeeds (line 144-151):
|
|
→ Port is OPEN
|
|
→ Try banner grab (async_read_some)
|
|
→ Print result with banner, decrement counter, call scan() again
|
|
|
|
IF connection fails (line 153-158):
|
|
→ Port is CLOSED
|
|
→ Print result, decrement counter, call scan() again
|
|
|
|
7. When queue is empty → io.run() completes → main.cpp:117-120 prints summary
|
|
```
|
|
|
|
Example with code references:
|
|
```
|
|
1. User runs command → main() (main.cpp:6)
|
|
Boost.Program_Options parses to variables
|
|
|
|
2. Variables → PortScanner.set_options() (PortScanner.cpp:85-95)
|
|
DNS resolution happens: resolver.resolve(domainName, "")
|
|
Stores endpoint for later use
|
|
|
|
3. PortScanner.start() → Fills queue, posts work (PortScanner.cpp:109-115)
|
|
100 async scan() operations begin
|
|
|
|
4. Each scan() → Creates socket + timer pair (PortScanner.cpp:123-127)
|
|
Both operations start simultaneously
|
|
Whoever completes first cancels the other
|
|
|
|
5. Completion handler → Determines port state (PortScanner.cpp:129-165)
|
|
Prints result, decrements active counter, calls scan() to grab next port from queue
|
|
|
|
6. Queue exhausted → io.run() returns (main.cpp:41)
|
|
Final statistics printed
|
|
```
|
|
|
|
### Secondary DNS Resolution Flow
|
|
|
|
Before any port scanning happens, we resolve the domain name:
|
|
```
|
|
1. User provides "-i scanme.nmap.org" → stored as string
|
|
2. PortScanner.set_options() calls resolver.resolve(domainName, "")
|
|
3. Boost.Asio performs DNS lookup (A or AAAA record)
|
|
4. Result converted to tcp::endpoint with IP address
|
|
5. All subsequent connections use this cached endpoint
|
|
```
|
|
|
|
This happens synchronously at startup. If DNS fails, the program errors immediately before any scanning begins. For IP addresses (like 192.168.1.1), resolution is trivial and just validates format.
|
|
|
|
## Design Patterns
|
|
|
|
### Async I/O with Completion Handlers
|
|
|
|
**What it is:**
|
|
Non-blocking I/O where operations return immediately and callbacks fire when complete. Instead of waiting for a socket connection (which might take seconds), we start the operation and provide a function to call when it finishes.
|
|
|
|
**Where we use it:**
|
|
Every network operation in the scanner:
|
|
- `async_connect` for TCP connections (PortScanner.cpp:138)
|
|
- `async_read_some` for banner grabbing (PortScanner.cpp:143)
|
|
- `async_wait` for timeout detection (PortScanner.cpp:128)
|
|
|
|
**Why we chose it:**
|
|
Scanning 65,535 ports synchronously would take hours. Even at 100ms per port (fast local network), that's 1.8 hours. With async I/O and 100 concurrent operations, we complete in minutes. The pattern also scales - changing thread count is one parameter.
|
|
|
|
**Trade-offs:**
|
|
- Pros: Massive concurrency with few actual threads, efficient resource usage, scales to thousands of simultaneous operations
|
|
- Cons: More complex code flow (callbacks instead of linear logic), harder to debug (stack traces show async machinery), requires understanding of event loops
|
|
|
|
Example implementation:
|
|
```cpp
|
|
// PortScanner.cpp:138-165
|
|
socket->async_connect(endpoint, boost::asio::bind_executor(strand,
|
|
[this, socket, timer, port, complete](boost::system::error_code ec) {
|
|
if (*complete) return; // Timer already fired, ignore this
|
|
*complete = true;
|
|
timer->cancel(); // Stop the race, we won
|
|
|
|
if (!ec) {
|
|
// Connection succeeded - port is OPEN
|
|
async_read_some(...); // Try to grab banner
|
|
} else {
|
|
// Connection failed - port is CLOSED
|
|
print_result(...);
|
|
}
|
|
scan(); // Tail recursion to get next port
|
|
}
|
|
));
|
|
```
|
|
|
|
The lambda captures shared state (`socket`, `timer`, `complete` flag) and runs later when the connection attempt finishes. This non-linear flow enables concurrency.
|
|
|
|
### Work Queue with Fixed Concurrency
|
|
|
|
**What it is:**
|
|
A queue of pending work (ports to scan) with a fixed number of workers pulling from it. As each worker completes, it grabs the next item. This prevents spawning 65,535 threads and overwhelming the system.
|
|
|
|
**Where we use it:**
|
|
- Queue: `std::queue<uint16_t> q` (PortScanner.hpp:24) filled in `setup_queue()` (PortScanner.cpp:77-82)
|
|
- Concurrency limit: `MAX_THREADS` (default 100) controls how many scans run simultaneously
|
|
- Work grabbing: `scan()` pops from queue (PortScanner.cpp:123), processes, then calls itself recursively for next port
|
|
|
|
**Why we chose it:**
|
|
Simple to understand and implement. The queue naturally handles work distribution - no complex scheduling logic. When a scan finishes quickly (closed port), the worker immediately grabs another. Slow scans (open ports with banner grabs) don't block other ports.
|
|
|
|
**Trade-offs:**
|
|
- Pros: Easy to reason about, automatic load balancing, simple thread limit enforcement
|
|
- Cons: Not perfectly efficient (if last few ports are slow, workers sit idle), doesn't prioritize interesting ports
|
|
|
|
### Strand for Thread Safety
|
|
|
|
**What it is:**
|
|
A Boost.Asio construct that serializes handler execution. When multiple async operations complete, the strand ensures their handlers don't run simultaneously. This provides thread safety without explicit locks.
|
|
|
|
**Where we use it:**
|
|
```cpp
|
|
// PortScanner.hpp:23
|
|
boost::asio::strand<boost::asio::io_context::executor_type> strand{io.get_executor()};
|
|
|
|
// All async operations wrapped in bind_executor(strand, ...)
|
|
// PortScanner.cpp:111, 129, 139, 144
|
|
boost::asio::post(strand, [this]() { scan(); });
|
|
boost::asio::bind_executor(strand, [...](...) { ... });
|
|
```
|
|
|
|
**Why we chose it:**
|
|
Multiple completion handlers modify shared state (`cnt`, `q`, statistics counters). Without synchronization, race conditions corrupt data. The strand guarantees that even though 100 operations run concurrently, their completion handlers execute one at a time.
|
|
|
|
**Trade-offs:**
|
|
- Pros: Thread-safe without manual locks, no risk of deadlock, clean code without mutex management
|
|
- Cons: Slight performance cost from serialization (negligible for our workload), all handlers must be wrapped consistently
|
|
|
|
## Layer Separation
|
|
|
|
The scanner has three distinct layers:
|
|
```
|
|
┌────────────────────────────────────┐
|
|
│ Presentation Layer │
|
|
│ - CLI parsing (main.cpp) │
|
|
│ - Output formatting │
|
|
│ - Color codes for terminal │
|
|
└────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────┐
|
|
│ Business Logic Layer │
|
|
│ - PortScanner class │
|
|
│ - Scanning algorithm │
|
|
│ - State management (counters) │
|
|
└────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────┐
|
|
│ I/O Layer │
|
|
│ - Boost.Asio runtime │
|
|
│ - Socket operations │
|
|
│ - Timer operations │
|
|
└────────────────────────────────────┘
|
|
```
|
|
|
|
### Why Layers?
|
|
|
|
Separation of concerns makes each component testable and replaceable:
|
|
|
|
- Want a GUI instead of CLI? Replace presentation layer, keep business logic.
|
|
- Want to switch from Boost.Asio to raw POSIX sockets? Replace I/O layer, business logic unchanged.
|
|
- Want to add different scan types (UDP, SYN scan)? Extend business logic without touching presentation.
|
|
|
|
### What Lives Where
|
|
|
|
**Presentation Layer (main.cpp):**
|
|
- Files: `main.cpp`
|
|
- Imports: Can import business logic (PortScanner class), uses Boost.Program_Options for CLI parsing
|
|
- Forbidden: Must not directly create sockets or timers, must not implement scanning logic
|
|
|
|
**Business Logic Layer (PortScanner class):**
|
|
- Files: `src/PortScanner.hpp`, `src/PortScanner.cpp`
|
|
- Imports: Can import I/O layer (Boost.Asio), cannot import presentation layer
|
|
- Forbidden: Must not handle command line parsing or output formatting (just returns data)
|
|
|
|
**I/O Layer (Boost.Asio):**
|
|
- Files: External library (Boost)
|
|
- Imports: Standard library, OS-level socket APIs
|
|
- Forbidden: No business logic about ports or scanning
|
|
|
|
This structure means main.cpp knows about PortScanner, PortScanner knows about Asio, but Asio doesn't know about scanning, and scanning doesn't know about CLI flags.
|
|
|
|
## Data Models
|
|
|
|
### Port Queue Entry
|
|
```cpp
|
|
// PortScanner.hpp:24
|
|
std::queue<std::uint16_t> q;
|
|
```
|
|
|
|
**Fields explained:**
|
|
- Just the port number (0-65535) stored as `uint16_t` to save memory
|
|
- Queue processed FIFO - ports scanned in order (80, 81, 82, ...)
|
|
|
|
**Relationships:**
|
|
- Populated by `parse_port()` which converts user input like "80-443" into individual port numbers
|
|
- Consumed by `scan()` which pops ports one at a time
|
|
|
|
### Scanner State
|
|
```cpp
|
|
// PortScanner.hpp:25-29
|
|
int cnt = 0; // Active concurrent scans
|
|
int MAX_THREADS = 0; // Concurrency limit
|
|
int open_ports = 0; // Statistics
|
|
int closed_ports = 0;
|
|
int filtered_ports = 0;
|
|
```
|
|
|
|
**Fields explained:**
|
|
- `cnt`: How many `scan()` operations are currently in flight. Prevents spawning too many workers.
|
|
- `MAX_THREADS`: User-configurable limit on concurrency. Defaults to 100 in main.cpp:15.
|
|
- Statistics counters: Incremented as results come in, printed at the end for summary.
|
|
|
|
**Relationships:**
|
|
- `cnt` guards the work queue - if `cnt >= MAX_THREADS`, no more scans start even if queue has ports
|
|
- Statistics tracked per completion handler (PortScanner.cpp:135, 148, 156)
|
|
|
|
### Well-Known Ports Map
|
|
```cpp
|
|
// PortScanner.cpp:3-24
|
|
const std::unordered_map<uint16_t, std::string> PortScanner::basicPorts{
|
|
{21, "FTP"},
|
|
{22, "SSH"},
|
|
{80, "HTTP"},
|
|
{443, "HTTPS"},
|
|
...
|
|
};
|
|
```
|
|
|
|
**Fields explained:**
|
|
- Static constant mapping from port numbers to service names
|
|
- Used for display only - doesn't affect scanning logic
|
|
|
|
**Relationships:**
|
|
- Looked up in completion handler (PortScanner.cpp:142) to show service name instead of just port number
|
|
- Missing ports display as "---" (PortScanner.cpp:140)
|
|
|
|
## Security Architecture
|
|
|
|
### Threat Model
|
|
|
|
What we're protecting against:
|
|
|
|
1. **Accidental network disruption** - Scanning too aggressively could crash target systems or network equipment. Thread limits and timeouts prevent overwhelming targets.
|
|
|
|
2. **Legal liability** - Scanning networks you don't own is often illegal (CFAA in the US). The tool includes usage warnings to educate users about legal boundaries.
|
|
|
|
3. **IDS/IPS detection** - While not stealth-focused, the scanner can be configured with lower thread counts and longer timeouts to reduce detection likelihood.
|
|
|
|
What we're NOT protecting against (out of scope):
|
|
|
|
- **Detection avoidance** - This is a basic scanner. Advanced IDS will catch it. Stealth techniques (SYN scans, fragmentation, decoys) are out of scope for a beginner project.
|
|
- **Target system DoS** - We limit threads but don't implement sophisticated rate limiting or backoff. A misconfigured scan could still overwhelm a weak target.
|
|
|
|
### Defense Layers
|
|
|
|
The scanner itself is a reconnaissance tool, but understanding defense-in-depth helps users protect against being scanned:
|
|
```
|
|
Layer 1: Firewall (prevents scan completion)
|
|
↓
|
|
Layer 2: IDS (detects scan pattern)
|
|
↓
|
|
Layer 3: Rate limiting (slows attacker)
|
|
```
|
|
|
|
**Why multiple layers?**
|
|
|
|
If the firewall fails (misconfigured rule), IDS alerts the security team. If IDS misses the scan (evasion technique), rate limiting prevents rapid enumeration. Each layer compensates for failures in others.
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
This scanner uses command line arguments, not environment variables:
|
|
```bash
|
|
./simplePortScanner \
|
|
-i TARGET # IP or domain name (default: 127.0.0.1)
|
|
-p PORT_RANGE # "80" or "1-1024" or "22,80,443" (default: 1-1024)
|
|
-t THREADS # Max concurrent scans (default: 100)
|
|
-e TIMEOUT # Seconds to wait before marking filtered (default: 2)
|
|
-v # Verbose output (not yet implemented)
|
|
-h # Help message
|
|
```
|
|
|
|
### Configuration Strategy
|
|
|
|
**Development:**
|
|
Use low thread counts (`-t 10`) and small port ranges (`-p 80-100`) to test without overwhelming your network. Scan localhost to verify functionality.
|
|
|
|
**Production:**
|
|
Real scans use higher concurrency (`-t 200` or more) for speed. Adjust timeout based on network latency - local networks can use 1 second, internet scans need 3-5 seconds. Always get permission before scanning external hosts.
|
|
|
|
## Performance Considerations
|
|
|
|
### Bottlenecks
|
|
|
|
Where this system gets slow under load:
|
|
|
|
1. **Network latency dominates** - Even with high concurrency, you can't scan faster than the network round-trip time. On a 50ms latency connection, each port takes at least 50ms regardless of how many threads you use.
|
|
|
|
2. **DNS resolution is synchronous** - The initial `resolver.resolve()` call blocks. For domains with slow DNS, this delays scan start. Caching resolved IPs could help repeated scans.
|
|
|
|
### Optimizations
|
|
|
|
What we did to make it faster:
|
|
|
|
- **Asynchronous I/O**: The big win. Synchronous scanning of 10,000 ports at 100ms each = 16 minutes. Async with 100 threads = ~10 seconds.
|
|
|
|
- **Shared pointer optimization** (PortScanner.cpp:125-127): Socket and timer created as `std::shared_ptr`. Completion handlers capture these, ensuring lifetime management without manual cleanup.
|
|
|
|
### Scalability
|
|
|
|
**Vertical scaling:**
|
|
Increase MAX_THREADS (up to ~1000 before hitting file descriptor limits on most systems). More threads = more concurrent scans = faster completion, but with diminishing returns beyond network capacity.
|
|
|
|
**Horizontal scaling:**
|
|
Split IP ranges across multiple scanner instances. Scan 192.168.1.0/24 by running 4 instances each handling 64 IPs. This parallelizes the bottleneck (network latency) across machines.
|
|
|
|
## Design Decisions
|
|
|
|
### Decision 1: Connect Scan vs SYN Scan
|
|
|
|
**What we chose:**
|
|
Full TCP connect scan (complete three-way handshake)
|
|
|
|
**Alternatives considered:**
|
|
- SYN scan (half-open scan): Send SYN, read SYN-ACK, send RST instead of completing handshake
|
|
- ACK scan: Send ACK packet to detect firewall rules
|
|
- UDP scan: Send UDP packets to check non-TCP services
|
|
|
|
**Why we chose connect scan:**
|
|
SYN scanning requires raw sockets, which need root privileges on Linux. This adds deployment complexity and security risk. Connect scans work as unprivileged users and integrate cleanly with Boost.Asio's high-level API.
|
|
|
|
**Trade-offs:**
|
|
- Pros: No special privileges needed, simpler code, cross-platform (works on Windows/Linux/macOS), less likely to crash buggy network stacks
|
|
- Cons: Noisier (shows up clearly in logs as completed connections), slightly slower (full handshake vs SYN only), some systems log connect attempts differently than SYNs
|
|
|
|
### Decision 2: Timer-Based Filtering vs. ICMP Analysis
|
|
|
|
**What we chose:**
|
|
Use timeout duration to infer filtered ports
|
|
|
|
**Alternatives considered:**
|
|
- Listen for ICMP "port unreachable" messages to distinguish closed from filtered
|
|
- Send multiple probe types (SYN, ACK, FIN) and correlate responses
|
|
|
|
**Why we chose timeouts:**
|
|
ICMP listening requires raw sockets (again, root privileges). Packet filters often drop ICMP anyway, making it unreliable. Timeouts work everywhere and handle the common case (firewall silently drops packets) correctly.
|
|
|
|
**Trade-offs:**
|
|
- Pros: Works without privileges, handles filtered ports correctly, simple to implement
|
|
- Cons: Adds latency to scans (must wait full timeout), can't distinguish "filtered by firewall" from "network down", false positives if network is just slow
|
|
|
|
### Decision 3: Recursive scan() vs. Worker Pool
|
|
|
|
**What we chose:**
|
|
Recursive tail calls to `scan()` for work distribution
|
|
|
|
**Alternatives considered:**
|
|
- Pre-spawn N worker threads that loop pulling from queue
|
|
- Use a thread pool library with work stealing
|
|
|
|
**Why we chose recursion:**
|
|
Fits naturally with async completion handlers. When a scan finishes, the completion handler just calls `scan()` again. The Boost.Asio event loop handles the scheduling.
|
|
|
|
**Trade-offs:**
|
|
- Pros: Minimal code, no manual thread management, automatic work distribution
|
|
- Cons: Stack depth increases (though tail call optimization helps), less control over worker lifecycle, harder to implement advanced scheduling
|
|
|
|
## Next Steps
|
|
|
|
Now that you understand the architecture:
|
|
|
|
1. Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for detailed code walkthrough showing how async operations coordinate
|
|
2. Try modifying the concurrency model - what happens if you remove the strand? (Race conditions will corrupt counters)
|
|
3. Experiment with timeout values to see how network latency affects scan duration
|