Skip to content

Commit

Permalink
feature: ports file cli flag (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
v-byte-cpu committed Mar 6, 2022
1 parent cdbbfd4 commit d420294
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 21 deletions.
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ FROM golang:1.17-alpine as builder
RUN apk add --no-cache libpcap-dev libc-dev gcc linux-headers
ADD . /app
WORKDIR /app
RUN go build -ldflags "-w -s" -o /sx
RUN go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o /sx

FROM alpine:3.14
FROM alpine:3.15

RUN apk add libpcap
COPY --from=builder /sx /sx

ENTRYPOINT ["/sx"]
ENTRYPOINT ["/sx"]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ or individual ports:
cat arp.cache | sx tcp -p 22,443 192.168.0.171
```

or use the `--ports-file` option to specify a file with ports or port ranges to scan, one per line.

scan ip/port pairs from a file with JSON output:

```
Expand Down
48 changes: 48 additions & 0 deletions command/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func (o *ipScanCmdOpts) getGatewayMAC(iface *net.Interface, cache *arp.Cache) (m

type ipPortScanCmdOpts struct {
ipScanCmdOpts
portFile string
portRanges []*scan.PortRange

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

func (o *ipPortScanCmdOpts) parseRawOptions() (err error) {
Expand All @@ -322,6 +324,15 @@ func (o *ipPortScanCmdOpts) parseRawOptions() (err error) {
return
}
}
if len(o.portFile) > 0 {
portRanges, err := parsePortsFile(func() (io.ReadCloser, error) {
return os.Open(o.portFile)
})
if err != nil {
return err
}
o.portRanges = append(o.portRanges, portRanges...)
}
return
}

Expand Down Expand Up @@ -359,6 +370,7 @@ func (o *ipPortScanCmdOpts) newIPPortGenerator() (reqgen scan.RequestGenerator)
type genericScanCmdOpts struct {
json bool
ipFile string
portFile string
portRanges []*scan.PortRange
workers int
rateCount int
Expand All @@ -374,6 +386,7 @@ type genericScanCmdOpts struct {
func (o *genericScanCmdOpts) initCliFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.json, "json", false, "enable JSON output")
cmd.Flags().StringVarP(&o.rawPortRanges, "ports", "p", "", "set ports to scan")
cmd.Flags().StringVar(&o.portFile, "ports-file", "", "set file with ports or port ranges to scan, one-per line")
cmd.Flags().StringVarP(&o.ipFile, "file", "f", "", "set JSONL file with ip/port pairs to scan")
cmd.Flags().IntVarP(&o.workers, "workers", "w", defaultWorkerCount, "set workers count")
cmd.Flags().StringVar(&o.rawExcludeFile, "exclude", "",
Expand All @@ -398,6 +411,16 @@ func (o *genericScanCmdOpts) parseRawOptions() (err error) {
return
}
}
if len(o.portFile) > 0 {
portRanges, err := parsePortsFile(func() (io.ReadCloser, error) {
return os.Open(o.portFile)
})
if err != nil {
return err
}
o.portRanges = append(o.portRanges, portRanges...)
}
// TODO parsePortsFile
if len(o.rawRateLimit) > 0 {
if o.rateCount, o.rateWindow, err = parseRateLimit(o.rawRateLimit); err != nil {
return
Expand Down Expand Up @@ -587,3 +610,28 @@ func parseExcludeFile(openFile openFileFunc) (excludeIPs scan.IPContainer, err e
excludeIPs = ranger
return
}

func parsePortsFile(openFile openFileFunc) (result []*scan.PortRange, err error) {
input, err := openFile()
if err != nil {
return
}
defer input.Close()
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
if comment := strings.Index(line, "#"); comment != -1 {
line = line[:comment]
}
line = strings.Trim(line, " ")
if len(line) == 0 {
continue
}
ports, err := parsePortRange(line)
if err != nil {
return nil, err
}
result = append(result, ports)
}
return
}
131 changes: 129 additions & 2 deletions command/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestIPPortScanCmdOptsInitCliFlags(t *testing.T) {
err := cmd.ParseFlags(strings.Split(
strings.Join([]string{
"--json -i eth0 --srcip 192.168.0.1 --srcmac 00:11:22:33:44:55 -r 500/7s --exit-delay 10s --exclude ips.txt",
"--gwmac 11:22:33:44:55:66 -f ip_file.jsonl -a arp.cache",
"--gwmac 11:22:33:44:55:66 -f ip_file.jsonl -a arp.cache --ports-file ports.txt",
"-p 23-57,71-2733",
}, " "), " "))

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

require.Equal(t, "23-57,71-2733", opts.rawPortRanges)
require.Equal(t, "ports.txt", opts.portFile)
}

func TestIPPortScanCmdOptsParseRawOptions(t *testing.T) {
Expand Down Expand Up @@ -157,11 +158,12 @@ func TestGenericScanCmdOptsInitCliFlags(t *testing.T) {

opts.initCliFlags(cmd)
err := cmd.ParseFlags(strings.Split(
"--json -p 23-57,71-2733 -f ip_file.jsonl -w 300 -r 500/7s --exit-delay 10s --exclude ips.txt", " "))
"--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", " "))

require.NoError(t, err)
require.Equal(t, true, opts.json)
require.Equal(t, "23-57,71-2733", opts.rawPortRanges)
require.Equal(t, "ports.txt", opts.portFile)
require.Equal(t, "ip_file.jsonl", opts.ipFile)
require.Equal(t, 300, opts.workers)
require.Equal(t, "500/7s", opts.rawRateLimit)
Expand Down Expand Up @@ -928,6 +930,131 @@ func TestParseExcludeFile(t *testing.T) {
}
}

func TestParsePortsFileWithInvalidFile(t *testing.T) {
t.Parallel()
_, err := parsePortsFile(func() (io.ReadCloser, error) {
return nil, errors.New("open file error")
})
require.Error(t, err)
}

func TestParsePortsFile(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expected []*scan.PortRange
err bool
}{
{
name: "OnePort",
input: "80",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "OnePortRange",
input: "80-443",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 443},
},
},
{
name: "TwoPorts",
input: "80\n443",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
{StartPort: 443, EndPort: 443},
},
},
{
name: "TwoPortRanges",
input: "80-443\n1123-1679",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 443},
{StartPort: 1123, EndPort: 1679},
},
},
{
name: "ParseError",
input: "abc",
err: true,
},
{
name: "ParseErrorAfterOnePort",
input: "80\nabc",
err: true,
},
{
name: "WithNewLines",
input: "\n\n80\n\n",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "WithSpaces",
input: " 80 ",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "WithNewLinesAndSpaces",
input: "\n \n 80\n\n",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "WithComment",
input: "# comment\n80",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "WithSpaceAndComment",
input: " # comment\n80",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
{
name: "WithCommentOnLine",
input: "80 # comment",
expected: []*scan.PortRange{
{StartPort: 80, EndPort: 80},
},
},
}

for _, vtt := range tests {
tt := vtt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

done := make(chan interface{})
go func() {
defer close(done)

ports, err := parsePortsFile(func() (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader(tt.input)), nil
})
if tt.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.expected, ports)
}()
waitDone(t, done)
})
}
}

func waitDone(t *testing.T, done <-chan interface{}) {
t.Helper()
select {
Expand Down
34 changes: 26 additions & 8 deletions command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"context"
"fmt"
"math/rand"
"os"
"sync"
Expand Down Expand Up @@ -54,7 +55,7 @@ type bpfFilterFunc func(r *scan.Range) (filter string, maxPacketLength int)

type engineConfig struct {
logger log.Logger
scanRange *scan.Range
scanRange scan.Range
exitDelay time.Duration
}

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

func withScanRange(r *scan.Range) engineConfigOption {
return func(c *engineConfig) {
c.scanRange = r
c.scanRange = *r
}
}

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

type packetScanConfig struct {
*engineConfig
engineConfig
scanMethod scan.PacketMethod
bpfFilter bpfFilterFunc
rateCount int
Expand All @@ -101,7 +102,7 @@ type packetScanConfigOption func(c *packetScanConfig)

func withPacketEngineConfig(conf *engineConfig) packetScanConfigOption {
return func(c *packetScanConfig) {
c.engineConfig = conf
c.engineConfig = *conf
}
}

Expand Down Expand Up @@ -143,8 +144,25 @@ func newPacketScanConfig(opts ...packetScanConfigOption) *packetScanConfig {
return c
}

func startPortScanEngine(ctx context.Context, conf *packetScanConfig) error {
// BPF filter doesn't accept large list of port ranges
chunkSize := 200
for i := 0; i < len(conf.scanRange.Ports); i += chunkSize {
end := i + chunkSize
if end > len(conf.scanRange.Ports) {
end = len(conf.scanRange.Ports)
}
newConf := *conf
newConf.scanRange.Ports = conf.scanRange.Ports[i:end]
if err := startPacketScanEngine(ctx, &newConf); err != nil {
return err
}
}
return nil
}

func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
r := conf.scanRange
r := &conf.scanRange

// setup network interface to read/write packets
ps, err := afpacket.NewPacketSource(r.Interface.Name, conf.vpnMode)
Expand All @@ -154,7 +172,7 @@ func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
defer ps.Close()
err = ps.SetBPFFilter(conf.bpfFilter(r))
if err != nil {
return err
return fmt.Errorf("BPFFilter: %w", err)
}
var rw packet.ReadWriter = ps
// setup rate limit for sending packets
Expand All @@ -163,7 +181,7 @@ func startPacketScanEngine(ctx context.Context, conf *packetScanConfig) error {
ratelimit.New(conf.rateCount, ratelimit.Per(conf.rateWindow)))
}
engine := scan.SetupPacketEngine(rw, conf.scanMethod)
return startScanEngine(ctx, engine, conf.engineConfig)
return startScanEngine(ctx, engine, &conf.engineConfig)
}

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

// start scan
done, errc := engine.Start(ctx, conf.scanRange)
done, errc := engine.Start(ctx, &conf.scanRange)
go func() {
defer cancel()
<-done
Expand Down
2 changes: 1 addition & 1 deletion command/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func newTCPFlagsCmd() *tcpFlagsCmd {
withTCPPacketFlags(tcp.AllFlags),
)

return startPacketScanEngine(ctx, newPacketScanConfig(
return startPortScanEngine(ctx, newPacketScanConfig(
withPacketScanMethod(m),
withPacketBPFFilter(tcp.BPFFilter),
withRateCount(c.opts.rateCount),
Expand Down
2 changes: 1 addition & 1 deletion command/tcp_fin.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func newTCPFINCmd() *tcpFINCmd {
withTCPPacketFlags(tcp.AllFlags),
)

return startPacketScanEngine(ctx, newPacketScanConfig(
return startPortScanEngine(ctx, newPacketScanConfig(
withPacketScanMethod(m),
withPacketBPFFilter(tcp.BPFFilter),
withRateCount(c.opts.rateCount),
Expand Down
Loading

0 comments on commit d420294

Please sign in to comment.