eycorsican / go-tun2socks

A tun2socks implementation written in Go.
MIT License
1.31k stars 434 forks source link

关于tcp流量的阻塞问题 #100

Closed raintean closed 4 years ago

raintean commented 4 years ago

目前发现一个问题: 在有TCP数据从tun方向过来的时候. 会进到Receive方法

func (conn *tcpConn) Receive(data []byte) error {
    if err := conn.receiveCheck(); err != nil {
        return err
    }
    n, err := conn.sndPipeWriter.Write(data)
    if err != nil {
        return NewLWIPError(LWIP_ERR_CLSD)
    }
    C.tcp_recved(conn.pcb, C.u16_t(n))
    return NewLWIPError(LWIP_ERR_OK)
}

然后再往pipe中写入, 等待对端Read, 然后使用自己的逻辑(直连/Socks等)发送出去.
但是这里存在一个问题. 如果没法及时调用Read方法(比如转发速度太慢等), 会造成Receive方法长时间阻塞在 conn.sndPipeWriter.Write(data). 由于lwip是单线程, 所以其对于Receive的调用迟迟无法返回. 造成其他的网络流量, 全部阻塞.
目前作者有没有什么好的方案来解决该问题.
个人认为这就是一个流的背压传递问题.

fbion commented 4 years ago

https://github.com/FlowerWrong/tun2socks 这个有这个问题吗

raintean commented 4 years ago

https://github.com/FlowerWrong/tun2socks 这个有这个问题吗

这个应该没有, 我记得他是基于google的netstack实现的. 不过我更看好作者的这个实现, 轻量, 内存可控性好.

eycorsican commented 4 years ago

如果暂时没法处理数据,比如连接的发送缓存满了,可以给 lwip 返回一个错误 https://github.com/eycorsican/go-tun2socks/blob/eebb391cf216724da69c9594f9ed46fbce958661/core/tcp_callback_export.go#L96

但 go 现在的连接是同步写,所以还要在 handler 里实现多一级缓存,考虑实际上连接的写操作很少会有阻塞,我觉得没什么必要。

raintean commented 4 years ago

在使用过程中确实会出现这样的情况, 比如上传走的是代理路线, 如果代理服务器不稳定. 那么按这个情况来说, 是会阻塞直连, 或者其他代理路线的上传(包括下载). 而且在网络环境较差的情况下(这个项目用来做啥,大家众所周知,所以网络环境....)特别明显.

目前我的解决方案是:

type tcpConn struct {
    sync.Mutex

    pcb           *C.struct_tcp_pcb
    handler       TCPConnHandler
    remoteAddr    *net.TCPAddr
    localAddr     *net.TCPAddr
    connKeyArg    unsafe.Pointer
    connKey       uint32
    canWrite      *sync.Cond // Condition variable to implement TCP backpressure.
    state         tcpConnState
    sndPipeReader *io.PipeReader    //换成带缓存的实现
    sndPipeWriter *io.PipeWriter    //换成带缓存的实现
    closeOnce     sync.Once
    closeErr      error
}

将pipe出来的 sndPipeReader, sndPipeWriter 更换成带缓存的实现. 其次将

func (conn *tcpConn) Receive(data []byte) error {
    if err := conn.receiveCheck(); err != nil {
        return err
    }
    n, err := conn.sndPipeWriter.Write(data)
    if err != nil {
        return NewLWIPError(LWIP_ERR_CLSD)
    }
    C.tcp_recved(conn.pcb, C.u16_t(n))    //删除这行
    return NewLWIPError(LWIP_ERR_OK)
}

中的 C.tcp_recved(conn.pcb, C.u16_t(n)) 删掉. 因为就算无阻塞写入了缓存pipe中, 也并不意味着数据已经接收. 如果这个时候tcp_recved 那么lwip还是会疯狂发数据, 造成缓存疯狂增长(在来不及Read的情况下). 最后在Read方法中:

func (conn *tcpConn) Read(data []byte) (int, error) {
         // xxxxxxxx

    lwipMutex.Lock()
    C.tcp_recved(conn.pcb, C.u16_t(n))   //在真正read的时候,告诉lwip,数据已经接收
    lwipMutex.Unlock()
    return n, err
}

调用 tcp_recved, 表示数据已经进入到转发那边去了, 让转发的背压, 能够很好的传递给 lwip 层.

个人认为, 这就是一个因为cgo的实现, 造成的阻塞世界和非阻塞世界思想不匹配的问题.

另外我觉得在应用的层面去实现多一级缓存, 并不清真, 让本身库的职责外泄了. 不优雅.

eycorsican commented 4 years ago

把缓存放在 core 里也是可以的,但缓存不可能设到无限大,满了后依然要返回错误给 lwip

raintean commented 4 years ago

把缓存放在 core 里也是可以的,但缓存不可能设到无限大,满了后依然要返回错误给 lwip

这不会有缓存扩大的问题. 写入时只是为了不阻塞, 放入缓存, 如果不去调用tcp_recved来更新可用的发送窗口, 实际上, lwip不会再次传入数据了. 除非你在read的时候调用tcp_recved让窗口更新. 所以不需要判断缓存满不满的情况, 因为缓存最大也就是lwip的发送窗口大小. 这边缓存到达最大. 那边就是0了. lwip不会再发数据过来了.

eycorsican commented 4 years ago

那你实现一下?

raintean commented 4 years ago

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif
wongsyrone commented 4 years ago

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif

能把改过的代码库推上来吗

raintean commented 4 years ago

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif

能把改过的代码库推上来吗

@eycorsican @wongsyrone 代码已经上来

wongsyrone commented 4 years ago

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

raintean commented 4 years ago

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

根据lwip的文档中tcp_recved的说明, 以及tcp协议关于发送窗口的规定. 是没有问题.

wongsyrone commented 4 years ago

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

根据lwip的文档中tcp_recved的说明, 以及tcp协议关于发送窗口的规定. 是没有问题.

不过目前repo中使用的lwip配置,貌似窗口都是固定大小

raintean commented 4 years ago

这里只是和wnd的scale无关, 影响的是可用的wnd

Fndroid commented 4 years ago

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

raintean commented 4 years ago

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

没太明白你的意思, 可否讲的详细一点

Fndroid commented 4 years ago

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

没太明白你的意思, 可否讲的详细一点

情况是这样的,我这里使用go-tun2socks/core的时候,实现了一个core.TCPConnnHandler,里面的Handler会把TAP收到的net.Conn传过来,但是这个时候我还需要去Dial远端服务器才能relay,设置了5s的超时,这个时候这个要是一个请求阻塞了,后面的就都得等5s,不知道有没有办法解决

raintean commented 4 years ago

@Fndroid 你这个问题和该issue没关系, 不过我可以回答一下你. 其实作者在代码里面也写了. 不要让handle被阻塞, 那么问题很明显了. 你handle方法里面 直接开一个协程去处理过来的net.Conn, 让handle能够快速返回 不就OK了嘛.

Fndroid commented 4 years ago

@raintean 感谢回复。我刚认真测试了一下,我发现的现象是Handle传过来的Conn在Read(copyBuffer)的时候要等很长一段时间,请问这个是什么原因造成的呢?也试过拿到Conn的时候直接Read,但是也需要很长的时间才能EOF

raintean commented 4 years ago

@raintean 感谢回复。我刚认真测试了一下,我发现的现象是Handle传过来的Conn在Read(copyBuffer)的时候要等很长一段时间,请问这个是什么原因造成的呢?也试过拿到Conn的时候直接Read,但是也需要很长的时间才能EOF

可能就是没有数据能read呢? 对不对...

Fndroid commented 4 years ago

@raintean 最后问一下大佬,这个duplexConn的意义是什么呢?Relay的时候Dst Conn是不是也需要实现这个接口呢,谢谢

https://github.com/eycorsican/go-tun2socks/blob/74929501c548cd9b869ced0d7ba903ac0fad7f32/proxy/socks/tcp.go#L35-L39

raintean commented 4 years ago

@Fndroid 具体不清楚, 字面上的意思是全双工连接. 看接口你能明白, 他是可以单独关闭读取或者写入的, net.Conn的话, 好像是close就全部关闭了. 这个在reply的时候有意义. 比如 A <-> B 直接做pipe, A已经EOF了, 这个时候其实是可以关闭B的写入的. 大概就是这么一个意思. 你不太需要关心这个. 直接net.Conn也能满足了. 这个只是对上下行都做了分开控制.

github-actions[bot] commented 4 years ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days