go实现一个内网穿透工具

在做web开发,公司提出一个内网穿透的需求,考虑使用花生壳之类的工具,原来对这部分内容一直比较感兴趣,顺手研究了一下;
代码在最后,先整理一下思路:

1.

外网用户访问一个网页,首先需要一个固定的地址/域名,这样你必须有一台拥有公网IP的服务器,来实现
用户 ==> 中介服务器
这个请求。

2.

当外网用户访问中介服务器之后,怎么从内网服务器获取数据呢?
首先外网服务器不能主动访问内网内容,但是可以被动应答;
所以我们可以在中介服务器开启一个tcp服务监听,让内网主机主动连接中介服务器,当需要获取数据的时候就可以通过这种方式获取数据;这样
中介服务器 <== 内网主机的链接就实现了

3.

然后链接1,2步骤中的内容就可以实现内网穿透了,当然这个要求内网主机是可以访问外网资源的,
用户 <==> 中介服务器 <==> 内网主机
如果内网主机不能访问外网资源,那就需要一台能否同时访问内网和外网的主机来实现
用户 <==> 中介服务器 <==> DMZ主机 <==> 内网主机

大概的思路就是这样,实现中还需要一个在内网里运行的程序中做一个控制指令tcp长链接,用于当用户请求网址时中介主机向内网发送通知说“用户请求内容了,赶紧跟我的隧道监听端口通信”,这样才能建立起一个临时的通信隧道。

代码如下

安装在中介服务器上的程序

package main

import (
    "fmt"
    "io"
    "net"
    "strconv"
    "sync"
    "time"
)

/**
本程序为用户可以直接访问的中介服务器上运行的穿透服务端
工作流程:
    1启动 ControlPort 的监听,等待在内网环境运行的受控端连接
        (内网主机要连接到这个端口等待控制指令和心跳)
    2启动 ListenPort 的监听,等待用户的web请求
    3启动 TunnelPort 的监听,等待内网受控端链接隧道
    4启动定时释放任务,清楚过期链接和无效链接
    5创建一个阻塞等待配对器组装
用户访问时:
    1. ListenPort 接受到用户的请求,
        通过addConnMatchAccept新建(未完全配置,tunnel为空)一个配对器ConnMatch到全局map connListMap中,
    2.通过ControlPort向内网被控主机发送一个链接通知“new\n”
    3.-内网主机在接收到消息后,打通真实web服务器和TunnelPort的隧道
    4.监听在TunnelPort的程序得到一个新的请求链接
    5.在makeForward方法中通过configConnListTunnel方法将1步骤创建的不完整的配对器补充完整
    6.通过向通道connListMapUpdate中传值通信,运行tcpForward中joinConn方法,将来用户的链接和来自内网客户端的链接绑定
从而实现内网穿透
 */

func main() {
    //监听控制端口8009
    go makeControl()
    //监听服务端口8007
    go makeAccept()
    //监听转发端口8008
    go makeForward()
    //定时释放连接
    go releaseConnMatch()
    //执行tcp转发
    tcpForward()
}

const (
    //与安装在内网服务器的client通信长链接接口
    ControlPort = ":8009"
    //链路实际通信端口
    TunnelPort  = ":8008"
    //用户通信监听端口,即用户严重的web服务器地址
    ListenPort  = ":8007"
)

var cache *net.TCPConn = nil

func makeControl() {
    var tcpAddr *net.TCPAddr
    tcpAddr, _ = net.ResolveTCPAddr("tcp", ControlPort)
    //打开一个tcp断点监听
    tcpListener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        panic(err)
    }
    fmt.Println("控制端口已经监听")
    for {
        tcpConn, err := tcpListener.AcceptTCP()
        if err != nil {
            panic(err)
        }
        fmt.Println("新的客户端连接到控制端服务进程:" + tcpConn.RemoteAddr().String())
        if cache != nil {
            fmt.Println("已经存在一个客户端连接!")
            //直接关闭掉多余的客户端请求
            tcpConn.Close()
        } else {
            cache = tcpConn
        }
        go control(tcpConn)
    }
}
func control(conn *net.TCPConn) {
    go func() {
        for {
            //一旦有客户端连接到服务端的话,服务端每隔2秒发送hi消息给到客户端
            //如果发送不出去,则认为链路断了,清除cache连接
            _, e := conn.Write(([]byte)("hi\n"))
            if e != nil {
                cache = nil
            }
            time.Sleep(time.Second * 2)
        }
    }()
}

func makeAccept() {
    var tcpAddr *net.TCPAddr
    tcpAddr, _ = net.ResolveTCPAddr("tcp", ListenPort)
    tcpListener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        panic(err)
    }
    defer tcpListener.Close()
    for {
        tcpConn, err := tcpListener.AcceptTCP()
        if err != nil {
            fmt.Println(err)
            continue
        }
        fmt.Println("A client connected 8007:" + tcpConn.RemoteAddr().String())
        addConnMatchAccept(tcpConn)
        sendMessage("new\n")
    }
}

//配对器
type ConnMatch struct {
    accept        *net.TCPConn //8007 tcp链路 accept
    acceptAddTime int64        //接受请求的时间
    tunnel        *net.TCPConn //8008 tcp链路 tunnel
}

var connListMap = make(map[string]*ConnMatch)
var lock = sync.Mutex{}

func addConnMatchAccept(accept *net.TCPConn) {
    //加锁防止竞争读写map
    lock.Lock()
    defer lock.Unlock()
    now := time.Now().UnixNano()
    connListMap[strconv.FormatInt(now, 10)] = &ConnMatch{accept, time.Now().Unix(), nil}
}

func sendMessage(message string) {
    fmt.Println("send Message " + message)
    if cache != nil {
        _, e := cache.Write([]byte(message))
        if e != nil {
            fmt.Println("消息发送异常")
            fmt.Println(e.Error())
        }
    } else {
        fmt.Println("没有客户端连接,无法发送消息")
    }
}

func makeForward() {
    var tcpAddr *net.TCPAddr
    tcpAddr, _ = net.ResolveTCPAddr("tcp", TunnelPort)
    tcpListener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        panic(err)
    }
    defer tcpListener.Close()
    fmt.Println("Server ready to read ...")
    for {
        tcpConn, err := tcpListener.AcceptTCP()
        if err != nil {
            fmt.Println(err)
            continue
        }
        fmt.Println("A client connected 8008 :" + tcpConn.RemoteAddr().String())
        configConnListTunnel(tcpConn)
    }
}

var connListMapUpdate = make(chan int)

func configConnListTunnel(tunnel *net.TCPConn) {
    //加锁解决竞争问题//todo
    lock.Lock()
    used := false
    for _, connMatch := range connListMap {
        //找到tunnel为nil的而且accept不为nil的connMatch
        if connMatch.tunnel == nil && connMatch.accept != nil {
            //填充tunnel链路
            connMatch.tunnel = tunnel
            used = true
            //这里要break,是防止这条链路被赋值到多个connMatch!
            break
        }
    }
    if !used {
        //如果没有被使用的话,则说明所有的connMatch都已经配对好了,直接关闭多余的8008链路
        fmt.Println(len(connListMap))
        _ = tunnel.Close()
        fmt.Println("关闭多余的tunnel")
    }
    lock.Unlock()
    //使用channel机制来告诉另一个方法已经就绪
    connListMapUpdate <- 0
}
func tcpForward() {
    for {
        select {
        case <-connListMapUpdate:
            lock.Lock()
            for key, connMatch := range connListMap {
                //如果两个都不为空的话,建立隧道连接
                if connMatch.tunnel != nil && connMatch.accept != nil {
                    fmt.Println("建立tcpForward隧道连接")
                    go joinConn(connMatch.accept, connMatch.tunnel)
                    //从map中删除
                    delete(connListMap, key)
                }
            }
            lock.Unlock()
        }
    }
}
func joinConn(conn1 *net.TCPConn, conn2 *net.TCPConn) {
    f := func(local *net.TCPConn, remote *net.TCPConn) {
        //defer保证close
        defer local.Close()
        defer remote.Close()
        //使用io.Copy传输两个tcp连接,
        _, err := io.Copy(local, remote)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        fmt.Println("join Conn2 end")
    }
    go f(conn2, conn1)
    go f(conn1, conn2)
}

func releaseConnMatch() {
    for {
        lock.Lock()
        for key, connMatch := range connListMap {
            //如果在指定时间内没有tunnel的话,则释放该连接
            if connMatch.tunnel == nil && connMatch.accept != nil {
                if time.Now().Unix()-connMatch.acceptAddTime > 5 {
                    fmt.Println("释放超时连接")
                    err := connMatch.accept.Close()
                    if err != nil {
                        fmt.Println("释放连接的时候出错了:" + err.Error())
                    }
                    delete(connListMap, key)
                }
            }
        }
        lock.Unlock()
        time.Sleep(5 * time.Second)
    }
}

安装在内网服务器或者dmz主机的程序如下:

package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
)

/**
本程序为内网环境中web服务器所在的主机(或者可连接到内网web服务器同时可以访问外网的间机器)
工作流程:
    1.连接远端服务器ControlAddrPort,接受远端服务器的控制命令
    2.当用户访问远端服务器时,ControlAddrPort传来控制命令"new\n",
    3.执行combine方法,程序同时拨通TunnelAddrPort 和 ServerAddrPort,
    4.并通过joinConn方法,用io.Copy的方式讲TunnelAddrPort的通信数据和ServerAddrPort的通信数据配对,
实现将内网数据提交到中介服务器ControlAddrPort,再通过中介服务期上的程序实现内网穿透
 */

func main() {
    connectControl()
}

const (
    //中介服务器控制端程序连接地址和端口
    //我在内网测试所以填写内网地址,不要被误导
    ControlAddrPort = "192.168.3.99:8009"
    //链路实际通信连接地址和端口
    //我在内网测试所以填写内网地址,不要被误导
    TunnelAddrPort  = "192.168.3.99:8008"
    //内网服务程序地址和端口
    ServerAddrPort  = "127.0.0.1:80"
)

//连接到服务器的8009控制端口,随时接受服务器的控制请求,随时待命
func connectControl() {
    var tcpAddr *net.TCPAddr
    //这里在一台机测试,所以没有连接到公网,可以修改到公网ip
    tcpAddr, _ = net.ResolveTCPAddr("tcp", ControlAddrPort)
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Println("Client connect error ! " + err.Error())
        return
    }
    fmt.Println(conn.LocalAddr().String() + " : Client connected!8009")
    reader := bufio.NewReader(conn)
    for {
        s, err := reader.ReadString('\n')
        if err != nil || err == io.EOF {
            break
        } else {
            //接收到new的指令的时候,新建一个tcp连接
            if s == "new\n" {
                go combine()
            }
            if s == "hi" {
                //忽略掉hi的请求
            }
        }

    }
}
//combine方法的代码,整合local和remote的tcp连接
func combine() {
    local := connectLocal()
    remote := connectRemote()
    if local != nil && remote != nil {
        joinConn(local, remote)
    } else {
        if local != nil {
            err := local.Close()
            if err!=nil{
                fmt.Println("close local:" + err.Error())
            }
        }
        if remote != nil {
            err := remote.Close()
            if err!=nil{
                fmt.Println("close remote:" + err.Error())
            }

        }
    }
}
func joinConn(local *net.TCPConn, remote *net.TCPConn) {
    f := func(local *net.TCPConn, remote *net.TCPConn) {
        defer local.Close()
        defer remote.Close()
        _, err := io.Copy(local, remote)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        fmt.Println("end")
    }
    go f(local, remote)
    go f(remote, local)
}
//connectLocal 连接到内网web服务器!
func connectLocal() *net.TCPConn {
    var tcpAddr *net.TCPAddr
    tcpAddr, _ = net.ResolveTCPAddr("tcp", ServerAddrPort)

    conn, err := net.DialTCP("tcp", nil, tcpAddr)

    if err != nil {
        fmt.Println("Client connect error ! " + err.Error())
        return nil
    }

    fmt.Println(conn.LocalAddr().String() + " : Client connected!8000")
    return conn

}
//connectRemote 连接到服务端的8008端口!
func connectRemote() *net.TCPConn {
    var tcpAddr *net.TCPAddr
    tcpAddr, _ = net.ResolveTCPAddr("tcp", TunnelAddrPort)

    conn, err := net.DialTCP("tcp", nil, tcpAddr)

    if err != nil {
        fmt.Println("Client connect error ! " + err.Error())
        return nil
    }
    fmt.Println(conn.LocalAddr().String() + " : Client connected!8008")
    return conn;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容