Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ef64198

Browse files
committed
feat: release v2.4.0 with native SYN scan and detailed snort benchmark
1 parent 4ced963 commit ef64198

10 files changed

Lines changed: 537 additions & 24 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.3.1"
2+
".": "2.4.0"
33
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.4.0] - 2026-03-09
6+
7+
### Added
8+
- **Selectable scan engine**: new `--scan-type connect|syn` flag.
9+
- **Native SYN discovery mode**: `syn` mode uses GoMap raw TCP SYN probes to discover open ports before optional service detection.
10+
11+
### Changed
12+
- **Scan headers**: text output now indicates which scan type is being used.
13+
- **README benchmark section**: expanded with detailed CONNECT vs SYN vs GHOST lab measurements against Snort.
14+
15+
### Fixed
16+
- **Resilience**: automatic fallback to `connect` scan when SYN requirements are not met (unsupported OS or insufficient privileges).
17+
- **Native SYN response parsing**: improved packet decoding and batched response collection to reduce false negatives under heavier scans.
18+
519
## [2.3.1] - 2026-02-25
620

721
### Fixed

README.md

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A fast TCP port scanner written in Go, with optional service/version detection,
44

55
## Current scope
66

7-
- Fast concurrent TCP connect scanning.
7+
- Fast concurrent TCP scanning with selectable engine (`connect` or `syn`).
88
- Optional service and version detection (`-s`).
99
- Single host, hostname, comma-separated targets, and CIDR ranges.
1010
- CIDR active-host discovery by TCP probes (no ICMP ping).
@@ -51,6 +51,9 @@ go install github.com/NexusFireMan/gomap/v2@latest
5151
# Default scan (top common ports)
5252
./gomap 10.0.11.6
5353

54+
# Native SYN scan discovery (requires root/CAP_NET_RAW)
55+
./gomap --scan-type syn 10.0.11.6
56+
5457
# Service/version detection on selected ports
5558
./gomap -s -p 21,22,80,135,139,445,5985 10.0.11.6
5659

@@ -78,6 +81,7 @@ Usage:
7881
7982
Main options:
8083
-p ports to scan (example: 80,443 or 1-1024 or - for all)
84+
--scan-type connect|syn (default: connect)
8185
--top, --top-ports scan top N ports from curated top-1000 list
8286
--exclude-ports remove ports from final scan set
8387
-s enable service/version detection
@@ -128,40 +132,58 @@ When `-s` is enabled, gomap combines port-based hints and protocol/banner parsin
128132
- SSH/FTP/PostgreSQL/Redis/MySQL and other protocol banners.
129133
- SMB-oriented identification for `microsoft-ds` targets.
130134

131-
Important: banner-based detection is heuristic. Always validate critical findings with a second tool (`nmap -sV`, native service queries, or manual protocol checks).
135+
Important: banner-based detection is heuristic. Always validate critical findings with a second tool.
136+
137+
`--scan-type syn` notes:
138+
- Uses GoMap native raw TCP SYN probes for port discovery, then optional service detection on open ports.
139+
- If SYN scan cannot run (insufficient privileges or unsupported OS), GoMap falls back to `connect` scan automatically.
140+
- For noisy links, tune reliability explicitly with `--retries` and `--rate`.
132141

133142
Note: `--random-ip` randomizes HTTP headers only; it does not spoof the real TCP source IP.
134143

135144
## Stealth benchmark (lab)
136145

137-
Benchmark executed on **February 25, 2026** with:
146+
Benchmark executed on **March 9, 2026** with:
138147

139148
- Scanner host: `10.0.11.11`
140-
- Targets: `10.0.11.0/24` (Metasploitable3 Windows `10.0.11.6`, Linux `10.0.11.9`, Snort `10.0.11.8`)
149+
- Targets: `10.0.11.0/24` (Windows `10.0.11.6`, Linux `10.0.11.9`, Snort `10.0.11.8`)
141150
- IDS: Snort `2.9.20` (`10.0.11.8`)
142-
- Ports: `22,80,139,445,3389,5985`
151+
- Port set: `22,80,139,445,3389,5985`
152+
- Log analyzed: `/var/log/snort/snort.alert.fast`
153+
- Attribution filter: source `10.0.11.11`
143154

144155
Commands compared:
145156

146157
```bash
147-
# Normal
158+
# CONNECT normal
148159
gomap -s -p 22,80,139,445,3389,5985 10.0.11.0/24
149160

150-
# Ghost ultra-stealth
161+
# CONNECT ghost
151162
gomap -g -s --random-agent --random-ip -p 22,80,139,445,3389,5985 10.0.11.0/24
163+
164+
# SYN normal (native, requires root/CAP_NET_RAW)
165+
sudo gomap --scan-type syn -s -p 22,80,139,445,3389,5985 10.0.11.0/24
166+
167+
# SYN ghost
168+
sudo gomap -g -s --scan-type syn --random-agent --random-ip -p 22,80,139,445,3389,5985 10.0.11.0/24
152169
```
153170

154-
Observed results (Snort `snort.alert.fast`, TCP alerts with source `10.0.11.11`):
171+
Observed results (single run per profile):
155172

156-
| Mode | New alerts (all) | New TCP alerts from scanner | Scan duration |
157-
|------|-------------------|-----------------------------|---------------|
158-
| Normal | 104 | 89 | ~6.2s |
159-
| Ghost ultra-stealth | 41 | 20 | ~11.5s |
173+
| Profile | Duration | Hosts scanned | Open ports found | New alerts (all) | New alerts from scanner IP | New TCP alerts from scanner IP |
174+
|---|---:|---:|---:|---:|---:|---:|
175+
| CONNECT normal | 6.801s | 4 | 10 | 97 | 97 | 96 |
176+
| CONNECT ghost | 10.893s | 3 | 9 | 64 | 64 | 62 |
177+
| SYN normal | 9.26s | 4 | 10 | 104 | 104 | 103 |
178+
| SYN ghost | 11.793s | 3 | 9 | 48 | 48 | 47 |
160179

161-
Takeaway:
180+
Takeaways:
162181

163-
- Ghost ultra-stealth reduced scanner-attributed TCP alerts by about **77.5%** (`89 -> 20`).
164-
- Tradeoff is slower execution and less aggressive service/version fingerprinting.
182+
- `ghost` mode reduced scanner-attributed TCP alerts in both engines:
183+
- CONNECT: `96 -> 62` (about `-35.4%`)
184+
- SYN: `103 -> 47` (about `-54.4%`)
185+
- In this Snort rule set, SYN generated more alerts than CONNECT for the same target/ports.
186+
- Ghost CIDR discovery is intentionally conservative and may scan fewer active hosts (`3` vs `4` in this run).
165187

166188
## Output formats
167189

cmd/gomap/cli.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
// CLIOptions holds all parsed/validated CLI arguments.
1414
type CLIOptions struct {
1515
PortsFlag string
16+
ScanType string
1617
ExcludePorts string
1718
ServiceFlag bool
1819
GhostFlag bool
@@ -51,6 +52,7 @@ func ParseCLIOptions(args []string) (CLIOptions, error) {
5152
fs := flag.NewFlagSet("gomap", flag.ContinueOnError)
5253
fs.SetOutput(os.Stderr)
5354
fs.StringVar(&opts.PortsFlag, "p", "", "ports to scan (e.g., 80,443 or 1-1024 or - for all ports)")
55+
fs.StringVar(&opts.ScanType, "scan-type", "connect", "scan technique: connect|syn")
5456
fs.StringVar(&opts.ExcludePorts, "exclude-ports", "", "exclude ports (e.g., 80,443 or 1-1024)")
5557
fs.BoolVar(&opts.ServiceFlag, "s", false, "detect services and versions")
5658
fs.BoolVar(&opts.GhostFlag, "g", false, "ghost mode - slower, stealthy scan to evade IDS/Firewall detection")
@@ -138,6 +140,10 @@ func normalizeOptions(opts CLIOptions) (CLIOptions, error) {
138140
if opts.TopPortsAlias > 0 {
139141
opts.TopPorts = opts.TopPortsAlias
140142
}
143+
opts.ScanType = strings.ToLower(strings.TrimSpace(opts.ScanType))
144+
if opts.ScanType != "connect" && opts.ScanType != "syn" {
145+
return opts, errors.New("invalid --scan-type. Allowed: connect, syn")
146+
}
141147
if opts.TopPorts < 0 {
142148
return opts, errors.New("--top must be a positive number")
143149
}
@@ -214,6 +220,7 @@ func printHelp(w *os.File) {
214220
215221
%sTarget & Scan:%s
216222
-p <ports> ports to scan (80,443 | 1-1024 | -)
223+
--scan-type <type> connect|syn (syn requires root/CAP_NET_RAW)
217224
--top <N> scan top N ports from curated top-1000 list
218225
--top-ports <N> alias of --top
219226
--exclude-ports <ports> remove ports from final scan set
@@ -250,6 +257,7 @@ func printHelp(w *os.File) {
250257
251258
%sExamples:%s
252259
gomap 10.0.11.6
260+
gomap --scan-type syn 10.0.11.6
253261
gomap -s -p 21,22,80,445 10.0.11.9
254262
gomap -s --top-ports 300 10.0.11.0/24
255263
gomap -g -s --random-agent --random-ip 10.0.11.0/24

cmd/gomap/cli_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,20 @@ func TestParseCLIOptionsHelpFlag(t *testing.T) {
6464
t.Fatalf("expected errHelp, got: %v", err)
6565
}
6666
}
67+
68+
func TestParseCLIOptionsScanType(t *testing.T) {
69+
opts, err := ParseCLIOptions([]string{"--scan-type", "syn", "10.0.11.6"})
70+
if err != nil {
71+
t.Fatalf("unexpected error: %v", err)
72+
}
73+
if opts.ScanType != "syn" {
74+
t.Fatalf("expected scan type syn, got %q", opts.ScanType)
75+
}
76+
}
77+
78+
func TestParseCLIOptionsScanTypeInvalid(t *testing.T) {
79+
_, err := ParseCLIOptions([]string{"--scan-type", "udp", "10.0.11.6"})
80+
if err == nil {
81+
t.Fatal("expected error for invalid scan type")
82+
}
83+
}

cmd/gomap/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func Run() {
4747
req := app.ScanRequest{
4848
Target: opts.Host,
4949
PortsFlag: opts.PortsFlag,
50+
ScanType: opts.ScanType,
5051
ExcludePorts: opts.ExcludePorts,
5152
TopPorts: opts.TopPorts,
5253
Rate: opts.Rate,

cmd/gomap/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
var (
1111
// Version is injected at build time with -ldflags.
12-
Version = "2.3.1"
12+
Version = "2.4.0"
1313
// Commit is injected at build time with -ldflags.
1414
Commit = "dev"
1515
// Date is injected at build time with -ldflags.

pkg/app/scan.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
type ScanRequest struct {
1616
Target string
1717
PortsFlag string
18+
ScanType string
1819
ExcludePorts string
1920
TopPorts int
2021
Rate int
@@ -38,6 +39,10 @@ type ScanRequest struct {
3839
// ExecuteScan runs the complete scan workflow: target expansion, host discovery, scan, and rendering.
3940
func ExecuteScan(req ScanRequest) error {
4041
machineOutput := req.Format != "text"
42+
if req.ScanType == "" {
43+
req.ScanType = "connect"
44+
}
45+
scanLabel := strings.ToUpper(req.ScanType)
4146

4247
destWriter := output.DefaultWriter()
4348
var outFile *os.File
@@ -142,16 +147,16 @@ func ExecuteScan(req ScanRequest) error {
142147
if !machineOutput {
143148
if len(targets) == 1 {
144149
if req.GhostMode {
145-
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s ports) - %s (stealthy)", output.Host(targets[0]), output.Count(len(portsToScan)), output.Warning("Ghost mode"))))
150+
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s ports, %s scan) - %s (stealthy)", output.Host(targets[0]), output.Count(len(portsToScan)), output.Highlight(scanLabel), output.Warning("Ghost mode"))))
146151
} else {
147-
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s ports)", output.Host(targets[0]), output.Count(len(portsToScan)))))
152+
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s ports, %s scan)", output.Host(targets[0]), output.Count(len(portsToScan)), output.Highlight(scanLabel))))
148153
}
149154
} else {
150155
targetRange, _, _ := scanner.FormatCIDRInfo(req.Target)
151156
if req.GhostMode {
152-
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s active hosts, %s ports) - %s (stealthy)", output.Highlight(targetRange), output.Count(len(targets)), output.Count(len(portsToScan)), output.Warning("Ghost mode"))))
157+
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s active hosts, %s ports, %s scan) - %s (stealthy)", output.Highlight(targetRange), output.Count(len(targets)), output.Count(len(portsToScan)), output.Highlight(scanLabel), output.Warning("Ghost mode"))))
153158
} else {
154-
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s active hosts, %s ports)", output.Highlight(targetRange), output.Count(len(targets)), output.Count(len(portsToScan)))))
159+
fmt.Printf("%s\n\n", output.Info(fmt.Sprintf("🎯 Scanning %s (%s active hosts, %s ports, %s scan)", output.Highlight(targetRange), output.Count(len(targets)), output.Count(len(portsToScan)), output.Highlight(scanLabel))))
155160
}
156161
}
157162
}
@@ -186,9 +191,26 @@ func ExecuteScan(req ScanRequest) error {
186191
RandomIP: req.RandomIP,
187192
TargetCIDR: cidrForHeaders,
188193
})
189-
openPorts := s.Scan(portsToScan, req.ServiceDetect)
190-
if len(openPorts) > 0 {
191-
allResults[targetIP] = openPorts
194+
var openResults []scanner.ScanResult
195+
if req.ScanType == "syn" {
196+
synOpenPorts, synErr := scanner.DiscoverOpenPortsSYN(targetIP, portsToScan, scanner.SYNConfig{
197+
Rate: req.Rate,
198+
Retries: req.Retries,
199+
GhostMode: req.GhostMode,
200+
})
201+
if synErr != nil {
202+
if !machineOutput {
203+
fmt.Printf("%s\n", output.StatusWarn(fmt.Sprintf("SYN scan unavailable on %s (%v). Falling back to connect scan.", targetIP, synErr)))
204+
}
205+
openResults = s.Scan(portsToScan, req.ServiceDetect)
206+
} else {
207+
openResults = scanner.BuildResultsFromKnownOpenPorts(s, synOpenPorts, req.ServiceDetect)
208+
}
209+
} else {
210+
openResults = s.Scan(portsToScan, req.ServiceDetect)
211+
}
212+
if len(openResults) > 0 {
213+
allResults[targetIP] = openResults
192214
}
193215
}
194216
scanDuration := time.Since(scanStart)

0 commit comments

Comments
 (0)