Skip to content

Commit d420294

Browse files
authored
feature: ports file cli flag (#111)
1 parent cdbbfd4 commit d420294

12 files changed

Lines changed: 215 additions & 21 deletions

File tree

Dockerfile

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ FROM golang:1.17-alpine as builder
33
RUN apk add --no-cache libpcap-dev libc-dev gcc linux-headers
44
ADD . /app
55
WORKDIR /app
6-
RUN go build -ldflags "-w -s" -o /sx
6+
RUN go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o /sx
77

8-
FROM alpine:3.14
8+
FROM alpine:3.15
99

10-
RUN apk add libpcap
1110
COPY --from=builder /sx /sx
12-
13-
ENTRYPOINT ["/sx"]
11+
ENTRYPOINT ["/sx"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ or individual ports:
161161
cat arp.cache | sx tcp -p 22,443 192.168.0.171
162162
```
163163

164+
or use the `--ports-file` option to specify a file with ports or port ranges to scan, one per line.
165+
164166
scan ip/port pairs from a file with JSON output:
165167

166168
```

command/config.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ func (o *ipScanCmdOpts) getGatewayMAC(iface *net.Interface, cache *arp.Cache) (m
303303

304304
type ipPortScanCmdOpts struct {
305305
ipScanCmdOpts
306+
portFile string
306307
portRanges []*scan.PortRange
307308

308309
rawPortRanges string
@@ -311,6 +312,7 @@ type ipPortScanCmdOpts struct {
311312
func (o *ipPortScanCmdOpts) initCliFlags(cmd *cobra.Command) {
312313
o.ipScanCmdOpts.initCliFlags(cmd)
313314
cmd.Flags().StringVarP(&o.rawPortRanges, "ports", "p", "", "set ports to scan")
315+
cmd.Flags().StringVar(&o.portFile, "ports-file", "", "set file with ports or port ranges to scan, one-per line")
314316
}
315317

316318
func (o *ipPortScanCmdOpts) parseRawOptions() (err error) {
@@ -322,6 +324,15 @@ func (o *ipPortScanCmdOpts) parseRawOptions() (err error) {
322324
return
323325
}
324326
}
327+
if len(o.portFile) > 0 {
328+
portRanges, err := parsePortsFile(func() (io.ReadCloser, error) {
329+
return os.Open(o.portFile)
330+
})
331+
if err != nil {
332+
return err
333+
}
334+
o.portRanges = append(o.portRanges, portRanges...)
335+
}
325336
return
326337
}
327338

@@ -359,6 +370,7 @@ func (o *ipPortScanCmdOpts) newIPPortGenerator() (reqgen scan.RequestGenerator)
359370
type genericScanCmdOpts struct {
360371
json bool
361372
ipFile string
373+
portFile string
362374
portRanges []*scan.PortRange
363375
workers int
364376
rateCount int
@@ -374,6 +386,7 @@ type genericScanCmdOpts struct {
374386
func (o *genericScanCmdOpts) initCliFlags(cmd *cobra.Command) {
375387
cmd.Flags().BoolVar(&o.json, "json", false, "enable JSON output")
376388
cmd.Flags().StringVarP(&o.rawPortRanges, "ports", "p", "", "set ports to scan")
389+
cmd.Flags().StringVar(&o.portFile, "ports-file", "", "set file with ports or port ranges to scan, one-per line")
377390
cmd.Flags().StringVarP(&o.ipFile, "file", "f", "", "set JSONL file with ip/port pairs to scan")
378391
cmd.Flags().IntVarP(&o.workers, "workers", "w", defaultWorkerCount, "set workers count")
379392
cmd.Flags().StringVar(&o.rawExcludeFile, "exclude", "",
@@ -398,6 +411,16 @@ func (o *genericScanCmdOpts) parseRawOptions() (err error) {
398411
return
399412
}
400413
}
414+
if len(o.portFile) > 0 {
415+
portRanges, err := parsePortsFile(func() (io.ReadCloser, error) {
416+
return os.Open(o.portFile)
417+
})
418+
if err != nil {
419+
return err
420+
}
421+
o.portRanges = append(o.portRanges, portRanges...)
422+
}
423+
// TODO parsePortsFile
401424
if len(o.rawRateLimit) > 0 {
402425
if o.rateCount, o.rateWindow, err = parseRateLimit(o.rawRateLimit); err != nil {
403426
return
@@ -587,3 +610,28 @@ func parseExcludeFile(openFile openFileFunc) (excludeIPs scan.IPContainer, err e
587610
excludeIPs = ranger
588611
return
589612
}
613+
614+
func parsePortsFile(openFile openFileFunc) (result []*scan.PortRange, err error) {
615+
input, err := openFile()
616+
if err != nil {
617+
return
618+
}
619+
defer input.Close()
620+
scanner := bufio.NewScanner(input)
621+
for scanner.Scan() {
622+
line := scanner.Text()
623+
if comment := strings.Index(line, "#"); comment != -1 {
624+
line = line[:comment]
625+
}
626+
line = strings.Trim(line, " ")
627+
if len(line) == 0 {
628+
continue
629+
}
630+
ports, err := parsePortRange(line)
631+
if err != nil {
632+
return nil, err
633+
}
634+
result = append(result, ports)
635+
}
636+
return
637+
}

command/config_test.go

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestIPPortScanCmdOptsInitCliFlags(t *testing.T) {
104104
err := cmd.ParseFlags(strings.Split(
105105
strings.Join([]string{
106106
"--json -i eth0 --srcip 192.168.0.1 --srcmac 00:11:22:33:44:55 -r 500/7s --exit-delay 10s --exclude ips.txt",
107-
"--gwmac 11:22:33:44:55:66 -f ip_file.jsonl -a arp.cache",
107+
"--gwmac 11:22:33:44:55:66 -f ip_file.jsonl -a arp.cache --ports-file ports.txt",
108108
"-p 23-57,71-2733",
109109
}, " "), " "))
110110

@@ -122,6 +122,7 @@ func TestIPPortScanCmdOptsInitCliFlags(t *testing.T) {
122122
require.Equal(t, "arp.cache", opts.arpCacheFile)
123123

124124
require.Equal(t, "23-57,71-2733", opts.rawPortRanges)
125+
require.Equal(t, "ports.txt", opts.portFile)
125126
}
126127

127128
func TestIPPortScanCmdOptsParseRawOptions(t *testing.T) {
@@ -157,11 +158,12 @@ func TestGenericScanCmdOptsInitCliFlags(t *testing.T) {
157158

158159
opts.initCliFlags(cmd)
159160
err := cmd.ParseFlags(strings.Split(
160-
"--json -p 23-57,71-2733 -f ip_file.jsonl -w 300 -r 500/7s --exit-delay 10s --exclude ips.txt", " "))
161+
"--json -p 23-57,71-2733 -f ip_file.jsonl -w 300 -r 500/7s --exit-delay 10s --exclude ips.txt --ports-file ports.txt", " "))
161162

162163
require.NoError(t, err)
163164
require.Equal(t, true, opts.json)
164165
require.Equal(t, "23-57,71-2733", opts.rawPortRanges)
166+
require.Equal(t, "ports.txt", opts.portFile)
165167
require.Equal(t, "ip_file.jsonl", opts.ipFile)
166168
require.Equal(t, 300, opts.workers)
167169
require.Equal(t, "500/7s", opts.rawRateLimit)
@@ -928,6 +930,131 @@ func TestParseExcludeFile(t *testing.T) {
928930
}
929931
}
930932

933+
func TestParsePortsFileWithInvalidFile(t *testing.T) {
934+
t.Parallel()
935+
_, err := parsePortsFile(func() (io.ReadCloser, error) {
936+
return nil, errors.New("open file error")
937+
})
938+
require.Error(t, err)
939+
}
940+
941+
func TestParsePortsFile(t *testing.T) {
942+
t.Parallel()
943+
944+
tests := []struct {
945+
name string
946+
input string
947+
expected []*scan.PortRange
948+
err bool
949+
}{
950+
{
951+
name: "OnePort",
952+
input: "80",
953+
expected: []*scan.PortRange{
954+
{StartPort: 80, EndPort: 80},
955+
},
956+
},
957+
{
958+
name: "OnePortRange",
959+
input: "80-443",
960+
expected: []*scan.PortRange{
961+
{StartPort: 80, EndPort: 443},
962+
},
963+
},
964+
{
965+
name: "TwoPorts",
966+
input: "80\n443",
967+
expected: []*scan.PortRange{
968+
{StartPort: 80, EndPort: 80},
969+
{StartPort: 443, EndPort: 443},
970+
},
971+
},
972+
{
973+
name: "TwoPortRanges",
974+
input: "80-443\n1123-1679",
975+
expected: []*scan.PortRange{
976+
{StartPort: 80, EndPort: 443},
977+
{StartPort: 1123, EndPort: 1679},
978+
},
979+
},
980+
{
981+
name: "ParseError",
982+
input: "abc",
983+
err: true,
984+
},
985+
{
986+
name: "ParseErrorAfterOnePort",
987+
input: "80\nabc",
988+
err: true,
989+
},
990+
{
991+
name: "WithNewLines",
992+
input: "\n\n80\n\n",
993+
expected: []*scan.PortRange{
994+
{StartPort: 80, EndPort: 80},
995+
},
996+
},
997+
{
998+
name: "WithSpaces",
999+
input: " 80 ",
1000+
expected: []*scan.PortRange{
1001+
{StartPort: 80, EndPort: 80},
1002+
},
1003+
},
1004+
{
1005+
name: "WithNewLinesAndSpaces",
1006+
input: "\n \n 80\n\n",
1007+
expected: []*scan.PortRange{
1008+
{StartPort: 80, EndPort: 80},
1009+
},
1010+
},
1011+
{
1012+
name: "WithComment",
1013+
input: "# comment\n80",
1014+
expected: []*scan.PortRange{
1015+
{StartPort: 80, EndPort: 80},
1016+
},
1017+
},
1018+
{
1019+
name: "WithSpaceAndComment",
1020+
input: " # comment\n80",
1021+
expected: []*scan.PortRange{
1022+
{StartPort: 80, EndPort: 80},
1023+
},
1024+
},
1025+
{
1026+
name: "WithCommentOnLine",
1027+
input: "80 # comment",
1028+
expected: []*scan.PortRange{
1029+
{StartPort: 80, EndPort: 80},
1030+
},
1031+
},
1032+
}
1033+
1034+
for _, vtt := range tests {
1035+
tt := vtt
1036+
t.Run(tt.name, func(t *testing.T) {
1037+
t.Parallel()
1038+
1039+
done := make(chan interface{})
1040+
go func() {
1041+
defer close(done)
1042+
1043+
ports, err := parsePortsFile(func() (io.ReadCloser, error) {
1044+
return ioutil.NopCloser(strings.NewReader(tt.input)), nil
1045+
})
1046+
if tt.err {
1047+
require.Error(t, err)
1048+
return
1049+
}
1050+
require.NoError(t, err)
1051+
require.Equal(t, tt.expected, ports)
1052+
}()
1053+
waitDone(t, done)
1054+
})
1055+
}
1056+
}
1057+
9311058
func waitDone(t *testing.T, done <-chan interface{}) {
9321059
t.Helper()
9331060
select {

command/root.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package command
22

33
import (
44
"context"
5+
"fmt"
56
"math/rand"
67
"os"
78
"sync"
@@ -54,7 +55,7 @@ type bpfFilterFunc func(r *scan.Range) (filter string, maxPacketLength int)
5455

5556
type engineConfig struct {
5657
logger log.Logger
57-
scanRange *scan.Range
58+
scanRange scan.Range
5859
exitDelay time.Duration
5960
}
6061

@@ -68,7 +69,7 @@ func withLogger(logger log.Logger) engineConfigOption {
6869

6970
func withScanRange(r *scan.Range) engineConfigOption {
7071
return func(c *engineConfig) {
71-
c.scanRange = r
72+
c.scanRange = *r
7273
}
7374
}
7475

@@ -89,7 +90,7 @@ func newEngineConfig(opts ...engineConfigOption) *engineConfig {
8990
}
9091

9192
type packetScanConfig struct {
92-
*engineConfig
93+
engineConfig
9394
scanMethod scan.PacketMethod
9495
bpfFilter bpfFilterFunc
9596
rateCount int
@@ -101,7 +102,7 @@ type packetScanConfigOption func(c *packetScanConfig)
101102

102103
func withPacketEngineConfig(conf *engineConfig) packetScanConfigOption {
103104
return func(c *packetScanConfig) {
104-
c.engineConfig = conf
105+
c.engineConfig = *conf
105106
}
106107
}
107108

@@ -143,8 +144,25 @@ func newPacketScanConfig(opts ...packetScanConfigOption) *packetScanConfig {
143144
return c
144145
}
145146

147+
func startPortScanEngine(ctx context.Context, conf *packetScanConfig) error {
148+
// BPF filter doesn't accept large list of port ranges
149+
chunkSize := 200
150+
for i := 0; i < len(conf.scanRange.Ports); i += chunkSize {
151+
end := i + chunkSize
152+
if end > len(conf.scanRange.Ports) {
153+
end = len(conf.scanRange.Ports)
154+
}
155+
newConf := *conf
156+
newConf.scanRange.Ports = conf.scanRange.Ports[i:end]
157+
if err := startPacketScanEngine(ctx, &newConf); err != nil {
158+
return err
159+
}
160+
}
161+
return nil
162+
}
163+
146164
func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
147-
r := conf.scanRange
165+
r := &conf.scanRange
148166

149167
// setup network interface to read/write packets
150168
ps, err := afpacket.NewPacketSource(r.Interface.Name, conf.vpnMode)
@@ -154,7 +172,7 @@ func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
154172
defer ps.Close()
155173
err = ps.SetBPFFilter(conf.bpfFilter(r))
156174
if err != nil {
157-
return err
175+
return fmt.Errorf("BPFFilter: %w", err)
158176
}
159177
var rw packet.ReadWriter = ps
160178
// setup rate limit for sending packets
@@ -163,7 +181,7 @@ func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
163181
ratelimit.New(conf.rateCount, ratelimit.Per(conf.rateWindow)))
164182
}
165183
engine := scan.SetupPacketEngine(rw, conf.scanMethod)
166-
return startScanEngine(ctx, engine, conf.engineConfig)
184+
return startScanEngine(ctx, engine, &conf.engineConfig)
167185
}
168186

169187
func startScanEngine(ctx context.Context, engine scan.EngineResulter, conf *engineConfig) error {
@@ -181,7 +199,7 @@ func startScanEngine(ctx context.Context, engine scan.EngineResulter, conf *engi
181199
}()
182200

183201
// start scan
184-
done, errc := engine.Start(ctx, conf.scanRange)
202+
done, errc := engine.Start(ctx, &conf.scanRange)
185203
go func() {
186204
defer cancel()
187205
<-done

command/tcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func newTCPFlagsCmd() *tcpFlagsCmd {
6868
withTCPPacketFlags(tcp.AllFlags),
6969
)
7070

71-
return startPacketScanEngine(ctx, newPacketScanConfig(
71+
return startPortScanEngine(ctx, newPacketScanConfig(
7272
withPacketScanMethod(m),
7373
withPacketBPFFilter(tcp.BPFFilter),
7474
withRateCount(c.opts.rateCount),

command/tcp_fin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func newTCPFINCmd() *tcpFINCmd {
3737
withTCPPacketFlags(tcp.AllFlags),
3838
)
3939

40-
return startPacketScanEngine(ctx, newPacketScanConfig(
40+
return startPortScanEngine(ctx, newPacketScanConfig(
4141
withPacketScanMethod(m),
4242
withPacketBPFFilter(tcp.BPFFilter),
4343
withRateCount(c.opts.rateCount),

0 commit comments

Comments
 (0)