nodejs / nodejs/node

tls.connect() ignores the `session` option when `socket` is provided (regression v22.22.3 → v22.23.1)

Abierto
#64,402 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

net tls
Lenguaje dominante
JavaScript
Estrellas
122k
Forks
37.3k
Merge medio
4 d 2 h
PR fusionados (30 d)
283

Descripción

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.)

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

No se menciona ningún archivo del repositorio ni ninguna prueba. Empieza en el punto de entrada tls.connect() y ejecuta la reproducción autónoma en las versiones de Node.js afectada y anteriores, centrándote en la ruta socket-plus-session. Se considera terminado cuando la sesión proporcionada se reanuda allí, con cobertura de regresión tanto para conexiones nuevas como para conexiones con un socket existente.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
javascript, node.js
Área
networking, security
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Tranquilo
Claridad
Bastante claro
Aptitud para principiantes
55/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.