├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── appveyor.yml ├── cmd └── tcpscan │ ├── LICENSE │ ├── cmd │ └── root.go │ └── tcpscan.go ├── com.github.adedayo.libpcap.bpf-helper.plist ├── com.github.adedayo.libpcap.bpf-helper.sh ├── definitions.go ├── fix-bpf-permissions.sh ├── go.mod ├── go.sum ├── model.go ├── pcap_support_darwin.go ├── pcap_support_linux.go ├── persistence.go ├── portscan.go ├── portscan_test.go └── scan-service.go /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/tcpscan/tcpscan.go 3 | env: 4 | - CGO_ENABLED=1 5 | goos: 6 | - darwin 7 | goarch: 8 | - amd64 9 | archives: 10 | - id: darwin 11 | replacements: 12 | darwin: Darwin 13 | amd64: x86_64 14 | checksum: 15 | name_template: 'checksums.txt' 16 | snapshot: 17 | name_template: "{{ .Tag }}-next" 18 | changelog: 19 | sort: asc 20 | filters: 21 | exclude: 22 | - '^docs:' 23 | - '^test:' 24 | before: 25 | hooks: 26 | - go mod download 27 | brews: 28 | - 29 | github: 30 | owner: adedayo 31 | name: homebrew-tap 32 | homepage: "https://github.com/adedayo/tcpscan" 33 | description: "TCPScan is a simple utility for discovering open (or closed) TCP ports on servers. It uses gopacket(https://github.com/google/gopacket) to craft SYN packets, listening asynchronously for (SYN-)ACK or RST responses without completing the full TCP handshake. TCPScan uses goroutines for asynchronous scans and it searches for the most likely listening ports first, using NMap's port frequency ordering. Anecdotal results show that TCPScan is really fast!" 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | gobuild_args: -a -tags pcap -ldflags '-w -extldflags "-static"' 5 | before_install: 6 | - sudo apt-get install git cmake flex bison 7 | - export PCAP_VERSION=1.9.0 8 | - wget https://github.com/the-tcpdump-group/libpcap/archive/libpcap-$PCAP_VERSION.tar.gz 9 | - mkdir libpcap 10 | - tar xzf libpcap-$PCAP_VERSION.tar.gz -C libpcap --strip-components 1 11 | - pushd libpcap 12 | - cmake . && make && sudo make install 13 | - popd 14 | - go get ./... 15 | script: 16 | - make 17 | deploy: 18 | provider: releases 19 | api_key: 20 | secure: gECiYO8LFe749Hi44TRCrigHs7Ryf0mZB1IcCCXM6XE2c9nzalydqpNfGMmqNez5ddBVcHWj+E6CxtRBm1taFdSjrAxq5D0IOyv3HqrpowpdtahX8OIC08OThiM5LPQh5ch66bayKd5v8Lemspzc6SnRj3Qti6yv5OsTc7/ccIioJHJQ3kFhXE2Lr1hT2UKQHk/SrAjDX5mtId1CmJOq5VyQo5q/r2XSGjZyILgfE0d6rG3qHmIA5p9rw9uJYArZcQAmUXBQbBIcIWBeMYd8cLQu8nXia48M298c1zEGHaKMZflhWNFlfDxIPXb12hcfqAKY0q5GjzvIiQSdgaaoQiIoDOAFqg76nhNBsFMWyDvwaqC8NzSbNIEG35jslquqFa8qvjSwP26vHR8gV/fW60q6z0SkOK+/3lkejUvUYI7fOu1UCCHnXXsHI77ojSKeO+dXQotNYdOijWUATmrh0mrpEfLyK3nho61TVuDYjLsGwR6gZTFQV+jniHX1pTsnBTFPs7KFz8jyn5FXvG4vKcLeZAmc7JVaiZjo4TYVn77ApRvNI6llsvlxuhGP6orfwV92/XGbhjF2t6TlU88gvTwb6FrXaS3FqWTzMmrCERelcrdY0RvjPk4863xHvGJWWsPx2QuOZ4Hgn/w2QbDezfSreYNNc2LVQ6b6wTWVs9E= 21 | file_glob: true 22 | file: tcpscan*.tar.gz 23 | skip_cleanup: true 24 | on: 25 | tags: true 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Adedayo Adetoye (aka Dayo) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | BUILDFLAGS += "" 3 | else 4 | UNAME_S := $(shell uname -s) 5 | VERSION_TAG := $(shell git describe --abbrev=0 --tags) 6 | OUTFILE := "tcpscan_$(VERSION_TAG)_$(UNAME_S)_x86_64.tar.gz" 7 | ifeq ($(UNAME_S),Linux) 8 | BUILDFLAGS += -a -ldflags '-w -extldflags "-static"' 9 | endif 10 | ifeq ($(UNAME_S),Darwin) 11 | BUILDFLAGS += -a 12 | endif 13 | endif 14 | 15 | all: tar 16 | 17 | tar: build 18 | echo $(VERSION_TAG) $(OUTFILE) 19 | tar cvf $(OUTFILE) tcpscan -C $(GOPATH)/bin 20 | build: 21 | ifeq ($(UNAME_S),Linux) 22 | sed -i "s/0.0.0/$(VERSION_TAG)/g" "cmd/tcpscan/tcpscan.go" 23 | endif 24 | go build $(BUILDFLAGS) github.com/adedayo/tcpscan/cmd/tcpscan 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/adedayo/tcpscan.svg?branch=master)](https://travis-ci.org/adedayo/tcpscan) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/adedayo/tcpscan)](https://goreportcard.com/report/github.com/adedayo/tcpscan) 3 | ![GitHub release](https://img.shields.io/github/release/adedayo/tcpscan.svg) 4 | [![GitHub license](https://img.shields.io/github/license/adedayo/tcpscan.svg)](https://github.com/adedayo/tcpscan/blob/master/LICENSE) 5 | 6 | # TCPScan 7 | TCPScan is a simple utility for discovering open (or closed) TCP ports on servers. It uses `gopacket`(https://github.com/google/gopacket) to craft SYN packets, listening asynchronously for (SYN-)ACK or RST responses without completing the full TCP handshake. TCPScan uses goroutines for asynchronous scans and it searches for the most likely listening ports first, using NMap's "port frequency" ordering. Anecdotal results show that TCPScan is fast! 8 | 9 | TCPScan is not a replacement for the awesome NMap tool, but it promises to be a useful library for go applications that need a fast and simple TCP port scanning capability. 10 | 11 | ## Using it as a command-line tool 12 | TCPScan is also available as a command-line tool. 13 | 14 | ### Installation 15 | Prebuilt binaries may be found for your operating system here: https://github.com/adedayo/tcpscan/releases 16 | 17 | For macOS X, you could install via brew as follows: 18 | ```bash 19 | brew tap adedayo/tap 20 | brew install tcpscan 21 | ``` 22 | 23 | ### Scanning CIDR ranges 24 | 25 | ```bash 26 | tcpscan 192.168.2.5/30 10.11.12.13/31 27 | ``` 28 | 29 | For JSON-formatted output simply add the `--json` or `-j` flag: 30 | 31 | ```bash 32 | tcpscan --json 192.168.2.5/30 10.11.12.13/31 33 | ``` 34 | Depending on the fidelity of the network being scanned or the size of CIDR ranges, it may be expedient to adjust the scan timeout accordingly with the `--timeout` or `-t` flag, which indicates the number of seconds to wait for ACK or RST responses as follows: 35 | 36 | ```bash 37 | tcpscan --json --timeout 5 192.168.2.5/30 10.11.12.13/31 38 | ``` 39 | 40 | Note that scans generally run faster with shorter timeouts, but you may be sacrificing accuracy on slow networks or for large CIDR ranges. 41 | 42 | ### Command line options 43 | 44 | ```bash 45 | Usage: 46 | tcpscan [flags] 47 | 48 | Examples: 49 | tcpscan 8.8.8.8/32 10.10.10.1/30 50 | 51 | Flags: 52 | -h, --help help for tcpscan 53 | -j, --json generate JSON output 54 | -q, --quiet control whether to produce a running commentary of intermediate results or stay quiet till the end 55 | -r, --rate int the rate (in packets per second) that we should send SYN scan packets. This influences overall scan time, but be careful not to overwhelm your network (default 1000) 56 | -s, --service string[="data/config/TCPScanConfig.yml"] run tcpscan as a service (default "data/config/TCPScanConfig.yml") 57 | -t, --timeout int TIMEOUT (in seconds) to adjust how much we are willing to wait for servers to come back with responses. Smaller timeout sacrifices accuracy for speed (default 5) 58 | --version version for tcpscan 59 | ``` 60 | 61 | ## Using TCPScan as a library 62 | In order to start, go get this repository: 63 | ```go 64 | go get github.com/adedayo/tcpscan 65 | ``` 66 | 67 | ### Example 68 | In your code simply import as usual and enjoy: 69 | 70 | ```go 71 | package main 72 | 73 | import 74 | ( 75 | "fmt" 76 | "github.com/adedayo/tcpscan" 77 | ) 78 | 79 | func main() { 80 | cidr := "8.8.8.8/32" 81 | config := portscan.ScanConfig { 82 | Timeout: 5, 83 | } 84 | result := portscan.ScanCIDR(config, cidr) 85 | for ack := range result { 86 | fmt.Printf("%s:\tPort %s(%s) is %s\n", ack.Host, ack.Port, ack.GetServiceName(), status(ack)) 87 | } 88 | } 89 | 90 | func status(ack portscan.PortACK) string { 91 | if ack.IsClosed() { 92 | return "Closed" 93 | } 94 | if ack.IsOpen() { 95 | return "Open" 96 | } 97 | return "of Unknown Status" 98 | } 99 | 100 | ``` 101 | This should produce an output similar to the following: 102 | ``` 103 | 8.8.8.8: Port 443(https) is Open 104 | 8.8.8.8: Port 53(domain) is Open 105 | 8.8.8.8: Port 853(domain-s) is Open 106 | ``` 107 | 108 | ## An issue on macOS 109 | You may encounter errors such as 110 | ```bash 111 | panic: en0: You don't have permission to capture on that device ((cannot open BPF device) /dev/bpf0: Permission denied) 112 | ``` 113 | Fix the permission problem permanently by using the "Wireshark" approach of pre-allocating _/dev/bpf*_, and changing their permissions so that the _admin_ group can read from and write packets to the devices. I have provided the _fix-bpf-permissions.sh_ script to simplify the steps, you can run it as shown below. It will ask for your password for the privileged part of the script, but read the script to satisfy yourself that you trust what it is doing! You care about security, right? 114 | 115 | ```bash 116 | curl -O https://raw.githubusercontent.com/adedayo/tcpscan/master/fix-bpf-permissions.sh 117 | chmod +x fix-bpf-permissions.sh 118 | ./fix-bpf-permissions.sh 119 | ``` 120 | 121 | You should be good to go! You may need to reboot once, but this works across reboots. Note that this is a common problem for tools such as Wireshark, TCPDump etc. that need to read from or write to /dev/bpf*. This solution should fix the problem for all of them - the idea was actually stolen from Wireshark with some modifications :-). 122 | 123 | ## Running as non-root on Linux 124 | You ideally want to be able to run `tcpscan` as an ordinary user, say, `my_user`, but since `tcpscan` sends raw packets you need to adjust capabilities to allow it to do so. The following may be necessary: 125 | 126 | Ensure the following two lines are in _/etc/security/capability.conf_ 127 | ```bash 128 | cap_net_admin my_user 129 | none * 130 | ``` 131 | 132 | Also, in _/etc/pam.d/login_ add the following 133 | ```bash 134 | auth required pam_cap.so 135 | ``` 136 | 137 | Finally, grant the capability to the `tcpscan` file (assuming _/path/to_ is the absolute path to your `tcpscan` binary) 138 | ```bash 139 | setcap cap_net_raw,cap_net_admin=eip /path/to/tcpscan 140 | ``` 141 | ## License 142 | BSD 3-Clause License -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}.{build}' 2 | image: 3 | - Visual Studio 2017 4 | 5 | clone_folder: c:\gopath\src\github.com\adedayo\tcpscan 6 | 7 | environment: 8 | global: 9 | CC: gcc.exe 10 | MYAPP: tcpscan 11 | VERSION: ${APPVEYOR_REPO_TAG_NAME} 12 | GOARCH: amd64 13 | ARCH: x86_64 14 | matrix: 15 | - platform: x64 16 | configuration: Release 17 | GETH_ARCH: amd64 18 | MSYS2_ARCH: x86_64 19 | # MSYS2_BITS: 64 20 | MSYSTEM: MINGW64 21 | PATH: C:\msys64\mingw64\bin\;C:\msys64\;C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\;C:\Program Files (x86)\NSIS\;%PATH% #make gcc available 22 | stack: go 1.11 23 | 24 | platform: x64 25 | 26 | install: 27 | - gcc --version 28 | # - set PATH=%WIX%\bin;%PATH% #this is for WiX 29 | - choco install winflexbison -y 30 | - win_flex --version 31 | - win_bison --version 32 | - appveyor DownloadFile https://github.com/the-tcpdump-group/libpcap/archive/libpcap-1.9.0.zip 33 | - dir 34 | - 7z x libpcap-1.9.0.zip 35 | - move libpcap-libpcap-1.9.0 libpcap 36 | - cd libpcap 37 | # - choco install go-msi -y 38 | # - appveyor DownloadFile http://www.winpcap.org/install/bin/WpdPack_4_1_2.zip 39 | # - 7z x .\WpdPack_4_1_2.zip -oWin32 40 | # - cd .. 41 | # - cmake . 42 | 43 | build_script: 44 | - type NUL >.devel 45 | - md build 46 | - cd build 47 | # - cmake -DCMAKE_PREFIX_PATH=..\Win32\WpdPack -G"Visual Studio 12 2013" .. 48 | # - msbuild -nologo pcap.sln 49 | - cmake -DCMAKE_AR=ar.exe -DCMAKE_C_COMPILER=gcc.exe -DCMAKE_CXX_COMPILER=g++.exe -DCMAKE_MAKE_PROGRAM=make.exe -DCMAKE_PREFIX_PATH=..\libpcap -G"MSYS Makefiles" .. 50 | - msbuild /m /nologo /p:Configuration=Release pcap.sln 51 | # copy file to where cgo is expecting them to be, see https://github.com/google/gopacket/blob/c5b434497bea7a3417dc45b84c70282c4a5b3b3d/pcap/pcap.go 52 | - echo f | xcopy Release\pcap.exp C:\WpdPack\Lib\x64\wpcap.exp 53 | - echo f | xcopy Release\pcap.lib C:\WpdPack\Lib\x64\wpcap.lib 54 | - echo f | xcopy Release\pcap_static.lib C:\WpdPack\Lib\x64\wpcap_static.lib 55 | - echo d | xcopy ..\pcap C:\WpdPack\Include 56 | - echo d | xcopy ..\pcap C:\WpdPack\Include\pcap # some files are expecting the headers under pcap directory 57 | - cd .. 58 | - go get ./... 59 | - set BUILDFLAGS=-a -v -x -ldflags '-v -extldflags "-static"' #attempt to set static linker 60 | - go build %BUILDFLAGS% github.com\adedayo\tcpscan\cmd\tcpscan 61 | - dir 62 | 63 | # - set PATH=C:\Program Files\go-msi\;%PATH% #go-msi path 64 | # - go-msi make --msi tcpscan-%VERSION%-%GOARCH%.msi --version %VERSION% --arch %GOARCH% 65 | - 7z a %MYAPP%-%VERSION%-Windows-%ARCH%.zip *.exe 66 | - dir 67 | 68 | artifacts: 69 | - path: libpcap\tcpscan*.zip 70 | name: zip-x64 71 | 72 | deploy: 73 | - provider: GitHub 74 | artifact: zip-x64 75 | draft: false 76 | prerelease: false 77 | description: "Release ${VERSION}" 78 | auth_token: 79 | secure: WdtmPBg+FpFzcaV8TifOOP6BvkGo1OpvRAfWZv3aZJjVD5PdE/xLrMd8zhrnAxzc 80 | on: 81 | APPVEYOR_REPO_TAG: true 82 | 83 | 84 | 85 | # before_build: 86 | # - go get ./... 87 | # - go build github.com\adedayo\tcpscan\cmd\tcpscan 88 | 89 | # before_test: 90 | # - go vet ./... 91 | 92 | # test_script: 93 | # - go test ./... -------------------------------------------------------------------------------- /cmd/tcpscan/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Adedayo Adetoye (aka Dayo) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /cmd/tcpscan/cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Adedayo Adetoye (aka Dayo) 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its contributors 15 | // may be used to endorse or promote products derived from this software 16 | // without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | // POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package cmd 31 | 32 | import ( 33 | "encoding/json" 34 | "fmt" 35 | "net" 36 | "os" 37 | "sort" 38 | "strconv" 39 | "strings" 40 | "time" 41 | 42 | "github.com/adedayo/cidr" 43 | portscan "github.com/adedayo/tcpscan" 44 | "github.com/spf13/cobra" 45 | ) 46 | 47 | var ( 48 | cfgFile, iface, service string 49 | jsonOut, quiet bool 50 | timeout, rate int 51 | ) 52 | 53 | // rootCmd represents the base command when called without any subcommands 54 | var rootCmd = &cobra.Command{ 55 | Use: "tcpscan", 56 | Short: "Scan for open TCP ports on servers", 57 | Example: "tcpscan 8.8.8.8/32 10.10.10.1/30", 58 | RunE: runner, 59 | } 60 | 61 | // Execute adds all child commands to the root command and sets flags appropriately. 62 | // This is called by main.main(). It only needs to happen once to the rootCmd. 63 | func Execute(version string) { 64 | rootCmd.Version = version 65 | rootCmd.Long = fmt.Sprintf(`Scan for open (or closed) TCP ports on servers. 66 | 67 | Version: %s 68 | 69 | Author: Adedayo Adetoye (Dayo) `, version) 70 | if err := rootCmd.Execute(); err != nil { 71 | fmt.Println(err) 72 | os.Exit(1) 73 | } 74 | } 75 | 76 | func init() { 77 | configPath := portscan.TCPScanConfigPath 78 | app := "tcpscan" 79 | rootCmd.Flags().BoolVarP(&jsonOut, "json", "j", false, "generate JSON output") 80 | rootCmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "control whether to produce a running commentary of intermediate results or stay quiet till the end") 81 | rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 5, "TIMEOUT (in seconds) to adjust how much we are willing to wait for servers to come back with responses. Smaller timeout sacrifices accuracy for speed") 82 | rootCmd.Flags().IntVarP(&rate, "rate", "r", 1000, "the rate (in packets per second) that we should send SYN scan packets. This influences overall scan time, but be careful not to overwhelm your network") 83 | rootCmd.Flags().StringVarP(&service, "service", "s", configPath, fmt.Sprintf("run %s as a service", app)) 84 | rootCmd.Flag("service").NoOptDefVal = configPath 85 | } 86 | 87 | func runner(cmd *cobra.Command, args []string) error { 88 | // 89 | if len(args) == 0 && !cmd.Flag("service").Changed { 90 | return cmd.Usage() 91 | } 92 | 93 | if cmd.Flag("service").Changed { // run as a service 94 | portscan.Service(service) 95 | return nil 96 | } 97 | 98 | if !jsonOut { 99 | fmt.Printf("Starting TCPScan %s (%s)\n", cmd.Version, "https://github.com/adedayo/tcpscan") 100 | } 101 | t := time.Now() 102 | scan := make(map[string]portscan.PortACK) 103 | config := portscan.ScanConfig{ 104 | Timeout: timeout, 105 | PacketsPerSecond: rate, 106 | Quiet: quiet, 107 | Interface: iface, 108 | } 109 | for ack := range portscan.ScanCIDR(config, args...) { 110 | key := ack.Host + ack.Port 111 | if _, present := scan[key]; !present { 112 | scan[key] = ack 113 | } 114 | } 115 | 116 | var portAckList []portscan.PortACK 117 | for k := range scan { 118 | portAckList = append(portAckList, scan[k]) 119 | } 120 | 121 | sort.Sort(portscan.PortAckSorter(portAckList)) 122 | 123 | if jsonOut { 124 | outputJSON(portAckList) 125 | } else { 126 | outputText(portAckList) 127 | } 128 | hosts := []string{} 129 | for _, h := range args { 130 | hosts = append(hosts, cidr.Expand(h)...) 131 | } 132 | fmt.Printf("Scanned %d hosts in %f seconds\n", len(hosts), time.Since(t).Seconds()) 133 | return nil 134 | } 135 | 136 | func outputJSON(ports []portscan.PortACK) { 137 | hostnames := make(map[string][]string) 138 | results := make(map[string]jsonResult) 139 | for _, ack := range ports { 140 | ip := ack.Host 141 | if _, present := hostnames[ip]; !present { 142 | h, err := net.LookupAddr(ip) 143 | if err != nil { 144 | hostnames[ip] = []string{} 145 | } else { 146 | hostnames[ip] = h 147 | } 148 | } 149 | 150 | if res, present := results[ip]; !present { 151 | res = jsonResult{ 152 | IP: ip, 153 | Hostnames: hostnames[ip], 154 | Ports: []portState{}, 155 | } 156 | results[ip] = res 157 | } 158 | 159 | result := results[ip] 160 | if port, err := strconv.Atoi(ack.Port); err == nil { 161 | result.Ports = append(result.Ports, portState{ 162 | Port: port, 163 | Service: ack.GetServiceName(), 164 | State: ack.Status(), 165 | }) 166 | results[ip] = result 167 | } 168 | } 169 | data := []jsonResult{} 170 | for _, v := range results { 171 | data = append(data, v) 172 | } 173 | if out, err := json.MarshalIndent(data, "", " "); err == nil { 174 | 175 | println(string(out)) 176 | } 177 | } 178 | 179 | func outputText(ports []portscan.PortACK) { 180 | result := "" 181 | current := "" 182 | hostName := "" 183 | for _, p := range ports { 184 | if p.Host != current { 185 | current = p.Host 186 | h, err := net.LookupAddr(p.Host) 187 | if err != nil { 188 | hostName = "" 189 | } else { 190 | hostName = fmt.Sprintf("(%s)", strings.Join(h, ", ")) 191 | } 192 | result += fmt.Sprintf("\nTCPScan result for %s %s\n", p.Host, hostName) 193 | result += fmt.Sprintf("%-6s %-10s %s\n", "PORT", "STATE", "SERVICE") 194 | } 195 | result += fmt.Sprintf("%-6s %-10s %s\n", p.Port, p.Status(), p.GetServiceName()) 196 | } 197 | println(result) 198 | } 199 | 200 | func status(ack portscan.PortACK) string { 201 | if ack.IsClosed() { 202 | return "Closed" 203 | } 204 | if ack.IsOpen() { 205 | return "Open" 206 | } 207 | return "Unknown Status" 208 | } 209 | 210 | type jsonResult struct { 211 | IP string 212 | Hostnames []string 213 | Ports []portState 214 | } 215 | 216 | type portState struct { 217 | Port int 218 | State string 219 | Service string 220 | } 221 | -------------------------------------------------------------------------------- /cmd/tcpscan/tcpscan.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Adedayo Adetoye (aka Dayo) 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its contributors 15 | // may be used to endorse or promote products derived from this software 16 | // without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | // POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package main 31 | 32 | import "github.com/adedayo/tcpscan/cmd/tcpscan/cmd" 33 | 34 | var ( 35 | version = "0.0.0" // deployed version will be taken from release tags 36 | ) 37 | 38 | func main() { 39 | cmd.Execute(version) 40 | } 41 | -------------------------------------------------------------------------------- /com.github.adedayo.libpcap.bpf-helper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.adedayo.libpcap.bpf-helper 7 | ProgramArguments 8 | 9 | /Library/PrivilegedHelperTools/com.github.adedayo.libpcap.bpf-helper.sh 10 | 11 | RunAtLoad 12 | 13 | 14 | -------------------------------------------------------------------------------- /com.github.adedayo.libpcap.bpf-helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Adapted from https://github.com/boundary/wireshark/blob/master/packaging/macosx/ChmodBPF/ChmodBPF 4 | FORCE_CREATE_BPF_MAX=256 5 | SYSCTL_MAX=$( sysctl -n debug.bpf_maxdevices ) 6 | if [ "$FORCE_CREATE_BPF_MAX" -gt "$SYSCTL_MAX" ] ; then 7 | FORCE_CREATE_BPF_MAX=$SYSCTL_MAX 8 | fi 9 | 10 | syslog -s -l notice "ChmodBPF: Forcing creation and setting permissions for /dev/bpf*" 11 | 12 | CUR_DEV=0 13 | while [ "$CUR_DEV" -lt "$FORCE_CREATE_BPF_MAX" ] ; do 14 | # Try to do the minimum necessary to trigger the next device. 15 | read -n 0 < /dev/bpf$CUR_DEV > /dev/null 2>&1 16 | CUR_DEV=$(( $CUR_DEV + 1 )) 17 | done 18 | 19 | chgrp admin /dev/bpf* 20 | chmod g+rw /dev/bpf* 21 | -------------------------------------------------------------------------------- /fix-bpf-permissions.sh: -------------------------------------------------------------------------------- 1 | curl -O https://raw.githubusercontent.com/adedayo/tcpscan/master/com.github.adedayo.libpcap.bpf-helper.sh 2 | curl -O https://raw.githubusercontent.com/adedayo/tcpscan/master/com.github.adedayo.libpcap.bpf-helper.plist 3 | 4 | sudo sh -c "mkdir -p /Library/PrivilegedHelperTools;\ 5 | mv com.github.adedayo.libpcap.bpf-helper.sh /Library/PrivilegedHelperTools/;\ 6 | mv com.github.adedayo.libpcap.bpf-helper.plist /Library/LaunchDaemons/;\ 7 | chown root:wheel /Library/PrivilegedHelperTools/com.github.adedayo.libpcap.bpf-helper.sh;\ 8 | chown root:wheel /Library/LaunchDaemons/com.github.adedayo.libpcap.bpf-helper.plist;\ 9 | chmod 755 /Library/PrivilegedHelperTools/com.github.adedayo.libpcap.bpf-helper.sh;\ 10 | launchctl load -w /Library/LaunchDaemons/com.github.adedayo.libpcap.bpf-helper.plist" 11 | 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adedayo/tcpscan 2 | 3 | require ( 4 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7 // indirect 5 | github.com/adedayo/cidr v0.1.5 6 | github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 7 | github.com/dgraph-io/badger v2.0.0-rc2+incompatible 8 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f // indirect 9 | github.com/dustin/go-humanize v1.0.0 // indirect 10 | github.com/golang/protobuf v1.2.0 // indirect 11 | github.com/google/go-cmp v0.2.0 // indirect 12 | github.com/google/gopacket v1.1.16 13 | github.com/jackpal/gateway v1.0.5 14 | github.com/mdlayher/raw v0.0.0-20181016155347-fa5ef3332ca9 // indirect 15 | github.com/pkg/errors v0.8.1 // indirect 16 | github.com/sirupsen/logrus v1.3.0 17 | github.com/spf13/cobra v0.0.5 18 | github.com/uber-go/atomic v1.3.2 // indirect 19 | go.uber.org/atomic v1.3.2 // indirect 20 | go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 21 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect 22 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 23 | gopkg.in/yaml.v2 v2.2.2 24 | ) 25 | 26 | go 1.13 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7 h1:PqzgE6kAMi81xWQA2QIVxjWkFHptGgC547vchpUbtFo= 2 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/adedayo/cidr v0.1.3 h1:SrjHm2g80gq+ajZR/npN3p0C+hJs1iulnpuSPCy2PlQ= 5 | github.com/adedayo/cidr v0.1.3/go.mod h1:By6g82fmUcv8/Z/6JcDs1D4wO4gc/Ookb842bVCV6Io= 6 | github.com/adedayo/cidr v0.1.5 h1:O6N8M2CPOT7LAy2upOHnQ4YIKHD+VXAQc8/OalCsmnU= 7 | github.com/adedayo/cidr v0.1.5/go.mod h1:By6g82fmUcv8/Z/6JcDs1D4wO4gc/Ookb842bVCV6Io= 8 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 9 | github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI= 10 | github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw= 11 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 12 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 13 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 14 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dgraph-io/badger v2.0.0-rc2+incompatible h1:6fXfqViMStaKb73bxivvD9hgwYdWyiEKzwWNVkXhslE= 18 | github.com/dgraph-io/badger v2.0.0-rc2+incompatible/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= 19 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f h1:dDxpBYafY/GYpcl+LS4Bn3ziLPuEdGRkRjYAbSlWxSA= 20 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 21 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 22 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 23 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 28 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 29 | github.com/google/gopacket v1.1.16 h1:u6Afvia5C5srlLcbTwpHaFW918asLYPxieziOaWwz8M= 30 | github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw= 31 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 32 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 33 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 34 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 35 | github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= 36 | github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= 37 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 38 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 39 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 40 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 41 | github.com/mdlayher/raw v0.0.0-20181016155347-fa5ef3332ca9 h1:tOtO8DXiNGj9NshRKHWiZuGlSldPFzFCFYhNtsKTBCs= 42 | github.com/mdlayher/raw v0.0.0-20181016155347-fa5ef3332ca9/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY= 43 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 44 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 45 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 46 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 47 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 48 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 49 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 50 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 54 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= 55 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 56 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 57 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 58 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 59 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 60 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 61 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 62 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 63 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 64 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 65 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 66 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 67 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 69 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 70 | github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= 71 | github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= 72 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 73 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 74 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 75 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 76 | go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 77 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 78 | go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 h1:d9qaMM+ODpCq+9We41//fu/sHsTnXcrqd1en3x+GKy4= 79 | go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= 80 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 81 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= 82 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 84 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 85 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 86 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 87 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 88 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= 91 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 93 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 99 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | //TCPScanConfig config data structure for the scanner service 15 | type TCPScanConfig struct { 16 | DailySchedules []string `yaml:"dailySchedules"` // in the format 13:45, 01:20 etc 17 | IsProduction bool `yaml:"isProduction"` 18 | PacketsPerSecond int `yaml:"packetsPerSecond"` 19 | Timeout int `yaml:"timeout"` 20 | CIDRRanges []string `yaml:"cidrRanges"` 21 | } 22 | 23 | //ScanRequest is a model to describe a given TLS Audit scan 24 | type ScanRequest struct { 25 | CIDRs []string 26 | Config ScanConfig 27 | Day string //Date the scan was run in the format yyyy-mm-dd 28 | ScanID string //Non-empty ScanID means this is a ScanRequest to resume an existing, possibly incomplete, scan 29 | } 30 | 31 | //PersistedScanRequest persisted version of ScanRequest 32 | type PersistedScanRequest struct { 33 | Request ScanRequest 34 | Hosts []string 35 | ScanStart time.Time 36 | ScanEnd time.Time 37 | Progress int 38 | } 39 | 40 | //ScanConfig describes details of how the port scan should be carried out 41 | type ScanConfig struct { 42 | //How long to wait listening for TCP ACK/RST responses 43 | Timeout int 44 | //Number of Packets per Second to send out during scan 45 | PacketsPerSecond int 46 | //Should a running commentary of results be generated? 47 | Quiet bool 48 | //If not empty, indicates which network interface to use, bypassing automated guessing 49 | Interface string 50 | } 51 | 52 | //PortACK describes a port with an ACK after a TCP SYN request 53 | type PortACK struct { 54 | Host string 55 | Port string 56 | RST bool 57 | SYN bool 58 | } 59 | 60 | //IsClosed determines whether the port is filtered by e.g. by a firewall 61 | func (p PortACK) IsClosed() bool { 62 | return p.RST && !p.SYN 63 | } 64 | 65 | //IsOpen determines whether the port is open or not 66 | func (p PortACK) IsOpen() bool { 67 | return !p.RST || p.SYN 68 | } 69 | 70 | //Status is a string representation of the port status 71 | func (p PortACK) Status() string { 72 | if p.IsOpen() { 73 | return "open" 74 | } 75 | if p.IsClosed() { 76 | return "closed" 77 | } 78 | return "unknown" 79 | } 80 | 81 | //GetServiceName returns the service name indicated by the port number 82 | func (p PortACK) GetServiceName() string { 83 | if name, present := knownPortMap[p.Port]; present { 84 | return name 85 | } 86 | return "unknown" 87 | } 88 | 89 | //Piggybacking on NMap services list for common ports and frequency to determine the order of port scans. See: https://svn.nmap.org/nmap/nmap-services 90 | //See processNMAPServices for how we extract the data. This is an internal API at the moment, used to generate the data in definitions.go. We are interested only in TCP ports at the moment 91 | //processNMAPServices gets a sorted set of ports from the nmap-services file 92 | func processNMAPServices() { 93 | file, err := os.Open("nmap-services.txt") 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | defer file.Close() 98 | 99 | ports := []knownPort{} 100 | scanner := bufio.NewScanner(file) 101 | for scanner.Scan() { 102 | line := scanner.Text() 103 | if strings.HasPrefix(line, "#") { 104 | continue 105 | } 106 | data := strings.Split(line, "\t") 107 | if len(data) < 3 { 108 | continue 109 | } 110 | freq := float64(0) 111 | if f, err := strconv.ParseFloat(data[2], 64); err == nil { 112 | freq = f 113 | } 114 | if strings.HasSuffix(data[1], "/tcp") { 115 | //interesting line 116 | ports = append(ports, knownPort{ 117 | Name: data[0], 118 | ID: strings.Split(data[1], "/")[0], 119 | Frequency: freq, 120 | }) 121 | } 122 | } 123 | 124 | sort.Sort(knownPortSorter(ports)) 125 | 126 | out, err := os.Create("services.txt") 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | defer out.Close() 131 | 132 | out2, err := os.Create("ports-only.txt") 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | defer out2.Close() 137 | 138 | for _, p := range ports { 139 | fmt.Printf("%#v\n", p) 140 | 141 | port, err := strconv.Atoi(p.ID) 142 | if err != nil { 143 | panic(err) 144 | } 145 | //write in a format that can be easily copied into a map[string]string {...} 146 | out.WriteString(fmt.Sprintf("`%s`: `%s`,\n", p.ID, p.Name)) 147 | //write in a format that can be easily copied into an []int {...} 148 | out2.WriteString(fmt.Sprintf("%d,", port)) 149 | } 150 | } 151 | 152 | //knownPort is a struct for NMAP known ports dataset 153 | type knownPort struct { 154 | ID string 155 | Name string 156 | Frequency float64 157 | } 158 | 159 | type knownPortSorter []knownPort 160 | 161 | func (k knownPortSorter) Len() int { 162 | return len(k) 163 | } 164 | 165 | func (k knownPortSorter) Swap(i, j int) { 166 | k[i], k[j] = k[j], k[i] 167 | } 168 | func (k knownPortSorter) Less(i, j int) bool { 169 | return k[i].Frequency > k[j].Frequency //sort descending 170 | } 171 | 172 | //PortAckSorter sorts ack messages 173 | type PortAckSorter []PortACK 174 | 175 | func (k PortAckSorter) Len() int { 176 | return len(k) 177 | } 178 | 179 | func (k PortAckSorter) Swap(i, j int) { 180 | k[i], k[j] = k[j], k[i] 181 | } 182 | func (k PortAckSorter) Less(i, j int) bool { 183 | iPort, _ := strconv.Atoi(k[i].Port) 184 | jPort, _ := strconv.Atoi(k[j].Port) 185 | return k[i].Host < k[j].Host || (k[i].Host == k[j].Host && iPort <= jPort) 186 | } 187 | -------------------------------------------------------------------------------- /pcap_support_darwin.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "github.com/google/gopacket/pcap" 5 | ) 6 | 7 | func closeHandle(handle *pcap.Handle, host string, config ScanConfig, stop chan bool) { 8 | handle.Close() 9 | } 10 | -------------------------------------------------------------------------------- /pcap_support_linux.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/google/gopacket/pcap" 9 | ) 10 | 11 | func closeHandle(handle *pcap.Handle, connectHost string, config ScanConfig, stop chan bool) { 12 | go handle.Close() 13 | stop <- true 14 | //dial an arbitrary port to generate packet to ensure the handle closes - some weirdness on Linux versions using TPACKET_V3 15 | //see https://github.com/tsg/gopacket/pull/15 and https://github.com/elastic/beats/issues/6535 16 | net.DialTimeout("tcp", fmt.Sprintf("%s:443", connectHost), time.Second) 17 | } 18 | -------------------------------------------------------------------------------- /persistence.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/dgraph-io/badger" 16 | ) 17 | 18 | var ( 19 | dayFormat = "2006-01-02" 20 | baseScanDBDirectory = filepath.FromSlash("data/tcpscan/scan") 21 | psrCache = make(map[string]PersistedScanRequest) 22 | lock = sync.RWMutex{} 23 | scanCacheLock = sync.RWMutex{} 24 | myLogger = logger{} 25 | ) 26 | 27 | type logger struct { 28 | } 29 | 30 | func (log logger) Errorf(format string, params ...interface{}) {} 31 | func (log logger) Warningf(format string, params ...interface{}) {} 32 | func (log logger) Infof(format string, params ...interface{}) {} 33 | func (log logger) Debugf(format string, params ...interface{}) {} 34 | 35 | //ListScans returns the ScanID list of persisted scans 36 | func ListScans(rewindDays int, completed bool) (result []ScanRequest) { 37 | if rewindDays < 0 { 38 | log.Print("The number of days in the past must be non-negative.") 39 | return 40 | } 41 | dirs, err := ioutil.ReadDir(baseScanDBDirectory) 42 | if err != nil { 43 | log.Print(err) 44 | return 45 | } 46 | 47 | allowedDates := make(map[string]bool) 48 | today := time.Now() 49 | for d := rewindDays; d >= 0; d-- { 50 | allowedDates[fmt.Sprintf("%s", today.AddDate(0, 0, -1*d).Format(dayFormat))] = true 51 | } 52 | 53 | matchedDirs := []string{} 54 | for _, d := range dirs { 55 | dirName := d.Name() 56 | if _, present := allowedDates[dirName]; present { 57 | matchedDirs = append(matchedDirs, dirName) 58 | } 59 | } 60 | 61 | for _, d := range matchedDirs { 62 | dirs, err := ioutil.ReadDir(filepath.Join(baseScanDBDirectory, d)) 63 | if err != nil { 64 | log.Print(err) 65 | return 66 | } 67 | 68 | for _, sID := range dirs { 69 | scanID := sID.Name() 70 | //LoadScanRequest retrieves persisted scan request from folder following a layout pattern 71 | if psr, err := LoadScanRequest(d, scanID); err == nil && (len(psr.Hosts) == psr.Progress) == completed { 72 | result = append(result, psr.Request) 73 | } 74 | } 75 | } 76 | return 77 | } 78 | 79 | //PersistScans persists the result of scans per server 80 | func PersistScans(psr PersistedScanRequest, server string, scans []PortACK) { 81 | dbDir := filepath.Join(baseScanDBDirectory, psr.Request.Day, psr.Request.ScanID) 82 | opts := badger.DefaultOptions(dbDir) 83 | opts.Logger = myLogger 84 | opts.NumVersionsToKeep = 0 85 | db, err := badger.Open(opts) 86 | if err != nil { 87 | log.Fatal(err) 88 | return 89 | } 90 | defer db.Close() 91 | 92 | db.Update(func(txn *badger.Txn) error { 93 | return txn.Set([]byte(server), marshallPortAcks(scans)) 94 | }) 95 | } 96 | 97 | //LoadScanRequest retrieves persisted scan request from folder following a layout pattern 98 | func LoadScanRequest(dir, scanID string) (psr PersistedScanRequest, e error) { 99 | lock.Lock() 100 | if psr, ok := psrCache[fmt.Sprintf("%s:%s", dir, scanID)]; ok { 101 | lock.Unlock() 102 | return psr, nil 103 | } 104 | lock.Unlock() 105 | dbDir := filepath.Join(baseScanDBDirectory, dir, scanID, "request") 106 | opts := badger.DefaultOptions(dbDir) 107 | opts.Logger = myLogger 108 | opts.ReadOnly = true 109 | db, err := badger.Open(opts) 110 | if err != nil { 111 | return psr, err 112 | } 113 | defer db.Close() 114 | data := []byte{} 115 | outErr := db.View(func(txn *badger.Txn) error { 116 | item, err := txn.Get([]byte(scanID)) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | data, err = item.ValueCopy(nil) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | }) 127 | if outErr != nil { 128 | return psr, outErr 129 | } 130 | psr, e = UnmasharlPersistedScanRequest(data) 131 | if e == nil && len(psr.Hosts) == psr.Progress { 132 | lock.Lock() 133 | psrCache[fmt.Sprintf("%s:%s", dir, scanID)] = psr 134 | lock.Unlock() 135 | } 136 | return psr, e 137 | } 138 | 139 | //Marshall scan request 140 | func (psr PersistedScanRequest) Marshall() []byte { 141 | result := bytes.Buffer{} 142 | gob.Register(PersistedScanRequest{}) 143 | err := gob.NewEncoder(&result).Encode(&psr) 144 | if err != nil { 145 | log.Print(err) 146 | } 147 | return result.Bytes() 148 | } 149 | 150 | //UnmasharlPersistedScanRequest builds PersistedScanRequest from bytes 151 | func UnmasharlPersistedScanRequest(data []byte) (PersistedScanRequest, error) { 152 | 153 | psr := PersistedScanRequest{} 154 | gob.Register(psr) 155 | buf := bytes.NewBuffer(data) 156 | err := gob.NewDecoder(buf).Decode(&psr) 157 | if err != nil { 158 | return psr, err 159 | } 160 | return psr, nil 161 | } 162 | 163 | func marshallPortAcks(s []PortACK) []byte { 164 | result := bytes.Buffer{} 165 | gob.Register([]PortACK{}) 166 | err := gob.NewEncoder(&result).Encode(&s) 167 | if err != nil { 168 | log.Print(err) 169 | } 170 | return result.Bytes() 171 | } 172 | 173 | //PersistScanRequest persists scan request 174 | func PersistScanRequest(psr PersistedScanRequest) { 175 | dbDir := filepath.Join(baseScanDBDirectory, psr.Request.Day, psr.Request.ScanID, "request") 176 | opts := badger.DefaultOptions(dbDir) 177 | opts.Logger = myLogger 178 | opts.NumVersionsToKeep = 0 179 | db, err := badger.Open(opts) 180 | if err != nil { 181 | log.Fatal(err) 182 | return 183 | } 184 | defer db.Close() 185 | 186 | db.Update(func(txn *badger.Txn) error { 187 | return txn.Set([]byte(psr.Request.ScanID), psr.Marshall()) 188 | }) 189 | } 190 | 191 | //CompactDB reclaims space by pruning the database 192 | func CompactDB(dayPath, scanID string) { 193 | //compact the scan requests 194 | dbDir := filepath.Join(baseScanDBDirectory, dayPath, scanID, "request") 195 | opts := badger.DefaultOptions(dbDir) 196 | opts.Logger = myLogger 197 | opts.NumVersionsToKeep = 0 198 | db, err := badger.Open(opts) 199 | if err != nil { 200 | println(err.Error()) 201 | log.Fatal(err) 202 | return 203 | } 204 | lsmx, vlogx := db.Size() 205 | for db.RunValueLogGC(.8) == nil { 206 | lsmy, vlogy := db.Size() 207 | println("Compacted DB", opts.Dir) 208 | fmt.Printf("Before LSM: %d, VLOG: %d, After LSM: %d, VLOG: %d\n", lsmx, vlogx, lsmy, vlogy) 209 | lsmx, vlogx = lsmy, vlogy 210 | } 211 | db.Close() 212 | } 213 | 214 | //GetNextScanID returns the next unique scan ID 215 | func GetNextScanID() string { 216 | prefix := filepath.Join(baseScanDBDirectory, time.Now().Format(dayFormat)) 217 | if _, err := os.Stat(prefix); os.IsNotExist(err) { 218 | if err2 := os.MkdirAll(prefix, 0755); err2 != nil { 219 | log.Fatal("Could not create the path ", prefix) 220 | } 221 | } 222 | dir, err := ioutil.TempDir(prefix, "") 223 | if err != nil { 224 | log.Fatal(err) 225 | return "" 226 | } 227 | return strings.Replace(strings.TrimPrefix(dir, prefix), string(os.PathSeparator), "", -1) 228 | } 229 | -------------------------------------------------------------------------------- /portscan.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | mathrand "math/rand" 10 | "net" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/jackpal/gateway" 18 | 19 | "github.com/adedayo/cidr" 20 | 21 | "github.com/google/gopacket" 22 | "github.com/google/gopacket/layers" 23 | "github.com/google/gopacket/pcap" 24 | "go.uber.org/ratelimit" 25 | ) 26 | 27 | var ( 28 | options = gopacket.SerializeOptions{ 29 | FixLengths: true, 30 | ComputeChecksums: true, 31 | } 32 | 33 | tcpOptions = []layers.TCPOption{ 34 | layers.TCPOption{ 35 | OptionType: layers.TCPOptionKindMSS, 36 | OptionLength: 4, 37 | OptionData: []byte{0x5, 0xb4}, //1460 38 | }, 39 | layers.TCPOption{ 40 | OptionType: layers.TCPOptionKindNop, 41 | }, 42 | layers.TCPOption{ 43 | OptionType: layers.TCPOptionKindWindowScale, 44 | OptionLength: 3, 45 | OptionData: []byte{0x5}, 46 | }, 47 | layers.TCPOption{ 48 | OptionType: layers.TCPOptionKindNop, 49 | }, 50 | layers.TCPOption{ 51 | OptionType: layers.TCPOptionKindNop, 52 | }, 53 | layers.TCPOption{ 54 | OptionType: layers.TCPOptionKindTimestamps, 55 | OptionLength: 10, 56 | OptionData: []byte{0xac, 0x49, 0x31, 0xcb, 0x0, 0x0, 0x0, 0x0}, 57 | }, 58 | layers.TCPOption{ 59 | OptionType: layers.TCPOptionKindSACKPermitted, 60 | OptionLength: 2, 61 | }, 62 | layers.TCPOption{ 63 | OptionType: layers.TCPOptionKindEndList, 64 | }, 65 | } 66 | 67 | lastConfig ScanConfig 68 | lastRoute routeFinder 69 | ) 70 | 71 | type routeFinder struct { 72 | IsPPP bool 73 | IsTun bool 74 | SrcHardwareAddr net.HardwareAddr 75 | DstHardwareAddr net.HardwareAddr 76 | SrcIP net.IP 77 | Device pcap.Interface 78 | Interface net.Interface 79 | } 80 | 81 | //ScanCIDR scans for open TCP ports in IP addresses within a CIDR range 82 | func ScanCIDR(config ScanConfig, cidrAddresses ...string) <-chan PortACK { 83 | defer func() { 84 | if r := recover(); r != nil { 85 | fmt.Printf("Error: %+v\n", r) 86 | os.Exit(1) 87 | } 88 | }() 89 | rate := 1000 90 | if config.PacketsPerSecond > 0 { 91 | rate = config.PacketsPerSecond 92 | } else { 93 | panic(fmt.Errorf("Invalid packets per second: %d. Stopping", config.PacketsPerSecond)) 94 | } 95 | rl := ratelimit.New(rate) //ratelimit number of packets per second 96 | route := getRoute(config) 97 | cidrPortMap := make(map[string][]int) 98 | for _, cidrX := range cidrAddresses { 99 | ports := []int{} 100 | if strings.Contains(cidrX, ":") { 101 | cidrPorts := strings.Split(cidrX, ":") 102 | cRange := "" 103 | if strings.Contains(cidrX, "/") { 104 | cRange = "/" + strings.Split(cidrX, "/")[1] 105 | } 106 | cidrX = cidrPorts[0] + cRange 107 | ports = parsePorts(strings.Split(cidrPorts[1], "/")[0]) 108 | } 109 | cidrX = getNet(cidrX) 110 | if len(ports) == 0 { 111 | ports = knownPorts[:] 112 | } 113 | if currentPorts, present := cidrPortMap[cidrX]; !present { 114 | cidrPortMap[cidrX] = ports 115 | } else { 116 | cidrPortMap[cidrX] = append(currentPorts, ports...) 117 | } 118 | } 119 | cidrXs := []string{} 120 | for cidrX := range cidrPortMap { 121 | cidrXs = append(cidrXs, "net "+cidrX) 122 | } 123 | 124 | //restrict filtering to the specified CIDR IPs and listen for inbound ACK packets 125 | filter := fmt.Sprintf(`(%s) and not src host %s`, strings.Join(cidrXs, " or "), route.SrcIP.String()) 126 | handle := getHandle(filter, config) 127 | stop := make(chan bool, 1) 128 | 129 | out := listenForACKPackets(handle, route, config, stop) 130 | 131 | go func() { 132 | sampleIP := "" 133 | for cidrX, cidrPorts := range cidrPortMap { 134 | ipAdds := cidr.Expand(cidrX) 135 | if len(ipAdds) == 0 { 136 | continue 137 | } 138 | //shuffle the IP addresses pseudo-randomly 139 | mathrand.Shuffle(len(ipAdds), func(i, j int) { 140 | ipAdds[i], ipAdds[j] = ipAdds[j], ipAdds[i] 141 | }) 142 | sampleIP = ipAdds[0] 143 | 144 | count := 1 //Number of SYN packets to send per port (make this a parameter) 145 | //Send SYN packets asynchronously 146 | go func(ips []string, ports []int) { // run the scans in parallel 147 | writeHandle := getNonFilteredHandle(config) 148 | defer writeHandle.Close() 149 | stopPort := 65535 150 | sourcePort := 50000 151 | for _, dstPort := range ports { 152 | for _, dstIP := range ips { 153 | dst := net.ParseIP(dstIP) 154 | // Send a specified number of SYN packets 155 | for i := 0; i < count; i++ { 156 | rl.Take() 157 | err := sendSYNPacket(route.SrcIP, dst, sourcePort, dstPort, route, writeHandle) 158 | bailout(err) 159 | sourcePort++ 160 | if sourcePort > stopPort { 161 | sourcePort = 50000 162 | } 163 | } 164 | } 165 | } 166 | 167 | }(ipAdds, cidrPorts) 168 | } 169 | timeout := time.Duration(config.Timeout) * time.Second 170 | select { 171 | case <-time.After(timeout): 172 | closeHandle(handle, sampleIP, config, stop) 173 | } 174 | return 175 | }() 176 | return out 177 | } 178 | 179 | //a hopefully more performant route finder that doesnt re-do the work 180 | func getRoute(config ScanConfig) routeFinder { 181 | //use precomputed 182 | if config == lastConfig { 183 | return lastRoute 184 | } 185 | //get the network interface to use for scanning: ppp0, eth0, en0 etc. 186 | route := routeFinder{} 187 | dev, netIface, err := getPreferredDevice(config) 188 | bailout(err) 189 | route.Device = dev 190 | route.Interface = netIface 191 | iface, err := getIPv4InterfaceAddress(dev) 192 | bailout(err) 193 | route.SrcIP = iface.IP 194 | if strings.HasPrefix(dev.Name, "ppp") { 195 | route.IsPPP = true 196 | } else if strings.HasPrefix(dev.Name, "utun") { 197 | route.IsTun = true 198 | } else { 199 | routerHW, err := determineRouterHardwareAddress(config) 200 | bailout(err) 201 | route.SrcHardwareAddr = netIface.HardwareAddr 202 | route.DstHardwareAddr = routerHW 203 | } 204 | lastConfig = config 205 | lastRoute = route 206 | return route 207 | } 208 | 209 | func parsePorts(portsString string) []int { 210 | ports := []int{} 211 | pp := strings.Split(portsString, ",") 212 | for _, p := range pp { 213 | if strings.Contains(p, "-") { 214 | ps := strings.Split(p, "-") 215 | 216 | if len(ps) != 2 { 217 | continue 218 | } 219 | i, err := strconv.Atoi(ps[0]) 220 | if err != nil { 221 | continue 222 | } 223 | j, err := strconv.Atoi(ps[1]) 224 | if err != nil { 225 | continue 226 | } 227 | 228 | if j < i { 229 | i, j = j, i 230 | } 231 | for index := i; index < j; index++ { 232 | ports = append(ports, index) 233 | } 234 | 235 | } else { 236 | i, err := strconv.Atoi(p) 237 | if err != nil { 238 | continue 239 | } 240 | ports = append(ports, i) 241 | } 242 | } 243 | uniquePorts := make(map[int]bool) 244 | for _, p := range ports { 245 | uniquePorts[p] = true 246 | } 247 | ports = []int{} 248 | for p := range uniquePorts { 249 | ports = append(ports, p) 250 | } 251 | return ports 252 | } 253 | 254 | //allow domains to be used in CIDRs 255 | func getNet(cidrX string) (result string) { 256 | adds := strings.Split(cidrX, "/") 257 | rng := "/32" 258 | if strings.Contains(cidrX, "/") { 259 | rng = "/" + adds[1] 260 | } 261 | ips, err := net.LookupIP(adds[0]) 262 | if err != nil { 263 | return 264 | } 265 | return ips[0].String() + rng 266 | } 267 | 268 | func merge(stoppers ...<-chan bool) <-chan bool { 269 | var wg sync.WaitGroup 270 | out := make(chan bool) 271 | output := func(c <-chan bool) { 272 | for n := range c { 273 | out <- n 274 | } 275 | wg.Done() 276 | } 277 | wg.Add(len(stoppers)) 278 | for _, c := range stoppers { 279 | go output(c) 280 | } 281 | go func() { 282 | wg.Wait() 283 | close(out) 284 | }() 285 | return out 286 | } 287 | 288 | func sendSYNPacket(src, dst net.IP, srcPort, dstPrt int, route routeFinder, handle *pcap.Handle) error { 289 | var firstLayer gopacket.SerializableLayer 290 | if route.IsPPP { 291 | ppp := layers.PPP{ 292 | PPPType: layers.PPPTypeIPv4, 293 | } 294 | ppp.Contents = []byte{0xff, 0x03} 295 | firstLayer = &ppp 296 | } else if route.IsTun { 297 | tun := layers.Loopback{ 298 | Family: layers.ProtocolFamilyIPv4, 299 | } 300 | firstLayer = &tun 301 | } else { 302 | eth := layers.Ethernet{ 303 | SrcMAC: route.SrcHardwareAddr, 304 | DstMAC: route.DstHardwareAddr, 305 | EthernetType: layers.EthernetTypeIPv4, 306 | } 307 | firstLayer = ð 308 | } 309 | ip4 := layers.IPv4{ 310 | SrcIP: src, 311 | DstIP: dst, 312 | Version: 4, 313 | TOS: 0, 314 | Id: 0, 315 | Flags: layers.IPv4Flag(2), 316 | FragOffset: 0, 317 | TTL: 255, 318 | Protocol: layers.IPProtocolTCP, 319 | } 320 | timebuf := make([]byte, 8) 321 | rand.Read(timebuf) 322 | for i := 4; i < 8; i++ { 323 | timebuf[i] = 0x0 324 | } 325 | tcpOptions[5] = layers.TCPOption{ 326 | OptionType: layers.TCPOptionKindTimestamps, 327 | OptionLength: 10, 328 | OptionData: timebuf, 329 | } 330 | tcp := layers.TCP{ 331 | SrcPort: layers.TCPPort(srcPort), 332 | DstPort: layers.TCPPort(dstPrt), 333 | SYN: true, // we'd like to send a SYN packet 334 | Window: 65535, 335 | Options: tcpOptions, 336 | } 337 | 338 | tcp.SetNetworkLayerForChecksum(&ip4) 339 | buf := gopacket.NewSerializeBuffer() 340 | if err := gopacket.SerializeLayers(buf, options, firstLayer, &ip4, &tcp); err != nil { 341 | return err 342 | } 343 | return handle.WritePacketData(buf.Bytes()) 344 | } 345 | 346 | //determineRouterHardwareAddress finds the router by looking at the ethernet frames that returns the TCP ACK handshake from "google" 347 | func determineRouterHardwareAddress(config ScanConfig) (net.HardwareAddr, error) { 348 | outAlt, altHandle := alternativeGatewayHWDiscovery(config) 349 | google := "www.google.com" 350 | _, iface, err := getPreferredDevice(config) 351 | bailout(err) 352 | handle := getTimedHandle(fmt.Sprintf("host %s and ether dst %s", google, iface.HardwareAddr.String()), 5*time.Second, config) 353 | defer handle.Close() 354 | 355 | out := listenForEthernetPackets(handle) 356 | go func() { 357 | _, err = net.DialTimeout("tcp", fmt.Sprintf("%s:443", google), 5*time.Second) 358 | bailout(err) 359 | }() 360 | select { 361 | case hwAddress := <-out: //found via TCP connect 362 | altHandle.Close() // ensure the ARP handle is closed 363 | return hwAddress, nil 364 | case hwAddress := <-outAlt: //found via ARP 365 | return hwAddress, nil 366 | case <-time.After(30 * time.Second): 367 | return nil, errors.New("Timeout error: could not determine the router hardware address in time") 368 | } 369 | } 370 | 371 | func alternativeGatewayHWDiscovery(config ScanConfig) (<-chan net.HardwareAddr, *pcap.Handle) { 372 | dev, iface, err := getPreferredDevice(config) 373 | bailout(err) 374 | var srcIP net.IP 375 | for _, add := range dev.Addresses { 376 | if !add.IP.IsLoopback() && add.IP.To4() != nil { 377 | srcIP = add.IP 378 | break 379 | } 380 | } 381 | dstIP, err := gateway.DiscoverGateway() 382 | if err != nil { 383 | bailout(err) 384 | } 385 | handle, err := pcap.OpenLive(dev.Name, 65536, true, 30*time.Second) 386 | if err != nil { 387 | bailout(err) 388 | } 389 | out := readARP(handle, dstIP) 390 | writeArp(handle, &iface, srcIP, dstIP) 391 | return out, handle 392 | } 393 | 394 | func readARP(handle *pcap.Handle, dstIP []byte) <-chan net.HardwareAddr { 395 | packetSource := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet) 396 | output := make(chan net.HardwareAddr) 397 | targetIP := net.IP(dstIP) 398 | go func() { 399 | defer close(output) 400 | for packet := range packetSource.Packets() { 401 | if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil { 402 | arp := arpLayer.(*layers.ARP) 403 | if srcIP := net.IP(arp.SourceProtAddress); arp != nil && srcIP.Equal(targetIP) { 404 | out := net.HardwareAddr(arp.SourceHwAddress) 405 | output <- out 406 | return 407 | } 408 | } 409 | } 410 | }() 411 | return output 412 | } 413 | 414 | func writeArp(handle *pcap.Handle, srcIFace *net.Interface, srcIP, dstIP net.IP) { 415 | eth := layers.Ethernet{ 416 | SrcMAC: srcIFace.HardwareAddr, 417 | DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 418 | EthernetType: layers.EthernetTypeARP, 419 | } 420 | arp := layers.ARP{ 421 | Operation: layers.ARPRequest, 422 | AddrType: layers.LinkTypeEthernet, 423 | Protocol: layers.EthernetTypeIPv4, 424 | HwAddressSize: 6, 425 | ProtAddressSize: 4, 426 | SourceHwAddress: []byte(srcIFace.HardwareAddr), 427 | SourceProtAddress: []byte(srcIP), 428 | DstHwAddress: []byte{0, 0, 0, 0, 0, 0}, 429 | DstProtAddress: []byte(dstIP), 430 | } 431 | buf := gopacket.NewSerializeBuffer() 432 | gopacket.SerializeLayers(buf, options, ð, &arp) 433 | if err := handle.WritePacketData(buf.Bytes()); err != nil { 434 | bailout(err) 435 | } 436 | } 437 | 438 | //listenForEthernetPackets collects packets on the network that meet port scan specifications 439 | func listenForEthernetPackets(handle *pcap.Handle) <-chan net.HardwareAddr { 440 | output := make(chan net.HardwareAddr) 441 | go func() { 442 | defer func() { 443 | close(output) 444 | handle.Close() //timeout doesn't seem to close the handle 445 | }() 446 | var eth layers.Ethernet 447 | parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð) 448 | decodedLayers := []gopacket.LayerType{} 449 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 450 | for packet := range packetSource.Packets() { 451 | parser.DecodeLayers(packet.Data(), &decodedLayers) 452 | for _, lyr := range decodedLayers { 453 | //Look for Ethernet frames 454 | if lyr.Contains(layers.LayerTypeEthernet) { 455 | output <- eth.SrcMAC 456 | return 457 | } 458 | } 459 | } 460 | }() 461 | return output 462 | } 463 | 464 | // MyPPP is layers.PPP with CanDecode and other decoding operations implemented 465 | type MyPPP layers.PPP 466 | 467 | //CanDecode indicates that we can decode PPP packets 468 | func (ppp *MyPPP) CanDecode() gopacket.LayerClass { 469 | return layers.LayerTypePPP 470 | } 471 | 472 | //LayerType - 473 | func (ppp *MyPPP) LayerType() gopacket.LayerType { return layers.LayerTypePPP } 474 | 475 | //DecodeFromBytes as name suggest 476 | func (ppp *MyPPP) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error { 477 | if len(data) > 2 && data[0] == 0xff && data[1] == 0x03 { 478 | ppp.PPPType = layers.PPPType(binary.BigEndian.Uint16(data[2:4])) 479 | ppp.Contents = data 480 | ppp.Payload = data[4:] 481 | return nil 482 | } 483 | return errors.New("Not a PPP packet") 484 | } 485 | 486 | //NextLayerType gets type 487 | func (ppp *MyPPP) NextLayerType() gopacket.LayerType { 488 | return layers.LayerTypeIPv4 489 | } 490 | 491 | //listenForACKPackets collects packets on the network that meet port scan specifications 492 | func listenForACKPackets(handle *pcap.Handle, route routeFinder, config ScanConfig, stop chan bool) <-chan PortACK { 493 | output := make(chan PortACK) 494 | var ip layers.IPv4 495 | var tcp layers.TCP 496 | var parser *gopacket.DecodingLayerParser 497 | if route.IsPPP { 498 | var ppp MyPPP 499 | parser = gopacket.NewDecodingLayerParser(layers.LayerTypePPP, &ppp, &ip, &tcp) 500 | } else if route.IsTun { 501 | var tun layers.Loopback 502 | parser = gopacket.NewDecodingLayerParser(layers.LayerTypeLoopback, &tun, &ip, &tcp) 503 | } else { 504 | var eth layers.Ethernet 505 | parser = gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð, &ip, &tcp) 506 | } 507 | 508 | decodedLayers := []gopacket.LayerType{} 509 | 510 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 511 | go func() { 512 | defer func() { 513 | close(output) 514 | close(stop) 515 | }() 516 | for { 517 | select { 518 | case <-stop: 519 | return 520 | default: 521 | packet, err := packetSource.NextPacket() 522 | if err == io.EOF { 523 | return 524 | } 525 | if err != nil { 526 | if err.Error() != pcap.NextErrorTimeoutExpired.Error() { 527 | return 528 | } 529 | } 530 | parser.DecodeLayers(packet.Data(), &decodedLayers) 531 | for _, lyr := range decodedLayers { 532 | //Look for TCP ACK 533 | if lyr.Contains(layers.LayerTypeTCP) { 534 | ack := PortACK{ 535 | Host: ip.SrcIP.String(), 536 | Port: strings.Split(tcp.SrcPort.String(), "(")[0], 537 | SYN: tcp.SYN, 538 | RST: tcp.RST, 539 | } 540 | output <- ack 541 | if !config.Quiet && ack.IsOpen() { 542 | fmt.Printf("%s:%s (%s) is %s\n", ack.Host, ack.Port, ack.GetServiceName(), ack.Status()) 543 | } 544 | break 545 | } 546 | } 547 | } 548 | } 549 | }() 550 | return output 551 | } 552 | 553 | func bailout(err error) { 554 | if err != nil { 555 | panic(err.Error()) 556 | 557 | } 558 | } 559 | 560 | func getPreferredDevice(config ScanConfig) (pcap.Interface, net.Interface, error) { 561 | if config == lastConfig { //shortcut if we've already obtained the device 562 | return lastRoute.Device, lastRoute.Interface, nil 563 | } 564 | 565 | devices, err := pcap.FindAllDevs() 566 | if err != nil { 567 | return pcap.Interface{}, net.Interface{}, err 568 | } 569 | 570 | if config.Interface != "" { 571 | ifx, err := net.InterfaceByName(config.Interface) 572 | if err != nil { 573 | return pcap.Interface{}, net.Interface{}, err 574 | } 575 | dev := pcap.Interface{} 576 | for _, d := range devices { 577 | if d.Name == config.Interface { 578 | dev = d 579 | break 580 | } 581 | } 582 | return dev, *ifx, err 583 | } 584 | 585 | //search in this order: VPN, then non-VPN; from lower interface index 0 up till 4 586 | //i.e ppp0, ppp1, ..., ppp4, utun0, utun1, ..., utun4, en0, ..., en4, eth0, ..., eth4 587 | for _, iface := range []string{"ppp", "utun", "en", "eth"} { 588 | for i := 0; i < 5; i++ { 589 | for _, dev := range devices { 590 | if dev.Name == fmt.Sprintf("%s%d", iface, i) { 591 | if ifx, err := getRoutableInterface(dev); err == nil { 592 | return dev, ifx, err 593 | } 594 | } 595 | } 596 | } 597 | } 598 | 599 | // try guessing based on systemd's predictable network interface naming heuristics 600 | // see https://github.com/systemd/systemd/blob/master/src/udev/udev-builtin-net_id.c 601 | // note that this is a simple algorithm, not rigorous 602 | interfaces := []string{"en", "ib", "sl", "wl", "ww"} 603 | typeNames := []string{"b", "c", "o", "s", "p", "x", "v", "a"} 604 | for _, iface := range interfaces { 605 | for _, tn := range typeNames { 606 | for _, i := range []int{0, 1, 2, 3} { //only try the first four possible index numbers 607 | for _, dev := range devices { 608 | if strings.HasPrefix(dev.Name, fmt.Sprintf("%s%s", iface, tn)) && 609 | strings.HasSuffix(dev.Name, fmt.Sprintf("%d", i)) { 610 | ifx, err := getRoutableInterface(dev) 611 | if err != nil { 612 | continue 613 | } 614 | return dev, ifx, err 615 | } 616 | } 617 | } 618 | } 619 | } 620 | // try any interface with an IPv4 address 621 | for _, dev := range devices { 622 | ifx, err := getRoutableInterface(dev) 623 | if err != nil { 624 | continue 625 | } 626 | return dev, ifx, err 627 | } 628 | // give up and bail out 629 | return pcap.Interface{}, net.Interface{}, errors.New("Could not find a preferred interface with a routable IP") 630 | } 631 | 632 | func getRoutableInterface(dev pcap.Interface) (net.Interface, error) { 633 | if strings.HasPrefix(dev.Name, "ppp") || strings.HasPrefix(dev.Name, "utun") { 634 | _, err := getIPv4InterfaceAddress(dev) 635 | return net.Interface{}, err 636 | } 637 | 638 | for _, add := range dev.Addresses { 639 | if !add.IP.IsLoopback() && strings.Contains(add.IP.String(), ".") { 640 | iface, err := net.InterfaceByName(dev.Name) 641 | if err != nil { 642 | return net.Interface{}, err 643 | } 644 | return *iface, nil 645 | } 646 | } 647 | return net.Interface{}, errors.New("No routable interface") 648 | } 649 | 650 | func getIPv4InterfaceAddress(iface pcap.Interface) (pcap.InterfaceAddress, error) { 651 | for _, add := range iface.Addresses { 652 | if strings.Contains(add.IP.String(), ".") { 653 | return add, nil 654 | } 655 | } 656 | return pcap.InterfaceAddress{}, errors.New("Could not find an interface with IPv4 address") 657 | } 658 | 659 | func getTimedHandle(bpfFilter string, timeOut time.Duration, config ScanConfig) *pcap.Handle { 660 | dev, _, err := getPreferredDevice(config) 661 | bailout(err) 662 | handle, err := pcap.OpenLive(dev.Name, 65535, false, timeOut) 663 | bailout(err) 664 | handle.SetBPFFilter(bpfFilter) 665 | return handle 666 | } 667 | 668 | func getHandle(bpfFilter string, config ScanConfig) *pcap.Handle { 669 | dev, _, err := getPreferredDevice(config) 670 | bailout(err) 671 | handle, err := pcap.OpenLive(dev.Name, 65535, false, pcap.BlockForever) 672 | bailout(err) 673 | handle.SetBPFFilter(bpfFilter) 674 | return handle 675 | } 676 | 677 | func getNonFilteredHandle(config ScanConfig) *pcap.Handle { 678 | dev, _, err := getPreferredDevice(config) 679 | bailout(err) 680 | handle, err := pcap.OpenLive(dev.Name, 65535, false, pcap.BlockForever) 681 | bailout(err) 682 | return handle 683 | } 684 | -------------------------------------------------------------------------------- /portscan_test.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test(t *testing.T) { 8 | ipRange := "8.8.8.8/32" 9 | scan := make(map[string]PortACK) 10 | result := ScanCIDR(ScanConfig{Timeout: 10, 11 | PacketsPerSecond: 1000}, ipRange) 12 | for ack := range result { 13 | key := ack.Host + ack.Port 14 | if _, present := scan[key]; !present { 15 | scan[key] = ack 16 | } 17 | } 18 | if len(scan) == 0 { 19 | t.Error("The scan result is expected to be non-empty") 20 | } 21 | } 22 | 23 | func TestGetRouterHW(t *testing.T) { 24 | _, err := determineRouterHardwareAddress(ScanConfig{Timeout: 5, 25 | PacketsPerSecond: 1000}) 26 | if err != nil { 27 | t.Error(err.Error()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scan-service.go: -------------------------------------------------------------------------------- 1 | package portscan 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/adedayo/cidr" 18 | "github.com/carlescere/scheduler" 19 | log "github.com/sirupsen/logrus" 20 | yaml "gopkg.in/yaml.v2" 21 | ) 22 | 23 | var ( 24 | //TCPScanConfigPath is the default config path of the TCPScan service 25 | TCPScanConfigPath = filepath.Join("data", "config", "TCPScanConfig.yml") 26 | tcpScanPath = filepath.Join("data", "tcpscan", "scan") 27 | //control files 28 | runFlag = filepath.Join("data", "tcpscan", "runlock.txt") 29 | runFlag2 = filepath.Join("data", "tcpscan", "deletethistoresume.txt") 30 | workList = filepath.Join("data", "tcpscan", "worklist.txt") 31 | progress = filepath.Join("data", "tcpscan", "progress.txt") 32 | resolvedIPs = make(map[string]string) 33 | ipLock = sync.RWMutex{} 34 | ) 35 | 36 | //Service main service entry function 37 | func Service(configPath string) { 38 | println("Running TCPScan Service ...") 39 | TCPScanConfigPath = configPath 40 | ScheduleTCPScan(getIPsFromConfig) 41 | runtime.Goexit() 42 | } 43 | 44 | func getIPsFromConfig() []string { 45 | config, err := loadConfig(TCPScanConfigPath) 46 | if err != nil { 47 | return []string{} 48 | } 49 | ips := getIPsToScan(config) 50 | return ips 51 | } 52 | 53 | func getIPsToScan(config TCPScanConfig) []string { 54 | data := make(map[string]string) 55 | ips := []string{} 56 | for _, c := range config.CIDRRanges { 57 | ports := "" 58 | if strings.Contains(c, ":") { 59 | cc, p, err := extractPorts(c) 60 | if err != nil { 61 | continue 62 | } 63 | c = cc 64 | ports = p 65 | } 66 | for _, ip := range cidr.Expand(c) { 67 | ip = fmt.Sprintf("%s/32", ip) 68 | if ps, present := data[ip]; present { 69 | if ps == "" { 70 | data[ip] = ports 71 | } else if ports != "" { 72 | data[ip] = fmt.Sprintf("%s,%s", ps, ports) 73 | } 74 | } else { 75 | data[ip] = ports 76 | } 77 | } 78 | } 79 | for ip, ports := range data { 80 | x := ip 81 | 82 | if ports != "" { 83 | z := strings.Split(ip, "/") 84 | if len(z) != 2 { 85 | continue 86 | } 87 | x = fmt.Sprintf("%s:%s/%s", z[0], ports, z[1]) 88 | println(x) 89 | } 90 | ips = append(ips, x) 91 | } 92 | return ips 93 | } 94 | 95 | func extractPorts(cidrX string) (string, string, error) { 96 | cs := strings.Split(cidrX, ":") 97 | if len(cs) != 2 { 98 | return cidrX, "", fmt.Errorf("Bad CIDR with port format %s", cidrX) 99 | } 100 | ip := cs[0] 101 | if !strings.Contains(cs[1], "/") { 102 | return ip + "/32", cs[1], nil 103 | } 104 | rng := strings.Split(cs[1], "/") 105 | if len(rng) != 2 { 106 | return cidrX, "", fmt.Errorf("Bad CIDR with port format %s", cidrX) 107 | } 108 | return fmt.Sprintf("%s/%s", ip, rng[1]), rng[0], nil 109 | } 110 | 111 | //ScheduleTCPScan runs TCPScan service scan 112 | func ScheduleTCPScan(ipSource func() []string) { 113 | 114 | //a restart schould clear the lock file 115 | if _, err := os.Stat(runFlag2); !os.IsNotExist(err) { // there is a runlock 116 | if err := os.Remove(runFlag2); err != nil { 117 | println(err.Error()) 118 | log.Error(err) 119 | } 120 | } 121 | 122 | scanJob := func() { 123 | runTCPScan(ipSource) 124 | } 125 | 126 | if config, err := loadConfig(TCPScanConfigPath); err == nil { 127 | for _, t := range config.DailySchedules { 128 | if config.IsProduction { 129 | println("Running next at ", t) 130 | scheduler.Every().Day().At(t).Run(scanJob) 131 | } else { 132 | scheduler.Every(2).Hours().Run(scanJob) 133 | } 134 | } 135 | //run a scan immediately if there is a previous incomplete scan 136 | if _, err := os.Stat(workList); !os.IsNotExist(err) { 137 | runTCPScan(ipSource) 138 | } 139 | } 140 | } 141 | 142 | func loadConfig(path string) (config TCPScanConfig, e error) { 143 | configFile, err := ioutil.ReadFile(path) 144 | if err != nil { 145 | log.Error(err) 146 | return config, err 147 | } 148 | err = yaml.Unmarshal(configFile, &config) 149 | if err != nil { 150 | log.Error(err) 151 | return config, err 152 | } 153 | return 154 | } 155 | 156 | //runTCPScan accepts generator of IP addresses to scan and a function to map the IPs to hostnames (if any) - function to allow the hostname resolution happen in parallel if necessary 157 | func runTCPScan(ipSource func() []string) { 158 | //create a directory, if not exist, for tlsaudit to keep temporary file 159 | path := tcpScanPath 160 | if _, err := os.Stat(path); os.IsNotExist(err) { 161 | if err2 := os.MkdirAll(path, 0755); err2 != nil { 162 | log.Errorln("Could not create the path ", path) 163 | } 164 | } 165 | 166 | //prevent concurrent runs 167 | if _, err := os.Stat(runFlag2); !os.IsNotExist(err) { // there is a runlock 168 | //do not start a new scan 169 | return 170 | } 171 | psr := PersistedScanRequest{} 172 | 173 | hosts := []string{} 174 | if _, err := os.Stat(workList); !os.IsNotExist(err) { // there is a worklist (due to a previous crash!) 175 | //load the list of IPs from there 176 | println("Resuming due to a worklist") 177 | file, err := os.Open(workList) 178 | if err != nil { 179 | log.Error(err) 180 | return 181 | } 182 | defer file.Close() 183 | 184 | scanner := bufio.NewScanner(file) 185 | for scanner.Scan() { 186 | hosts = append(hosts, scanner.Text()) 187 | } 188 | 189 | day, err := ioutil.ReadFile(runFlag) 190 | if err != nil { 191 | log.Error(err) 192 | return 193 | } 194 | d := strings.TrimSpace(string(day)) 195 | println("Resuming on date ", d, filepath.Join(path, d)) 196 | dirs, err := ioutil.ReadDir(filepath.Join(path, d)) 197 | if err != nil { 198 | println(err.Error()) 199 | log.Error(err) 200 | return 201 | } 202 | fmt.Printf("%#v\n", dirs) 203 | for _, sID := range dirs { 204 | scanID := sID.Name() 205 | println(scanID) 206 | if p, err := LoadScanRequest(d, scanID); err == nil { 207 | psr = p 208 | break 209 | } 210 | } 211 | fmt.Printf("Will be scanning with PSR %#v", psr) 212 | } else { // starting a fresh scan 213 | hosts = ipSource() 214 | //shuffle hosts randomly 215 | rand.Shuffle(len(hosts), func(i, j int) { 216 | hosts[i], hosts[j] = hosts[j], hosts[i] 217 | }) 218 | 219 | //write shuffled hosts into worklist file 220 | if err := ioutil.WriteFile(workList, []byte(strings.Join(hosts, "\n")+"\n"), 0644); err != nil { 221 | log.Error(err) 222 | return 223 | } 224 | 225 | //track progress in the progress file 226 | if err := ioutil.WriteFile(progress, []byte(fmt.Sprintf("-1,%d", len(hosts))), 0644); err != nil { 227 | log.Error(err) 228 | return 229 | } 230 | 231 | //create the lock file with the start day 232 | today := time.Now().Format(dayFormat) 233 | if err := ioutil.WriteFile(runFlag, []byte(today), 0644); err != nil { 234 | log.Error(err) 235 | return 236 | } 237 | 238 | if err := ioutil.WriteFile(runFlag2, []byte{}, 0644); err != nil { 239 | log.Error(err) 240 | return 241 | } 242 | psr.Hosts = hosts 243 | request := ScanRequest{} 244 | request.Day = today 245 | request.ScanID = GetNextScanID() 246 | config, _ := loadConfig(TCPScanConfigPath) 247 | scanConfig := ScanConfig{ 248 | PacketsPerSecond: config.PacketsPerSecond, 249 | Timeout: config.Timeout, 250 | } 251 | request.Config = scanConfig 252 | psr.Request = request 253 | } 254 | 255 | //get ready to scan 256 | //get where we "stopped" last time possibly after a crash 257 | stopped := 0 258 | p, err := ioutil.ReadFile(progress) 259 | if err != nil { 260 | log.Error(err) 261 | return 262 | } 263 | stopped, err = strconv.Atoi(strings.Split(string(p), ",")[0]) 264 | if err != nil { 265 | log.Error(err) 266 | return 267 | } 268 | psr.Progress = stopped 269 | 270 | PersistScanRequest(psr) 271 | 272 | count := len(hosts) 273 | 274 | //scan hosts 275 | for index, host := range hosts { 276 | //skip already scanned hosts, if any 277 | if index <= stopped { 278 | fmt.Printf("Skipping host %s\n", host) 279 | continue 280 | } 281 | counter := index + 1 282 | scan := make(map[string]PortACK) 283 | results := []<-chan PortACK{} 284 | scanResults := []PortACK{} 285 | fmt.Printf("Scanning Host %s (%d of %d)\n", host, counter, count) 286 | results = append(results, ScanCIDR(psr.Request.Config, host)) 287 | for result := range mergePortAckChannels(results...) { 288 | key := result.Host + result.Port 289 | if _, present := scan[key]; !present { 290 | scan[key] = result 291 | scanResults = append(scanResults, result) 292 | fmt.Printf("Got result for Host: %-16s Port: %-6s Status: %-7s Service: %s\n", result.Host, result.Port, result.Status(), result.GetServiceName()) 293 | } 294 | } 295 | sort.Sort(PortAckSorter(scanResults)) 296 | PersistScans(psr, host, scanResults) 297 | 298 | if err := ioutil.WriteFile(progress, []byte(fmt.Sprintf("%d,%d", counter, len(hosts))), 0644); err != nil { 299 | log.Error(err) 300 | return 301 | } 302 | 303 | psr.Progress = counter 304 | PersistScanRequest(psr) 305 | } 306 | 307 | //cleanup 308 | if err := os.Remove(runFlag); err != nil { 309 | log.Error(err) 310 | } 311 | if err := os.Remove(runFlag2); err != nil { 312 | log.Error(err) 313 | } 314 | if err := os.Remove(progress); err != nil { 315 | log.Error(err) 316 | } 317 | if err := os.Remove(workList); err != nil { 318 | log.Error(err) 319 | } 320 | } 321 | 322 | func mergePortAckChannels(channels ...<-chan PortACK) <-chan PortACK { 323 | var wg sync.WaitGroup 324 | out := make(chan PortACK) 325 | output := func(c <-chan PortACK) { 326 | for n := range c { 327 | out <- n 328 | } 329 | wg.Done() 330 | } 331 | wg.Add(len(channels)) 332 | for _, c := range channels { 333 | go output(c) 334 | } 335 | 336 | go func() { 337 | wg.Wait() 338 | close(out) 339 | }() 340 | return out 341 | } 342 | --------------------------------------------------------------------------------