You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2014/11/17 21:12:48 UTC

[2/2] mina-sshd git commit: [SSHD-371] Support Socks proxy with ssh tunnelling on the client side

[SSHD-371] Support Socks proxy with ssh tunnelling on the client side

Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/4b7a87f1
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/4b7a87f1
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/4b7a87f1

Branch: refs/heads/master
Commit: 4b7a87f1a887c82e1f324856e82d47d89d5f1968
Parents: b0cce8c
Author: Guillaume Nodet <gn...@apache.org>
Authored: Mon Nov 17 21:12:33 2014 +0100
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Mon Nov 17 21:12:33 2014 +0100

----------------------------------------------------------------------
 .../java/org/apache/sshd/ClientSession.java     |  17 ++
 .../main/java/org/apache/sshd/SshClient.java    |  63 ++--
 .../sshd/client/session/ClientSessionImpl.java  |   8 +
 .../org/apache/sshd/common/TcpipForwarder.java  |   5 +
 .../common/forward/DefaultTcpipForwarder.java   | 143 ++++++----
 .../apache/sshd/common/forward/SocksProxy.java  | 286 +++++++++++++++++++
 .../test/java/org/apache/sshd/ProxyTest.java    | 154 ++++++++++
 7 files changed, 598 insertions(+), 78 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index cbcc507..482719b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -194,6 +194,23 @@ public interface ClientSession extends Session {
     void stopRemotePortForwarding(SshdSocketAddress remote) throws IOException;
 
     /**
+     * Start dynamic local port forwarding using a SOCKS proxy.
+     *
+     * @param local
+     * @return
+     * @throws IOException
+     */
+    SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress local) throws IOException;
+
+    /**
+     * Stop a previously started dynamic port forwarding.
+     *
+     * @param local
+     * @throws IOException
+     */
+    void stopDynamicPortForwarding(SshdSocketAddress local) throws IOException;
+
+    /**
      * Wait for a specific state.
      */
     int waitFor(int mask, long timeout);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/SshClient.java
index b9fe07d..33b7eeb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/SshClient.java
@@ -62,6 +62,7 @@ import org.apache.sshd.common.Closeable;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.DefaultIoServiceFactoryFactory;
 import org.apache.sshd.common.io.IoConnectFuture;
@@ -362,6 +363,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
         boolean agentForward = false;
         List<String> command = null;
         int logLevel = 0;
+        int socksPort = -1;
         boolean error = false;
         List<String> identities = new ArrayList<String>();
 
@@ -373,6 +375,13 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
                     break;
                 }
                 port = Integer.parseInt(args[++i]);
+            } else if (command == null && "-D".equals(args[i])) {
+                if (i + 1 >= args.length) {
+                    System.err.println("option requires an argument: " + args[i]);
+                    error = true;
+                    break;
+                }
+                socksPort = Integer.parseInt(args[++i]);
             } else if (command == null && "-l".equals(args[i])) {
                 if (i + 1 >= args.length) {
                     System.err.println("option requires an argument: " + args[i]);
@@ -386,11 +395,11 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
                 logLevel += 2;
             } else if (command == null && "-vvv".equals(args[i])) {
                 logLevel += 3;
-            } else if ("-A".equals(args[i])) {
+            } else if (command == null && "-A".equals(args[i])) {
                 agentForward = true;
-            } else if ("-a".equals(args[i])) {
+            } else if (command == null && "-a".equals(args[i])) {
                 agentForward = false;
-            } else if ("-i".equals(args[i])) {
+            } else if (command == null && "-i".equals(args[i])) {
                 if (i + 1 >= args.length) {
                     System.err.println("option requires and argument: " + args[i]);
                     error = true;
@@ -402,7 +411,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
                 error = true;
                 break;
             } else {
-                if (host == null) {
+                if (command == null && host == null) {
                     host = args[i];
                 } else {
                     if (command == null) {
@@ -417,7 +426,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
             error = true;
         }
         if (error) {
-            System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-l login] [-p port] hostname [command]");
+            System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-D socksPort] [-l login] [-p port] hostname [command]");
             System.exit(-1);
         }
         if (logLevel <= 0) {
@@ -475,6 +484,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
             public void welcome(String banner) {
                 System.out.println(banner);
             }
+
             public String[] interactive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {
                 String[] answers = new String[prompt.length];
                 try {
@@ -507,28 +517,33 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
         ClientSession session = client.connect(login, host, port).await().getSession();
         session.auth().verify();
 
-        ClientChannel channel;
-        if (command == null) {
-            channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
-            ((ChannelShell) channel).setAgentForwarding(agentForward);
-            channel.setIn(new NoCloseInputStream(System.in));
+        if (socksPort >= 0) {
+            session.startDynamicPortForwarding(new SshdSocketAddress("localhost", socksPort));
+            Thread.sleep(Long.MAX_VALUE);
         } else {
-            channel = session.createChannel(ClientChannel.CHANNEL_EXEC);
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            Writer w = new OutputStreamWriter(baos);
-            for (String cmd : command) {
-                w.append(cmd).append(" ");
+            ClientChannel channel;
+            if (command == null) {
+                channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
+                ((ChannelShell) channel).setAgentForwarding(agentForward);
+                channel.setIn(new NoCloseInputStream(System.in));
+            } else {
+                channel = session.createChannel(ClientChannel.CHANNEL_EXEC);
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                Writer w = new OutputStreamWriter(baos);
+                for (String cmd : command) {
+                    w.append(cmd).append(" ");
+                }
+                w.append("\n");
+                w.close();
+                channel.setIn(new ByteArrayInputStream(baos.toByteArray()));
             }
-            w.append("\n");
-            w.close();
-            channel.setIn(new ByteArrayInputStream(baos.toByteArray()));
+            channel.setOut(new NoCloseOutputStream(System.out));
+            channel.setErr(new NoCloseOutputStream(System.err));
+            channel.open().await();
+            channel.waitFor(ClientChannel.CLOSED, 0);
+            session.close(false);
+            client.stop();
         }
-        channel.setOut(new NoCloseOutputStream(System.out));
-        channel.setErr(new NoCloseOutputStream(System.err));
-        channel.open().await();
-        channel.waitFor(ClientChannel.CLOSED, 0);
-        session.close(false);
-        client.stop();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index ea3b53d..e4c0258 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -260,6 +260,14 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
         getConnectionService().getTcpipForwarder().stopRemotePortForwarding(remote);
     }
 
+    public SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress local) throws IOException {
+        return getConnectionService().getTcpipForwarder().startDynamicPortForwarding(local);
+    }
+
+    public void stopDynamicPortForwarding(SshdSocketAddress local) throws IOException {
+        getConnectionService().getTcpipForwarder().stopDynamicPortForwarding(local);
+    }
+
     protected void handleMessage(Buffer buffer) throws Exception {
         synchronized (lock) {
             super.handleMessage(buffer);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/common/TcpipForwarder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/TcpipForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/TcpipForwarder.java
index aedbf54..cfed8e3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/TcpipForwarder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/TcpipForwarder.java
@@ -84,4 +84,9 @@ public interface TcpipForwarder extends Closeable {
      */
     @Deprecated
     void close();
+
+    SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress local) throws IOException;
+
+    void stopDynamicPortForwarding(SshdSocketAddress local) throws IOException;
+
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
index d3256ad..b1b340e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
@@ -49,12 +49,13 @@ import org.apache.sshd.common.util.Readable;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable implements TcpipForwarder, IoHandler {
+public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable implements TcpipForwarder {
 
     private final ConnectionService service;
     private final Session session;
     private final Map<Integer, SshdSocketAddress> localToRemote = new HashMap<Integer, SshdSocketAddress>();
     private final Map<Integer, SshdSocketAddress> remoteToLocal = new HashMap<Integer, SshdSocketAddress>();
+    private final Map<Integer, SocksProxy> dynamicLocal = new HashMap<Integer, SocksProxy>();
     private final Set<SshdSocketAddress> localForwards = new HashSet<SshdSocketAddress>();
     protected IoAcceptor acceptor;
 
@@ -83,7 +84,7 @@ public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable
         if (isClosing()) {
             throw new IllegalStateException("TcpipForwarder is closing");
         }
-        SshdSocketAddress bound = doBind(local);
+        SshdSocketAddress bound = doBind(local, new StaticIoHandler());
         localToRemote.put(bound.getPort(), remote);
         return bound;
     }
@@ -124,6 +125,36 @@ public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable
         }
     }
 
+    public synchronized SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress local) throws IOException {
+        if (local == null) {
+            throw new IllegalArgumentException("Local address is null");
+        }
+        if (local.getPort() < 0) {
+            throw new IllegalArgumentException("Invalid local port: " + local.getPort());
+        }
+        if (isClosed()) {
+            throw new IllegalStateException("TcpipForwarder is closed");
+        }
+        if (isClosing()) {
+            throw new IllegalStateException("TcpipForwarder is closing");
+        }
+        SocksProxy socksProxy = new SocksProxy(service);
+        SshdSocketAddress bound = doBind(local, new SocksProxy(service));
+        dynamicLocal.put(bound.getPort(), socksProxy);
+        return bound;
+    }
+
+    public synchronized void stopDynamicPortForwarding(SshdSocketAddress local) throws IOException {
+        Closeable obj = dynamicLocal.remove(local.getPort());
+        if (obj != null) {
+            obj.close(true);
+            acceptor.unbind(local.toInetSocketAddress());
+            if (acceptor.getBoundAddresses().isEmpty()) {
+                close();
+            }
+        }
+    }
+
     public synchronized SshdSocketAddress getForwardedPort(int remotePort) {
         return remoteToLocal.get(remotePort);
     }
@@ -139,7 +170,7 @@ public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable
         if (filter == null || !filter.canListen(local, session)) {
             throw new IOException("Rejected address: " + local);
         }
-        SshdSocketAddress bound = doBind(local);
+        SshdSocketAddress bound = doBind(local, new StaticIoHandler());
         localForwards.add(bound);
         return bound;
     }
@@ -160,64 +191,16 @@ public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable
 
     @Override
     protected synchronized Closeable getInnerCloseable() {
-        return builder().close(acceptor).build();
-    }
-
-    //
-    // IoHandler implementation
-    //
-
-    public void sessionCreated(final IoSession session) throws Exception {
-        final TcpipClientChannel channel;
-        int localPort = ((InetSocketAddress) session.getLocalAddress()).getPort();
-        if (localToRemote.containsKey(localPort)) {
-            SshdSocketAddress remote = localToRemote.get(localPort);
-            channel = new TcpipClientChannel(TcpipClientChannel.Type.Direct, session, remote);
-        } else {
-            channel = new TcpipClientChannel(TcpipClientChannel.Type.Forwarded, session, null);
-        }
-        session.setAttribute(TcpipClientChannel.class, channel);
-        this.service.registerChannel(channel);
-        channel.open().addListener(new SshFutureListener<OpenFuture>() {
-            public void operationComplete(OpenFuture future) {
-                Throwable t = future.getException();
-                if (t != null) {
-                    DefaultTcpipForwarder.this.service.unregisterChannel(channel);
-                    channel.close(false);
-                }
-            }
-        });
-    }
-
-    public void sessionClosed(IoSession session) throws Exception {
-        TcpipClientChannel channel = (TcpipClientChannel) session.getAttribute(TcpipClientChannel.class);
-        if (channel != null) {
-            log.debug("IoSession {} closed, will now close the channel", session);
-            channel.close(false);
-        }
-    }
-
-    public void messageReceived(IoSession session, Readable message) throws Exception {
-        TcpipClientChannel channel = (TcpipClientChannel) session.getAttribute(TcpipClientChannel.class);
-        Buffer buffer = new Buffer();
-        buffer.putBuffer(message);
-        channel.waitFor(ClientChannel.OPENED | ClientChannel.CLOSED, Long.MAX_VALUE);
-        channel.getInvertedIn().write(buffer.array(), buffer.rpos(), buffer.available());
-        channel.getInvertedIn().flush();
-    }
-
-    public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
-        cause.printStackTrace();
-        session.close(false);
+        return builder().parallel(dynamicLocal.values()).close(acceptor).build();
     }
 
     //
     // Private methods
     //
 
-    private SshdSocketAddress doBind(SshdSocketAddress address) throws IOException {
+    private SshdSocketAddress doBind(SshdSocketAddress address, IoHandler handler) throws IOException {
         if (acceptor == null) {
-            acceptor = session.getFactoryManager().getIoServiceFactory().createAcceptor(this);
+            acceptor = session.getFactoryManager().getIoServiceFactory().createAcceptor(handler);
         }
         Set<SocketAddress> before = acceptor.getBoundAddresses();
         try {
@@ -244,4 +227,56 @@ public class DefaultTcpipForwarder extends CloseableUtils.AbstractInnerCloseable
         return getClass().getSimpleName() + "[" + session + "]";
     }
 
+    //
+    // Static IoHandler implementation
+    //
+
+    class StaticIoHandler implements IoHandler {
+
+        public void sessionCreated(final IoSession session) throws Exception {
+            final TcpipClientChannel channel;
+            int localPort = ((InetSocketAddress) session.getLocalAddress()).getPort();
+            if (localToRemote.containsKey(localPort)) {
+                SshdSocketAddress remote = localToRemote.get(localPort);
+                channel = new TcpipClientChannel(TcpipClientChannel.Type.Direct, session, remote);
+            } else {
+                channel = new TcpipClientChannel(TcpipClientChannel.Type.Forwarded, session, null);
+            }
+            session.setAttribute(TcpipClientChannel.class, channel);
+            service.registerChannel(channel);
+            channel.open().addListener(new SshFutureListener<OpenFuture>() {
+                public void operationComplete(OpenFuture future) {
+                    Throwable t = future.getException();
+                    if (t != null) {
+                        DefaultTcpipForwarder.this.service.unregisterChannel(channel);
+                        channel.close(false);
+                    }
+                }
+            });
+        }
+
+        public void sessionClosed(IoSession session) throws Exception {
+            TcpipClientChannel channel = (TcpipClientChannel) session.getAttribute(TcpipClientChannel.class);
+            if (channel != null) {
+                log.debug("IoSession {} closed, will now close the channel", session);
+                channel.close(false);
+            }
+        }
+
+        public void messageReceived(IoSession session, Readable message) throws Exception {
+            TcpipClientChannel channel = (TcpipClientChannel) session.getAttribute(TcpipClientChannel.class);
+            Buffer buffer = new Buffer();
+            buffer.putBuffer(message);
+            channel.waitFor(ClientChannel.OPENED | ClientChannel.CLOSED, Long.MAX_VALUE);
+            channel.getInvertedIn().write(buffer.array(), buffer.rpos(), buffer.available());
+            channel.getInvertedIn().flush();
+        }
+
+        public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
+            cause.printStackTrace();
+            session.close(false);
+        }
+
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
new file mode 100644
index 0000000..3f61908
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.common.forward;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.SshdSocketAddress;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoHandler;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.util.Buffer;
+import org.apache.sshd.common.util.CloseableUtils;
+
+/**
+ * SOCKS proxy server, supporting simple socks4/5 protocols.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SocksProxy extends CloseableUtils.AbstractCloseable implements IoHandler {
+
+    private final ConnectionService service;
+    private final Map<IoSession, Proxy> proxies = new ConcurrentHashMap<IoSession, Proxy>();
+
+    public SocksProxy(ConnectionService service) {
+        this.service = service;
+    }
+
+    public void sessionCreated(IoSession session) throws Exception {
+        if (isClosing()) {
+            throw new SshException("SocksProxy is closing or closed");
+        }
+    }
+
+    public void sessionClosed(IoSession session) throws Exception {
+        Proxy proxy = proxies.remove(session);
+        if (proxy != null) {
+            proxy.close();
+        }
+    }
+
+    public void messageReceived(final IoSession session, org.apache.sshd.common.util.Readable message) throws Exception {
+        Buffer buffer = new Buffer(message.available());
+        buffer.putBuffer(message);
+        Proxy proxy = proxies.get(session);
+        if (proxy == null) {
+            int version = buffer.getByte();
+            if (version == 0x04) {
+                proxy = new Socks4(session);
+            } else if (version == 0x05) {
+                proxy = new Socks5(session);
+            } else {
+                throw new IllegalStateException("Unsupported version: " + version);
+            }
+            proxy.onMessage(buffer);
+            proxies.put(session, proxy);
+        } else {
+            proxy.onMessage(buffer);
+        }
+    }
+
+    public void exceptionCaught(IoSession ioSession, Throwable cause) throws Exception {
+        log.warn("Exception caught, closing socks proxy", cause);
+        ioSession.close(false);
+    }
+
+    public abstract class Proxy {
+
+        IoSession session;
+        TcpipClientChannel channel;
+
+        protected Proxy(IoSession session) {
+            this.session = session;
+        }
+
+        protected void onMessage(Buffer buffer) throws IOException {
+            channel.getInvertedIn().write(buffer.array(), buffer.rpos(), buffer.available());
+            channel.getInvertedIn().flush();
+        }
+
+        public void close() {
+            if (channel != null) {
+                channel.close(false);
+            }
+        }
+
+        protected int getUByte(Buffer buffer) {
+            return buffer.getByte() & 0xFF;
+        }
+
+        protected int getUShort(Buffer buffer) {
+            return (getUByte(buffer) << 8) + getUByte(buffer);
+        }
+    }
+
+    public class Socks4 extends Proxy {
+        public Socks4(IoSession session) {
+            super(session);
+        }
+
+        @Override
+        protected void onMessage(Buffer buffer) throws IOException {
+            if (channel == null) {
+                int cmd = buffer.getByte();
+                if (cmd != 1) {
+                    throw new IllegalStateException("Unsupported socks command: " + cmd);
+                }
+                int port = getUShort(buffer);
+                String host = Integer.toString(getUByte(buffer)) + "."
+                            + Integer.toString(getUByte(buffer)) + "."
+                            + Integer.toString(getUByte(buffer)) + "."
+                            + Integer.toString(getUByte(buffer));
+                String userId = getNTString(buffer);
+                // Socks4a
+                if (host.startsWith("0.0.0.")) {
+                    host = getNTString(buffer);
+                }
+                log.debug("Received socks4 connection request to {}:{}", host, port);
+                SshdSocketAddress remote = new SshdSocketAddress(host, port);
+                channel = new TcpipClientChannel(TcpipClientChannel.Type.Direct, session, remote);
+                service.registerChannel(channel);
+                channel.open().addListener(new SshFutureListener<OpenFuture>() {
+                    public void operationComplete(OpenFuture future) {
+                        onChannelOpened(future);
+                    }
+                });
+            } else {
+                super.onMessage(buffer);
+            }
+        }
+
+        protected void onChannelOpened(OpenFuture future) {
+            Buffer buffer = new Buffer(8);
+            buffer.putByte((byte) 0x00);
+            Throwable t = future.getException();
+            if (t != null) {
+                service.unregisterChannel(channel);
+                channel.close(false);
+                buffer.putByte((byte) 0x5b);
+            } else {
+                buffer.putByte((byte) 0x5a);
+            }
+            buffer.putByte((byte) 0x00);
+            buffer.putByte((byte) 0x00);
+            buffer.putByte((byte) 0x00);
+            buffer.putByte((byte) 0x00);
+            buffer.putByte((byte) 0x00);
+            buffer.putByte((byte) 0x00);
+            session.write(buffer);
+        }
+
+        private String getNTString(Buffer buffer) {
+            StringBuilder sb = new StringBuilder();
+            char c;
+            while ((c = (char) getUByte(buffer)) != 0) {
+                sb.append(c);
+            }
+            return sb.toString();
+        }
+
+    }
+
+    public class Socks5 extends Proxy {
+
+        byte[] authMethods;
+        Buffer response;
+
+        public Socks5(IoSession session) {
+            super(session);
+        }
+
+        @Override
+        protected void onMessage(Buffer buffer) throws IOException {
+            if (authMethods == null) {
+                int nbAuthMethods = getUByte(buffer);
+                authMethods = new byte[nbAuthMethods];
+                buffer.getRawBytes(authMethods);
+                boolean foundNoAuth = false;
+                for (int i = 0; i < nbAuthMethods; i++) {
+                    foundNoAuth |= authMethods[i] == 0;
+                }
+                buffer = new Buffer(8);
+                buffer.putByte((byte) 0x05);
+                buffer.putByte((byte) (foundNoAuth ? 0x00 : 0xFF));
+                session.write(buffer);
+                if (!foundNoAuth) {
+                    throw new IllegalStateException("Received socks5 greeting without NoAuth method");
+                } else {
+                    log.debug("Received socks5 greeting");
+                }
+            } else if (channel == null) {
+                response = buffer;
+                int version = getUByte(buffer);
+                if (version != 0x05) {
+                    throw new IllegalStateException("Unexpected version: " + version);
+                }
+                int cmd = buffer.getByte();
+                if (cmd != 1) {
+                    throw new IllegalStateException("Unsupported socks command: " + cmd);
+                }
+                final int res = buffer.getByte();
+                int type = buffer.getByte();
+                String host;
+                if (type == 0x01) {
+                    host = Integer.toString(getUByte(buffer)) + "."
+                         + Integer.toString(getUByte(buffer)) + "."
+                         + Integer.toString(getUByte(buffer)) + "."
+                         + Integer.toString(getUByte(buffer));
+                } else if (type == 0x03) {
+                    host = getBLString(buffer);
+                } else if (type == 0x04) {
+                    host = Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer)) + ":"
+                         + Integer.toHexString(getUShort(buffer));
+                } else {
+                    throw new IllegalStateException("Unsupported address type: " + type);
+                }
+                int port = getUShort(buffer);
+                log.debug("Received socks5 connection request to {}:{}", host, port);
+                SshdSocketAddress remote = new SshdSocketAddress(host, port);
+                channel = new TcpipClientChannel(TcpipClientChannel.Type.Direct, session, remote);
+                service.registerChannel(channel);
+                channel.open().addListener(new SshFutureListener<OpenFuture>() {
+                    public void operationComplete(OpenFuture future) {
+                        onChannelOpened(future);
+                    }
+                });
+            } else {
+                log.debug("Received socks5 connection message");
+                super.onMessage(buffer);
+            }
+        }
+
+        protected void onChannelOpened(OpenFuture future) {
+            int wpos = response.wpos();
+            response.rpos(0);
+            response.wpos(1);
+            Throwable t = future.getException();
+            if (t != null) {
+                service.unregisterChannel(channel);
+                channel.close(false);
+                response.putByte((byte) 0x01);
+            } else {
+                response.putByte((byte) 0x00);
+            }
+            response.wpos(wpos);
+            session.write(response);
+        }
+
+        private String getBLString(Buffer buffer) {
+            int length = getUByte(buffer);
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < length; i++) {
+                sb.append((char) getUByte(buffer));
+            }
+            return sb.toString();
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4b7a87f1/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java b/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
new file mode 100644
index 0000000..e8170a5
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd;
+
+import java.io.IOError;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.service.IoAcceptor;
+import org.apache.mina.core.service.IoHandlerAdapter;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
+import org.apache.sshd.common.SshdSocketAddress;
+import org.apache.sshd.util.BaseTest;
+import org.apache.sshd.util.BogusForwardingFilter;
+import org.apache.sshd.util.BogusPasswordAuthenticator;
+import org.apache.sshd.util.EchoShellFactory;
+import org.apache.sshd.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.sshd.util.Utils.getFreePort;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Port forwarding tests
+ */
+public class ProxyTest extends BaseTest {
+
+    private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
+
+    private SshServer sshd;
+    private int sshPort;
+    private int echoPort;
+    private IoAcceptor acceptor;
+    private SshClient client;
+
+    @Before
+    public void setUp() throws Exception {
+        sshPort = getFreePort();
+        echoPort = getFreePort();
+
+        sshd = SshServer.setUpDefaultServer();
+        sshd.getProperties().put(SshServer.WINDOW_SIZE, "2048");
+        sshd.getProperties().put(SshServer.MAX_PACKET_SIZE, "256");
+        sshd.setPort(sshPort);
+        sshd.setKeyPairProvider(Utils.createTestHostKeyProvider());
+        sshd.setShellFactory(new EchoShellFactory());
+        sshd.setPasswordAuthenticator(new BogusPasswordAuthenticator());
+        sshd.setTcpipForwardingFilter(new BogusForwardingFilter());
+        sshd.start();
+
+        NioSocketAcceptor acceptor = new NioSocketAcceptor();
+        acceptor.setHandler(new IoHandlerAdapter() {
+            @Override
+            public void messageReceived(IoSession session, Object message) throws Exception {
+                IoBuffer recv = (IoBuffer) message;
+                IoBuffer sent = IoBuffer.allocate(recv.remaining());
+                sent.put(recv);
+                sent.flip();
+                session.write(sent);
+            }
+        });
+        acceptor.setReuseAddress(true);
+        acceptor.bind(new InetSocketAddress(echoPort));
+        this.acceptor = acceptor;
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            sshd.stop(true);
+        }
+        if (acceptor != null) {
+            acceptor.dispose(true);
+        }
+        if (client != null) {
+            client.stop();
+        }
+    }
+
+    @Test
+    public void testSocksProxy() throws Exception {
+        ClientSession session = createNativeSession();
+
+        SshdSocketAddress dynamic = session.startDynamicPortForwarding(new SshdSocketAddress("localhost", 0));
+
+        for (int i = 0; i < 10; i++) {
+            Socket s = new Socket(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", dynamic.getPort())));
+            s.connect(new InetSocketAddress("localhost", echoPort));
+            s.getOutputStream().write("foo".getBytes());
+            s.getOutputStream().flush();
+            byte[] buf = new byte[1024];
+            int l = s.getInputStream().read(buf);
+            s.close();
+            assertEquals("foo", new String(buf, 0, l));
+        }
+
+        session.stopDynamicPortForwarding(dynamic);
+
+        try {
+            Socket s = new Socket(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", dynamic.getPort())));
+            s.connect(new InetSocketAddress("localhost", echoPort));
+            s.getOutputStream().write("foo".getBytes());
+            fail("Expected IOException");
+        } catch (IOException e) {
+            // expected
+        }
+
+        session.close(false).await();
+    }
+
+    protected ClientSession createNativeSession() throws Exception {
+        client = SshClient.setUpDefaultClient();
+        client.getProperties().put(SshServer.WINDOW_SIZE, "2048");
+        client.getProperties().put(SshServer.MAX_PACKET_SIZE, "256");
+        client.setTcpipForwardingFilter(new BogusForwardingFilter());
+        client.start();
+
+        ClientSession session = client.connect("sshd", "localhost", sshPort).await().getSession();
+        session.addPasswordIdentity("sshd");
+        session.auth().verify();
+        return session;
+    }
+
+
+}
+
+