美文网首页
问题与问题

问题与问题

作者: zizon | 来源:发表于2020-08-23 02:29 被阅读0次

    最近在用quic-go写个代理.

    开始的想法是兼容socks5协议之后,透明地端对端.
    只不过传输部分从tcp变为quic.

    因为看接口是兼容io.ReadWrite的.
    所以最简单的想法就是直接copy到服务端之后再考虑协议解析.

    写了一部分之后发现socks5协议其实算是支持重定向的.
    response部分带有bind的地址和端口信息.

    理论上client端应该是利用这个信息重新连接到指定地址和端口做流量copy的.

    这样的话从协议角度,client端还是会利用tcp去连接对应服务端口.
    所以从协议角度来说,需要让这段response里的IP/port信息是本地的端口.

    也就是一般实现的,socks5的client会在这个response后复用现有连接.

    那么这里的一个问题是,服务端可能并不知道这个local listening的端口是什么.

    当然,这里可以约定一个固定端口.

    但是更flexible的是能够有一个机制适应这端口.

    所以无非就是intercept一些协议,把本地端口信息给对端, 然后在原始协议里echo回来.
    要么是local端在解析response的时候modify回本地信息.

    但这样的话就意味着两端有一个协议上的握手阶段.
    以为遵循的是原始socsk5里的握手协定.

    当然,实现上可以把modified的response speculate地返回给发起方,然后再去连接对端.
    因为实际上如果能正常连接的话,返回的响应内容是预先知道/固定的.

    但这里但问题就是,其实就没有必要跟对端但协议也是走socks5了.
    因为如果把socks5的整个流程放在本地,local完成之后再起一套新的协议走quic的话就简单很多.

    所以目前的做法是在本地走完socks5的流程之后,再简单地把目标host和port通知quic,然后就直接互相copy后续的流量了.

    因为目前这个协议是没有做校验/鉴权的.
    所以理论上来说从协议角度,可以去连接比如内网的机器.

    当然,这个对于socks5的非鉴权模式也是有这个问题.

    而这类tunnel服务的另外一个可能更现实一点的问题就是,如果能把tunnel目标的信息部分更改/替换为特定的服务ip端口的话,实际是很容易定点发现的.
    因为只要这个服务目标单一,凡是连接过来的都是susceptive的.

    连接内容加密的话或者说明文部分特征不明显的话,相对来说问题没那么明显.
    但问题可能就是加密协议的白名单机制了.

    另外一个问题就是,想tcp这类有状态的协议,通过构造异常中断连接回比较轻松.
    以为需要关注的包的数量不会太多.

    但是udp的话因为本身无状态,所以如果要track的话成本会比较高.
    因为像quic这类encapsulate可以无限做下去,只要还有允许的协议存在.'

    所以最坏的情况可能就是udp整个被去掉.

    当然,理论上来说也可以把类似udp的逻辑over tcp,同样embed在合法的协议里面.

    如果是到这样一个地步的话,可能就是服务注册制了.
    未经允许的服务不得运行,保证协议的可读性和可控豁免.

    某种程度上目前的网站备案制就是了.
    像云服务上未备案的对应http服务等.

    这些其实没什么好说的.
    毕竟也没有什么解决方案.

    只是个纯粹问题.

    另外一些就是关于go本身的一些体验或者说不太便利的地方吧.

    原则上来说,go的设计会比较适合做服务端尤其是这类网络方面的应用.
    因为netpoller和syscall的隐式coroutine调用会使得写的时候认知负担没那么重.

    比如如果是java等写网络io,通常需要select配合线程调度去把io尽可能地并发处理起来.
    而go则是把这类syscall的G调度suspend,等ready的时候再resume回来.

    虽然这类行为在其他语言上也不是不能实现模仿.
    但是也只是针对network的一个特殊场景.
    以及局限在语言和库提供的表达能力上.

    但是像http.get之类的就依赖于库的表现了.
    而且从一般调用上来说,即使提供了异步接口,还是需要写一些逻辑去做简单调度.

    比如,
    // search something
    result = http.search()
    // audit log
    audit.log(event)

    其中search是blocking的.
    也就是意味着当前线程是被block的.
    而一般这个线程会是属于一个有限数量的线程池资源里的一个.
    block的结果就意味着可能负载能力的一个限制.

    变通的一种方式是
    http.server()
    .then(()->audit.log())
    这类promise/future的回调方式.

    但它本质上也是需要对应blocking的底层是提供了类似epoll这类有限blocking的实现的.
    像如果里面涉及到文件io读写的话,又不是pollable的话,就是block谁/哪个线程池的问题了.

    而这里的认知负担就在于需要比较清楚一个方法调用的开销和block状态.
    以便是否需要做异步化.

    这个就是go的syscall调度的隐式coroutine的一点优势了.
    runtime层面就会做这些关键调用的suspend和resume.
    所以可以比较不用太关心这类资源的使用问题.

    但是goroutine的问题在于,它形态上是一样完全异步的过程.
    一旦go出去之后是没什么控制权的.
    而有时这种有时需要的.

    比如
    go rpc(endpoint1)
    go rpc(endpoint2)
    这种race访问同样服务的不同实例,以期减少/抹掉服务异常导致的tail latency的场景.

    你需要得到早返回的一个,同时尽可能地cancel另外一个.
    以标准库的方式的话,可能就需要

    go rpc(endpoint1,chan1,ctx1)
    go rpc(endpoint2,chan2,ctx2)

    select {
    case <-chan1: ctx2.cancel()
    case <-chan2: ctx1.cancle()
    }

    而这种场景下,其他语言可能就是
    promise.race(
    promise.ask(()->rpc(endpoint1)),
    promise.ask(()->rpc(endpoint2))
    ).join()

    当然,后者这里可能只是库封装程度的问题.

    但是考虑如果要在go里实现类似的风格的话.
    你需要额外的ctx和chan对象,或者合并为一个struct.

    所以最终形态可能是
    ctx := ...
    promise = Go(func(){rpc(endpoint)},ctx)

    如果还需要又返回值和异常信息的话,ctx需要带的东西会更多.

    目前标注库里的context接口是之后err和一个等价join/wait的Done() channel的.

    所以如果要cover场景的话,至少还得扩展出cancel和value等部分.

    而更麻烦的一点就在于,go没有范型 .
    所以ctx对不同value要么需要单独适配,要么一个interface做type assert.

    以及标准的error结构更多只是一个message的作用,你需要有其他机制/风格去带上堆栈信息.
    比如直接堆栈成string带过去.
    或者有在err的时候附带print stacktrace的风格.

    当然,这里的范型问题只是个编译器问题.
    就目前来说go的compiler生成的代码也不少.
    做一个类型生成应该也不是太大的问题.

    就像现在实际上对每个struct的value function都有生成一个对应的pointer value的版本的.
    只要有用到的话.

    由于没有运行期动态生成代码的方式,所以实际做类型擦除之类的应该也没什么问题.
    毕竟像interface本身实现的机制就有点类似了.

    其他的一些认知负担可能就是pass by value的一些点了.

    因为如果pass by value,那么调用之间都是copy by value的.
    像有深度调用链条,而每一层只需要一部分的就有可能逐步地下层少一些字段/小一些的结构.
    所做的只是栈上/gstack上的动作.

    理论上来说除了一些特殊的slice/map/chan之类的对象的话,其他都可以栈上解决.
    没有/比较少gc的问题.

    但是因为前面一些问题的存在,直观上写不逃逸的代码会比较麻烦或者说恶心.
    尤其像匿名function,需要多一层参数传递.

    而且有时候不看compile optimization decision也想不到.

    而这么做的效果/代价其实也可能根本划不来.
    一个是维护成本/可读性问题.
    一个是拷贝的累计代价问题.

    所以这个怎么说呢,也是一个问题和解决方案的事情.
    大多数时候其实也不太现实.

    只不够可能是确实有解决方案存在而已.

    相关文章

      网友评论

          本文标题:问题与问题

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