Socks5ClientConnector.java

  1. /*
  2.  * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd.proxy;

  11. import static java.nio.charset.StandardCharsets.US_ASCII;
  12. import static java.nio.charset.StandardCharsets.UTF_8;
  13. import static java.text.MessageFormat.format;

  14. import java.io.IOException;
  15. import java.net.InetAddress;
  16. import java.net.InetSocketAddress;

  17. import org.apache.sshd.client.session.ClientSession;
  18. import org.apache.sshd.common.io.IoSession;
  19. import org.apache.sshd.common.util.Readable;
  20. import org.apache.sshd.common.util.buffer.Buffer;
  21. import org.apache.sshd.common.util.buffer.BufferUtils;
  22. import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  23. import org.eclipse.jgit.annotations.NonNull;
  24. import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
  25. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  26. import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
  27. import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
  28. import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
  29. import org.eclipse.jgit.transport.SshConstants;
  30. import org.ietf.jgss.GSSContext;

  31. /**
  32.  * A {@link AbstractClientProxyConnector} to connect through a SOCKS5 proxy.
  33.  *
  34.  * @see <a href="https://tools.ietf.org/html/rfc1928">RFC 1928</a>
  35.  */
  36. public class Socks5ClientConnector extends AbstractClientProxyConnector {

  37.     // private static final byte SOCKS_VERSION_4 = 4;
  38.     private static final byte SOCKS_VERSION_5 = 5;

  39.     private static final byte SOCKS_CMD_CONNECT = 1;
  40.     // private static final byte SOCKS5_CMD_BIND = 2;
  41.     // private static final byte SOCKS5_CMD_UDP_ASSOCIATE = 3;

  42.     // Address types

  43.     private static final byte SOCKS_ADDRESS_IPv4 = 1;

  44.     private static final byte SOCKS_ADDRESS_FQDN = 3;

  45.     private static final byte SOCKS_ADDRESS_IPv6 = 4;

  46.     // Reply codes

  47.     private static final byte SOCKS_REPLY_SUCCESS = 0;

  48.     private static final byte SOCKS_REPLY_FAILURE = 1;

  49.     private static final byte SOCKS_REPLY_FORBIDDEN = 2;

  50.     private static final byte SOCKS_REPLY_NETWORK_UNREACHABLE = 3;

  51.     private static final byte SOCKS_REPLY_HOST_UNREACHABLE = 4;

  52.     private static final byte SOCKS_REPLY_CONNECTION_REFUSED = 5;

  53.     private static final byte SOCKS_REPLY_TTL_EXPIRED = 6;

  54.     private static final byte SOCKS_REPLY_COMMAND_UNSUPPORTED = 7;

  55.     private static final byte SOCKS_REPLY_ADDRESS_UNSUPPORTED = 8;

  56.     /**
  57.      * Authentication methods for SOCKS5.
  58.      *
  59.      * @see <a href=
  60.      *      "https://www.iana.org/assignments/socks-methods/socks-methods.xhtml">SOCKS
  61.      *      Methods, IANA.org</a>
  62.      */
  63.     private enum SocksAuthenticationMethod {

  64.         ANONYMOUS(0),
  65.         GSSAPI(1),
  66.         PASSWORD(2),
  67.         // CHALLENGE_HANDSHAKE(3),
  68.         // CHALLENGE_RESPONSE(5),
  69.         // SSL(6),
  70.         // NDS(7),
  71.         // MULTI_AUTH(8),
  72.         // JSON(9),
  73.         NONE_ACCEPTABLE(0xFF);

  74.         private byte value;

  75.         SocksAuthenticationMethod(int value) {
  76.             this.value = (byte) value;
  77.         }

  78.         public byte getValue() {
  79.             return value;
  80.         }
  81.     }

  82.     private enum ProtocolState {
  83.         NONE,

  84.         INIT {
  85.             @Override
  86.             public void handleMessage(Socks5ClientConnector connector,
  87.                     IoSession session, Buffer data) throws Exception {
  88.                 connector.versionCheck(data.getByte());
  89.                 SocksAuthenticationMethod authMethod = connector.getAuthMethod(
  90.                         data.getByte());
  91.                 switch (authMethod) {
  92.                 case ANONYMOUS:
  93.                     connector.sendConnectInfo(session);
  94.                     break;
  95.                 case PASSWORD:
  96.                     connector.doPasswordAuth(session);
  97.                     break;
  98.                 case GSSAPI:
  99.                     connector.doGssApiAuth(session);
  100.                     break;
  101.                 default:
  102.                     throw new IOException(
  103.                             format(SshdText.get().proxyCannotAuthenticate,
  104.                                     connector.proxyAddress));
  105.                 }
  106.             }
  107.         },

  108.         AUTHENTICATING {
  109.             @Override
  110.             public void handleMessage(Socks5ClientConnector connector,
  111.                     IoSession session, Buffer data) throws Exception {
  112.                 connector.authStep(session, data);
  113.             }
  114.         },

  115.         CONNECTING {
  116.             @Override
  117.             public void handleMessage(Socks5ClientConnector connector,
  118.                     IoSession session, Buffer data) throws Exception {
  119.                 // Special case: when GSS-API authentication completes, the
  120.                 // client moves into CONNECTING as soon as the GSS context is
  121.                 // established and sends the connect request. This is per RFC
  122.                 // 1961. But for the server, RFC 1961 says it _should_ send an
  123.                 // empty token even if none generated when its server side
  124.                 // context is established. That means we may actually get an
  125.                 // empty token here. That message is 4 bytes long (and has
  126.                 // content 0x01, 0x01, 0x00, 0x00). We simply skip this message
  127.                 // if we get it here. If the server for whatever reason sends
  128.                 // back a "GSS failed" message (it shouldn't, at this point)
  129.                 // it will be two bytes 0x01 0xFF, which will fail the version
  130.                 // check.
  131.                 if (data.available() != 4) {
  132.                     connector.versionCheck(data.getByte());
  133.                     connector.establishConnection(data);
  134.                 }
  135.             }
  136.         },

  137.         CONNECTED,

  138.         FAILED;

  139.         public void handleMessage(Socks5ClientConnector connector,
  140.                 @SuppressWarnings("unused") IoSession session, Buffer data)
  141.                 throws Exception {
  142.             throw new IOException(
  143.                     format(SshdText.get().proxySocksUnexpectedMessage,
  144.                             connector.proxyAddress, this,
  145.                             BufferUtils.toHex(data.array())));
  146.         }
  147.     }

  148.     private ProtocolState state;

  149.     private AuthenticationHandler<Buffer, Buffer> authenticator;

  150.     private GSSContext context;

  151.     private byte[] authenticationProposals;

  152.     /**
  153.      * Creates a new {@link Socks5ClientConnector}. The connector supports
  154.      * anonymous connections as well as username-password or Kerberos5 (GSS-API)
  155.      * authentication.
  156.      *
  157.      * @param proxyAddress
  158.      *            of the proxy server we're connecting to
  159.      * @param remoteAddress
  160.      *            of the target server to connect to
  161.      */
  162.     public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
  163.             @NonNull InetSocketAddress remoteAddress) {
  164.         this(proxyAddress, remoteAddress, null, null);
  165.     }

  166.     /**
  167.      * Creates a new {@link Socks5ClientConnector}. The connector supports
  168.      * anonymous connections as well as username-password or Kerberos5 (GSS-API)
  169.      * authentication.
  170.      *
  171.      * @param proxyAddress
  172.      *            of the proxy server we're connecting to
  173.      * @param remoteAddress
  174.      *            of the target server to connect to
  175.      * @param proxyUser
  176.      *            to authenticate at the proxy with
  177.      * @param proxyPassword
  178.      *            to authenticate at the proxy with
  179.      */
  180.     public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
  181.             @NonNull InetSocketAddress remoteAddress,
  182.             String proxyUser, char[] proxyPassword) {
  183.         super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
  184.         this.state = ProtocolState.NONE;
  185.     }

  186.     @Override
  187.     public void sendClientProxyMetadata(ClientSession sshSession)
  188.             throws Exception {
  189.         init(sshSession);
  190.         IoSession session = sshSession.getIoSession();
  191.         // Send the initial request
  192.         Buffer buffer = new ByteArrayBuffer(5, false);
  193.         buffer.putByte(SOCKS_VERSION_5);
  194.         context = getGSSContext(remoteAddress);
  195.         authenticationProposals = getAuthenticationProposals();
  196.         buffer.putByte((byte) authenticationProposals.length);
  197.         buffer.putRawBytes(authenticationProposals);
  198.         state = ProtocolState.INIT;
  199.         session.writeBuffer(buffer).verify(getTimeout());
  200.     }

  201.     private byte[] getAuthenticationProposals() {
  202.         byte[] proposals = new byte[3];
  203.         int i = 0;
  204.         proposals[i++] = SocksAuthenticationMethod.ANONYMOUS.getValue();
  205.         proposals[i++] = SocksAuthenticationMethod.PASSWORD.getValue();
  206.         if (context != null) {
  207.             proposals[i++] = SocksAuthenticationMethod.GSSAPI.getValue();
  208.         }
  209.         if (i == proposals.length) {
  210.             return proposals;
  211.         }
  212.         byte[] result = new byte[i];
  213.         System.arraycopy(proposals, 0, result, 0, i);
  214.         return result;
  215.     }

  216.     private void sendConnectInfo(IoSession session) throws Exception {
  217.         GssApiMechanisms.closeContextSilently(context);

  218.         byte[] rawAddress = getRawAddress(remoteAddress);
  219.         byte[] remoteName = null;
  220.         byte type;
  221.         int length = 0;
  222.         if (rawAddress == null) {
  223.             remoteName = remoteAddress.getHostString().getBytes(US_ASCII);
  224.             if (remoteName == null || remoteName.length == 0) {
  225.                 throw new IOException(
  226.                         format(SshdText.get().proxySocksNoRemoteHostName,
  227.                                 remoteAddress));
  228.             } else if (remoteName.length > 255) {
  229.                 // Should not occur; host names must not be longer than 255
  230.                 // US_ASCII characters. Internal error, no translation.
  231.                 throw new IOException(format(
  232.                         "Proxy host name too long for SOCKS (at most 255 characters): {0}", //$NON-NLS-1$
  233.                         remoteAddress.getHostString()));
  234.             }
  235.             type = SOCKS_ADDRESS_FQDN;
  236.             length = remoteName.length + 1;
  237.         } else {
  238.             length = rawAddress.length;
  239.             type = length == 4 ? SOCKS_ADDRESS_IPv4 : SOCKS_ADDRESS_IPv6;
  240.         }
  241.         Buffer buffer = new ByteArrayBuffer(4 + length + 2, false);
  242.         buffer.putByte(SOCKS_VERSION_5);
  243.         buffer.putByte(SOCKS_CMD_CONNECT);
  244.         buffer.putByte((byte) 0); // Reserved
  245.         buffer.putByte(type);
  246.         if (remoteName != null) {
  247.             buffer.putByte((byte) remoteName.length);
  248.             buffer.putRawBytes(remoteName);
  249.         } else {
  250.             buffer.putRawBytes(rawAddress);
  251.         }
  252.         int port = remoteAddress.getPort();
  253.         if (port <= 0) {
  254.             port = SshConstants.SSH_DEFAULT_PORT;
  255.         }
  256.         buffer.putByte((byte) ((port >> 8) & 0xFF));
  257.         buffer.putByte((byte) (port & 0xFF));
  258.         state = ProtocolState.CONNECTING;
  259.         session.writeBuffer(buffer).verify(getTimeout());
  260.     }

  261.     private void doPasswordAuth(IoSession session) throws Exception {
  262.         GssApiMechanisms.closeContextSilently(context);
  263.         authenticator = new SocksBasicAuthentication();
  264.         session.addCloseFutureListener(f -> close());
  265.         startAuth(session);
  266.     }

  267.     private void doGssApiAuth(IoSession session) throws Exception {
  268.         authenticator = new SocksGssApiAuthentication();
  269.         session.addCloseFutureListener(f -> close());
  270.         startAuth(session);
  271.     }

  272.     private void close() {
  273.         AuthenticationHandler<?, ?> handler = authenticator;
  274.         authenticator = null;
  275.         if (handler != null) {
  276.             handler.close();
  277.         }
  278.     }

  279.     private void startAuth(IoSession session) throws Exception {
  280.         Buffer buffer = null;
  281.         try {
  282.             authenticator.setParams(null);
  283.             authenticator.start();
  284.             buffer = authenticator.getToken();
  285.             state = ProtocolState.AUTHENTICATING;
  286.             if (buffer == null) {
  287.                 // Internal error; no translation
  288.                 throw new IOException(
  289.                         "No data for proxy authentication with " //$NON-NLS-1$
  290.                                 + proxyAddress);
  291.             }
  292.             session.writeBuffer(buffer).verify(getTimeout());
  293.         } finally {
  294.             if (buffer != null) {
  295.                 buffer.clear(true);
  296.             }
  297.         }
  298.     }

  299.     private void authStep(IoSession session, Buffer input) throws Exception {
  300.         Buffer buffer = null;
  301.         try {
  302.             authenticator.setParams(input);
  303.             authenticator.process();
  304.             buffer = authenticator.getToken();
  305.             if (buffer != null) {
  306.                 session.writeBuffer(buffer).verify(getTimeout());
  307.             }
  308.         } finally {
  309.             if (buffer != null) {
  310.                 buffer.clear(true);
  311.             }
  312.         }
  313.         if (authenticator.isDone()) {
  314.             sendConnectInfo(session);
  315.         }
  316.     }

  317.     private void establishConnection(Buffer data) throws Exception {
  318.         byte reply = data.getByte();
  319.         switch (reply) {
  320.         case SOCKS_REPLY_SUCCESS:
  321.             state = ProtocolState.CONNECTED;
  322.             setDone(true);
  323.             return;
  324.         case SOCKS_REPLY_FAILURE:
  325.             throw new IOException(format(
  326.                     SshdText.get().proxySocksFailureGeneral, proxyAddress));
  327.         case SOCKS_REPLY_FORBIDDEN:
  328.             throw new IOException(
  329.                     format(SshdText.get().proxySocksFailureForbidden,
  330.                             proxyAddress, remoteAddress));
  331.         case SOCKS_REPLY_NETWORK_UNREACHABLE:
  332.             throw new IOException(
  333.                     format(SshdText.get().proxySocksFailureNetworkUnreachable,
  334.                             proxyAddress, remoteAddress));
  335.         case SOCKS_REPLY_HOST_UNREACHABLE:
  336.             throw new IOException(
  337.                     format(SshdText.get().proxySocksFailureHostUnreachable,
  338.                             proxyAddress, remoteAddress));
  339.         case SOCKS_REPLY_CONNECTION_REFUSED:
  340.             throw new IOException(
  341.                     format(SshdText.get().proxySocksFailureRefused,
  342.                             proxyAddress, remoteAddress));
  343.         case SOCKS_REPLY_TTL_EXPIRED:
  344.             throw new IOException(
  345.                     format(SshdText.get().proxySocksFailureTTL, proxyAddress));
  346.         case SOCKS_REPLY_COMMAND_UNSUPPORTED:
  347.             throw new IOException(
  348.                     format(SshdText.get().proxySocksFailureUnsupportedCommand,
  349.                             proxyAddress));
  350.         case SOCKS_REPLY_ADDRESS_UNSUPPORTED:
  351.             throw new IOException(
  352.                     format(SshdText.get().proxySocksFailureUnsupportedAddress,
  353.                             proxyAddress));
  354.         default:
  355.             throw new IOException(format(
  356.                     SshdText.get().proxySocksFailureUnspecified, proxyAddress));
  357.         }
  358.     }

  359.     @Override
  360.     public void messageReceived(IoSession session, Readable buffer)
  361.             throws Exception {
  362.         try {
  363.             // Dispatch according to protocol state
  364.             ByteArrayBuffer data = new ByteArrayBuffer(buffer.available(),
  365.                     false);
  366.             data.putBuffer(buffer);
  367.             data.compact();
  368.             state.handleMessage(this, session, data);
  369.         } catch (Exception e) {
  370.             state = ProtocolState.FAILED;
  371.             if (authenticator != null) {
  372.                 authenticator.close();
  373.                 authenticator = null;
  374.             }
  375.             try {
  376.                 setDone(false);
  377.             } catch (Exception inner) {
  378.                 e.addSuppressed(inner);
  379.             }
  380.             throw e;
  381.         }
  382.     }

  383.     private void versionCheck(byte version) throws Exception {
  384.         if (version != SOCKS_VERSION_5) {
  385.             throw new IOException(
  386.                     format(SshdText.get().proxySocksUnexpectedVersion,
  387.                             Integer.toString(version & 0xFF)));
  388.         }
  389.     }

  390.     private SocksAuthenticationMethod getAuthMethod(byte value) {
  391.         if (value != SocksAuthenticationMethod.NONE_ACCEPTABLE.getValue()) {
  392.             for (byte proposed : authenticationProposals) {
  393.                 if (proposed == value) {
  394.                     for (SocksAuthenticationMethod method : SocksAuthenticationMethod
  395.                             .values()) {
  396.                         if (method.getValue() == value) {
  397.                             return method;
  398.                         }
  399.                     }
  400.                     break;
  401.                 }
  402.             }
  403.         }
  404.         return SocksAuthenticationMethod.NONE_ACCEPTABLE;
  405.     }

  406.     private static byte[] getRawAddress(@NonNull InetSocketAddress address) {
  407.         InetAddress ipAddress = GssApiMechanisms.resolve(address);
  408.         return ipAddress == null ? null : ipAddress.getAddress();
  409.     }

  410.     private static GSSContext getGSSContext(
  411.             @NonNull InetSocketAddress address) {
  412.         if (!GssApiMechanisms.getSupportedMechanisms()
  413.                 .contains(GssApiMechanisms.KERBEROS_5)) {
  414.             return null;
  415.         }
  416.         return GssApiMechanisms.createContext(GssApiMechanisms.KERBEROS_5,
  417.                 GssApiMechanisms.getCanonicalName(address));
  418.     }

  419.     /**
  420.      * @see <a href="https://tools.ietf.org/html/rfc1929">RFC 1929</a>
  421.      */
  422.     private class SocksBasicAuthentication
  423.             extends BasicAuthentication<Buffer, Buffer> {

  424.         private static final byte SOCKS_BASIC_PROTOCOL_VERSION = 1;

  425.         private static final byte SOCKS_BASIC_AUTH_SUCCESS = 0;

  426.         public SocksBasicAuthentication() {
  427.             super(proxyAddress, proxyUser, proxyPassword);
  428.         }

  429.         @Override
  430.         public void process() throws Exception {
  431.             // Retries impossible. RFC 1929 specifies that the server MUST
  432.             // close the connection if authentication is unsuccessful.
  433.             done = true;
  434.             if (params.getByte() != SOCKS_BASIC_PROTOCOL_VERSION
  435.                     || params.getByte() != SOCKS_BASIC_AUTH_SUCCESS) {
  436.                 throw new IOException(format(
  437.                         SshdText.get().proxySocksAuthenticationFailed, proxy));
  438.             }
  439.         }

  440.         @Override
  441.         protected void askCredentials() {
  442.             super.askCredentials();
  443.             adjustTimeout();
  444.         }

  445.         @Override
  446.         public Buffer getToken() throws IOException {
  447.             if (done) {
  448.                 return null;
  449.             }
  450.             try {
  451.                 byte[] rawUser = user.getBytes(UTF_8);
  452.                 if (rawUser.length > 255) {
  453.                     throw new IOException(format(
  454.                             SshdText.get().proxySocksUsernameTooLong, proxy,
  455.                             Integer.toString(rawUser.length), user));
  456.                 }

  457.                 if (password.length > 255) {
  458.                     throw new IOException(
  459.                             format(SshdText.get().proxySocksPasswordTooLong,
  460.                                     proxy, Integer.toString(password.length)));
  461.                 }
  462.                 ByteArrayBuffer buffer = new ByteArrayBuffer(
  463.                         3 + rawUser.length + password.length, false);
  464.                 buffer.putByte(SOCKS_BASIC_PROTOCOL_VERSION);
  465.                 buffer.putByte((byte) rawUser.length);
  466.                 buffer.putRawBytes(rawUser);
  467.                 buffer.putByte((byte) password.length);
  468.                 buffer.putRawBytes(password);
  469.                 return buffer;
  470.             } finally {
  471.                 clearPassword();
  472.                 done = true;
  473.             }
  474.         }
  475.     }

  476.     /**
  477.      * @see <a href="https://tools.ietf.org/html/rfc1961">RFC 1961</a>
  478.      */
  479.     private class SocksGssApiAuthentication
  480.             extends GssApiAuthentication<Buffer, Buffer> {

  481.         private static final byte SOCKS5_GSSAPI_VERSION = 1;

  482.         private static final byte SOCKS5_GSSAPI_TOKEN = 1;

  483.         private static final int SOCKS5_GSSAPI_FAILURE = 0xFF;

  484.         public SocksGssApiAuthentication() {
  485.             super(proxyAddress);
  486.         }

  487.         @Override
  488.         protected GSSContext createContext() throws Exception {
  489.             return context;
  490.         }

  491.         @Override
  492.         public Buffer getToken() throws Exception {
  493.             if (token == null) {
  494.                 return null;
  495.             }
  496.             Buffer buffer = new ByteArrayBuffer(4 + token.length, false);
  497.             buffer.putByte(SOCKS5_GSSAPI_VERSION);
  498.             buffer.putByte(SOCKS5_GSSAPI_TOKEN);
  499.             buffer.putByte((byte) ((token.length >> 8) & 0xFF));
  500.             buffer.putByte((byte) (token.length & 0xFF));
  501.             buffer.putRawBytes(token);
  502.             return buffer;
  503.         }

  504.         @Override
  505.         protected byte[] extractToken(Buffer input) throws Exception {
  506.             if (context == null) {
  507.                 return null;
  508.             }
  509.             int version = input.getUByte();
  510.             if (version != SOCKS5_GSSAPI_VERSION) {
  511.                 throw new IOException(
  512.                         format(SshdText.get().proxySocksGssApiVersionMismatch,
  513.                                 remoteAddress, Integer.toString(version)));
  514.             }
  515.             int msgType = input.getUByte();
  516.             if (msgType == SOCKS5_GSSAPI_FAILURE) {
  517.                 throw new IOException(format(
  518.                         SshdText.get().proxySocksGssApiFailure, remoteAddress));
  519.             } else if (msgType != SOCKS5_GSSAPI_TOKEN) {
  520.                 throw new IOException(format(
  521.                         SshdText.get().proxySocksGssApiUnknownMessage,
  522.                         remoteAddress, Integer.toHexString(msgType & 0xFF)));
  523.             }
  524.             if (input.available() >= 2) {
  525.                 int length = (input.getUByte() << 8) + input.getUByte();
  526.                 if (input.available() >= length) {
  527.                     byte[] value = new byte[length];
  528.                     if (length > 0) {
  529.                         input.getRawBytes(value);
  530.                     }
  531.                     return value;
  532.                 }
  533.             }
  534.             throw new IOException(
  535.                     format(SshdText.get().proxySocksGssApiMessageTooShort,
  536.                             remoteAddress));
  537.         }
  538.     }
  539. }