Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StreamWrap doesn't properly close the wrapped connection #14605

Closed
tigerbot opened this issue Aug 3, 2017 · 11 comments
Closed

StreamWrap doesn't properly close the wrapped connection #14605

tigerbot opened this issue Aug 3, 2017 · 11 comments
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. tls Issues and PRs related to the tls subsystem.

Comments

@tigerbot
Copy link

tigerbot commented Aug 3, 2017

When emitting anything other than a fully opened net.Socket object to a TLS server that stream/connection gets wrapped in a way that prevents it from being properly closed when the decrypted side of the connection is destroyed. This makes it difficult to wrap encrypted sockets to get around issue #8752 when I need to peek at the data on the connection before giving it the the TLS server.

You can see that the client connection never receives the close event in the following test case

'use strict';
function log() {
  var args = Array.prototype.slice.call(arguments);
  args.unshift(process.uptime().toFixed(3));
  console.log.apply(console, args);
}

var net = require('net');
var tls = require('tls');
var socketPair = require('socket-pair');
var tlsOpts = require('localhost.daplie.me-certificates').merge({});

var tlsServer = tls.createServer(tlsOpts, function (socket) {
  socket.on('close', function () {
    log('decrypted connection closed');
  });
  socket.write('Hello World');
  socket.destroy();
});

function wrapConn(conn, firstChunk) {
  var reader = socketPair.create(function (err, writer) {
    if (err) {
      reader.emit('error', err);
      return;
    }

    conn.pipe(writer);
    writer.pipe(conn);
    process.nextTick(function () {
      conn.unshift(firstChunk);
    });

    conn.on('close', writer.destroy.bind(writer));
    writer.on('close', conn.destroy.bind(conn));
  });

  return reader;
}

var server = net.createServer(function (conn) {
  // tlsServer.emit('connection', conn);
  conn.once('data', function (chunk) {
    // use data to determine what to do with connection.
    tlsServer.emit('connection', wrapConn(conn, chunk));
  });
});

server.listen(0, function () {
  var port = server.address().port;
  var conn = tls.connect({port: port, rejectUnauthorized: false}, function () {
    conn.on('data', function (chunk) {
      log('client received data:', chunk.toString());
    });
    conn.on('close', function () {
      log('client connection closed');
      server.close();
      socketPair.closeAll();
    });
  });
});
@coolaj86
Copy link
Contributor

coolaj86 commented Aug 3, 2017

following

@mscdex mscdex added c++ Issues and PRs that require attention from people who are familiar with C++. tls Issues and PRs related to the tls subsystem. labels Aug 3, 2017
@mcollina
Copy link
Member

mcollina commented Aug 4, 2017

Is this a bug just in Node 8, or in the previous Node.js versions as well?

@tigerbot
Copy link
Author

tigerbot commented Aug 4, 2017

I just ran the script I included earlier using node 7.10.0 and encountered the same problem.

@mcollina
Copy link
Member

mcollina commented Aug 8, 2017

@tigerbot there is no link to the source code of socket-pair. Can you please rewrite the example to not use that module?
The problem it is supposed to fix is fixed in:
b23d414

See #12716 (comment).

@coolaj86
Copy link
Contributor

coolaj86 commented Aug 9, 2017

Here is the source of socket-pair:

https://git.daplie.com/Daplie/socket-pair/blob/master/lib/socket-pair.js

(also fixed the package.json to use an https link rather than a git link)

@coolaj86
Copy link
Contributor

coolaj86 commented Aug 9, 2017

@mcollina This is a workaround that's about 4 levels deep. The topmost error that needs to be tested is #8752.

However, if that solution happens to work now, it only means that the workaround for this current issue is to use that prior solution... as a workaround - meaning that there is still a bug.

@tigerbot Have you tried using stream-pair? Or nothing at all?

@tigerbot
Copy link
Author

tigerbot commented Aug 9, 2017

I recall briefly trying stream-pair without success, but I think that was before I had narrowed down the problem so it's possible I had encountered some other issue. I do however know that our other method for wrapping a socket has the same problem.

var sockFuncs = [
  'address',
  'destroy',
  'ref',
  'unref',
  'setEncoding',
  'setKeepAlive',
  'setNoDelay',
  'setTimeout',
];
function wrapConnDuplex(conn, firstChunk) {
  var myDuplex = new require('stream').Duplex();

  // Handle everything needed for the write part of the Duplex. We need to overwrite the
  // `end` function because there is no other way to know when the other side calls it.
  myDuplex._write = conn.write.bind(conn);
  myDuplex.end = conn.end.bind(conn);

  // Handle everything needed for the read part of the Duplex. See the example under
  // https://nodejs.org/api/stream.html#stream_readable_push_chunk_encoding.
  myDuplex._read = function () {
    conn.resume();
  };
  if (!myDuplex.push(firstChunk)) {
    conn.pause();
  }
  conn.on('data', function (chunk) {
    if (!myDuplex.push(chunk)) {
      conn.pause();
    }
  });
  conn.on('end', function () {
    myDuplex.push(null);
  });

  // Handle the the things not directly related to reading or writing
  conn.on('error', function (err) {
    console.error('[error] wrapped socket errored - ' + err.toString());
    myDuplex.emit('error', err);
  });
  conn.on('close', function () {
    myDuplex.emit('close');
  });
  conn.on('timeout', function () {
    myDuplex.emit('timeout');
  });
  sockFuncs.forEach(function (name) {
    if (typeof conn[name] !== 'function') {
      console.warn('expected `'+name+'` to be a function on wrapped socket');
    } else {
      myDuplex[name] = conn[name].bind(conn);
    }
  });

  return myDuplex;
}

If you use the same code I included above and replace the wrapConn with this function you can see the same problem.

@tigerbot
Copy link
Author

tigerbot commented Aug 9, 2017

I just tested again with stream-pair and it's still hanging for me. Just like before you can replace wrapConn with the function below to reproduce the problem.

function wrapConnStreamPair(conn, firstChunk) {
  var reader = require('stream-pair').create();
  var writer = reader.other;

  writer.write(firstChunk);
  conn.pipe(writer);
  writer.pipe(conn);

  writer.on('close', conn.destroy.bind(conn));
  if (writer.destroy) {
    conn.on('close', writer.destroy.bind(writer));
  }

  return reader;
}

@tigerbot
Copy link
Author

tigerbot commented Aug 9, 2017

Also on the note of not trying to wrap it, if you use the following function instead of wrapConn you can not only get it to hang forever, but you don't even get the events for the decrypted side being closed or the data being received.

function noWrap(conn, firstChunk) {
  process.nextTick(function () {
    conn.unshift(firstChunk);
  });
  return conn;
}

@coolaj86
Copy link
Contributor

coolaj86 commented May 4, 2018

This is still an issues in node v9.11.1

@jasnell
Copy link
Member

jasnell commented Aug 11, 2018

@addaleax ... should this have been resolved with the refactoring you did here?

oyyd added a commit to oyyd/node that referenced this issue Oct 14, 2018
When sockets of the "net" module destroyed, they will call
`this._handle.close()` which will also emit EOF if not emitted
before. This feature makes sockets on the other side emit "end" and
"close" even though we haven't called `end()`. As `stream` of
`StreamWrap` are likely to be instances of `net.Socket`, calling
`destroy()` manually will avoid issues that don't properly close
wrapped connections.

Fixes: nodejs#14605
jasnell pushed a commit that referenced this issue Oct 21, 2018
When sockets of the "net" module destroyed, they will call
`this._handle.close()` which will also emit EOF if not emitted
before. This feature makes sockets on the other side emit "end" and
"close" even though we haven't called `end()`. As `stream` of
`StreamWrap` are likely to be instances of `net.Socket`, calling
`destroy()` manually will avoid issues that don't properly close
wrapped connections.

Fixes: #14605

PR-URL: #23654
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MylesBorins pushed a commit that referenced this issue Nov 26, 2018
When sockets of the "net" module destroyed, they will call
`this._handle.close()` which will also emit EOF if not emitted
before. This feature makes sockets on the other side emit "end" and
"close" even though we haven't called `end()`. As `stream` of
`StreamWrap` are likely to be instances of `net.Socket`, calling
`destroy()` manually will avoid issues that don't properly close
wrapped connections.

Fixes: #14605

PR-URL: #23654
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: James M Snell <[email protected]>
rvagg pushed a commit that referenced this issue Nov 28, 2018
When sockets of the "net" module destroyed, they will call
`this._handle.close()` which will also emit EOF if not emitted
before. This feature makes sockets on the other side emit "end" and
"close" even though we haven't called `end()`. As `stream` of
`StreamWrap` are likely to be instances of `net.Socket`, calling
`destroy()` manually will avoid issues that don't properly close
wrapped connections.

Fixes: #14605

PR-URL: #23654
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: James M Snell <[email protected]>
MylesBorins pushed a commit that referenced this issue Nov 29, 2018
When sockets of the "net" module destroyed, they will call
`this._handle.close()` which will also emit EOF if not emitted
before. This feature makes sockets on the other side emit "end" and
"close" even though we haven't called `end()`. As `stream` of
`StreamWrap` are likely to be instances of `net.Socket`, calling
`destroy()` manually will avoid issues that don't properly close
wrapped connections.

Fixes: #14605

PR-URL: #23654
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. tls Issues and PRs related to the tls subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants