tls.connect() ignores the `session` option when `socket` is provided (regression v22.22.3 → v22.23.1)
还没有人认领这个 Issue。
- 主要语言
- JavaScript
- 星标
- 122k
- 派生
- 37.3k
- 平均合并
- 4 天 2 小时
- 30 天内合并 PR
- 283
描述
Version
v22.23.1
Platform
Linux (docker node:22.23.1-bookworm-slim, x86_64); also reproduced on the non-slim image
Subsystem
tls
What steps will reproduce the bug?
Run the self-contained script below (starts a local TLS 1.2 server, then connects twice offering the first connection's session):
// Repro: tls.connect() ignores the `session` option when `socket` is provided.
// Resuming a TLS 1.2 session works with a direct tls.connect({host, port,
// session}) but silently does a full handshake when upgrading an existing
// net.Socket via tls.connect({socket, session}). Regressed between v22.22.3
// (wrapped path resumes) and v22.23.1 (wrapped path ignores the session).
//
// Real-world impact: FTPS servers with vsftpd require_ssl_reuse (e.g. Bambu
// Lab H2D 3D printers) reject every data connection from clients like
// basic-ftp, which upgrade the already-connected data socket and pass
// `session` exactly this way.
'use strict'
const tls = require('node:tls')
const net = require('node:net')
// Throwaway self-signed localhost cert so the repro is a single file.
const key = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDsN1UvQ0kux/m8
snGI8rPBL5kXYXe2E1IV/V8ImQDbFouay2bZIL4vPdhVsNGH7+oprUtQ9sgpKNue
GN96aGcIsUSZN6HcvFZhS78dk6FTBDaSVbUFijp59WS1WdLmmkFYva7SLGJ5VE9A
2tB4DSPcw0ofqESV2YMhHfsc4RhXJ6FcumWPv8yosT3lJAdZjxoBuxGtysY89bff
6ZNBIFnS8tb1W+L2qAk3p7GEIklJUDemmt1HDwu4vA/820Y0UB+KFNLNT4S/wUKw
J7nWJfLQ1A9BiQiRsJgHqRykXYFB9BO0uuQw+SxXtMEqU5CtlnB8Jf2LOfLbhjBx
acwD7oqfAgMBAAECggEAE+l8LE5DOobP9giiynUPEw9km9RzB22sgz8HBk4DhPRr
E0LnKhF5BrlzZZHQr+FY/2dkzG/pIpMXpEWbfRGU8eYjjrjiU52quGvusdsSg9F8
mixQZKWo1UQK18C5JwhEzuq6dGwaZvjk72YioaQV7FOoCXAhS/J4e8+vrdkJuLrs
iT5VvaB+Lpw6Se3OszyWQ4ceFs9WZPLljlxTFFXAADVvgIdMryygotag7zHvbXes
q7Gw/XaIm/RME1I3b7wxYQLeHn1lVQwb5ZjaSZB/2UEa6/s9PCNuerxJMg52cXKA
BDX6txAOyNoQBXU2aHmw7hGWfE2zYvSR9LO4j0RyMQKBgQD2wSwqd/CvYjEAS49e
hjPYr0jbrXVPHxUZeJvliQq3J4ocH+ARDUankVVbJmgxa4WccmDbLGFQil2UXHad
PMTAwysL1Fgvq9NabPJjqsBHLw0UPupDysI99c0K9EvfgjsUVBvtHhx1OSPJRtoa
hLhQpoAO0ASMn7iE7NezuzcW0QKBgQD1ERPNx+7RsQ/NuvC6qgI85DAaVxtqi6oN
6A/ddsz7wh5A0y0Io6Yca7ax8LJ86cPtUqeRfkKXjAZMYN887gQIIwAnJ+I7r8sb
96QXnZwNpoGIhpApbl6UW0WESqAZ6QNrdBVjUdjyHPmD2sxYWtzmmAQ08KD9YmH1
gjjtKobGbwKBgFJel5SxpwmUuJDFqF1AZUw+7w5N7+vyj0OKbFgKECalr0fGKDDp
Ap3rONgNkRh2MQrRb52aSf3twmFFIF9Kqs9CFzuCrdF0BUCZP6yfkkHw5efNPLxW
kdLHG6Q9eppoybn2fcAAnjVPVq/Y0/OoPDLH8dWAARTEOH8+5J5dr30RAoGBAIrR
9anny3y1FAKyFpNmooXjrOv+0+Ty4I2oGvSUy2EBgSmvdh8itIH88iyx8CmaMgZJ
f+qG3yoBg7/YUByvp1EEop2LljfBSQi6qxVFjpSc8Vto/li50JGxchuSOkbzG6DO
zR7Jyl670wvB8WdLcT6ediOW+1sNU/hCoASYxme5AoGAGTx0YXAEPlyzvv3+AKXW
e3XdzG9K+JMnFVgXF0KaMfzsS8zdQsfH2hRENiFBwPlzd0GH/CnbXZZjYtf1zvBR
m73fmbSH3K2CF2pf1+D8P+j9NCyPvBfINX+2EBsTjoDj8NswMiqO+yyFLgIIm/Yh
SwMI12dwWQ9jqc1PN6/pxkM=
-----END PRIVATE KEY-----`
const cert = `-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUTSO0VACyi1UT4Hr3aytSSsWVuwAwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcxMDA1MTUxNVoXDTM2MDcw
NzA1MTUxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA7DdVL0NJLsf5vLJxiPKzwS+ZF2F3thNSFf1fCJkA2xaL
mstm2SC+Lz3YVbDRh+/qKa1LUPbIKSjbnhjfemhnCLFEmTeh3LxWYUu/HZOhUwQ2
klW1BYo6efVktVnS5ppBWL2u0ixieVRPQNrQeA0j3MNKH6hEldmDIR37HOEYVyeh
XLplj7/MqLE95SQHWY8aAbsRrcrGPPW33+mTQSBZ0vLW9Vvi9qgJN6exhCJJSVA3
pprdRw8LuLwP/NtGNFAfihTSzU+Ev8FCsCe51iXy0NQPQYkIkbCYB6kcpF2BQfQT
tLrkMPksV7TBKlOQrZZwfCX9izny24YwcWnMA+6KnwIDAQABo1MwUTAdBgNVHQ4E
FgQUCn1orlDMdSN/3DcjQ0vIJGEamMswHwYDVR0jBBgwFoAUCn1orlDMdSN/3Dcj
Q0vIJGEamMswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAp+a/
cjmmEP0kbSNX8DMQfhhOV+sJhIQaeRP/uFlUmOwJq/BjoZyWUqfU94j6/VLM3ssz
6vYduhgTmQsy3oC/a73YHB5SwmwiT2CbQQ99zyoj65ZXIAFgsrPEEe3g4ufSuP5t
IFtKM6+QspM4VBWlXiUpI/AzPr7L2d9sJGoKTDSgnL+0YQx4PDeAixXtABKa6C+r
+olQEjaqnucctM3Rvr25co2scxbm7broFGLiFF26JyRKZjoatROB26Yp19QGdQHl
L5+7VMHzOa4fc5plpfZ+pEcGDdWA0jGhBTnwami9N9FpMW2/D/2PYh2TGj7nJ5vx
KfOP+J+ltK+bfdET5Q==
-----END CERTIFICATE-----`
const server = tls.createServer({ key, cert, maxVersion: 'TLSv1.2' })
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port
const connectFresh = (session) => new Promise((resolve, reject) => {
const s = tls.connect({ host: '127.0.0.1', port, rejectUnauthorized: false, maxVersion: 'TLSv1.2', session }, () => resolve(s))
s.once('error', reject)
})
const connectWrapped = (session) => new Promise((resolve, reject) => {
const plain = net.connect({ host: '127.0.0.1', port }, () => {
const s = tls.connect({ socket: plain, rejectUnauthorized: false, maxVersion: 'TLSv1.2', session }, () => resolve(s))
s.once('error', reject)
})
plain.once('error', reject)
})
const first = await connectFresh()
const session = first.getSession()
first.destroy()
const fresh = await connectFresh(session)
console.log(`fresh tls.connect({host, port, session}): isSessionReused = ${fresh.isSessionReused()}`)
fresh.destroy()
const wrapped = await connectWrapped(session)
console.log(`wrapped tls.connect({socket, session}): isSessionReused = ${wrapped.isSessionReused()}`)
const ok = wrapped.isSessionReused()
wrapped.destroy()
console.log(ok ? 'OK' : 'BUG: session option ignored on the wrapped-socket path')
server.close()
process.exitCode = ok ? 0 : 1
})
How often does it reproduce? Is there a required condition?
Always on v22.23.1. The required condition is passing an already-connected net.Socket via the socket option together with session.
What is the expected behavior? Why is that the expected behavior?
Both connections should resume the offered session (isSessionReused() === true), as documented for the session option and as v22.22.3 behaves:
fresh tls.connect({host, port, session}): isSessionReused = true
wrapped tls.connect({socket, session}): isSessionReused = true
OK
What do you see instead?
On v22.23.1 the wrapped-socket path silently performs a full handshake — the session option is accepted but has no effect:
fresh tls.connect({host, port, session}): isSessionReused = true
wrapped tls.connect({socket, session}): isSessionReused = false
BUG: session option ignored on the wrapped-socket path
Bisected across release lines: v22.22.3 resumes on both paths; v22.23.1 only on the fresh path.
Additional information
Real-world impact: FTPS servers running vsftpd with require_ssl_reuse (notably Bambu Lab H2-series 3D printers) require every data connection to resume the control connection's TLS session, and reject the transfer otherwise (522 SSL connection failed: session reuse required). FTP clients such as basic-ftp implement this by upgrading the already-connected passive data socket with tls.connect({ socket, session: controlSocket.getSession() }) — exactly the path this regression breaks — so on v22.23.x every such transfer fails while the same code works on earlier releases. (Working around it in our application by using a fresh tls.connect for the data connection.)
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
未指定任何仓库文件或测试。请从 tls.connect() 入口点开始,在受影响的 Node.js 版本和之前的版本上运行自包含复现,重点关注 socket-plus-session 路径。完成的标准是提供的 session 在此处能够恢复,并且为新建 socket 连接和现有 socket 连接都提供回归覆盖。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- javascript, node.js
- 领域
- networking, security
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 基本清楚
- 新手友好度
- 55/100