内网穿透:建立外网到本地服务的隧道,让居于内网主机上的服务可以暴露给外网
能找到这里的人应该已经对这些东西有所了解,我就不再赘述..
开发工具:
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
参考资料: