Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: ports file cli flag #111

Merged
merged 1 commit into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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