Go实现TCP端口扫描器和代理


理解TCP握手包

TCP是面向连接的,可靠的也是现代网络的基础,所以要想搞定网络就必须先搞定TCP协议。

分别对应端口打开的情况,端口关闭的情况,使用防火墙的情况.

使用端口转发绕过防火墙

防火墙可以阻止或允许客户端访问某个服务器和端口,我们可以使用一个中间代理来绕过防火墙这项技术被称为端口转发。比如防火墙不允许访问evil.com但是允许访问stacktitan.com那么就可以使用stacktitan.com作为跳板访问evil.com

写一个TCP Scanner

想要掌握TCP端口的概念,一个有效的方法就是实现一个TCP Scanner,我们从一个基本的Scanner开始,然后使用并行等技术来加速扫描速度。

测试端口是否开放

首先让我们编写一个Go程序实现客户端和服务器的TCP连接。

package main

import (
    "fmt"
    "net"
)

func main() {
    host := "127.0.0.1"  // 本机地址
    port := 1080  // 本机开启的神秘服务
    address := fmt.Sprintf(host + ":%d", port)  // Sprintf进行字符串拼接并返回
    /*
        net.Dial第一个参数指明哪种服务类型,第二个参数接收一个字符串类型,在“tcp”下
        格式为"host:port"
        返回conn句柄和错误信息。

  */
    conn, err := net.Dial("tcp", address) 
    if err != nil {
        fmt.Printf("[ERROR] 连接服务器%v[端口%d]失败:%v\n", host, port, err)
        return
    }
    fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", host, port)
    _ = conn.Close() //检测完成关闭连接
}

实现一个简单的端口扫描器(too simple版)

有了上面的代码做基础,随便加加个for不就能实现端口的扫描了?

package main

import (
    "fmt"
    "net"
)

func main() {
    host := "127.0.0.1"
    // 一把梭
    for port := 1; port <= 65535; port++ {
        address := fmt.Sprintf(host + ":%d", port)
        conn, err := net.Dial("tcp", address)
        if err != nil {
            // 简单起见仅输出可以连接的端口
            continue
        }
        fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", host, port)
        _ = conn.Close()
    }

}

/*
运行go run main.go 得到:
[SUCC] 成功连接服务器127.0.0.1[端口443].
[SUCC] 成功连接服务器127.0.0.1[端口631].
[SUCC] 成功连接服务器127.0.0.1[端口902].
[SUCC] 成功连接服务器127.0.0.1[端口1080].
[SUCC] 成功连接服务器127.0.0.1[端口2333].
[SUCC] 成功连接服务器127.0.0.1[端口6942].
[SUCC] 成功连接服务器127.0.0.1[端口6943].
[SUCC] 成功连接服务器127.0.0.1[端口8307].
[SUCC] 成功连接服务器127.0.0.1[端口12333].
[SUCC] 成功连接服务器127.0.0.1[端口63342].
[SUCC] 成功连接服务器127.0.0.1[端口63343].
*/

实现并发扫描(too young版)

上面的两个程序都是单个程序扫描多个端口,我们的目标是并发的扫描多个端口,所以我们要借助goroutines的力量,go允许创建N多个goroutines(只要你的系统可以handle的了)而且每个goroutines需要的内存很少。

package main

import (
    "fmt"
    "net"
)

func main() {
    host := "127.0.0.1"
    // 一把梭
    for port := 1; port <= 65535; port++ {
        go func(port int) {
            address := fmt.Sprintf(host + ":%d", port)
            conn, err := net.Dial("tcp", address)
            if err != nil {
                // 简单起见仅输出可以连接的端口
                return
            }
            fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", host, port)
            _ = conn.Close()
        }(port)
    }

}

如果我们运行go run main.go会发现程序并没有输出就结束了,怎么回事?让我们仔细分析一下程序,主函数中仅包含这一个循环,那么当循环结束后主程序就退出了,换句话说循环所创建的goroutine还没来得及发送packet主程序就结束了,资源也就被释放了。

我们可以使用sync.WaitGroup来解决这个问题 ,WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减详细信息

package main

import (
    "fmt"
    "net"
    "sync"
)

func main() {
    host := "127.0.0.1"
    // 一把梭
    var wg sync.WaitGroup // 创建变量
    for port := 1; port <= 65535; port++ {
        wg.Add(1)
        go func(port int) {
            defer wg.Done()
            address := fmt.Sprintf(host + ":%d", port)
            conn, err := net.Dial("tcp", address)
            if err != nil {
                // 简单起见仅输出可以连接的端口
                return
            }
            fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", host, port)
            _ = conn.Close()
        }(port)
    }
    wg.Wait() // 阻塞程序直至计数器为零
}

/*
[SUCC] 成功连接服务器127.0.0.1[端口2333].
[SUCC] 成功连接服务器127.0.0.1[端口6942].
[SUCC] 成功连接服务器127.0.0.1[端口8307].
[SUCC] 成功连接服务器127.0.0.1[端口443].
[SUCC] 成功连接服务器127.0.0.1[端口631].
[SUCC] 成功连接服务器127.0.0.1[端口902].
[SUCC] 成功连接服务器127.0.0.1[端口1080].
[SUCC] 成功连接服务器127.0.0.1[端口6943].
[SUCC] 成功连接服务器127.0.0.1[端口63342].
[SUCC] 成功连接服务器127.0.0.1[端口63343].
[SUCC] 成功连接服务器127.0.0.1[端口12333].
*/

这个版本的程序确实比之前好了一些但是还不够好,在这个程序中我们创建了65535个gorountine,但事实上我们并不需要这么多gorountine,这么多的gorountine会造成资源的浪费,如果计算机资源耗尽很可能会导致其他的错误如, 程序运行结果不一致。

使用worker pool的端口扫描器

首先我们使用for循环来创建制定数目的worker作为我们的资源池然后在”主线程”中使用channel来给worker提供工作。

package main

import (
    "fmt"
    "net"
    "sync"
)

const HOST  = "127.0.0.1"

//WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址
func worker(ports chan int, wg *sync.WaitGroup) {
    for p := range ports { // 持续从ports中获取数据直至channel被关闭
        address := fmt.Sprintf(HOST + ":%d", p)
        conn, err := net.Dial("tcp", address)
        if err != nil {
            // 简单起见仅输出可以连接的端口
            wg.Done()
            continue
        }
        fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", HOST, p)
        _ = conn.Close()
        wg.Done()
    }
}
func main() {
    /*
        buffered channel, sender不需要等待receiver获取,buffered channel适用于多个生产者和消费
    者的情况,在buffer满了之后sender就阻塞
    */
    ports := make(chan int, 100) 
    var wg sync.WaitGroup

    for i := 0; i < cap(ports); i++ {
        go worker(ports, &wg)
    }

    for i := 1; i <= 65535; i++ {
        wg.Add(1)
        ports <- i
    }
    wg.Wait()
}

多通道通信

可以使用通道将sync.WaitGroup去掉,不过要注意通道也会加重系统资源的消耗。

package main

import (
    "fmt"
    "net"
)

const HOST = "127.0.0.1"

//WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址
func worker(ports, results chan int) {
    for p := range ports {
        address := fmt.Sprintf(HOST+":%d", p)
        conn, err := net.Dial("tcp", address)
        if err != nil {
            // 简单起见仅输出可以连接的端口
            results <- 0
            continue
        }

        _ = conn.Close()
        results <- p
    }
}
func main() {
    ports := make(chan int, 100)
    results := make(chan int, 100)

    for i := 0; i < cap(ports); i++ {
        go worker(ports, results)
    }

    go func() {
        for i := 1; i <= 65535; i++ {
            ports <- i
        }
    }()

    for i := 0; i < 65535; i++ {
        port := <-results
        if port != 0 {
            fmt.Printf("[SUCC] 成功连接服务器%v[端口%d].\n", HOST, port)
        }
    }

}

搭建一个TCP代理

首先我们会实现一个“echo server”,然后实现一个TCP端口转发器.

Using io.Reader and io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}

这两个类型用于几乎所有的输入输出任务(TCP,HTTP, filesystem),这两个类型是任何消息传递的基础,这两个类型被定义成接口类型,可以把它理解为一种抽象的方法,所有实现了Read和Write的结构体就是Reader或者Writer

type FooReader struct {}
func (fooReader *FooReader) Read(p []byte) (int, error) {
    // Read some data
    return len(data), nil
}

type FooWriter struct {}
func (fooWriter *FooWriter) Write(p []byte) (int, error) {
    // Write data somewhere.
    return len(data), nil
}

我们用上面的知识来创建一个


文章作者: Hanjun Liu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hanjun Liu !
 上一篇
Chapter 12. Custom Models and Training with TensorFlow Chapter 12. Custom Models and Training with TensorFlow
虽然Tensorflow的高级API-keras差不多够用了,但以防万一本章将深入学习Tensorflow的低级API. A Quick Tour of TensorflowTensorflow不仅仅局限于深度学习,可以用在任何的大规模计算
2020-02-04
下一篇 
中国软件杯--南京游记 中国软件杯--南京游记
启程 2018.8.28早晨9点多打车到流亭机场, 中途停车加气,最后花了253,反正学校报销,花就是了。 流亭机场上的飞机,延误了一会儿才起飞。 大约一个小时就到南京了,没想到8月底了南京还是这么热,背着大书包坐地铁到定好的酒店,
2018-09-04
  目录