基于Twisted的内网穿透工具

内网穿透:建立外网到本地服务的隧道,让居于内网主机上的服务可以暴露给外网

能找到这里的人应该已经对这些东西有所了解,我就不再赘述..


开发工具:

    twisted、decouple


GitHub: https://github.com/a916169754/Ipen-jia


流程:



其中, control 连接用于客户端与服务端沟通, proxy连接作为隧道流量的承载者。


目录结构:

/client
    /.env 配置文件
    /client.py
    /connected.py (管理proxy连接)
    /controller.py (管理control连接)
    /parse.py (解析配置文件)
/server
    /.env
    /connection.py
    /control.py
    /parse.py
    /server.py
    /tunnel.py (管理tunnel)

代码:

server/server.py

...
def main():
    log.startLogging(sys.stdout)

    opt = parse_args()

    control = Control(opt['control_port'], opt['tls_conf'])
    control.start_listen()

    from twisted.internet import reactor

    reactor.run()

...

服务端main函数中,仅仅创建一个tls服务器,等待客户端连接。

具体创建过程在control.py 中:

server/control.py

...
    def start_listen(self):
        """
        建立一个链接,开始监听端口
        """
        ssl_context = ssl.DefaultOpenSSLContextFactory(
            self.tls_conf['private'],
            self.tls_conf['cert'],
        )

        factory = protocol.ServerFactory()
        factory.protocol = ControlProtocol
        factory.tls_conf = self.tls_conf

        from twisted.internet import reactor
        reactor.listenSSL(self.port, factory, ssl_context)
...

然后看看他的Protocol:

server/control.py

...
class ControlProtocol(NetstringReceiver):
    def connectionMade(self):
        log.msg("receive request .... ", self.transport.getPeer())
        
        client_id = ''.join(random.sample(string.ascii_letters + string.digits, 8)).lower()
        res = {
            'client_id': client_id,
            'res': 'success'
        }
        self.sendString(json.dumps(res).encode('utf8'))

    def stringReceived(self, info):
        req_data = json.loads(info.decode('utf8'))
        handel = HandelRequest(req_data, self, self.factory.tls_conf)
        handel.start()

    def connectionLost(self, reason):
        log.msg('control connection lost ', self.transport.getPeer())

...

当有客户端连接的时候, 利用python的random库为他创建一个client id。

ps:并不了解random,所以不知道有没有随机到两个相同id的可能性,但即使有,概率应该也是极低的吧,所以这里就不考虑了。

stringReceived方法接受到客户端的数据后,直接交给HandelRequest处理。这里继承了twisted中的NetstringReceiver,保证了数据的完整。

HandelRequest中仅处理两个命令, new_proxy和new_tunnel。分别是新建隧道,和建立proxy连接。

server/control.py

...
    def get_handel_fun(self):
        return {
            'new_tunnel': self.__new_tunnel,
            'new_proxy': self.__new_proxy,
        }.get(self.req_data.get('cmd'), 'error')

    def __new_tunnel(self):
        tunnel = Tunnel()
        port = tunnel.new_tunnel(self.req_data.get('port', 0), self.req_data.get('client_id'))

        res = {
            'tunnel_port': port,
            'client_id': self.req_data.get('client_id'),
            'res': 'start_tunnel'
        }
        self.protocol.sendString(json.dumps(res).encode('utf8'))

    def __new_proxy(self):
        log.msg(self.req_data)
        log.msg(Tunnel.tunnels)
        tunnel = Tunnel.tunnels.get(str(self.req_data['tunnel_port']))
        proxy = ProxyConnServer(0, self.tls_conf, self.req_data['client_id'], tunnel)
        port = proxy.listen()

        res = {
            'port': port,
            'res': 'start_proxy'
        }
        self.protocol.sendString(json.dumps(res).encode('utf8'))
...

这里主要看一下Tunnel

server/tunnel.py

...
class Tunnel(object):
    tunnels = {}

    def __init__(self):
        self.clients = {}

    def new_tunnel(self, port, client_id):
        tunnel = Tunnel.tunnels.get(str(port), None)
        if tunnel:
            tunnel.clients[str(client_id)] = None
        else:
            factory = TunnelFactory(self)

            from twisted.internet import reactor

            p = reactor.listenTCP(port, factory, interface='0.0.0.0')
            #  记录
            self.clients[str(client_id)] = None

            if not port:
                port = p.getHost().port
            Tunnel.tunnels[str(port)] = self
        return port
...

类变量tunnels 以端口号记录所有开启的tunnel,便于多个客户端复用同一个端口。

clients根据client id记录了与客户端的proxy连接, 初始为None,在proxy连接建立的时候赋予真正的值。


client中的东西相对少一些

client/client.py

...
def main():
    log.startLogging(sys.stdout)

    opt = parse_args()

    control = Controller(
        opt['domain'], opt['host'], opt['control_port'], opt['tls_conf'], opt['local_port'], opt['tunnel_port']
    )
    control.connection()

    from twisted.internet import reactor

    reactor.run()
...

在main()中发起到服务的连接。

连接成功后,会相继向服务端发起new_tunnel、new_proxy等请求。

在接收到服务端已经准备好接受proxy连接的答复后,开始建立一条proxy连接,相关代码在client/connected.py中:

client/connected.py

...
    def dataReceived(self, data):
        log.msg(data)
        req_data = json.loads(data.decode('utf8'))

        factory = LocalFactory(self, req_data['id'], req_data['body'])
        from twisted.internet import reactor
        reactor.connectTCP('localhost', self.factory.local_port, factory)
...

看一下proxy 连接的 Protocol,

当他接受到数据时,会建立一条到本地服务的连接,并把数据转发过去,在本地服务应答之后,再将数据回传给服务端。

到这里基本上所有工作就完成了。

最后记录一下生成自签证书的命令:

openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout private.pem -out cert.crt

openssl pkcs8 -topk8 -inform PEM -in private.pem -outform DER -nocrypt -out private.der


参考资料:

铂金ngrok(https://ngrok.bob.kim)原理浅析

内网穿透工具的原理与开发实战

Using TLS in Twisted

twisted与异步编程入门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值