Go内网穿透的简单实现

knoci 发布于 2025-02-07 97 次阅读


Go内网穿透的简单实现

​ 众所周知,内网穿透其实就是靠中间服务器搭建隧道实现,那么我们就可以自己动手,用go语言来简单实现一个隧道,能够用单机或双机实现Http简单代理。

image-20241127220009621

Proxy端

​ 首先让我们从隧道的proxy端开始着手,Proxy段作为中间代理人,负责转发用户的请求和Source的回应

​ 我们先定义好结构,包括监听的Tcp端口,Http端口,连接表。

type proxy struct {
    tcpPort  int
    httpPort int
    listener net.Listener
    connList chan net.Conn
}

​ 创建Proxy实例的接口,开启和关闭一个监听。

func NewProxy(tcpPort, udpPort, httpPort int) *proxy {
    return &proxy{
        tcpPort:  tcpPort,
        udpPort:  udpPort,
        httpPort: httpPort,
        connList: make(chan net.Conn, 10),
    }
}

func (s *proxy) Start() error {
    var err error
    s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.tcpPort))
    if err != nil {
        return err
    }
    go s.httpserver()
    return s.tcpserver()
}

func (s *proxy) Close() error {
    if s.listener != nil {
        err := s.listener.Close()
        s.listener = nil
        return err
    }
    return errors.New("Not Initiate")
}

Tcp连接

​ 我们这里获取一个Tcp连接对象,goroutine一个协程交给客户端进程函数处理

func (s *proxy) tcpserver() error {
    var err error
    for ; ; {
        conn, err := s.listener.Accept()// 接受下一个传入呼叫并返回新连接
        if err != nil {
            log.Println(err)
            continue
        }
        go s.tcphandle(conn)
    }
    return err
}

获取Tcp连接对象后,设置了5s超时时间并且进行校验。这是为了处理潜在的网络延迟或客户端无响应的情况。如果客户端在5秒内没有发送任何数据,连接将被视为超时,从而触发超时处理逻辑,确保客户端能够和服务端进行通信。

如果连接验证成功,服务器将重置读取超时时间,将其设置为无限等待。意味着一旦客户端通过了验证,服务器将无限期地等待客户端的后续数据,直到客户端主动关闭连接或发生其他错误。

设置连接为保持活动状态,周期1s。这一步是为了确保连接在没有数据传输的情况下仍然保持活动状态。SetKeepAlive(true) 启用了 TCP 层面的心跳机制,即 TCP 保活(Keepalive)功能。这个功能会在连接空闲时定期发送心跳包,以确保连接没有因为网络问题或其他原因而断开。SetKeepAlivePeriod(time.Duration(1 * time.Second)) 设置了心跳包的发送周期为1秒。

func (s *proxy) tcphandle(conn net.Conn) error {
    tcpconn, ok := conn.(*net.TCPConn)
    if !ok {
        return fmt.Errorf("Not TCP Connect")
    }
    // 设置5s读超时时间
    tcpconn.SetReadDeadline(time.Now().Add(time.Duration(5) * time.Second))
    // 校验连接
    verify := make([]byte, 32)
    _, err := tcpconn.Read(verify)
    if err != nil {
        log.Println("Client Read Out of Time. Address: ", tcpconn.RemoteAddr())
        conn.Close()
        return err
    }
    if bytes.Compare(verify, getverifyval()[:]) != 0 {
        log.Println("Connect Verify Fail, Closing: ", tcpconn.RemoteAddr())
        // 告知客户端vkey验证失败
        tcpconn.Write([]byte("vkey"))
        tcpconn.Close()
        return err
    }
    // 如果验证成功,重置读取操作的超时时间,使其无限等待
    tcpconn.SetReadDeadline(time.Time{})
    log.Println("Connect to New Client: ", tcpconn.RemoteAddr())
    // 设置连接为保持活动状态,心跳周期1s
    tcpconn.SetKeepAlive(true)
    tcpconn.SetKeepAlivePeriod(time.Duration(1 * time.Second))
    // 加入连接表
    s.connList <- tcpconn
    return nil
}

​ 获取一个SHA-256(安全哈希算法256)加密的校验值

package main

import (
    "crypto/sha256"
    "time"
)

// 简单的一个校验值
func getverifyval() []byte {
    // 使用当前时间字符串和密钥拼接
    timestamp := time.Now().Format("2006-01-02 15")
    key := *verifyKey
    data := timestamp + key
    hash := sha256.Sum256([]byte(data))
    return hash[:]
}

Http代理

​ 注册路由,goto循环从连接表中取出一个连接,然后用于处理 HTTP 请求,并将请求转发到后端的 TCP 连接,然后将响应返回给客户端。如果处理过程中出现任何错误,将尝试重新获取一个新的 TCP 连接并重试请求,直到成功或资源耗尽。

​ 这里先用write转发请求,然后调用read读取响应,是http/1.x的请求对应一个响应。

​ 在自定义的writeread函数中,我们把http.Request或者Response转为[]byte,又把[]byte转回http.Response这么做的原因是http是应用层的协议,Tcp是传输层协议只能传输字节流,要把http转成字节切片才能在传输层传输。

​ 字节切片使用二进制格式,确保数据格式的一致性和跨平台,同时可以高效的数据存储和传输。

func (s *proxy) httpserver() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    retry:
        if len(s.connList) == 0 {
            http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
            return
        }
        conn := <-s.connList
        log.Println(r.RequestURI)
        err := s.write(r, conn)
        if err != nil {
            log.Println(err)
            conn.Close()
            goto retry
        }
        err = s.read(w, conn)
        if err != nil {
            log.Println(err)
            conn.Close()
            goto retry
        }
        s.connList <- conn
        conn = nil
    })
    log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", s.httpPort), nil))
}

​ 在自定义的写函数中,我们对*http.Request进行自定义,带上 签名+长度+是否加密, 这是我们自己约定的通信格式,客户端收到后会对应解码。

func (s *proxy) write(r *http.Request, conn net.Conn) error {
    // 把http.Request转为[]byte
    raw, err := EncodeRequest(r)
    if err != nil {
        return err
    }
    c, err := conn.Write(raw)
    if err != nil {
        return err
    }
    if c != len(raw) {
        return errors.New("Bytes write return wrong length")
    }
    return nil
}

func EncodeRequest(r *http.Request) ([]byte, error) {
    // *Buffer类型实现了io.Writer接口
    raw := bytes.NewBuffer([]byte{})
    // 写签名
    binary.Write(raw, binary.LittleEndian, []byte("sign"))
    reqBytes, err := httputil.DumpRequest(r, true)
    if err != nil {
        return nil, err
    }
    // 写body数据长度 + 1
    binary.Write(raw, binary.LittleEndian, int32(len(reqBytes)+1))
    // 判断是否为http或者https的标识1字节
    binary.Write(raw, binary.LittleEndian, r.URL.Scheme == "https")
    if err := binary.Write(raw, binary.LittleEndian, reqBytes); err != nil {
        return nil, err
    }
    return raw.Bytes(), nil
}

​ 在自定义的读函数中,我们拿出签名并且判断,是 sign 就通过定义buff缓存分段读取,并且比对读取的raw长度和实际接收长度。然后通过DecodeResponse函数,把[]byte转为http.Response类型,然后拆分成Header和Body写回给http.ResponseWriter

func (s *TRPServer) read(w http.ResponseWriter, conn net.Conn) error {
    val := make([]byte, 4)
    _, err := conn.Read(val)
    if err != nil {
        return err
    }
    // 拿出签名并且判断
    flags := string(val)
    switch flags {
    case "sign":
        _, err = conn.Read(val)
        if err != nil {
            return err
        }

        nlen := int(binary.LittleEndian.Uint32(val))
        if nlen == 0 {
            return errors.New("Read in wrong length")
        }
        log.Println("total recieve bytes :", nlen)

        raw := make([]byte, 0)
        buff := make([]byte, 1024)
        c := 0
        for {
            clen, err := conn.Read(buff)
            if err != nil && err != io.EOF {
                return err
            }
            raw = append(raw, buff[:clen]...)
            c += clen
            if c >= nlen {
                break
            }
        }
        log.Println("Finish with read bytes:", c, "Actually read bytes:", len(raw))
        if c != nlen {
            return fmt.Errorf("Read %dbytes,Need %dbytes", c, nlen)
        }
        // 把byte转回http.Response
        resp, err := DecodeResponse(raw)
        if err != nil {
            return err
        }
        bodyBytes, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            return err
        }
        // 遍历响应结构体中的头部信息 resp.Header,将其中的每个键值对设置到 http.ResponseWriter 的头部中,用于后续响应给客户端
        for k, v := range resp.Header {
            for _, v2 := range v {
                w.Header().Set(k, v2)
            }
        }
        w.WriteHeader(resp.StatusCode)
        w.Write(bodyBytes)
    case "msg0":
        return nil
    default:
        log.Println("Undefine Error: ", string(val))
    }
    return nil
}

func DecodeResponse(data []byte) (*http.Response, error) {
    // http.ReadResponse从r的*bufio.Reader读取并返回*Response。req参数可选地指定与此响应对应的Request。如果为nil,则假定为GET请求。
    resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(data)), nil)
    if err != nil {
        return nil, err
    }
    return resp, nil
}

Source端

​ 接收Proxy端发来的请求,给出回应。

​ 结构相对简单,Proxy端的地址,要建立的tcp连接数量,还有一个互斥锁。

type source struct {
    proxyAddr string
    tcpNum  int
    sync.Mutex
}

func NewSource(addr string, tcpNum int) *source {
    c := new(source)
    c.svrAddr = svraddr
    c.tcpNum = tcpNum
    return c
}

func (c *source) Start() error {
    for i := 0; i < c.tcpNum; i++ {
        go c.newConn()
    }
    // 允许运行超时重连
    for {
        time.Sleep(5 * time.Second)
    }
    return nil
}

Tcp连接

​ 互斥锁防止多个 go 协程同时执行 newConn的资源竞争,同时保证了net.Dial建立tcp连接的原子性。

func (c *source) newConn() error {
    c.Lock()
    conn, err := net.Dial("tcp", c.svrAddr)
    if err != nil {
        log.Println("Connect failed, retry later...")
        time.Sleep(time.Second * 5)
        c.Unlock()
        c.newConn()
        return err
    }
    c.Unlock()
    conn.(*net.TCPConn).SetKeepAlive(true)
    conn.(*net.TCPConn).SetKeepAlivePeriod(time.Duration(1 * time.Second))
    return c.process(conn)
}

​ 对于conn的连接建立完成后,我们首先从Proxy返回的数据,拿出签名并且判断,返回sign无误后,进行处理。

func (c *source) process(conn net.Conn) error {
    if _, err := conn.Write(getverifyval()); err != nil {
        return err
    }
    val := make([]byte, 4)
    for {
        _, err := conn.Read(val)
        if err != nil {
            log.Println("Proxy disconnect, retry later..", err)
            time.Sleep(5 * time.Second)
            go c.newConn()
            return err
        }
        // 获取签名并且判断
        flags := string(val)
        switch flags {
        case "vkey":
            log.Fatal("vkey error")
        case "sign":
            c.deal(conn)
        case "msg0":
            log.Println("Proxy return an error)
        default:
            log.Println("Undefine Error")
        }
    }
    return nil
}

Http请求的处理

​ 从tcp连接 conn 接收数据,将接收到的数据解析并作为请求发送给本地的 HTTP 客户端,获取响应后再对响应进行编码处理,最后通过原网络连接 conn 将响应数据回写回去。

CheckRedirect指定了处理重定向的策略。如果CheckRedirect不为nil,则客户端在执行HTTP重定向之前调用它。参数req和via是即将到来的请求和已经发出的请求,最早的请求排在第一位。如果CheckRedirect返回错误,客户端的Get方法将返回之前的Response(其Body已关闭)和CheckRedirect的错误(包裹在url. error中),而不是发出Request请求。

func (c *TRPClient) deal(conn net.Conn) error {
    val := make([]byte, 4)
    _, err := conn.Read(val)
    nlen := binary.LittleEndian.Uint32(val)
    log.Println("total recieve bytes ", nlen)
    if nlen <= 0 {
        log.Println("Read in wrong length ")
        c.werror(conn)
        return errors.New("Read in wrong length")
    }
    raw := make([]byte, nlen)
    n, err := conn.Read(raw)
    if err != nil {
        return err
    }
    if n != int(nlen) {
        log.Printf("Read %dbytes,Need %dbytesn", n, nlen)
        c.werror(conn)
        return errors.New("Read pro")
    }
    // 将字节转为request
    req, err := DecodeRequest(raw)
    if err != nil {
        log.Println("DecodeRequest:", err)
        conn.Write([]byte("msg0"))
        return err
    }
    // 把请求发送给本地的 HTTP 客户端
    rawQuery := ""
    if req.URL.RawQuery != "" {
        rawQuery = "?" + req.URL.RawQuery
    }
    log.Println(req.URL.Path + rawQuery)
    client := new(http.Client)

    // 自定义重定向检测逻辑,返回报错
    client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
        return errors.New("disabled redirect.")
    }
    // Do发送HTTP请求并返回HTTP响应,遵循客户端配置的策略(如重定向、Cookie、身份验证)。
    resp, err := client.Do(req)
    disRedirect := err != nil && strings.Contains(err.Error(), disabledRedirect.Error())
    if err != nil && !disRedirect {
        log.Println("Request local http error:", err)
        conn.Write([]byte("msg0"))
        return err
    }
    if !disRedirect {
        defer resp.Body.Close()
    } else {
        resp.Body = nil
        resp.ContentLength = 0
    }
    // 将response转为字节
    respBytes, err := EncodeResponse(resp)
    if err != nil {
        log.Println("EncodeResponse错误:", err)
        c.werror(conn)
        return err
    }
    n, err = conn.Write(respBytes)
    if err != nil {
        log.Println("发送数据错误,错误:", err)
        return err
    }
    if n != len(respBytes) {
        log.Printf("发送数据长度错误,已经发送:%dbyte,总字节长:%dbyten", n, len(respBytes))
    } else {
        log.Printf("本次请求成功完成,共发送:%dbyten", n)
    }
    return nil
}

func DecodeRequest(data []byte) (*http.Request, error) {
    if len(data) <= 100 {
        return nil, errors.New("length too short")
    }
    req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(data[1:])))
    if err != nil {
        return nil, err
    }
    // 分割,获取ip或url
    str := strings.Split(req.Host, ":")
    req.Host, err = getHost(str[0])
    if err != nil {
        return nil, err
    }
    // 获取TLS
    scheme := "http"
    if data[0] == 1 {
        scheme = "https"
    }
    req.URL, _ = url.Parse(fmt.Sprintf("%s://%s%s", scheme, req.Host, req.RequestURI))
    req.RequestURI = ""
    return req, nil
}

func EncodeResponse(r *http.Response) ([]byte, error) {
    raw := bytes.NewBuffer([]byte{})
    // 写入签名
    binary.Write(raw, binary.LittleEndian, []byte("sign"))
    // 将 http.Response 对象转换为字节切片
    respBytes, err := httputil.DumpResponse(r, true)
    // 如果配置了Replace,就从配置的站点列表中替换
    if config.Replace == 1 {
        respBytes = replaceHost(respBytes)
    }
    if err != nil {
        return nil, err
    }
    // 写入长度
    binary.Write(raw, binary.LittleEndian, int32(len(respBytes)))
    // 写入数据
    if err := binary.Write(raw, binary.LittleEndian, respBytes); err != nil {
        return nil, err
    }
    return raw.Bytes(), nil
}

​ 获取Host和修改Host

func getHost(str string) (string, error) {
    for _, v := range config.SiteList {
        if v.Host == str {
            return v.Url + ":" + strconv.Itoa(v.Port), nil
        }
    }
    return "", errors.New("Undefine host!")
}

// 从配置的站点列表中替换
func replaceHost(resp []byte) []byte {
    str := string(resp)
    for _, v := range config.SiteList {
        // 把str中出现的 v.Url加上v.Port组合字符串替换为v.Host
        str = strings.Replace(str, v.Url+":"+strconv.Itoa(v.Port), v.Host, -1)
        // 将str中单独出现的v.Url也替换为 v.Host
        str = strings.Replace(str, v.Url, v.Host, -1)
    }
    return []byte(str)
}

读取配置并启动

​ flag要命令行手动输入,可自定义配置文件路径,tcp端口,http端口,启动模式,vkey

var (
    configPath = flag.String("config", "config.json", "config path")
    tcpPort    = flag.Int("tcpport", 8284, "tcp port")
    httpPort   = flag.Int("httpport", 8024, "http port")
    rpMode     = flag.String("mode", "source", "setup mode, choose from proxy and source")
    verifyKey  = flag.String("vkey", "123", "verify key")
    config     Config
    err        error
)

func main() {
    flag.Parse()
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
    if *rpMode == "source" {
        JsonParse := NewJsonStruct()
        config, err = JsonParse.Load(*configPath)
        if err != nil {
            log.Fatalln(err)
        }
        *verifyKey = config.Server.Vkey
        log.Println("source setup, Host:", config.Server.Ip, ", Port:", config.Server.Tcp)
        sou := NewSource(fmt.Sprintf("%s:%d", config.Server.Ip, config.Server.Tcp), config.Server.Num)
        sou.Start()
    } else if *rpMode == "proxy" {
        if *verifyKey == "" {
            log.Fatalln("empty vkey")
        }
        if *tcpPort <= 0 || *tcpPort >= 65536 {
            log.Fatalln("illegal tcp port")
        }
        if *httpPort <= 0 || *httpPort >= 65536 {
            log.Fatalln("illegal http port")
        }
        log.Println("proxy setup, tcpport: ", *tcpPort, ", httpport:", *httpPort)
        pro := NewProxy (*tcpPort, *httpPort)
        if err := pro.Start(); err != nil {
            log.Fatalln(err)
        }
        defer pro.Close()
    }
}

结语

​ 虽然这个Http代理略显简陋,代码也不多,但还是实现了内网穿透的基本功能,希望能帮助到理解应用层http和传输层tcp之间的交互,提升对Go语言网络编程和网络数据交互流程的了解。