5

Is it possible to find the reason why the Android OpenSSLSocketImpl is closing the Socket? (how do I debug the Android internal libraries?)

Background: The source where I'm trying to create the SSLContext can be found in TLSNetSocketUtil.java during the call to resultSocket.getSession() the underlying Socket are getting closed.

I've logged a Stacktrace when the Socket gets closed:

 at org.silvertunnel_ng.netlib.layer.logger.LoggingNetSocket.close(LoggingNetSocket.java:66)
        at org.silvertunnel_ng.netlib.api.impl.NetSocket2SocketImpl.close(NetSocket2SocketImpl.java:144)
        at java.net.Socket.close(Socket.java:319)
        at com.android.org.conscrypt.OpenSSLSocketImpl.closeUnderlyingSocket(OpenSSLSocketImpl.java:1134)
        at com.android.org.conscrypt.OpenSSLSocketImpl.shutdownAndFreeSslNative(OpenSSLSocketImpl.java:1127)
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:406)
        at com.android.org.conscrypt.OpenSSLSocketImpl.waitForHandshake(OpenSSLSocketImpl.java:623)
        at com.android.org.conscrypt.OpenSSLSocketImpl.getSession(OpenSSLSocketImpl.java:787)
        at org.silvertunnel_ng.netlib.layer.tls.TLSNetSocketUtil.createTLSSocket(TLSNetSocketUtil.java:111)

I think I've nailed it down to lines 528 - 555 in OpenSSLSocketImpl.java, so it seems that something with the Handshake didnt worked, but what?

Running on other JVMs than Android it works fine.

Any suggestions?

Update 1: Stacktrace from startHandshake-Method:

   java.net.SocketException: Socket closed
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318)
        at org.silvertunnel_ng.netlib.layer.tls.TLSNetSocketUtil.createTLSSocket(TLSNetSocketUtil.java:110)
        at org.silvertunnel_ng.netlib.layer.tls.TLSNetLayer.createNetSocket(TLSNetLayer.java:101)
        at org.silvertunnel_ng.netlib.layer.logger.LoggingNetLayer.createNetSocket(LoggingNetLayer.java:130)
        at org.silvertunnel_ng.netlib.layer.tor.circuit.TLSConnection.<init>(TLSConnection.java:128)
        at org.silvertunnel_ng.netlib.layer.tor.circuit.TLSConnectionAdmin.getConnection(TLSConnectionAdmin.java:118)
        at org.silvertunnel_ng.netlib.layer.tor.circuit.Circuit.<init>(Circuit.java:299)
        at org.silvertunnel_ng.netlib.layer.tor.clientimpl.TorBackgroundMgmtThread$1.run(TorBackgroundMgmtThread.java:157)
jww
  • 97,681
  • 90
  • 411
  • 885
B4dT0bi
  • 623
  • 4
  • 21
  • Could you provide the URL it tries to access? Could you provide a packet capture? It probably is not possible to help with only this few information. – Steffen Ullrich May 16 '15 at 14:08
  • The accessed URL is a Tor-Server, as it doesnt work with all Tor-Servers (but works on other JVMs than Android) it wouldnt add any additional value to the question to list all around 4000 Servers to this Question. – B4dT0bi May 16 '15 at 14:19
  • I managed to get an Exception... the getSession method from OpenSSLSocketImpl is not rethrowing the exceptions which are coming from startHandshake method. – B4dT0bi May 16 '15 at 14:21
  • It seems to be an issue with the FileDescriptor, does anyboday know how the FileDescriptor should look like in a Socket? Is it enough to set the impl.fd to new FileDescriptor() ? – B4dT0bi May 16 '15 at 14:48
  • Hard to say what's really going on there. It is very common for a TLS server to just close a connection when something goes wrong and there will be no information what the problem is. But from this trace it is not even clear if this is the situation we face. Since you are having a working code without Android JVM I suggest that you make a packet capture with both and then compare the TLS handshake. It might be ciphers, it might be some TLS extensions... – Steffen Ullrich May 16 '15 at 14:52
  • Unfortunately it doesnt even come to sending stuff to the server. It already dies in NativeCrypto.SSL_do_handshake as the FileDescriptor of the Socket seems to be -1 – B4dT0bi May 16 '15 at 14:56
  • If nothing is send then you don't even have a TCP connection. – Steffen Ullrich May 16 '15 at 15:35
  • The whole source is linked. – B4dT0bi May 17 '15 at 13:37
  • Also see [Issue 174010: getSession closes Socket](https://code.google.com/p/android/issues/detail?id=174010) in the Android Issue Tracker. – jww May 19 '15 at 18:30

1 Answers1

3

The native code at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake is checking the provided FileDescriptor from the underlying SocketImpl of the Socket class.

As it is not easily possible to fake this I had to implement the use of LocalSockets to make it work.


import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import org.silvertunnel_ng.netlib.layer.tor.util.TorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.*;
import java.nio.channels.SocketChannel;
import java.util.UUID;

/**
 * Created by b4dt0bi on 17.05.15.
 */
public class LocalProxySocket extends Socket {

    private LocalServerSocket localServerSocket;
    private LocalSocket localSocketSend;
    private LocalSocket localSocketRecv;
    private Socket originalSocket;

    private static final Logger LOG = LoggerFactory.getLogger(LocalProxySocket.class);

    public LocalProxySocket(Socket original) throws TorException {
        super();
        try {
            // Prepare LocalSocket which will be used to trick the SSLSocket (or any other one)
            localSocketSend = new LocalSocket();
            // Local socket name
            String socketName = "local" + UUID.randomUUID();
            localServerSocket = new LocalServerSocket(socketName);
            localSocketSend.connect(new LocalSocketAddress(socketName));
            localSocketRecv = localServerSocket.accept();
            this.originalSocket = original;
            // Create 2 Threads which are taking care of the communication between the LocalSocket and the original Socket
            LocalProxyWorker lpw1 = new LocalProxyWorker(localSocketRecv.getInputStream(), originalSocket.getOutputStream(), "to");
            LocalProxyWorker lpw2 = new LocalProxyWorker(originalSocket.getInputStream(), localSocketRecv.getOutputStream(), "from");
            Thread t1 = new Thread(lpw1);
            Thread t2 = new Thread(lpw2);
            t1.start();
            t2.start();
            // Prepare this Socket to contain the FileDescriptor of the LocalSocket
            FileDescriptor fd = localSocketSend.getFileDescriptor();
            SocketImpl socketImpl = (SocketImpl) Class.forName("java.net.PlainSocketImpl").getConstructor(FileDescriptor.class).newInstance(fd);
            Field implField = this.getClass().getSuperclass().getDeclaredField("impl");
            implField.setAccessible(true);
            implField.set(this, socketImpl);
        } catch (Exception e) {
            LOG.debug("Got Exception while trying to create LocalProxySocket", e);
            throw new TorException("could not create LocalProxySocket", e);
        }
    }

    private class LocalProxyWorker implements Runnable {
        private InputStream inputStream;
        private OutputStream outputStream;
        private String direction;

        public LocalProxyWorker(InputStream inputStream, OutputStream outputStream, String direction) {
            this.inputStream = inputStream;
            this.outputStream = outputStream;
            this.direction = direction;
        }

        // TODO : cleanup exception handling
        @Override
        public void run() {
            boolean error = false;
            while (!error) {
                try {
                    if (inputStream.available() > 0) {
                        copyStream(inputStream, outputStream);
                    }
                } catch (IOException e) {
                    LOG.debug("got Exception during copy", e);
                    error = true;
                    try {
                        inputStream.close();
                    } catch (IOException e1) {
                        LOG.debug("got exception during close of inputStream", e1);
                    }
                    try {
                        outputStream.close();
                    } catch (IOException e1) {
                        LOG.debug("got exception during close of outputStream", e1);
                    }
                }
            }
        }

        void copyStream(InputStream input, OutputStream output)
                throws IOException {
            byte[] buffer = new byte[1024]; // Adjust if you want
            int bytesRead;
            while ((bytesRead = input.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
            }
        }
    }

    /**
     * Closes the originalSocket. It is not possible to reconnect or rebind to this
     * originalSocket thereafter which means a new originalSocket instance has to be created.
     *
     * @throws IOException if an error occurs while closing the originalSocket.
     */
    public synchronized void close() throws IOException {
        super.close();
        originalSocket.close();
        localSocketRecv.close();
        LOG.debug("LocalProxySocket", "close() called", new Throwable());
    }

    /**
     * Returns the IP address of the target host this originalSocket is connected to, or null if this
     * originalSocket is not yet connected.
     */
    public InetAddress getInetAddress() {
        LOG.debug("LocalProxySocket", "getInetAddress() called", new Throwable());
        return originalSocket.getInetAddress();
    }

    /**
     * Returns an input stream to read data from this originalSocket. If the originalSocket has an associated
     * {@link SocketChannel} and that channel is in non-blocking mode then reads from the
     * stream will throw a {@link java.nio.channels.IllegalBlockingModeException}.
     *
     * @return the byte-oriented input stream.
     * @throws IOException if an error occurs while creating the input stream or the
     *                     originalSocket is in an invalid state.
     */
    public InputStream getInputStream() throws IOException {
        LOG.debug("LocalProxySocket", "getInputStream() called", new Throwable());
        return super.getInputStream();
    }

    /**
     * Returns this originalSocket's {@link SocketOptions#SO_KEEPALIVE} setting.
     */
    public boolean getKeepAlive() throws SocketException {
        LOG.debug("LocalProxySocket", "getKeepAlive() called", new Throwable());
        return originalSocket.getKeepAlive();
    }

    /**
     * Returns the local IP address this originalSocket is bound to, or an address for which
     * {@link InetAddress#isAnyLocalAddress()} returns true if the originalSocket is closed or unbound.
     */
    public InetAddress getLocalAddress() {
        LOG.debug("LocalProxySocket", "getLocalAddress() called", new Throwable());
        return originalSocket.getLocalAddress();
    }

    /**
     * Returns the local port this originalSocket is bound to, or -1 if the originalSocket is unbound. If the originalSocket
     * has been closed this method will still return the local port the originalSocket was bound to.
     */
    public int getLocalPort() {
        LOG.debug("LocalProxySocket", "getLocalPort() called", new Throwable());
        return originalSocket.getLocalPort();
    }

    /**
     * Returns an output stream to write data into this originalSocket. If the originalSocket has an associated
     * {@link SocketChannel} and that channel is in non-blocking mode then writes to the
     * stream will throw a {@link java.nio.channels.IllegalBlockingModeException}.
     *
     * @return the byte-oriented output stream.
     * @throws IOException if an error occurs while creating the output stream or the
     *                     originalSocket is in an invalid state.
     */
    public OutputStream getOutputStream() throws IOException {
        LOG.debug("LocalProxySocket", "getOutputStream() called", new Throwable());
        return super.getOutputStream();
    }

    /**
     * Returns the port number of the target host this originalSocket is connected to, or 0 if this originalSocket
     * is not yet connected.
     */
    public int getPort() {
        LOG.debug("LocalProxySocket", "getPort() called", new Throwable());
        return originalSocket.getPort();
    }

    /**
     * Returns this originalSocket's {@link SocketOptions#SO_LINGER linger} timeout in seconds, or -1
     * for no linger (i.e. {@code close} will return immediately).
     */
    public int getSoLinger() throws SocketException {
        LOG.debug("LocalProxySocket", "getSoLinger() called", new Throwable());
        return originalSocket.getSoLinger();
    }

    /**
     * Returns this originalSocket's {@link SocketOptions#SO_RCVBUF receive buffer size}.
     */
    public synchronized int getReceiveBufferSize() throws SocketException {
        LOG.debug("LocalProxySocket", "getReceiveBufferSize() called", new Throwable());
        return originalSocket.getReceiveBufferSize();
    }

    /**
     * Returns this originalSocket's {@link SocketOptions#SO_SNDBUF send buffer size}.
     */
    public synchronized int getSendBufferSize() throws SocketException {
        LOG.debug("LocalProxySocket", "getSendBufferSize() called", new Throwable());
        return originalSocket.getSendBufferSize();
    }

    /**
     * Returns this originalSocket's {@link SocketOptions#SO_TIMEOUT receive timeout}.
     */
    public synchronized int getSoTimeout() throws SocketException {
        LOG.debug("LocalProxySocket", "getSoTimeout() called", new Throwable());
        return originalSocket.getSoTimeout();
    }

    /**
     * Returns this originalSocket's {@code SocketOptions#TCP_NODELAY} setting.
     */
    public boolean getTcpNoDelay() throws SocketException {
        LOG.debug("LocalProxySocket", "getTcpNoDelay() called", new Throwable());
        return originalSocket.getTcpNoDelay();
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#SO_KEEPALIVE} option.
     */
    public void setKeepAlive(boolean keepAlive) throws SocketException {
        LOG.debug("LocalProxySocket", "setKeepAlive() called", new Throwable());
        originalSocket.setKeepAlive(keepAlive);
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#SO_SNDBUF send buffer size}.
     */
    public synchronized void setSendBufferSize(int size) throws SocketException {
        LOG.debug("LocalProxySocket", "setSendBufferSize() called", new Throwable());
        originalSocket.setSendBufferSize(size);
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#SO_RCVBUF receive buffer size}.
     */
    public synchronized void setReceiveBufferSize(int size) throws SocketException {
        LOG.debug("LocalProxySocket", "setReceiveBufferSize() called", new Throwable());
        originalSocket.setReceiveBufferSize(size);
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#SO_LINGER linger} timeout in seconds.
     * If {@code on} is false, {@code timeout} is irrelevant.
     */
    public void setSoLinger(boolean on, int timeout) throws SocketException {
        LOG.debug("LocalProxySocket", "setSoLinger() called", new Throwable());
        originalSocket.setSoLinger(on, timeout);
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#SO_TIMEOUT read timeout} in milliseconds.
     * Use 0 for no timeout.
     * To take effect, this option must be set before the blocking method was called.
     */
    public synchronized void setSoTimeout(int timeout) throws SocketException {
        LOG.debug("LocalProxySocket", "setSoTimeout() called", new Throwable());
        originalSocket.setSoTimeout(timeout);
    }

    /**
     * Sets this originalSocket's {@link SocketOptions#TCP_NODELAY} option.
     */
    public void setTcpNoDelay(boolean on) throws SocketException {
        LOG.debug("LocalProxySocket", "setTcpNoDelay() called", new Throwable());
        originalSocket.setTcpNoDelay(on);
    }


    /**
     * Returns a {@code String} containing a concise, human-readable description of the
     * originalSocket.
     *
     * @return the textual representation of this originalSocket.
     */
    @Override
    public String toString() {
        LOG.debug("LocalProxySocket", "toString() called", new Throwable());
        return "LocalProxySocket : " + super.toString() + " - " + originalSocket.toString() + " - " + localSocketSend.toString();
    }

    /**
     * Closes the input stream of this originalSocket. Any further data sent to this
     * originalSocket will be discarded. Reading from this originalSocket after this method has
     * been called will return the value {@code EOF}.
     *
     * @throws IOException     if an error occurs while closing the originalSocket input stream.
     * @throws SocketException if the input stream is already closed.
     */
    public void shutdownInput() throws IOException {
        LOG.debug("LocalProxySocket", "shutdownInput() called", new Throwable());
        originalSocket.shutdownInput();
        localSocketRecv.shutdownInput();
    }

    /**
     * Closes the output stream of this originalSocket. All buffered data will be sent
     * followed by the termination sequence. Writing to the closed output stream
     * will cause an {@code IOException}.
     *
     * @throws IOException     if an error occurs while closing the originalSocket output stream.
     * @throws SocketException if the output stream is already closed.
     */
    public void shutdownOutput() throws IOException {
        LOG.debug("LocalProxySocket", "shutdownOutput() called", new Throwable());
        originalSocket.shutdownOutput();
        localSocketRecv.shutdownOutput();
    }

    /**
     * Returns the local address and port of this originalSocket as a SocketAddress or null if the originalSocket
     * has never been bound. If the originalSocket is closed but has previously been bound then an address
     * for which {@link InetAddress#isAnyLocalAddress()} returns true will be returned with the
     * previously-bound port. This is useful on multihomed hosts.
     */
    public SocketAddress getLocalSocketAddress() {
        LOG.debug("LocalProxySocket", "getLocalSocketAddress() called", new Throwable());
        return originalSocket.getLocalSocketAddress();
    }

    /**
     * Returns the remote address and port of this originalSocket as a {@code
     * SocketAddress} or null if the originalSocket is not connected.
     *
     * @return the remote originalSocket address and port.
     */
    public SocketAddress getRemoteSocketAddress() {
        LOG.debug("LocalProxySocket", "getRemoteSocketAddress() called", new Throwable());
        return originalSocket.getRemoteSocketAddress();
    }

    /**
     * Returns whether this originalSocket is bound to a local address and port.
     *
     * @return {@code true} if the originalSocket is bound to a local address, {@code
     * false} otherwise.
     */
    public boolean isBound() {
        LOG.debug("LocalProxySocket", "isBound() called", new Throwable());
        return originalSocket.isBound();
    }

    /**
     * Returns whether this originalSocket is connected to a remote host.
     *
     * @return {@code true} if the originalSocket is connected, {@code false} otherwise.
     */
    public boolean isConnected() {
        LOG.debug("LocalProxySocket", "isConnected() called", new Throwable());
        return originalSocket.isConnected();
        //return true;
    }

    /**
     * Returns whether this originalSocket is closed.
     *
     * @return {@code true} if the originalSocket is closed, {@code false} otherwise.
     */
    public boolean isClosed() {
        LOG.debug("LocalProxySocket", "isClosed() called", new Throwable());
        return originalSocket.isClosed();
    }

    /**
     * Binds this originalSocket to the given local host address and port specified by
     * the SocketAddress {@code localAddr}. If {@code localAddr} is set to
     * {@code null}, this originalSocket will be bound to an available local address on
     * any free port.
     *
     * @param localAddr the specific address and port on the local machine to bind to.
     * @throws IllegalArgumentException if the given SocketAddress is invalid or not supported.
     * @throws IOException              if the originalSocket is already bound or an error occurs while
     *                                  binding.
     */
    public void bind(SocketAddress localAddr) throws IOException {
        LOG.debug("LocalProxySocket", "bind(localAddr) called", new Throwable());
        originalSocket.bind(localAddr);
    }

    /**
     * Connects this originalSocket to the given remote host address and port specified
     * by the SocketAddress {@code remoteAddr}.
     *
     * @param remoteAddr the address and port of the remote host to connect to.
     * @throws IllegalArgumentException if the given SocketAddress is invalid or not supported.
     * @throws IOException              if the originalSocket is already connected or an error occurs while
     *                                  connecting.
     */
    public void connect(SocketAddress remoteAddr) throws IOException {
        LOG.debug("LocalProxySocket", "connect(remoteAddr) called", new Throwable());
        originalSocket.connect(remoteAddr);
    }

    /**
     * Connects this originalSocket to the given remote host address and port specified
     * by the SocketAddress {@code remoteAddr} with the specified timeout. The
     * connecting method will block until the connection is established or an
     * error occurred.
     *
     * @param remoteAddr the address and port of the remote host to connect to.
     * @param timeout    the timeout value in milliseconds or {@code 0} for an infinite
     *                   timeout.
     * @throws IllegalArgumentException if the given SocketAddress is invalid or not supported or the
     *                                  timeout value is negative.
     * @throws IOException              if the originalSocket is already connected or an error occurs while
     *                                  connecting.
     */
    public void connect(SocketAddress remoteAddr, int timeout) throws IOException {
        LOG.debug("LocalProxySocket", "connect(remoteAddr, timeout) called", new Throwable());
        originalSocket.connect(remoteAddr, timeout);
    }

    /**
     * Returns whether the incoming channel of the originalSocket has already been
     * closed.
     *
     * @return {@code true} if reading from this originalSocket is not possible anymore,
     * {@code false} otherwise.
     */
    public boolean isInputShutdown() {
        LOG.debug("LocalProxySocket", "isInputShutdown() called", new Throwable());
        return originalSocket.isInputShutdown() || localSocketRecv.isInputShutdown();
    }

    /**
     * Returns whether the outgoing channel of the originalSocket has already been
     * closed.
     *
     * @return {@code true} if writing to this originalSocket is not possible anymore,
     * {@code false} otherwise.
     */
    public boolean isOutputShutdown() {
        LOG.debug("LocalProxySocket", "isOutputShutdown() called", new Throwable());
        return originalSocket.isOutputShutdown() || localSocketRecv.isOutputShutdown();
    }
}
jww
  • 97,681
  • 90
  • 411
  • 885
B4dT0bi
  • 623
  • 4
  • 21