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

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的请求对应一个响应。
在自定义的write
和read
函数中,我们把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语言网络编程和网络数据交互流程的了解。
Comments NOTHING