tls.connect() ignores the `session` option when `socket` is provided (regression v22.22.3 → v22.23.1)
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- JavaScript
- Star
- 122k
- Fork
- 37.3k
- Merge trung bình
- 4 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 283
Mô tả
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.)
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Không có tệp nào trong repository hoặc test nào được nêu tên. Hãy bắt đầu tại điểm vào tls.connect() và chạy bản tái hiện tự chứa trên các phiên bản Node.js bị ảnh hưởng và các phiên bản trước đó, tập trung vào đường dẫn socket-plus-session. Công việc được xem là hoàn tất khi session được cung cấp được tiếp tục tại đó, với coverage hồi quy cho cả các kết nối socket mới và các kết nối socket hiện có.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- javascript, node.js
- Lĩnh vực
- networking, security
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Ít trao đổi
- Độ rõ ràng
- Khá rõ ràng
- Mức phù hợp với người mới
- 55/100