深入Pyrlang源码:EPMD注册与Erlang分布式协议challenge-response握手认证实现原理
【免费下载链接】PyrlangErlang node implemented in Python 3.5+ (Asyncio-based)项目地址: https://gitcode.com/gh_mirrors/py/Pyrlang
Pyrlang是一个用 Python 3.5+(基于 Asyncio)实现的 Erlang 分布式节点库,它能让 Python 进程以标准 Erlang 节点的身份参与分布式系统。本文带你深入Pyrlang 源码,拆解两大核心机制:如何通过EPMD 注册让节点互相发现,以及Erlang 分布式协议中challenge-response 握手认证是如何完成双向身份校验的——无需任何代码基础也能看懂整个流程。
🧭 为什么需要这两步:EPMD注册 + 挑战应答握手
想象 Python 节点要加入一个 Erlang 集群,它必须完成两件事:
- 登记门牌:向每台机器上的EPMD 守护进程(Erlang Port Mapper Daemon,默认监听 4369 端口)报告"我是谁、我的分布端口是多少",这样其他节点才能通过名字找到它;
- 验证身份:与对端节点建立 TCP 连接后,双方通过challenge-response(挑战-应答)交换基于 cookie 的 MD5 摘要,证明彼此持有相同的密钥(cookie),防止陌生节点混入集群。
这两个过程分别由三个核心模块承担:
| 职责 | 模块路径 |
|---|---|
| EPMD 连接、注册与查询 | pyrlang/dist_proto/epmd_client.py |
| 节点网络生命周期编排 | pyrlang/dist_proto/distribution.py |
| 握手状态机(收发两侧) | pyrlang/dist_proto/base_dist_protocol.py、pyrlang/dist_proto/server.py、pyrlang/dist_proto/client.py |
📋 EPMD注册全流程:节点启动的三步走
编排入口是ErlangDistribution.start_distribution(),它在 distribution.py 中只有寥寥数行,却勾勒出完整启动链:
第 1 步:先起本地监听器。start_server()在0.0.0.0的随机端口上创建 TCP 服务(distribution.py),记下分配到的in_port_。端口随机的意义在于:一台机器上可以跑无数个 Python 节点互不冲突。
第 2 步:连接 EPMD。epmd_client.py 中的EPMDClient.connect()会尝试最多5 次、每次间隔 5 秒地连接本机127.0.0.1:4369,并给出epmd -daemon的提示。注意这条连接是长连接——EPMD 靠连接断开来感知节点退出,所以close()注释里写明"关闭连接即从 EPMD 节点列表中注销自己"(epmd_client.py)。
第 3 步:发送 ALIVE2 注册报文。这是 epmd_client.py 的alive2()方法,报文结构在_make_req_alive2()中打包(epmd_client.py):
- 命令字
REQ_ALIVE2 = 120,附带本节点类型(普通节点72/ 隐藏节点77)、协议类型0(即 TCP/IP)、支持的分布式协议版本区间(5, 6)(见 version.py,v5 覆盖到很老的 R6B,v6 自 OTP-23 引入并在 OTP-25 成为强制); - 节点名取
@之前的部分,如py@127.0.0.1只注册py。
EPMD 的应答分两种:ALIVE2_RESP [121, 0, Creation:16位]或双方都支持 v6 时的ALIVE2_X_RESP [118, 0, Creation:32位]。解析逻辑见 _read_alive2_reply()。这里返回的Creation 值非常关键——Pyrlang 用 EPMD 颁发的 creation 参与生成每个进程 PID 的唯一标识(node.py),这也是启动流程中"先注册 EPMD、再派生进程"的原因。
🔎 反向查询:其他节点如何找到它?
当 Python 节点要主动连接远端时,走的是即发即忘的查询通道:EPMDClient.query_node()(epmd_client.py)。它把@后的主机名做 DNS 解析,向目标机器的 EPMD 发送PORT_PLEASE2 (122)命令,读完RESP_PORT2 (119)应答就立即断开(_fire_forget_query())。应答中包含对方端口、节点类型、协议与版本区间,Pyrlang 会先用dist_version_check()判断版本是否重叠,再取双方最大公共同版本(epmd_client.py)——这一步决定了后续握手走 v5 还是 v6 报文格式。
🤝 challenge-response握手:四步完成双向认证
TCP 连接建立后,真正的Erlang 分布式协议握手才开始。整个流程被建模为状态机,状态定义集中在 base_dist_protocol.py。有一个容易被忽略的细节:握手阶段数据包长度前缀是 2 字节,认证完成后切换为 4 字节(packet_len_size_,见 base_dist_protocol.py)。
客户端(发起方) 服务端(被动方) │ │ │──── ① n/N: 节点名+flags ──────▶│ │◀─── ② s:ok + n/N: 挑战值C1 ────│ │──── ③ r: 自报C2 + MD5(C1) ────▶│ │◀─── ④ a: MD5(C2) ─────────────│ │ 切换4字节包头,进入CONNECTED① 交换名片(RECV_NAME 状态)
发起方connection_made()后立即发送欢迎包(client.py):v5 是'n'+ 版本 + 能力标志dflags+ 节点名;v6 是'N'+ 64 位 flags + creation + 名字。被动方on_packet_recvname()(server.py)解析后,先回一个简短的'sok'确认,随后生成一个随机挑战值my_challenge_ = int(random.random() * 0x7fffffff)并立即发出自己的挑战包。
② 发起挑战(CHALLENGE)
挑战包由_send_challenge()发出(server.py),携带本节点名、flags 和那个随机数。随机数即challenge——每次连接都不同,保证摘要不可重放。
③ 应答摘要(CHALLENGE_REPLY)
客户端收到挑战后,在 _send_challenge_reply() 中计算MD5(cookie + str(对方挑战值))作为摘要,连同自己生成的新挑战值 C2一起用'r'包发出。
④ 双向确认(CHALLENGE_ACK)
服务端在on_packet_challengereply()(server.py)中用自己的 cookie重算摘要并比对;比对通过后才用 C2 和 cookie 算出确认摘要,以'a'包回发,并把包头切换为 4 字节、置为CONNECTED。客户端在 on_packet_recvchallenge_ack() 做同样的反向校验,任何一方摘要不符都会直接抛DistributionError断开。
💡 这就是"challenge-response"的精髓:双方各自出题、对方交卷,从而相互证明都持有同一 cookie。摘要算法集中在 base_dist_protocol.py 的make_digest()/check_digest()两个静态方法中:
# 摘要 = MD5(cookie的ASCII字节 + 挑战值的字符串形式) result = md5(bytes(cookie, "ascii") + bytes(str(challenge), "ascii")).digest()🍪 cookie从哪来?常见失败点与调试技巧
cookie 在创建节点时传入:Node("py@127.0.0.1", "COOKIE")(node.py),由DistributionFlags统一保管(flags.py)。默认能力位DEFAULT_DFLAGS也在此定义,其中DFLAGS_HANDSHAKE_23是 OTP 25+ 强制要求的"新版握手"标志位(flags.py)。
对照源码,排查连接失败可沿以下线索:
- 连不上 EPMD:日志会提示 "Is local EPMD running? Try
epmd -daemon",对应 epmd_client.py 的重试逻辑; - cookie 不匹配:服务端日志打印
"Disallowed node connection (check the cookie)"(server.py)——这是最常见的配置错误,检查 Erlang 侧~/.erlang.cookie与 Python 侧传入的 cookie 是否一致; - 版本不重叠:EPMD 查询阶段就会报 "supports protocol version %s and we support %s"(epmd_client.py);
- 重复连接:若对端已存在连接,会收到
salive状态并协商true/false决定新连接是否保留(client.py); - 保活机制:连接建立后每 15 秒发空心跳、20 秒检查一次活跃度、30 秒无交互即销毁连接(base_dist_protocol.py)。
值得一提的是,握手成功后 Pyrlang 节点里注册了net_kernel进程,它会响应 Erlang 侧net_adm:ping发出的is_auth探测(net_kernel.py),这与你日常用net_adm验证节点互信的操作直接对应。
🎯 小结:一张图记住 Pyrlang 的分布式认证链路
| 阶段 | 关键动作 | 源码位置 |
|---|---|---|
| 启动 | 随机端口监听 → EPMD 长连接 → ALIVE2 注册拿 creation | distribution.py |
| 发现 | PORT_PLEASE2查询远端端口与协议版本 | epmd_client.py |
| 握手 | n/N → sok → 挑战 → r摘要 → a确认,2 字节转 4 字节包头 | server.py、client.py |
| 认证 | MD5(cookie + challenge)双向比对 | base_dist_protocol.py |
至此,Pyrlang 的 EPMD 注册解决了"被发现"的问题,challenge-response 握手认证解决了"被信任"的问题——两者叠加,Python 节点才能真正无缝融入 Erlang 分布式生态。想继续深入,可以从 examples/ 目录下的端到端示例入手,例如 Python 与 Erlang 节点互相监控的演示脚本。
【免费下载链接】PyrlangErlang node implemented in Python 3.5+ (Asyncio-based)项目地址: https://gitcode.com/gh_mirrors/py/Pyrlang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考