美文网首页
写一个做端口转发的小工具

写一个做端口转发的小工具

作者: MathCEC | 来源:发表于2020-05-24 22:10 被阅读0次

    场景

    有的时候,服务器并不是有那么多端口供我们访问,甚至于22,80,443这些常用的端口都会被关闭来防止有人恶意如入侵。开放到公网的端口将会非常少,当我们部署的应用一多起来,端口可能就不够用了,这个时候,可能就需要用到端口转发,当然如果你是一个经常使用Shell的人,Putty和XShell中的隧道你可能不陌生,这边我们就模拟着实现一个类似的小工具。甚至于我们可以给这个小工具再做一次加密和鉴权,当然此次没有实现。只是实现了端口转发时公网端口的复用。

    源代码

    package main
    
    import (
        "flag"
        "fmt"
        "io/ioutil"
        "net"
        "os"
        "os/exec"
        "strconv"
        "strings"
        "time"
    )
    
    var (
        confFilePath string
        signal       string
        help         bool
        background   bool
        fork         bool
        confList     []conf
    )
    
    func init() {
        bingOptions()
    }
    
    func main() {
        processOption()
    }
    //读取命令行的参数
    func bingOptions() {
        //使用命令
        flag.StringVar(&confFilePath, "c", "tcpStation.etc", "紧跟配置文件的绝对路径或者相对路径")
        flag.StringVar(&signal, "s", "", "stop 停止监听;reload 重新加载配置文件")
        flag.BoolVar(&help, "h", false, "展示帮助信息")
        flag.BoolVar(&background, "b", false, "后台运行")
        flag.BoolVar(&fork, "f", false, "你别随便乱用")
        flag.Parse()
    }
    //解析命令行的参数
    func processOption() {
        if help {
            showHelp()
        }
        if background {
            startBackgroundProcess()
        }
        if fork {
            pid := fmt.Sprintf("%d", os.Getpid())
            _ = ioutil.WriteFile("./tcp.pid", []byte(pid), os.ModePerm)
            loadConfToConfStruct()
            server()
        }
        switch signal {
        case "stop":
            bs, _ := ioutil.ReadFile("./tcp.pid")
            cmd := "kill -9 " + string(bs)
            execute(cmd)
        case "reload":
                    //  此处没有实现重新加载的逻辑;
            loadConfToConfStruct()
        default:
            showHelp()
        }
    }
    
    func startBackgroundProcess() {
        file := os.Args[0]
        //> /dev/null 2>&1
        cmd := "nohup " + file + " -f -c " + confFilePath + " > log.log 2>&1 &"
        execute(cmd)
        os.Exit(0)
    }
    
    func execute(cmd string) {
        _, err := exec.Command("bash", "-c", cmd).CombinedOutput()
        if err != nil {
            println(err.Error())
        }
    }
    
    func showHelp() {
        println(`
    隧道建立工具
        -b  后台启动本程序
        -c  指定配置文件
        -h  展示本帮助
        -s  signal  发送信号量 stop 停止监听;reload 重新加载配置文件`)
        os.Exit(-1)
    }
    
    type conf struct {
        //看看ProxyIp和Port是不是-1:-1
        toProxy bool
        //看看targetProxy是不是-1:-1
        fromProxy bool
        localPort string
        target    string
        proxy     string
    }
    
    func loadConfToConfStruct() {
        data, err := ioutil.ReadFile(confFilePath)
        if err != nil {
            println(err.Error())
            os.Exit(-1)
        }
        dataStr := string(data)
        configArr := strings.Split(dataStr, "\n")
        for _, line := range configArr {
            line = strings.TrimSpace(line)
            println(strings.Index(line, "#"))
            if strings.Index(line, "#") == 0 {
                continue
            }
            items := strings.Split(line, ",")
            if len(items) != 3 {
                continue
            }
            confList = append(confList, conf{
                toProxy:   items[2] != "-1:-1", // fix by mathcec 2020.8.10 逻辑反了
                fromProxy: items[1] == "-1:-1",
                localPort: items[0],
                target:    items[1],
                proxy:     items[2],
            })
        }
        //confList = []conf{{
        //  toProxy:    false,
        //  fromProxy:  true,
        //  localPort:  "8080",
        //  targetIp:   "-1",
        //  targetPort: -1,
        //  proxyIp:    "-1",
        //  proxyPort:  -1,
        //}, {
        //  toProxy:    true,
        //  fromProxy:  false,
        //  localPort:  "9000",
        //  targetIp:   "127.0.0.1",
        //  targetPort: 9001,
        //  proxyIp:    "127.0.0.1",
        //  proxyPort:  8080,
        //}, {
        //  toProxy:    false,
        //  fromProxy:  false,
        //  localPort:  "9002",
        //  targetIp:   "127.0.0.1",
        //  targetPort: 9001,
        //  proxyIp:    "-1",
        //  proxyPort:  -1,
        //}}
    }
    
    func server() {
        for _, c := range confList {
            go startProxy(c)
        }
        for {
            time.Sleep(1 * 1000000000)
        }
    }
    
    func startProxy(localConf conf) {
        proxyListen, err := net.Listen("tcp", ":"+localConf.localPort)
        if err != nil {
            println(err.Error())
            return
        }
        defer proxyListen.Close()
        for {
            proxyConn, err := proxyListen.Accept()
            if err != nil {
                println(err.Error())
                continue
            }
            var buffer []byte
            if localConf.fromProxy {
                buffer = make([]byte, 12)
            } else {
                buffer = make([]byte, 6)
            }
            n, err := proxyConn.Read(buffer)
            if err != nil {
                println(err.Error())
                continue
            }
            var targetConn net.Conn;
            //判断是不是需要进行代理 需要的话 在最开始设置6位作为target
            if localConf.toProxy {
                buffer = append(ip2byte(localConf.target), buffer...)
                targetConn, err = net.Dial("tcp", localConf.proxy)
            } else {
                //判断是不是代理 如果是代理
                var target = ""
                if localConf.fromProxy {
                    target = byte2ip(buffer[:6])
                } else {
                    target = localConf.target
                }
                targetConn, err = net.Dial("tcp", target)
            }
            if err != nil {
                println(err.Error())
                proxyConn.Close()
                continue
            }
            //如果代理过来的,前6为需要拿掉
            if localConf.fromProxy {
                //因为只拿了6个,所以不能发送其余的内容
                n, err = targetConn.Write(buffer[6:n])
            } else {
                //这里有BUG的风险,如果没有6个呢
                n, err = targetConn.Write(buffer[:])
            }
            if err != nil {
                println(err.Error())
                proxyConn.Close()
                targetConn.Close()
                continue
            }
            //在前面
            go proxy(proxyConn, targetConn)
            go proxy(targetConn, proxyConn)
    
        }
    }
    
    func proxy(c1, c2 net.Conn) {
        defer c1.Close()
        defer c2.Close()
        var buffer = make([]byte, 409600)
        for {
            n, err := c1.Read(buffer)
            if err != nil {
                break
            }
            n, err = c2.Write(buffer[:n])
            if err != nil {
                break
            }
        }
    
    }
    
    func ip2byte(ipAndPort string) []byte {
        brr := strings.Split(ipAndPort, ":")
        arr := strings.Split(brr[0], ".")
        res := make([]byte, 6)
        for i, a := range arr {
            u, _ := strconv.ParseUint(a, 10, 8)
            res[i] = byte(u)
        }
        u, _ := strconv.ParseUint(brr[1], 10, 16)
        res[4] = byte(u / 256)
        res[5] = byte(u - u/256*256)
        return res
    }
    
    func byte2ip(byte []byte) string {
        return fmt.Sprintf("%d.%d.%d.%d:%d", byte[0], byte[1], byte[2], byte[3], int(byte[4])*256+int(byte[5]))
    }
    
    

    原理

    原理其实很简单。我们将监听模式分为两种,一种是监听本地端口并转发至目标端口,一种是监听本地端口并转发至代理端口,由代理端口转发至目标端口。

    第一种很简单,当获端口监听到内容时,会首先读取一点点内容,然后开启两个个协程去处理。每个协程都会将自己接收到的输出流写入到另一端的输入流中。

    第二中就比较麻烦了,也是端口监听到内容,此时会将真正的目的地写到整个流的初始位置。代理端口收到后会建立和目标端口的连接,同时将流中的标记数据删除后转发到目的端口。

    配置文件是这个样子的

    # 本地端口,目标IP:目标端口,代理服务器:代理端口;
    # 代理服务器和代理端口不是-1:-1则认为有代理服务器
    # 当目标服务器和端口是-1:-1时认为从数据包中获取目标服务器
    
    8080,-1:-1,-1:-1
    9000,127.0.0.1:9001,127.0.0.1:8080
    

    这个时候,你可以启动一个Http服务器测试一下

    package main
    
    import "net/http"
    
    func main() {
        http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
            writer.Write([]byte("HelloWorld"))
        })
        http.ListenAndServe(":9001", nil)
    }
    

    此时按照配置文件的描述,你访问本地的9000端口,就会经过8080端口转发到9001端口。此处是将两个配置写到一起了。理论上这两个配置是要分到两个程序中。代理端口8080,-1:-1,-1:-1需要部署在服务器上。转发端口9000,127.0.0.1:9001,127.0.0.1:8080随便部署在一个你机器和服务器网络都通的地方。

    考虑到拓展,你这个时候是不是可以在配置文件中写一个盐值对时间戳加盐,然后就可以双端校验了,甚至于,你都以可仿照SSH免密登陆的形式,搞一个密钥对。当然这不属于基本功能,这边就先不实现了。

    同时,还实现了Go语言在后台执行,不过这边使用的是nohup,感觉应该还会有更好的办法。不过不太想在这种小工具中引入过多的三方库。我本人对这种小工具或者小工具包调用第三方库还是不是理解的。

    相关文章

      网友评论

          本文标题:写一个做端口转发的小工具

          本文链接:https://www.haomeiwen.com/subject/iadsahtx.html