diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c38fa4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da66bf8 --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# go-portScan + +高性能端口扫描器 + +High-performance port scanner. + +## Feature + +- Syn stateless scan +- Syn Automatic ARP detection on the Intranet +- Scanning for large address segments has low occupancy (by iprange) +- Scanning the address is shuffled +- Concurrent high performance (by ants) +- TCP scan +- Port Fingerprint Identification + +## Use as a library + +### 1. SYN scanner + +```go +package main + +import ( + "github.com/XinRoom/go-portScan/core/host" + "github.com/XinRoom/go-portScan/core/port" + "github.com/XinRoom/go-portScan/core/port/syn" + "github.com/XinRoom/iprange" + "log" + "time" +) + +func main() { + single := make(chan struct{}) + retChan := make(chan port.OpenIpPort, 65535) + go func() { + for { + select { + case ret := <-retChan: + if ret.Port == 0 { + single <- struct{}{} + return + } + log.Println(ret) + default: + time.Sleep(time.Millisecond * 10) + } + } + }() + + // 解析端口字符串并且优先发送 TopTcpPorts 中的端口, eg: 1-65535,top1000 + ports, err := port.ShuffleParseAndMergeTopPorts("top1000") + if err != nil { + log.Fatal(err) + } + + // parse ip + it, startIp, _ := iprange.NewIter("1.1.1.1/30") + + // scanner + ss, err := syn.NewSynScanner(startIp, retChan, syn.DefaultSynOption) + if err != nil { + log.Fatal(err) + } + + start := time.Now() + for i := uint64(0); i < it.TotalNum(); i++ { // ip索引 + ip := it.GetIpByIndex(i) + if !host.IsLive(ip.String()) { // ping + continue + } + for _, _port := range ports { // port + ss.WaitLimiter() + ss.Scan(ip, _port) // syn 不能并发,默认以网卡和驱动最高性能发包 + } + } + ss.Close() + <-single + log.Println(time.Since(start)) +} +``` + +### 2. TCP scanner + +```go +package main + +import ( + "github.com/XinRoom/go-portScan/core/host" + "github.com/XinRoom/go-portScan/core/port" + "github.com/XinRoom/iprange" + "log" + "net" + "sync" + "time" +) + +func main() { + single := make(chan struct{}) + retChan := make(chan port.OpenIpPort, 65535) + go func() { + for { + select { + case ret := <-retChan: + if ret.Port == 0 { + single <- struct{}{} + return + } + log.Println(ret) + default: + time.Sleep(time.Millisecond * 10) + } + } + }() + + // 解析端口字符串并且优先发送 TopTcpPorts 中的端口, eg: 1-65535,top1000 + ports, err := port.ShuffleParseAndMergeTopPorts("top1000") + if err != nil { + log.Fatal(err) + } + + // parse Ip + it, _, _ := iprange.NewIter("1.1.1.1/30") + + // scanner + ss, err := port.NewTcpScanner(retChan, port.DefaultTcpOption) + if err != nil { + log.Fatal(err) + } + + start := time.Now() + var wg sync.WaitGroup + for i := uint64(0); i < it.TotalNum(); i++ { // ip索引 + ip := make(net.IP, len(it.GetIpByIndex(0))) + copy(ip, it.GetIpByIndex(i)) // Note: dup copy []byte when concurrent (GetIpByIndex not to do dup copy) + if !host.IsLive(ip.String()) { // ping + continue + } + for _, _port := range ports { // port + ss.WaitLimiter() + wg.Add(1) + go func(ip net.IP, port uint16) { + ss.Scan(ip, port) + wg.Done() + }(ip, _port) + } + } + ss.Close() + <-single + log.Println(time.Since(start)) +} +``` + +### 3. For More + +To see [./cmd/go-portScan.go](./cmd/go-portScan.go) + +## Cmd Build + +``` +git clone https://github.com/XinRoom/go-portScan +cd go-portScan +go get +go build cmd/go-portScan.go +``` + +## Cmd Usage + +`.\go-portScan.exe -ip 1.1.1.1/30 [-nP] [-sT] [-sV] [-rate num] [-rateP num] [-timeout num(ms)]` + +``` + .\go-portScan.exe -h +Usage of D:\pc\sync\projects\Porject\go-portScan\go-portScan.exe: + -ip string + target ip, eg: "1.1.1.1/30,1.1.1.1-1.1.1.2,1.1.1.1-2" + -nP + no ping probe + -port string + eg: "top1000,5612,65120" (default "top1000") + -rate int + number of packets sent per second. If set -1, TCP-mode is 1000, SYN-mode is 2000(SYN-mode is restricted by the network adapter) (default -1) + -rateP int + concurrent num when ping probe each ip (default 300) + -sT + TCP-mode(support IPv4 and IPv6); Use SYN-mode(Only IPv4) if not set + -sV + port service identify + -timeout int + TCP-mode timeout. unit is ms. If set -1, 800ms. (default -1) + +``` \ No newline at end of file diff --git a/cmd/go-portScan.go b/cmd/go-portScan.go new file mode 100644 index 0000000..7f3ca47 --- /dev/null +++ b/cmd/go-portScan.go @@ -0,0 +1,190 @@ +package main + +import ( + "flag" + "fmt" + "github.com/XinRoom/go-portScan/core/host" + "github.com/XinRoom/go-portScan/core/port" + "github.com/XinRoom/go-portScan/core/port/fingerprint" + "github.com/XinRoom/go-portScan/core/port/syn" + "github.com/XinRoom/go-portScan/util" + "github.com/XinRoom/iprange" + "github.com/panjf2000/ants/v2" + "net" + "os" + "strings" + "sync" + "time" +) + +var ( + ipStr string + portStr string + nP bool + sT bool + rate int + sV bool + timeout int + rateP int +) + +func parseFlag() { + flag.StringVar(&ipStr, "ip", "", "target ip, eg: \"1.1.1.1/30,1.1.1.1-1.1.1.2,1.1.1.1-2\"") + flag.StringVar(&portStr, "port", "top1000", "eg: \"top1000,5612,65120\"") + flag.BoolVar(&nP, "nP", false, "no ping probe") + flag.IntVar(&rateP, "rateP", 300, "concurrent num when ping probe each ip") + flag.BoolVar(&sT, "sT", false, "TCP-mode(support IPv4 and IPv6); Use SYN-mode(Only IPv4) if not set") + flag.BoolVar(&sV, "sV", false, "port service identify") + flag.IntVar(&rate, "rate", -1, fmt.Sprintf("number of packets sent per second. If set -1, TCP-mode is %d, SYN-mode is %d(SYN-mode is restricted by the network adapter)", port.DefaultTcpOption.Rate, syn.DefaultSynOption.Rate)) + flag.IntVar(&timeout, "timeout", -1, "TCP-mode timeout. unit is ms. If set -1, 800ms.") + flag.Parse() +} + +func main() { + parseFlag() + if ipStr == "" { + flag.PrintDefaults() + os.Exit(0) + } + ipRangeGroup := make([]*iprange.Iter, 0) + // ip parse + var firstIp net.IP + ips := strings.Split(ipStr, ",") + for _, _ip := range ips { + it, startIp, err := iprange.NewIter(_ip) + if err != nil { + fmt.Fprintf(os.Stderr, "[error] %s is not ip!\n", _ip) + os.Exit(-1) + } + if firstIp == nil { + firstIp = startIp + } + ipRangeGroup = append(ipRangeGroup, it) + } + // port parse + ports, err := port.ShuffleParseAndMergeTopPorts(portStr) + if err != nil { + fmt.Fprintf(os.Stderr, "[error] %s is not port!", err) + os.Exit(-1) + } + + // recv + single := make(chan struct{}) + retChan := make(chan port.OpenIpPort, 65535) + // port fingerprint + var wgPortIdentify sync.WaitGroup + poolPortIdentify, _ := ants.NewPoolWithFunc(500, func(ipPort interface{}) { + ret := ipPort.(port.OpenIpPort) + fmt.Printf("%s:%d %s\n", ret.Ip, ret.Port, fingerprint.PortIdentify("tcp", ret.Ip, ret.Port)) + wgPortIdentify.Done() + }) + defer poolPortIdentify.Release() + go func() { + for { + select { + case ret := <-retChan: + if ret.Port == 0 { + single <- struct{}{} + return + } + if sV { + // port fingerprint + wgPortIdentify.Add(1) + poolPortIdentify.Invoke(ret) + } else { + fmt.Printf("%v:%d\n", ret.Ip, ret.Port) + } + default: + time.Sleep(time.Millisecond * 10) + } + } + }() + + // Initialize the Scanner + var s port.Scanner + option := port.Option{ + Rate: rate, + Timeout: timeout, + } + if sT { + // tcp + if option.Rate == -1 { + option.Rate = port.DefaultTcpOption.Rate + } + if option.Timeout == -1 { + option.Timeout = port.DefaultTcpOption.Timeout + } + s, err = port.NewTcpScanner(retChan, option) + } else { + // syn + if option.Rate == -1 { + option.Rate = syn.DefaultSynOption.Rate + } + s, err = syn.NewSynScanner(firstIp, retChan, option) + } + if err != nil { + fmt.Fprintf(os.Stderr, "[error] Initialize Scanner: %s", err) + os.Exit(-1) + } + + start := time.Now() + var wgScan sync.WaitGroup + var wgPing sync.WaitGroup + + // Pool - port scan + size := 0 + if !sT { + // syn-mode Concurrency is not recommended !!! + // The default nic is sent at the maximum rate + size = 1 + } + poolScan, _ := ants.NewPoolWithFunc(size, func(ipPort interface{}) { + _ipPort := ipPort.(port.OpenIpPort) + s.Scan(_ipPort.Ip, _ipPort.Port) + wgScan.Done() + }) + defer poolScan.Release() + + // port scan func + portScan := func(ip net.IP) { + for _, _port := range ports { // port + s.WaitLimiter() // limit rate + wgScan.Add(1) + _ = poolScan.Invoke(port.OpenIpPort{ + Ip: ip, + Port: _port, + }) + } + } + + // Pool - ping and port scan + poolPing, _ := ants.NewPoolWithFunc(rateP, func(ip interface{}) { + _ip := ip.(net.IP) + if host.IsLive(_ip.String()) { + portScan(_ip) + } + wgPing.Done() + }) + defer poolPing.Release() + + // start scan + for _, ir := range ipRangeGroup { // ip group + shuffle := util.NewShuffle(ir.TotalNum()) // shuffle + for i := uint64(0); i < ir.TotalNum(); i++ { // ip index + ip := make(net.IP, len(ir.GetIpByIndex(0))) + copy(ip, ir.GetIpByIndex(shuffle.Get(i))) // Note: dup copy []byte when concurrent (GetIpByIndex not to do dup copy) + if !nP { // ping + wgPing.Add(1) + _ = poolPing.Invoke(ip) + } else { + portScan(ip) + } + } + } + wgScan.Wait() // 扫描器-发 + wgPing.Wait() // PING组 + s.Close() // 扫描器-收 + <-single // 接收器-收 + wgPortIdentify.Wait() // 识别器-收 + fmt.Printf("[*] const time: %s\n", time.Since(start)) +} diff --git a/core/host/ping.go b/core/host/ping.go new file mode 100644 index 0000000..64fb58a --- /dev/null +++ b/core/host/ping.go @@ -0,0 +1,69 @@ +package host + +import ( + "bytes" + "github.com/go-ping/ping" + "os/exec" + "runtime" + "strings" + "time" +) + +var CanIcmp bool + +// 判断是否支持发送icmp包 +func init() { + if IcmpOK("localhost") { + CanIcmp = true + } +} + +// IsLive 判断ip是否存活 +func IsLive(ip string) bool { + if CanIcmp { + return IcmpOK(ip) + } else { + return PingOk(ip) + } +} + +// PingOk Ping命令模式 +func PingOk(host string) bool { + switch runtime.GOOS { + case "linux": + cmd := exec.Command("ping", "-c", "1", "-W", "1", host) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Run() + if strings.Contains(out.String(), "ttl=") { + return true + } + case "windows": + cmd := exec.Command("ping", "-n", "1", "-w", "500", host) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Run() + if strings.Contains(out.String(), "TTL=") { + return true + } + } + return false +} + +// IcmpOK 直接发ICMP包 +func IcmpOK(host string) bool { + pinger, err := ping.NewPinger(host) + if err != nil { + return false + } + pinger.SetPrivileged(true) + pinger.Count = 1 + pinger.Timeout = 800 * time.Millisecond + if pinger.Run() != nil { // Blocks until finished. return err + return false + } + if stats := pinger.Statistics(); stats.PacketsRecv > 0 { + return true + } + return false +} diff --git a/core/port/fingerprint/fingerprint.go b/core/port/fingerprint/fingerprint.go new file mode 100644 index 0000000..129f99c --- /dev/null +++ b/core/port/fingerprint/fingerprint.go @@ -0,0 +1,157 @@ +package fingerprint + +import ( + "bytes" + "crypto/tls" + "fmt" + "net" + "regexp" + "strconv" + "time" +) + +type Action uint8 + +const ( + ActionRecv = Action(iota) + ActionSend +) + +// +type ruleData struct { + Action Action // send or recv + Data []byte // send or match data + Regexps []*regexp.Regexp +} + +type serviceRule struct { + Tls bool + DataGroup []ruleData +} + +var serviceRules = make(map[string]serviceRule) + +// PortIdentify 端口识别 +func PortIdentify(network string, ip net.IP, _port uint16) string { + + matchedRule := make(map[string]struct{}) + + // 优先判断port可能的服务 + if serviceNames, ok := portServiceOrder[_port]; ok { + for _, serviceName := range serviceNames { + matchedRule[serviceName] = struct{}{} + if matchRule(network, ip, _port, serviceRules[serviceName]) { + return serviceName + } + } + } + + // 优先判断Top服务 + for _, service := range serviceOrder { + _, ok := matchedRule[service] + if ok { + continue + } + matchedRule[service] = struct{}{} + if matchRule(network, ip, _port, serviceRules[service]) { + return service + } + } + + // other + for service, rule := range serviceRules { + _, ok := matchedRule[service] + if ok { + continue + } + if matchRule(network, ip, _port, rule) { + return service + } + } + + return "unknown" +} + +// 匹配规则 +func matchRule(network string, ip net.IP, _port uint16, serviceRule serviceRule) bool { + var err error + var isTls bool + var conn net.Conn + var connTls *tls.Conn + address := fmt.Sprintf("%s:%d", ip, _port) + + // 建立连接 + if serviceRule.Tls { + // tls + connTls, err = tls.DialWithDialer(&net.Dialer{Timeout: 2 * time.Second}, network, address, &tls.Config{ + InsecureSkipVerify: true, + }) + if err != nil || connTls == nil { + return false + } + defer connTls.Close() + isTls = true + } else { + conn, err = net.DialTimeout(network, address, time.Second*2) + if err != nil || conn == nil { + return false + } + defer conn.Close() + } + + buf := make([]byte, 4096) + + // 读函数 + read := func() (int, error) { + if isTls { + connTls.SetReadDeadline(time.Now().Add(time.Second)) + return connTls.Read(buf[:]) + } else { + conn.SetReadDeadline(time.Now().Add(time.Second)) + return conn.Read(buf[:]) + } + } + + data := []byte("") + // 逐个判断 + for _, rule := range serviceRule.DataGroup { + if rule.Data != nil { + data = rule.Data + } + data = bytes.Replace(rule.Data, []byte("{IP}"), []byte(ip.String()), -1) + data = bytes.Replace(data, []byte("{PORT}"), []byte(strconv.Itoa(int(_port))), -1) + if rule.Action == ActionSend { + if isTls { + connTls.SetWriteDeadline(time.Now().Add(time.Second)) + _, err = connTls.Write(data) + } else { + conn.SetWriteDeadline(time.Now().Add(time.Second)) + _, err = conn.Write(data) + } + if err != nil { + // 出错就退出 + return false + } + } else { + var n int + n, err = read() + // 出错就退出 + if err != nil || n == 0 { + return false + } + // 包含数据就正确 + if rule.Regexps != nil { + for _, _regex := range rule.Regexps { + if _regex.Match(buf[:n]) { + return true + } + } + } + if bytes.Compare(data, []byte("")) != 0 && bytes.Contains(buf[:n], data) { + return true + } + } + } + + return false +} diff --git a/core/port/fingerprint/rules.go b/core/port/fingerprint/rules.go new file mode 100644 index 0000000..0f57857 --- /dev/null +++ b/core/port/fingerprint/rules.go @@ -0,0 +1,264 @@ +package fingerprint + +import "regexp" + +// port fingerprint def +// ref https://raw.githubusercontent.com/nmap/nmap/master/nmap-service-probes + +// Available variables: {PORT},{IP} + +var serviceOrder = []string{"http", "https"} + +var portServiceOrder = map[uint16][]string{ + 21: {"21"}, + 22: {"ssh"}, + 80: {"http", "https"}, + 443: {"https", "http"}, + 1035: {"oracle"}, + 1080: {"socks5", "socks4"}, + 1081: {"socks5", "socks4"}, + 1082: {"socks5", "socks4"}, + 1083: {"socks5", "socks4"}, + 1433: {"sqlserver"}, + 1521: {"oracle"}, + 1522: {"oracle"}, + 1525: {"oracle"}, + 1526: {"oracle"}, + 1574: {"oracle"}, + 1748: {"oracle"}, + 1754: {"oracle"}, + 3306: {"mysql"}, + 6379: {"redis"}, + 9001: {"mongodb"}, + 14238: {"oracle"}, + 27017: {"mongodb"}, + 20000: {"oracle"}, + 49153: {"mongodb"}, +} + +func init() { + // http + serviceRules["http"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("HEAD / HTTP/1.1\r\nHost: {IP}\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0\r\nAccept: */*\r\nAccept-Language: en\r\nAccept-Encoding: deflate\r\n\r\n"), + nil, + }, + { + ActionRecv, + []byte("HTTP/"), + nil, + }, + }, + } + // https + serviceRules["https"] = serviceRule{ + Tls: true, + DataGroup: serviceRules["http"].DataGroup, + } + + // ssh + serviceRules["ssh"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`^SSH-([\d.]+)-`), + regexp.MustCompile(`^SSH-(\d[\d.]+)-`), + regexp.MustCompile(`^SSH-(\d[\d.]*)-`), + regexp.MustCompile(`^SSH-2\.0-`), + regexp.MustCompile(`^SSH-1\.`), + }, + }, + }, + } + + // ftp + serviceRules["ftp"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`^220 ([-/.+\w]+) FTP server`), + regexp.MustCompile(`^220 3Com `), + regexp.MustCompile(`^220-GuildFTPd`), + regexp.MustCompile(`^220-.*\r\n220`), + regexp.MustCompile(`^220 Internet Rex`), + regexp.MustCompile(`^530 Connection refused,`), + regexp.MustCompile(`^220 IIS ([\w._-]+) FTP`), + regexp.MustCompile(`^220 PizzaSwitch `), + regexp.MustCompile(`^220 ([-.+\w]+) FTP`), + regexp.MustCompile(`(?i)^220[ |-](.*?)FTP`), + }, + }, + }, + } + + // socks4 + serviceRules["socks4"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x04\x01\x00\x16\x7f\x00\x00\x01root\x00"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`^\x00\x5a`), + regexp.MustCompile(`^\x00\x5b`), + regexp.MustCompile(`^\x00\x5c`), + regexp.MustCompile(`^\x00\x5d`), + }, + }, + }, + } + + // socks5 + serviceRules["socks5"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x05\x04\x00\x01\x02\x80\x05\x01\x00\x03\x0dwww.baidu.com\x00\x50GET / HTTP/1.0\r\n\r\n"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`^\x05\x00\x05\x01`), + regexp.MustCompile(`^\x05\x00\x05\x00\x00\x01.{6}HTTP`), + regexp.MustCompile(`^\x05\x02`), + regexp.MustCompile(`^\x05\x00`), + }, + }, + }, + } + + // tls + serviceRules["tls"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x16\x03\x00\x00S\x01\x00\x00O\x03\x00?G\xd7\xf7\xba,\xee\xea\xb2`~\xf3\x00\xfd\x82{\xb9\xd5\x96\xc8w\x9b\xe6\xc4\xdb<=\xdbo\xef\x10n\x00\x00(\x00\x16\x00\x13\x00\x0a\x00f\x00\x05\x00\x04\x00e\x00d\x00c\x00b\x00a\x00`\x00\x15\x00\x12\x00\x09\x00\x14\x00\x11\x00\x08\x00\x06\x00\x03\x01\x00"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`^[\x16\x15]\x03\x00`), + regexp.MustCompile(`^[\x16\x15]\x03...\x02`), + }, + }, + }, + } + + // Db + // mysql + serviceRules["mysql"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`(?s)^.\x00\x00\x00\xff..Host .* is not allowed to connect to this .* server$`), + regexp.MustCompile(`^.\x00\x00\x00\xff..Too many connections`), + regexp.MustCompile(`(?s)^.\x00\x00\x00\xff..Host .* is blocked because of many connection errors`), + regexp.MustCompile(`(?s)^.\x00\x00\x00\x0a(\d\.[-_~.+:\w]+MariaDB-[-_~.+:\w]+)`), + regexp.MustCompile(`(?s)^.\x00\x00\x00\x0a(\d\.[-_~.+\w]+)\x00`), + regexp.MustCompile(`(?s)^.\x00\x00\x00\xffj\x04'[\d.]+' .* MySQL`), + }, + }, + }, + } + // redis + serviceRules["redis"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("GET / HTTP/1.1\r\n"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`-ERR operation not permitted\r\n`), + regexp.MustCompile(`-ERR wrong number of arguments for 'get' command\r\n`), + }, + }, + }, + } + + // sqlserver + serviceRules["sqlserver"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00\x01\x02\x00\x1c\x00\x0c\x03\x00\x28\x00\x04\xff\x08\x00\x01\x55\x00\x00\x00\x4d\x53\x53\x51\x4c\x53\x65\x72\x76\x65\x72\x00\x48\x0f\x00\x00"), + nil, + }, + { + ActionRecv, + []byte("\x04\x01\x00\x25\x00\x00\x01\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00\x01\x02\x00\x1c\x00\x01\x03\x00\x1d\x00\x00\xff"), + nil, + }, + }, + } + + // oracle + serviceRules["oracle"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x00Z\x00\x00\x01\x00\x00\x00\x016\x01,\x00\x00\x08\x00\x7F\xFF\x7F\x08\x00\x00\x00\x01\x00 \x00:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\xE6\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00(CONNECT_DATA=(COMMAND=version))"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`(?s)^\x00\x20\x00\x00\x02\x00\x00\x00\x016\x00\x00\x08\x00\x7f\xff\x01\x00\x00\x00\x00\x20`), + regexp.MustCompile(`^\+\x00\x00\x00$`), + }, + }, + }, + } + + // mongodb + serviceRules["mongodb"] = serviceRule{ + Tls: false, + DataGroup: []ruleData{ + { + ActionSend, + []byte("\x41\x00\x00\x00\x3a\x30\x00\x00\xff\xff\xff\xff\xd4\x07\x00\x00\x00\x00\x00\x00test.$cmd\x00\x00\x00\x00\x00\xff\xff\xff\xff\x1b\x00\x00\x00\x01serverStatus\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00"), + nil, + }, + { + ActionRecv, + nil, + []*regexp.Regexp{ + regexp.MustCompile(`(?s)^.*version.....([.\d]+)`), + regexp.MustCompile(`(?s)^\xcb\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa7\x00\x00\x00\x01uptime\x00\x00\x00\x00\x00\x00 ` + "`" + `@\x03globalLock\x009\x00\x00\x00\x01totalTime\x00\x00\x00\x00\x7c\xf0\x9a\x9eA\x01lockTime\x00\x00\x00\x00\x00\x00\xac\x9e@\x01ratio\x00!\xc6\$G\xeb\x08\xf0>\x00\x03mem\x00<\x00\x00\x00\x10resident\x00\x03\x00\x00\x00\x10virtual\x00\xa2\x00\x00\x00\x08supported\x00\x01\x12mapped\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\xf0\?\x00$`), + regexp.MustCompile(`(?s)^.\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\+\x00\x00\x00\x02errmsg\x00\x0e\x00\x00\x00need to login\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`), + regexp.MustCompile(`(?s)^.\x00\x00\x00....:0\x00\x00\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00.\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00.\x00\x00\x00not authorized on`), + }, + }, + }, + } +} diff --git a/core/port/port.go b/core/port/port.go new file mode 100644 index 0000000..cad7b21 --- /dev/null +++ b/core/port/port.go @@ -0,0 +1,196 @@ +package port + +import ( + "errors" + "github.com/XinRoom/go-portScan/util" + "net" + "strconv" + "strings" +) + +// TopTcpPorts 常见端口 ref https://github.com/robertdavidgraham/masscan/blob/master/src/main-conf.c +var TopTcpPorts = []uint16{ + 1, 3, 4, 6, 7, 9, 13, 17, 19, 20, 21, 22, 23, 24, 25, 26, 30, 32, 33, 37, 42, 43, 49, 53, 70, + 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 99, 100, 106, 109, 110, 111, 113, 119, 125, 135, + 139, 143, 144, 146, 161, 163, 179, 199, 211, 212, 222, 254, 255, 256, 259, 264, 280, + 301, 306, 311, 340, 366, 389, 406, 407, 416, 417, 425, 427, 443, 444, 445, 458, 464, + 465, 481, 497, 500, 512, 513, 514, 515, 524, 541, 543, 544, 545, 548, 554, 555, 563, + 587, 593, 616, 617, 625, 631, 636, 646, 648, 666, 667, 668, 683, 687, 691, 700, 705, + 711, 714, 720, 722, 726, 749, 765, 777, 783, 787, 800, 801, 808, 843, 873, 880, 888, + 898, 900, 901, 902, 903, 911, 912, 981, 987, 990, 992, 993, 995, 999, 1000, 1001, + 1002, 1007, 1009, 1010, 1011, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, + 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, + 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, + 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, + 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, + 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, + 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1102, 1104, 1105, 1106, 1107, 1108, + 1110, 1111, 1112, 1113, 1114, 1117, 1119, 1121, 1122, 1123, 1124, 1126, 1130, + 1131, 1132, 1137, 1138, 1141, 1145, 1147, 1148, 1149, 1151, 1152, 1154, 1163, + 1164, 1165, 1166, 1169, 1174, 1175, 1183, 1185, 1186, 1187, 1192, 1198, 1199, + 1201, 1213, 1216, 1217, 1218, 1233, 1234, 1236, 1244, 1247, 1248, 1259, 1271, + 1272, 1277, 1287, 1296, 1300, 1301, 1309, 1310, 1311, 1322, 1328, 1334, 1352, + 1417, 1433, 1434, 1443, 1455, 1461, 1494, 1500, 1501, 1503, 1521, 1524, 1533, + 1556, 1580, 1583, 1594, 1600, 1641, 1658, 1666, 1687, 1688, 1700, 1717, 1718, + 1719, 1720, 1721, 1723, 1755, 1761, 1782, 1783, 1801, 1805, 1812, 1839, 1840, + 1862, 1863, 1864, 1875, 1900, 1914, 1935, 1947, 1971, 1972, 1974, 1984, 1998, + 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2013, + 2020, 2021, 2022, 2030, 2033, 2034, 2035, 2038, 2040, 2041, 2042, 2043, 2045, + 2046, 2047, 2048, 2049, 2065, 2068, 2099, 2100, 2103, 2105, 2106, 2107, 2111, + 2119, 2121, 2126, 2135, 2144, 2160, 2161, 2170, 2179, 2190, 2191, 2196, 2200, + 2222, 2251, 2260, 2288, 2301, 2323, 2366, 2381, 2382, 2383, 2393, 2394, 2399, + 2401, 2492, 2500, 2522, 2525, 2557, 2601, 2602, 2604, 2605, 2607, 2608, 2638, + 2701, 2702, 2710, 2717, 2718, 2725, 2800, 2809, 2811, 2869, 2875, 2909, 2910, + 2920, 2967, 2968, 2998, 3000, 3001, 3003, 3005, 3006, 3007, 3011, 3013, 3017, + 3030, 3031, 3052, 3071, 3077, 3128, 3168, 3211, 3221, 3260, 3261, 3268, 3269, + 3283, 3300, 3301, 3306, 3322, 3323, 3324, 3325, 3333, 3351, 3367, 3369, 3370, + 3371, 3372, 3389, 3390, 3404, 3476, 3493, 3517, 3527, 3546, 3551, 3580, 3659, + 3689, 3690, 3703, 3737, 3766, 3784, 3800, 3801, 3809, 3814, 3826, 3827, 3828, + 3851, 3869, 3871, 3878, 3880, 3889, 3905, 3914, 3918, 3920, 3945, 3971, 3986, + 3995, 3998, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4045, 4111, 4125, 4126, + 4129, 4224, 4242, 4279, 4321, 4343, 4443, 4444, 4445, 4446, 4449, 4550, 4567, + 4662, 4848, 4899, 4900, 4998, 5000, 5001, 5002, 5003, 5004, 5009, 5030, 5033, + 5050, 5051, 5054, 5060, 5061, 5080, 5087, 5100, 5101, 5102, 5120, 5190, 5200, + 5214, 5221, 5222, 5225, 5226, 5269, 5280, 5298, 5357, 5405, 5414, 5431, 5432, + 5440, 5500, 5510, 5544, 5550, 5555, 5560, 5566, 5631, 5633, 5666, 5678, 5679, + 5718, 5730, 5800, 5801, 5802, 5810, 5811, 5815, 5822, 5825, 5850, 5859, 5862, + 5877, 5900, 5901, 5902, 5903, 5904, 5906, 5907, 5910, 5911, 5915, 5922, 5925, + 5950, 5952, 5959, 5960, 5961, 5962, 5963, 5987, 5988, 5989, 5998, 5999, 6000, + 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6009, 6025, 6059, 6100, 6101, 6106, + 6112, 6123, 6129, 6156, 6346, 6389, 6502, 6510, 6543, 6547, 6565, 6566, 6567, + 6580, 6646, 6666, 6667, 6668, 6669, 6689, 6692, 6699, 6779, 6788, 6789, 6792, + 6839, 6881, 6901, 6969, 7000, 7001, 7002, 7004, 7007, 7019, 7025, 7070, 7100, + 7103, 7106, 7200, 7201, 7402, 7435, 7443, 7496, 7512, 7625, 7627, 7676, 7741, + 7777, 7778, 7800, 7911, 7920, 7921, 7937, 7938, 7999, 8000, 8001, 8002, 8007, + 8008, 8009, 8010, 8011, 8021, 8022, 8031, 8042, 8045, 8080, 8081, 8082, 8083, + 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8093, 8099, 8100, 8180, 8181, 8192, + 8193, 8194, 8200, 8222, 8254, 8290, 8291, 8292, 8300, 8333, 8383, 8400, 8402, + 8443, 8500, 8600, 8649, 8651, 8652, 8654, 8701, 8800, 8873, 8888, 8899, 8994, + 9000, 9001, 9002, 9003, 9009, 9010, 9011, 9040, 9050, 9071, 9080, 9081, 9090, + 9091, 9099, 9100, 9101, 9102, 9103, 9110, 9111, 9200, 9207, 9220, 9290, 9415, + 9418, 9485, 9500, 9502, 9503, 9535, 9575, 9593, 9594, 9595, 9618, 9666, 9876, + 9877, 9878, 9898, 9900, 9917, 9929, 9943, 9944, 9968, 9998, 9999, 10000, 10001, + 10002, 10003, 10004, 10009, 10010, 10012, 10024, 10025, 10082, 10180, 10215, + 10243, 10566, 10616, 10617, 10621, 10626, 10628, 10629, 10778, 11110, 11111, + 11967, 12000, 12174, 12265, 12345, 13456, 13722, 13782, 13783, 14000, 14238, + 14441, 14442, 15000, 15002, 15003, 15004, 15660, 15742, 16000, 16001, 16012, + 16016, 16018, 16080, 16113, 16992, 16993, 17877, 17988, 18040, 18101, 18988, + 19101, 19283, 19315, 19350, 19780, 19801, 19842, 20000, 20005, 20031, 20221, + 20222, 20828, 21571, 22939, 23502, 24444, 24800, 25734, 25735, 26214, 27000, + 27352, 27353, 27355, 27356, 27715, 28201, 30000, 30718, 30951, 31038, 31337, + 32768, 32769, 32770, 32771, 32772, 32773, 32774, 32775, 32776, 32777, 32778, + 32779, 32780, 32781, 32782, 32783, 32784, 32785, 33354, 33899, 34571, 34572, + 34573, 35500, 38292, 40193, 40911, 41511, 42510, 44176, 44442, 44443, 44501, + 45100, 48080, 49152, 49153, 49154, 49155, 49156, 49157, 49158, 49159, 49160, + 49161, 49163, 49165, 49167, 49175, 49176, 49400, 49999, 50000, 50001, 50002, + 50003, 50006, 50300, 50389, 50500, 50636, 50800, 51103, 51493, 52673, 52822, + 52848, 52869, 54045, 54328, 55055, 55056, 55555, 55600, 56737, 56738, 57294, + 57797, 58080, 60020, 60443, 61532, 61900, 62078, 63331, 64623, 64680, 65000, + 65129, 65389} + +type Scanner interface { + Close() + Scan(ip net.IP, dst uint16) error + WaitLimiter() error +} + +// OpenIpPort retChan +type OpenIpPort struct { + Ip net.IP + Port uint16 +} + +// Option ... +type Option struct { + Rate int // 每秒速度限制, 单位: s, 会在1s内平均发送, 相当于每个包之间的延迟 + Timeout int // TCP连接响应延迟, 单位: ms +} + +// ParsePortRangeStr 解析端口字符串 +func ParsePortRangeStr(portStr string) (out [][]uint16, err error) { + portsStrGroup := strings.Split(portStr, ",") + var portsStrGroup3 []string + var portStart, portEnd uint64 + for _, portsStrGroup2 := range portsStrGroup { + if portsStrGroup2 == "top1000" { + continue + } + portsStrGroup3 = strings.Split(portsStrGroup2, "-") + portStart, err = strconv.ParseUint(portsStrGroup3[0], 10, 16) + if err != nil { + return + } + portEnd = portStart + if len(portsStrGroup3) == 2 { + portEnd, err = strconv.ParseUint(portsStrGroup3[1], 10, 16) + } + if err != nil { + return + } + out = append(out, []uint16{uint16(portStart), uint16(portEnd)}) + } + return +} + +// IsInPortRange 判断port是否在端口范围里 +func IsInPortRange(port uint16, portRanges [][]uint16) bool { + for _, portRange := range portRanges { + if port >= portRange[0] && port <= portRange[1] { + return true + } + } + return false +} + +// ShuffleParseAndMergeTopPorts shuffle parse portStr and merge TopTcpPorts +func ShuffleParseAndMergeTopPorts(portStr string) (ports []uint16, err error) { + if portStr == "" { + ports = TopTcpPorts + return + } + var portRanges [][]uint16 + portRanges, err = ParsePortRangeStr(portStr) + if err != nil { + return + } + // 优先发送top端口 + selectTopPort := make(map[uint16]struct{}) // TopPort + hasTopStr := strings.Contains(portStr, "top1000") + for _, _port := range TopTcpPorts { + if hasTopStr || IsInPortRange(_port, portRanges) { + selectTopPort[_port] = struct{}{} + ports = append(ports, _port) + } + } + selectPort := make(map[uint16]struct{}) // OtherPort + for _, portRange := range portRanges { + var ok bool + for _port := portRange[0]; _port <= portRange[1]; _port++ { + if _port == 0 { + continue + } + if _, ok = selectTopPort[_port]; ok { + continue + } else if _, ok = selectPort[_port]; ok { + continue + } + selectPort[_port] = struct{}{} + ports = append(ports, _port) + } + } + if len(ports) == 0 { + err = errors.New("ports len is 0") + return + } + // 端口随机化 + skip := uint64(len(selectTopPort)) // 跳过Top + _ports := make([]uint16, len(ports)) + copy(_ports, ports) + sf := util.NewShuffle(uint64(len(ports)) - skip) + if sf != nil { + for i := skip; i < uint64(len(_ports)); i++ { + ports[i] = _ports[skip+sf.Get(i-skip)] + } + } + return +} diff --git a/core/port/syn/device.go b/core/port/syn/device.go new file mode 100644 index 0000000..ead5fdf --- /dev/null +++ b/core/port/syn/device.go @@ -0,0 +1,60 @@ +package syn + +import ( + "fmt" + "github.com/google/gopacket/pcap" + "github.com/jackpal/gateway" + "net" +) + +// GetDevByIp get dev name by dev ip (use pcap) +func GetDevByIp(ip net.IP) (devName string, err error) { + devices, err := pcap.FindAllDevs() + if err != nil { + return + } + for _, d := range devices { + for _, address := range d.Addresses { + _ip := address.IP.To4() + if _ip != nil && _ip.IsGlobalUnicast() && _ip.Equal(ip) { + return d.Name, nil + } + } + } + return +} + +// GetIfaceMac get interface mac addr by interface ip (use golang net) +func GetIfaceMac(ifaceAddr net.IP) (src net.IP, mac net.HardwareAddr) { + interfaces, _ := net.Interfaces() + for _, iface := range interfaces { + if addrs, err := iface.Addrs(); err == nil { + for _, addr := range addrs { + if addr.(*net.IPNet).Contains(ifaceAddr) { + return addr.(*net.IPNet).IP, iface.HardwareAddr + } + } + } + } + return nil, nil +} + +// GetRouterV4 get ipv6 router by dst ip +func GetRouterV4(dst net.IP) (srcIp net.IP, srcMac net.HardwareAddr, gw net.IP, devName string, err error) { + // 同网段 + srcIp, srcMac = GetIfaceMac(dst) + if srcIp == nil { + // 取第一个默认路由 + gw, err = gateway.DiscoverGateway() + gw = gw.To4() + if err == nil { + srcIp, srcMac = GetIfaceMac(gw) + } + } + srcIp = srcIp.To4() + devName, err = GetDevByIp(srcIp) + if srcIp == nil || err != nil { + return nil, nil, nil, "", fmt.Errorf("no router, %s", err) + } + return +} diff --git a/core/port/syn/syn.go b/core/port/syn/syn.go new file mode 100644 index 0000000..18dd4f6 --- /dev/null +++ b/core/port/syn/syn.go @@ -0,0 +1,359 @@ +package syn + +import ( + "context" + "errors" + "github.com/XinRoom/go-portScan/core/port" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + limiter "golang.org/x/time/rate" + "math/rand" + "net" + "sync" + "time" +) + +var DefaultSynOption = port.Option{ + Rate: 2000, + Timeout: 0, +} + +type synScanner struct { + srcMac, gwMac net.HardwareAddr // macAddr + devName string // eth dev(pcap) + + // gateway (if applicable), and source IP addresses to use. + gw, srcIp net.IP + + // pcap + handle *pcap.Handle + + // opts and buf allow us to easily serialize packets in the send() method. + opts gopacket.SerializeOptions + + // Buffer复用 + bufPool *sync.Pool + + // + retChan chan port.OpenIpPort // results chan + limiter *limiter.Limiter + ctx context.Context + watchIpStatusT *watchIpStatusTable // IpStatusCacheTable + watchMacCacheT *watchMacCacheTable // MacCaches + isDone bool +} + +// NewSynScanner firstIp: Used to select routes; retChan: Result return channel +func NewSynScanner(firstIp net.IP, retChan chan port.OpenIpPort, option port.Option) (ss *synScanner, err error) { + // option verify + if option.Rate <= 0 { + err = errors.New("rate can not set to 0") + return + } + + // get router info + srcIp, srcMac, gw, devName, err := GetRouterV4(firstIp) + if err != nil { + return + } + + rand.Seed(time.Now().Unix()) + + ss = &synScanner{ + opts: gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + }, + srcIp: srcIp, + srcMac: srcMac, + devName: devName, + bufPool: &sync.Pool{ + New: func() interface{} { + return gopacket.NewSerializeBuffer() + }, + }, + retChan: retChan, + limiter: limiter.NewLimiter(limiter.Every(time.Second/time.Duration(option.Rate)), option.Rate), + ctx: context.Background(), + watchIpStatusT: newWatchIpStatusTable(), + watchMacCacheT: newWatchMacCacheTable(), + } + + // Pcap + // 每个包最大读取长度1024, 不开启混杂模式, no TimeOut + handle, err := pcap.OpenLive(devName, 1024, false, pcap.BlockForever) + if err != nil { + return + } + // Set filter + handle.SetBPFFilter("tcp || arp") + ss.handle = handle + + // start listen recv + go ss.recv() + + if gw != nil { + // get gateway mac addr + var dstMac net.HardwareAddr + dstMac, err = ss.getHwAddrV4(gw) + if err != nil { + return + } + ss.gwMac = dstMac + } + + return +} + +// Scan scans the dst IP address and port of this scanner. +func (ss *synScanner) Scan(dstIp net.IP, dst uint16) (err error) { + if ss.isDone { + return errors.New("scanner is closed") + } + + dstIp = dstIp.To4() + if dstIp == nil { + return errors.New("is not ipv4") + } + + // watchIp, first + ipStr := dstIp.String() + ss.watchIpStatusT.UpdateLastTime(ipStr) + + // First off, get the MAC address we should be sending packets to. + var dstMac net.HardwareAddr + if ss.gwMac != nil { + dstMac = ss.gwMac + } else { + // 内网IP + mac := ss.watchMacCacheT.GetMac(ipStr) + if mac != nil { + dstMac = mac + } else { + dstMac, err = ss.getHwAddrV4(dstIp) + if err != nil { + return + } + } + } + + // Construct all the network layers we need. + eth := layers.Ethernet{ + SrcMAC: ss.srcMac, + DstMAC: dstMac, + EthernetType: layers.EthernetTypeIPv4, + } + ip4 := layers.IPv4{ + SrcIP: ss.srcIp, + DstIP: dstIp, + Version: 4, + TTL: 64, + Flags: layers.IPv4DontFragment, + Protocol: layers.IPProtocolTCP, + } + tcp := layers.TCP{ + SrcPort: layers.TCPPort(49000 + rand.Intn(5000)), // Random source port and used to determine recv dst port range + DstPort: layers.TCPPort(dst), + SYN: true, + Window: 65535, + Options: []layers.TCPOption{ + { + OptionType: layers.TCPOptionKindMSS, + OptionLength: 4, + OptionData: []byte{0x05, 0x84}, // 1412 + }, + }, + } + tcp.SetNetworkLayerForChecksum(&ip4) + + // Send one packet per loop iteration until we've sent packets + ss.send(ð, &ip4, &tcp) + + return +} + +// Close cleans up the handle and chan. +func (ss *synScanner) Close() { + // Delay 2s for a reply from the last packet + time.Sleep(time.Millisecond * 100) + if !ss.watchIpStatusT.IsEmpty() { + time.Sleep(time.Second * 2) + } + ss.isDone = true + ss.handle.Close() + ss.watchMacCacheT.Close() + ss.watchIpStatusT.Close() + ss.watchMacCacheT = nil + ss.watchIpStatusT = nil + close(ss.retChan) +} + +// WaitLimiter Waiting for the speed limit +func (ss *synScanner) WaitLimiter() error { + return ss.limiter.Wait(ss.ctx) +} + +// getHwAddrV4 get the destination hardware address for our packets. +func (ss *synScanner) getHwAddrV4(arpDst net.IP) (mac net.HardwareAddr, err error) { + // Prepare the layers to send for an ARP request. + eth := layers.Ethernet{ + SrcMAC: ss.srcMac, + DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + EthernetType: layers.EthernetTypeARP, + } + arp := layers.ARP{ + AddrType: layers.LinkTypeEthernet, + Protocol: layers.EthernetTypeIPv4, + HwAddressSize: 6, + ProtAddressSize: 4, + Operation: layers.ARPRequest, + SourceHwAddress: []byte(ss.srcMac), + SourceProtAddress: []byte(ss.srcIp), + DstHwAddress: []byte{0, 0, 0, 0, 0, 0}, + DstProtAddress: []byte(arpDst), + } + + ipStr := arpDst.String() + start := time.Now() + var retry int + + for { + ss.watchMacCacheT.UpdateLastTime(ipStr) // New one ip watch + mac = ss.watchMacCacheT.GetMac(ipStr) + if mac != nil { + return mac, nil + } + if retry%25 == 0 { + if err = ss.send(ð, &arp); err != nil { + return nil, err + } + } + + retry += 1 + + // Wait 3 seconds for an ARP reply. + if time.Since(start) > time.Second*3 { + return nil, errors.New("timeout getting ARP reply") + } + time.Sleep(time.Millisecond * 20) + } +} + +// send sends the given layers as a single packet on the network. +func (ss *synScanner) send(l ...gopacket.SerializableLayer) error { + buf := ss.bufPool.Get().(gopacket.SerializeBuffer) + defer func() { + buf.Clear() + ss.bufPool.Put(buf) + }() + if err := gopacket.SerializeLayers(buf, ss.opts, l...); err != nil { + return err + } + return ss.handle.WritePacketData(buf.Bytes()) +} + +// recv packet on the network. +func (ss *synScanner) recv() { + eth := layers.Ethernet{ + SrcMAC: ss.srcMac, + DstMAC: nil, + EthernetType: layers.EthernetTypeIPv4, + } + ip4 := layers.IPv4{ + SrcIP: ss.srcIp, + DstIP: []byte{}, + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolTCP, + } + tcp := layers.TCP{ + SrcPort: 0, + DstPort: 0, + RST: true, + ACK: true, + Seq: 1, + } + + // Decode + var ipLayer layers.IPv4 + var tcpLayer layers.TCP + var arpLayer layers.ARP + var ethLayer layers.Ethernet + var foundLayerTypes []gopacket.LayerType + + // Parse the packet. + parser := gopacket.NewDecodingLayerParser( + layers.LayerTypeEthernet, + ðLayer, + &ipLayer, + &tcpLayer, + &arpLayer, + ) + + // global var + var err error + var data []byte + var ipStr string + var _port uint16 + + for { + // Read in the next packet. + data, _, err = ss.handle.ReadPacketData() + if err != nil { + continue + } + + // Decode TCP Packet + err = parser.DecodeLayers(data, &foundLayerTypes) + if err != nil { + continue + } + + // is done + if ss.isDone { + return + } + + // arp + if arpLayer.SourceProtAddress != nil { + ipStr = net.IP(arpLayer.SourceProtAddress).String() + if ss.watchMacCacheT.IsNeedWatch(ipStr) { + ss.watchMacCacheT.SetMac(ipStr, arpLayer.SourceHwAddress) + } + arpLayer.SourceProtAddress = nil + continue + } + + // tcp Match ip and port + if tcpLayer.DstPort >= 49000 && tcpLayer.DstPort <= 54000 { + ipStr = ipLayer.SrcIP.String() + _port = uint16(tcpLayer.SrcPort) + if !ss.watchIpStatusT.HasIp(ipStr) { // IP + continue + } else { + if ss.watchIpStatusT.HasPort(ipStr, _port) { // PORT + continue + } else { + ss.watchIpStatusT.RecordPort(ipStr, _port) // record + } + } + + if tcpLayer.SYN && tcpLayer.ACK { + ss.retChan <- port.OpenIpPort{ + Ip: ipLayer.SrcIP, + Port: _port, + } + // reply to target + eth.DstMAC = ethLayer.SrcMAC + ip4.DstIP = ipLayer.SrcIP + tcp.DstPort = tcpLayer.SrcPort + tcp.SrcPort = tcpLayer.DstPort + // RST && ACK + tcp.Ack = tcpLayer.Seq + 1 + tcp.SetNetworkLayerForChecksum(&ip4) + ss.send(ð, &ip4, &tcp) + } + } + } +} diff --git a/core/port/syn/syn_test.go b/core/port/syn/syn_test.go new file mode 100644 index 0000000..c289687 --- /dev/null +++ b/core/port/syn/syn_test.go @@ -0,0 +1,60 @@ +package syn + +import ( + "github.com/XinRoom/go-portScan/core/host" + "github.com/XinRoom/go-portScan/core/port" + "github.com/XinRoom/iprange" + "log" + "testing" + "time" +) + +func TestSynScanner_Scan(t *testing.T) { + + single := make(chan struct{}) + retChan := make(chan port.OpenIpPort, 65535) + go func() { + for { + select { + case ret := <-retChan: + if ret.Port == 0 { + single <- struct{}{} + return + } + log.Println(ret) + default: + time.Sleep(time.Millisecond * 10) + } + } + }() + + // 解析端口字符串并且优先发送 TopTcpPorts 中的端口, eg: 1-65535,top1000 + ports, err := port.ShuffleParseAndMergeTopPorts("top1000") + if err != nil { + t.Fatal(err) + } + + // parse ip + it, startIp, _ := iprange.NewIter("1.1.1.1/30") + + // scanner + ss, err := NewSynScanner(startIp, retChan, DefaultSynOption) + if err != nil { + t.Fatal(err) + } + + start := time.Now() + for i := uint64(0); i < it.TotalNum(); i++ { // ip索引 + ip := it.GetIpByIndex(i) + if !host.IsLive(ip.String()) { // ping + continue + } + for _, _port := range ports { // port + ss.WaitLimiter() + ss.Scan(ip, _port) // syn 不能并发,默认以网卡和驱动最高性能发包 + } + } + ss.Close() + <-single + t.Log(time.Since(start)) +} diff --git a/core/port/syn/watchIpStatus.go b/core/port/syn/watchIpStatus.go new file mode 100644 index 0000000..41d0f33 --- /dev/null +++ b/core/port/syn/watchIpStatus.go @@ -0,0 +1,106 @@ +package syn + +import ( + "sync" + "time" +) + +type watchIpStatus struct { + ReceivedPort map[uint16]struct{} + LastTime time.Time +} + +// IP状态更新表 +type watchIpStatusTable struct { + watchIpS map[string]*watchIpStatus + lock sync.RWMutex + isDone bool +} + +func newWatchIpStatusTable() (w *watchIpStatusTable) { + w = &watchIpStatusTable{ + watchIpS: make(map[string]*watchIpStatus), + } + go w.cleanTimeout() + return +} + +// UpdateLastTime 新建或者更新LastTime +func (w *watchIpStatusTable) UpdateLastTime(ip string) { + lastTime := time.Now() + w.lock.Lock() + wi, ok := w.watchIpS[ip] + if ok { + wi.LastTime = lastTime + } else { + w.watchIpS[ip] = &watchIpStatus{LastTime: lastTime, ReceivedPort: make(map[uint16]struct{})} + } + w.lock.Unlock() +} + +// RecordPort 记录收到的端口 +func (w *watchIpStatusTable) RecordPort(ip string, port uint16) { + lastTime := time.Now() + w.lock.Lock() + wi, ok := w.watchIpS[ip] + if ok { + wi.LastTime = lastTime + wi.ReceivedPort[port] = struct{}{} + } else { + w.watchIpS[ip] = &watchIpStatus{LastTime: lastTime, ReceivedPort: make(map[uint16]struct{})} + w.watchIpS[ip].ReceivedPort[port] = struct{}{} + } + w.lock.Unlock() +} + +// HasPort 判断是否检测过对应端口 +func (w *watchIpStatusTable) HasPort(ip string, port uint16) (has bool) { + w.lock.RLock() + wi, ok := w.watchIpS[ip] + if ok { + _, has = wi.ReceivedPort[port] + } + w.lock.RUnlock() + return +} + +// HasIp 判断是否在监视对应IP +func (w *watchIpStatusTable) HasIp(ip string) (has bool) { + w.lock.RLock() + _, has = w.watchIpS[ip] + w.lock.RUnlock() + return +} + +// IsEmpty 判断目前表是否为空 +func (w *watchIpStatusTable) IsEmpty() (empty bool) { + w.lock.RLock() + if len(w.watchIpS) == 0 { + empty = true + } + w.lock.RUnlock() + return +} + +func (w *watchIpStatusTable) Close() { + w.isDone = true +} + +// 清理过期数据 +func (w *watchIpStatusTable) cleanTimeout() { + for { + if w.isDone { + break + } + time.Sleep(3 * time.Second) + w.lock.RLock() + for k, v := range w.watchIpS { + if time.Since(v.LastTime) > 5*time.Second { + w.lock.Lock() + delete(w.watchIpS, k) + w.lock.Unlock() + } + } + w.lock.RUnlock() + } +} diff --git a/core/port/syn/watchMacCache.go b/core/port/syn/watchMacCache.go new file mode 100644 index 0000000..23d42ec --- /dev/null +++ b/core/port/syn/watchMacCache.go @@ -0,0 +1,108 @@ +package syn + +import ( + "net" + "sync" + "time" +) + +type watchMacCache struct { + LastTime time.Time + Mac net.HardwareAddr +} + +// Mac缓存和监听表 +type watchMacCacheTable struct { + watchMacC map[string]*watchMacCache + lock sync.RWMutex + isDone bool +} + +func newWatchMacCacheTable() (w *watchMacCacheTable) { + w = &watchMacCacheTable{ + watchMacC: make(map[string]*watchMacCache), + } + go w.cleanTimeout() + return +} + +// UpdateLastTime 新建或者更新LastTime +func (w *watchMacCacheTable) UpdateLastTime(ip string) { + lastTime := time.Now() + w.lock.Lock() + wi, ok := w.watchMacC[ip] + if ok { + wi.LastTime = lastTime + } else { + w.watchMacC[ip] = &watchMacCache{LastTime: lastTime} + } + w.lock.Unlock() +} + +// SetMac 设置Mac地址 +func (w *watchMacCacheTable) SetMac(ip string, mac net.HardwareAddr) { + lastTime := time.Now() + w.lock.Lock() + wi, ok := w.watchMacC[ip] + if ok { + wi.LastTime = lastTime + wi.Mac = mac + } else { + w.watchMacC[ip] = &watchMacCache{LastTime: lastTime, Mac: mac} + wi.Mac = mac + } + w.lock.Unlock() +} + +// GetMac 获取Mac地址缓存 +func (w *watchMacCacheTable) GetMac(ip string) (mac net.HardwareAddr) { + w.lock.RLock() + wi, ok := w.watchMacC[ip] + if ok { + mac = wi.Mac + } + w.lock.RUnlock() + return +} + +// IsNeedWatch 判断是否需要监视 +func (w *watchMacCacheTable) IsNeedWatch(ip string) (has bool) { + w.lock.RLock() + wm, ok := w.watchMacC[ip] + has = ok && wm.Mac == nil + w.lock.RUnlock() + return +} + +// IsEmpty 判断目前表是否为空 +func (w *watchMacCacheTable) IsEmpty() (empty bool) { + w.lock.RLock() + if len(w.watchMacC) == 0 { + empty = true + } + w.lock.RUnlock() + return +} + +func (w *watchMacCacheTable) Close() { + w.isDone = true +} + +// 清理过期数据 +func (w *watchMacCacheTable) cleanTimeout() { + for { + if w.isDone { + break + } + time.Sleep(5 * time.Second) + w.lock.RLock() + for k, v := range w.watchMacC { + if time.Since(v.LastTime) > 20*time.Second { + w.lock.Lock() + delete(w.watchMacC, k) + w.lock.Unlock() + } + } + w.lock.RUnlock() + } +} diff --git a/core/port/tcp.go b/core/port/tcp.go new file mode 100644 index 0000000..1d51826 --- /dev/null +++ b/core/port/tcp.go @@ -0,0 +1,73 @@ +package port + +import ( + "context" + "errors" + "fmt" + limiter "golang.org/x/time/rate" + "net" + "time" +) + +var DefaultTcpOption = Option{ + Rate: 1000, + Timeout: 800, +} + +type tcpScanner struct { + ports []uint16 // 指定端口 + retChan chan OpenIpPort // 返回值队列 + limiter *limiter.Limiter + ctx context.Context + timeout time.Duration + isDone bool +} + +// NewTcpScanner Tcp扫描器 +func NewTcpScanner(retChan chan OpenIpPort, option Option) (ts *tcpScanner, err error) { + // option verify + if option.Rate <= 0 { + err = errors.New("rate can not set to 0") + return + } + if option.Timeout <= 0 { + err = errors.New("timeout can not set to 0") + return + } + + ts = &tcpScanner{ + retChan: retChan, + limiter: limiter.NewLimiter(limiter.Every(time.Second/time.Duration(option.Rate)), option.Rate), + ctx: context.Background(), + timeout: time.Duration(option.Timeout) * time.Millisecond, + } + + return +} + +// Scan 对指定IP和dis port进行扫描 +func (ts *tcpScanner) Scan(ip net.IP, dst uint16) error { + if ts.isDone { + return errors.New("scanner is closed") + } + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, dst), ts.timeout) + if err == nil && conn != nil { + ts.retChan <- OpenIpPort{ + Ip: ip, + Port: dst, + } + conn.Close() + } + return nil +} + +// Close chan +func (ts *tcpScanner) Close() { + ts.isDone = true + close(ts.retChan) +} + +// WaitLimiter Waiting for the speed limit +func (ts *tcpScanner) WaitLimiter() error { + return ts.limiter.Wait(ts.ctx) +} diff --git a/core/port/tcp_test.go b/core/port/tcp_test.go new file mode 100644 index 0000000..2dd400f --- /dev/null +++ b/core/port/tcp_test.go @@ -0,0 +1,67 @@ +package port + +import ( + "github.com/XinRoom/go-portScan/core/host" + "github.com/XinRoom/iprange" + "log" + "net" + "sync" + "testing" + "time" +) + +func TestTcpScanner_Scan(t *testing.T) { + + single := make(chan struct{}) + retChan := make(chan OpenIpPort, 65535) + go func() { + for { + select { + case ret := <-retChan: + if ret.Port == 0 { + single <- struct{}{} + return + } + log.Println(ret) + default: + time.Sleep(time.Millisecond * 10) + } + } + }() + + // 解析端口字符串并且优先发送 TopTcpPorts 中的端口, eg: 1-65535,top1000 + ports, err := ShuffleParseAndMergeTopPorts("top1000") + if err != nil { + t.Fatal(err) + } + + // parse Ip + it, _, _ := iprange.NewIter("1.1.1.1/30") + + // scanner + ss, err := NewTcpScanner(retChan, DefaultTcpOption) + if err != nil { + t.Fatal(err) + } + + start := time.Now() + var wg sync.WaitGroup + for i := uint64(0); i < it.TotalNum(); i++ { // ip索引 + ip := make(net.IP, len(it.GetIpByIndex(0))) + copy(ip, it.GetIpByIndex(i)) // Note: dup copy []byte when concurrent (GetIpByIndex not to do dup copy) + if !host.IsLive(ip.String()) { // ping + continue + } + for _, _port := range ports { // port + ss.WaitLimiter() + wg.Add(1) + go func(ip net.IP, port uint16) { + ss.Scan(ip, port) + wg.Done() + }(ip, _port) + } + } + ss.Close() + <-single + t.Log(time.Since(start)) +} diff --git a/core/port/udp.go b/core/port/udp.go new file mode 100644 index 0000000..9f6eed6 --- /dev/null +++ b/core/port/udp.go @@ -0,0 +1,3 @@ +package port + +// A large number of UDP ports need to send packets in the corresponding format to return. Do not do temporarily. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..47211c7 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/XinRoom/go-portScan + +go 1.17 + +require ( + github.com/XinRoom/iprange v1.1.1 + github.com/go-ping/ping v0.0.0-20211014180314-6e2b003bffdd + github.com/google/gopacket v1.1.19 + github.com/jackpal/gateway v1.0.7 + github.com/panjf2000/ants/v2 v2.4.6 + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac +) + +require ( + github.com/google/uuid v1.3.0 // indirect + golang.org/x/net v0.0.0-20211105192438-b53810dc28af // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..35c6d3c --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +github.com/XinRoom/iprange v1.1.1 h1:mcpn1YIYQzWceVJImL2Idvi9/f5jE+tt70aVQ9gxVJo= +github.com/XinRoom/iprange v1.1.1/go.mod h1:Q5oo7bnEdIYQ8wl5tUyx9x/KdpAJUk+nGw+6TAoxl/0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ping/ping v0.0.0-20211014180314-6e2b003bffdd h1:WwvAAgvP2MO5bxYtSlDovMFWiTd9vi+Ey7CJ54wAUrc= +github.com/go-ping/ping v0.0.0-20211014180314-6e2b003bffdd/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackpal/gateway v1.0.7 h1:7tIFeCGmpyrMx9qvT0EgYUi7cxVW48a0mMvnIL17bPM= +github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsSd1AcIbA= +github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ= +github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20211105192438-b53810dc28af h1:SMeNJG/vclJ5wyBBd4xupMsSJIHTd1coW9g7q6KOjmY= +golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005 h1:pDMpM2zh2MT0kHy037cKlSby2nEhD50SYqwQk76Nm40= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42 h1:G2DDmludOQZoWbpCr7OKDxnl478ZBGMcOhrv+ooX/Q4= +golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/util/shuffle.go b/util/shuffle.go new file mode 100644 index 0000000..21977d3 --- /dev/null +++ b/util/shuffle.go @@ -0,0 +1,57 @@ +package util + +import ( + "math/rand" +) + +type Shuffle struct { + rl []uint16 // 乱序序列 + rl2 []uint16 // 最后一轮乱序序列(无法整除时使用) + n uint16 // 乱序精度 + size uint64 +} + +// NewShuffle 局部乱序 +func NewShuffle(size uint64) *Shuffle { + if size == 0 { + return nil + } + sf := &Shuffle{size: size} + if size > 1000 { + sf.n = 1000 + } else { + sf.n = uint16(size) + } + // 通用轮次 + sf.rl = make([]uint16, sf.n) + for i := uint16(0); i < sf.n; i++ { + sf.rl[i] = i + } + // 洗牌方法 + r := rand.New(rand.NewSource(int64(size))) + r.Shuffle(int(sf.n), func(i, j int) { + sf.rl[i], sf.rl[j] = sf.rl[j], sf.rl[i] + }) + // 最后一轮无法整除时新建对应长度的rl2 + t := uint16(size % uint64(sf.n)) + if t != 0 { + sf.rl2 = make([]uint16, t) + for i := uint16(0); i < t; i++ { + sf.rl2[i] = i + } + r.Shuffle(int(t), func(i, j int) { + sf.rl2[i], sf.rl2[j] = sf.rl2[j], sf.rl2[i] + }) + } + return sf +} + +// Get 根据索引获取转换后的索引值 +func (sf *Shuffle) Get(index uint64) uint64 { + t := index % uint64(sf.n) + // 最后一轮无法整除时用rl2 + if index-t+uint64(sf.n) > sf.size { + return index - t + uint64(sf.rl2[uint16(t)]) + } + return index - t + uint64(sf.rl[uint16(t)]) +} diff --git a/util/shuffle_test.go b/util/shuffle_test.go new file mode 100644 index 0000000..d102a02 --- /dev/null +++ b/util/shuffle_test.go @@ -0,0 +1,17 @@ +package util + +import ( + "testing" +) + +func TestShuffle_Next(t *testing.T) { + size := 10001 + sf := NewShuffle(uint64(size)) + ret := make([]uint64, size) + for i := 0; i < size; i++ { + ret[i] = sf.Get(uint64(i)) + } + if ret[size-1] != uint64(size-1) { + t.Error(ret) + } +}