golang并发扫描

 
更多

Golang并发

经典并发模型

进程是资源分配的基本单位,每个进程有自己的虚拟地址空间、代码、数据和其它各种系统资源。线程是资源调度的基本单位,每个进程一开始都会有一个线程,这个线程被称为主线程,之后根据需要创建的其他线程被称为子线程,他们都在同一个进程中所以会共享数据和其他各种系统资源。

线程的几种分类

  • 内核级线程

正儿八经的实现了并行,而且各个线程之间不受影响, 但是消耗很大。

  • 用户级线程

实现了并发,消耗不是很大,线程之间某些情况会互相影响。

  • 混合型线程

    多对多的关系 golang使用的是这种模型

python实现并发的方式

重IO操作(url请求,爬虫)

重CPU操作(正则,大数运算,加解密)

遇到重IO操作推荐使用线程或协程那套

遇到重CPU操作推荐使用多进程那套

  • 普通多线程

    由于python有GIL python的多线程实际上效率还是等同于使用一个核。

  • 普通多进程

    突破GIL锁实现了并发,本质上是开启了多个python虚拟机,这样就实现了并行。

    都可以提高速度

  • 线程池

    相比普通多线程 省去了重复创建线程与删除线程的时间

  • 进程池

    相比普通多进程省去了重复创建进程与删除进程的时间

  • 协程

    python中协程可实现的地方很有限因为包很多地方还不支持。协程在线程之中存在,而且更加轻量级。原理可以理解为在线程中这样

    while True://在线程内部执行协程
        //当斜程处于阻塞状态或时间片用完就执行下一个斜程
    

Go并发: G-P-M模型

  • M

    内核级线程 go 程序启动时,会设置 M 的最大数量,默认 10000。M通过P获取要运行的G

  • G

    goroutine

    很类似python里面的协程,但是比python中的协程用途要更广范更方便更快速,在Go语言中,每一个goroutine是一个独立的执行单元,相较于每个OS线程固定分配2M内存的模式,goroutine的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且完全由golang自己的调度器 Go Scheduler 来调度。此外,GC还会周期性地将不再使用的内存回收,收缩栈空间。

  • P

    processor

    线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。GOMAXPROCS()可配置P的数量,可以把P理解为中介。

执行P对应的G队列,G1的时间片用完或产生阻塞操作继续执行队列中的G2,G1放到队列的末尾。全局队列不断的向各个队列中加入任务。

TCP全连接扫描

全连接会在服务端留下痕迹,而且大范围的全连接扫描可能会被认定为dos攻击

package main

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"sort"
	"strconv"
	"time"
)

type Result struct {
	Ip   string `json:"ip"`
	Port []int  `json:"port"`
}

func worker(ip string, ports, results chan int) {
	for p := range ports { //阻塞性处理ports
		address := fmt.Sprintf("%s:%d", ip, p)
		con, err := net.DialTimeout("tcp", address, 10*time.Second) //tcp探测,会在主机留下记录
		if err != nil {
			results <- 0 //端口未开放返回0
			continue
		}
		con.Close()
		results <- p //端口开放返回端口
	}
}

func scan(w http.ResponseWriter, r *http.Request) {
	ip := r.URL.Query().Get("ip")
	amount, _ := strconv.Atoi(r.URL.Query().Get("amount"))
	ports, results := make(chan int, 100), make(chan int) //ports是下发任务的通道,result是返回扫描结果的通道
	var openPorts = make([]int, 0, 20)                    //存放开放端口的数字类型切片 设容量为20

	for i := 0; i < 4000; i++ {
		go worker(ip, ports, results) // 开启4000个goroutine去探测端口-->换算成线程就小几十个线程而已
	}

	go func() {
		for i := 1; i <= amount; i++ {
			ports <- i //下发端口
		}
	}()

	for i := 1; i <= amount; i++ { //等待扫描结果
		port := <-results
		if port != 0 {
			openPorts = append(openPorts, port)
		}
	}
	close(ports)
	close(results)
	sort.Ints(openPorts)

	w.Header().Set("Content-Type", "application-json")
	result, _ := json.Marshal(Result{
		Ip:   ip,
		Port: openPorts,
	})
	_, _ = w.Write(result)
}

func main() {
	http.HandleFunc("/", scan)
	_ = http.ListenAndServe(":50000", nil)
}

现在扫描15002个端口用时17.33s,当然速度还可以提升。不过由于之后请求基本都是多个客户端请求,在保证返回数据准确性的情况下这里设置为开启4000个worker。

SYN半连接扫描

扫描速度快,因为根本就没有建立连接所以不会在对方系统留下记录SYN,但是SYN扫描由于其扫描行为较为明显,容易被入侵检测系统发现,也容易被防火墙屏蔽,且构造原始数据包需要较高系统权限。

如果能够收到SYN+ACK 数据包,则代表此端口开放,如收到RST 数据包,则证明此端口关闭,如未收到任何数据包,且确定该主机存在,则证明该端口被防火墙等安全设备过滤。

package main

import (
	"encoding/json"
	"fmt"
	"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"
	"net/http"
	"time"
)
type Result struct {
	Ip   string `json:"ip"`
	Port []uint16  `json:"port"`
}

func scan(w http.ResponseWriter, r *http.Request) {
	ip := r.URL.Query().Get("ip")
	openPorts:=make([]uint16,0,20)
	single := make(chan struct{})
	/*type OpenIpPort struct {
		Ip   net.IP
		Port uint16
	}*/
	retChan := make(chan port.OpenIpPort, 65535)
	go func() { // 开启一个goroutine专门处理扫描结果
		for {
			select {
			case ret := <-retChan:
				if ret.Port == 0 {
					single <- struct{}{}
					return
				}
				openPorts=append(openPorts, ret.Port)
			default:
				time.Sleep(time.Millisecond * 10)
			}
		}
	}()

	// 解析端口字符串并且优先发送 TopTcpPorts 中的端口, eg: 1-65535,top1000
	ports, err := port.ShuffleParseAndMergeTopPorts("1-15000")
	if err != nil {
		log.Fatal(err)
	}

	// parse ip
	it, startIp, _ := iprange.NewIter(fmt.Sprintf("%s",ip))
	fmt.Println(startIp)

	// scanner
	ss, err := syn.NewSynScanner(startIp, retChan, syn.DefaultSynOption)
	if err != nil {
		log.Fatal(err)
	}

	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


	w.Header().Set("Content-Type", "application-json")
	result, _ := json.Marshal(Result{
		Ip:   ip,
		Port: openPorts,
	})
	_, _ = w.Write(result)
}

func main()  {
	http.HandleFunc("/", scan)
	_ = http.ListenAndServe(":50000", nil)
}

由于golang很多内容并不完善,这里使用了第三方的库构建原始的tcp包实现syn半连接扫描。

虽然逻辑并不复杂 但是从头到尾全部都手动构建格式会很繁琐

打赏

本文固定链接: https://www.cxy163.net/archives/5231 | 绝缘体-小明哥的技术博客

该日志由 绝缘体.. 于 2025年05月25日 发表在 git, go, python, 开发工具, 编程语言, 首页 分类下, 你可以发表评论,并在保留原文地址及作者的情况下引用到你的网站或博客。
原创文章转载请注明: golang并发扫描 | 绝缘体-小明哥的技术博客
关键字: , , , ,
【上一篇】
【下一篇】

golang并发扫描:等您坐沙发呢!

发表评论


快捷键:Ctrl+Enter