diff --git a/apps/sam/doc/README-test.txt b/apps/sam/doc/README-test.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c1936274d9d8e8265396d7e6266666b229e91b4e
--- /dev/null
+++ b/apps/sam/doc/README-test.txt
@@ -0,0 +1,25 @@
+To run tests:
+
+Build and run standalone Java I2CP (no router):
+ant buildTest
+java -cp build/i2ptest.jar:build/routertest.jar -Djava.library.path=. net.i2p.router.client.LocalClientManager
+
+
+Build and run standalone SAM server:
+ant buildSAM
+java -cp build/i2p.jar:build/mstreaming.jar:build/streaming.jar:build/sam.jar -Djava.library.path=. net.i2p.sam.SAMBridge
+
+
+Build Java test clients:
+cd apps/sam/java
+ant clientjar
+cd ../../..
+
+Run sink client:
+mkdir samsinkdir
+java -cp build/i2p.jar:apps/sam/java/build/samclient.jar net.i2p.sam.client.SAMStreamSink samdest.txt samsinkdir -v 3.2
+run with no args to see usage
+
+Run send client:
+java -cp build/i2p.jar:apps/sam/java/build/samclient.jar net.i2p.sam.client.SAMStreamSend samdest.txt samtestdata -v 3.2
+run with no args to see usage
diff --git a/apps/sam/java/build.xml b/apps/sam/java/build.xml
index 5d93ae4768eb3d4f5957a181e6cb5a913d468a76..9ecc58b8c20f241abf90b09be514f52c856a23ef 100644
--- a/apps/sam/java/build.xml
+++ b/apps/sam/java/build.xml
@@ -21,7 +21,9 @@
         </depend>
     </target>
 
-    <property name="javac.compilerargs" value="" />
+    <!-- ignored for now, we require java 7 here -->
+    <property name="javac.compilerargs7" value="" />
+    <!-- ignored for now, we require java 7 here -->
     <property name="javac.version" value="1.6" />
 
     <!-- compile everything including client classes -->
@@ -30,22 +32,22 @@
         <mkdir dir="./build/obj" />
         <javac 
             srcdir="./src" 
-            debug="true" deprecation="on" source="${javac.version}" target="${javac.version}" 
+            debug="true" deprecation="on" source="1.7" target="1.7" 
             includeAntRuntime="false"
             destdir="./build/obj" 
             classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" >
-            <compilerarg line="${javac.compilerargs}" />
+            <compilerarg line="${javac.compilerargs7}" />
         </javac>
     </target>
 
     <target name="compileTest" depends="compile">
         <javac 
             srcdir="./test" 
-            debug="true" deprecation="on" source="${javac.version}" target="${javac.version}" 
+            debug="true" deprecation="on" source="1.7" target="1.7" 
             includeAntRuntime="false"
             destdir="./build/obj" 
             classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" >
-            <compilerarg line="${javac.compilerargs}" />
+            <compilerarg line="${javac.compilerargs7}" />
         </javac>
     </target>
 
diff --git a/apps/sam/java/src/net/i2p/sam/ReadLine.java b/apps/sam/java/src/net/i2p/sam/ReadLine.java
new file mode 100644
index 0000000000000000000000000000000000000000..2af5f19d31e105dfedf39bb96fc42ab10d58da2d
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/ReadLine.java
@@ -0,0 +1,73 @@
+package net.i2p.sam;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+
+/**
+ * Modified from I2PTunnelHTTPServer
+ *
+ * @since 0.9.24
+ */
+class ReadLine {
+
+    private static final int MAX_LINE_LENGTH = 8*1024;
+
+    /**
+     *  Read a line teriminated by newline, with a total read timeout.
+     *
+     *  Warning - strips \n but not \r
+     *  Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded
+     *
+     *  @param buf output
+     *  @param timeout throws SocketTimeoutException immediately if zero or negative
+     *  @throws SocketTimeoutException if timeout is reached before newline
+     *  @throws EOFException if EOF is reached before newline
+     *  @throws LineTooLongException if too long
+     *  @throws IOException on other errors in the underlying stream
+     */
+    public static void readLine(Socket socket, StringBuilder buf, int timeout) throws IOException {
+        if (timeout <= 0)
+            throw new SocketTimeoutException();
+        long expires = System.currentTimeMillis() + timeout;
+        // this reads and buffers extra bytes, so we can't use it
+        // unless we're going to decode UTF-8 on-the-fly, we're stuck with ASCII
+        //InputStreamReader in = new InputStreamReader(socket.getInputStream(), "UTF-8");
+        InputStream in = socket.getInputStream();
+        int c;
+        int i = 0;
+        socket.setSoTimeout(timeout);
+        while ( (c = in.read()) != -1) {
+            if (++i > MAX_LINE_LENGTH)
+                throw new LineTooLongException("Line too long - max " + MAX_LINE_LENGTH);
+            if (c == '\n')
+                break;
+            int newTimeout = (int) (expires - System.currentTimeMillis());
+            if (newTimeout <= 0)
+                throw new SocketTimeoutException();
+            buf.append((char)c);
+            if (newTimeout != timeout) {
+                timeout = newTimeout;
+                socket.setSoTimeout(timeout);
+            }
+        }
+        if (c == -1) {
+            if (System.currentTimeMillis() >= expires)
+                throw new SocketTimeoutException();
+            else
+                throw new EOFException();
+        }
+    }
+
+    private static class LineTooLongException extends IOException {
+        public LineTooLongException(String s) {
+            super(s);
+        }
+    }
+}
+
+
diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
index 383f44fb574b50a8ce6803eea3143a778143e67d..905ae808d0724f4329180247c76e23928b5455d5 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMBridge.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
@@ -9,15 +9,16 @@ package net.i2p.sam;
  */
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
-import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -27,14 +28,22 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+
+import gnu.getopt.Getopt;
+
 import net.i2p.I2PAppContext;
 import net.i2p.app.*;
 import static net.i2p.app.ClientAppState.*;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.I2PSSLSocketFactory;
 import net.i2p.util.Log;
 import net.i2p.util.PortMapper;
+import net.i2p.util.SystemVersion;
 
 /**
  * SAM bridge implementation.
@@ -48,7 +57,11 @@ public class SAMBridge implements Runnable, ClientApp {
     private final String _listenHost;
     private final int _listenPort;
     private final Properties i2cpProps;
+    private final boolean _useSSL;
+    private final File _configFile;
     private volatile Thread _runner;
+    private final Object _v3DGServerLock = new Object();
+    private SAMv3DatagramServer _v3DGServer;
 
     /** 
      * filename in which the name to private key mapping should 
@@ -65,21 +78,27 @@ public class SAMBridge implements Runnable, ClientApp {
     private volatile boolean acceptConnections = true;
 
     private final ClientAppManager _mgr;
-    private final String[] _args;
     private volatile ClientAppState _state = UNINITIALIZED;
 
     private static final int SAM_LISTENPORT = 7656;
     
     public static final String DEFAULT_SAM_KEYFILE = "sam.keys";
+    static final String DEFAULT_SAM_CONFIGFILE = "sam.config";
+    private static final String PROP_SAM_KEYFILE = "sam.keyfile";
+    private static final String PROP_SAM_SSL = "sam.useSSL";
     public static final String PROP_TCP_HOST = "sam.tcp.host";
     public static final String PROP_TCP_PORT = "sam.tcp.port";
-    protected static final String DEFAULT_TCP_HOST = "0.0.0.0";
+    public static final String PROP_AUTH = "sam.auth";
+    public static final String PROP_PW_PREFIX = "sam.auth.";
+    public static final String PROP_PW_SUFFIX = ".shash";
+    protected static final String DEFAULT_TCP_HOST = "127.0.0.1";
     protected static final String DEFAULT_TCP_PORT = "7656";
     
     public static final String PROP_DATAGRAM_HOST = "sam.udp.host";
     public static final String PROP_DATAGRAM_PORT = "sam.udp.port";
-    protected static final String DEFAULT_DATAGRAM_HOST = "0.0.0.0";
-    protected static final String DEFAULT_DATAGRAM_PORT = "7655";
+    protected static final String DEFAULT_DATAGRAM_HOST = "127.0.0.1";
+    protected static final int DEFAULT_DATAGRAM_PORT_INT = 7655;
+    protected static final String DEFAULT_DATAGRAM_PORT = Integer.toString(DEFAULT_DATAGRAM_PORT_INT);
 
 
     /**
@@ -95,11 +114,14 @@ public class SAMBridge implements Runnable, ClientApp {
     public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception {
         _log = context.logManager().getLog(SAMBridge.class);
         _mgr = mgr;
-        _args = args;
         Options options = getOptions(args);
         _listenHost = options.host;
         _listenPort = options.port;
+        _useSSL = options.isSSL;
+        if (_useSSL && !SystemVersion.isJava7())
+            throw new IllegalArgumentException("SSL requires Java 7 or higher");
         persistFilename = options.keyFile;
+        _configFile = options.configFile;
         nameToPrivKeys = new HashMap<String,String>(8);
         _handlers = new HashSet<Handler>(8);
         this.i2cpProps = options.opts;
@@ -123,13 +145,18 @@ public class SAMBridge implements Runnable, ClientApp {
      * @param persistFile location to store/load named keys to/from
      * @throws RuntimeException if a server socket can't be opened
      */
-    public SAMBridge(String listenHost, int listenPort, Properties i2cpProps, String persistFile) {
+    public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps,
+                     String persistFile, File configFile) {
         _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class);
         _mgr = null;
-        _args = new String[] {listenHost, Integer.toString(listenPort) };  // placeholder
         _listenHost = listenHost;
         _listenPort = listenPort;
+        _useSSL = isSSL;
+        if (_useSSL && !SystemVersion.isJava7())
+            throw new IllegalArgumentException("SSL requires Java 7 or higher");
+        this.i2cpProps = i2cpProps;
         persistFilename = persistFile;
+        _configFile = configFile;
         nameToPrivKeys = new HashMap<String,String>(8);
         _handlers = new HashSet<Handler>(8);
         loadKeys();
@@ -142,7 +169,6 @@ public class SAMBridge implements Runnable, ClientApp {
                            + ":" + listenPort, e);
             throw new RuntimeException(e);
         }
-        this.i2cpProps = i2cpProps;
         _state = INITIALIZED;
     }
 
@@ -150,17 +176,28 @@ public class SAMBridge implements Runnable, ClientApp {
      *  @since 0.9.6
      */
     private void openSocket() throws IOException {
-        if ( (_listenHost != null) && !("0.0.0.0".equals(_listenHost)) ) {
-            serverSocket = ServerSocketChannel.open();
-            serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort));
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("SAM bridge listening on "
-                           + _listenHost + ":" + _listenPort);
+        if (_useSSL) {
+            SSLServerSocketFactory fact = SSLUtil.initializeFactory(i2cpProps);
+            InetAddress addr;
+            if (_listenHost != null && !_listenHost.equals("0.0.0.0"))
+                addr = InetAddress.getByName(_listenHost);
+            else
+                addr = null;
+            SSLServerSocket sock = (SSLServerSocket) fact.createServerSocket(_listenPort, 0, addr);
+            I2PSSLSocketFactory.setProtocolsAndCiphers(sock);
+            serverSocket = new SSLServerSocketChannel(sock);
         } else {
             serverSocket = ServerSocketChannel.open();
-            serverSocket.socket().bind(new InetSocketAddress(_listenPort));
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("SAM bridge listening on 0.0.0.0:" + _listenPort);
+            if (_listenHost != null && !_listenHost.equals("0.0.0.0")) {
+                serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort));
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("SAM bridge listening on "
+                               + _listenHost + ":" + _listenPort);
+            } else {
+                serverSocket.socket().bind(new InetSocketAddress(_listenPort));
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("SAM bridge listening on 0.0.0.0:" + _listenPort);
+            }
         }
     }
 
@@ -320,6 +357,40 @@ public class SAMBridge implements Runnable, ClientApp {
         }
     }
 
+    /**
+     * Was a static singleton, now a singleton for this bridge.
+     * Instantiate and start server if it doesn't exist.
+     * We only listen on one host and port, as specified in the
+     * sam.udp.host and sam.udp.port properties.
+     * TODO we could have multiple servers on different hosts/ports in the future.
+     *
+     * @param props non-null instantiate and start server if it doesn't exist
+     * @param return non-null
+     * @throws IOException if can't bind to host/port, or if different than existing
+     * @since 0.9.24
+     */
+    SAMv3DatagramServer getV3DatagramServer(Properties props) throws IOException {
+        String host = props.getProperty(PROP_DATAGRAM_HOST, DEFAULT_DATAGRAM_HOST);
+        int port;
+        String portStr = props.getProperty(PROP_DATAGRAM_PORT, DEFAULT_DATAGRAM_PORT);
+        try {
+            port = Integer.parseInt(portStr);
+        } catch (NumberFormatException e) {
+            port = DEFAULT_DATAGRAM_PORT_INT;
+        }
+        synchronized (_v3DGServerLock) {
+            if (_v3DGServer == null) {
+                _v3DGServer = new SAMv3DatagramServer(this, host, port, props);
+                _v3DGServer.start();
+            } else {
+                if (_v3DGServer.getPort() != port || !_v3DGServer.getHost().equals(host))
+                    throw new IOException("Already have V3 DatagramServer with host=" + host + " port=" + port);
+            }
+            return _v3DGServer;
+        }
+    }
+
+
     ////// begin ClientApp interface, use only if using correct construtor
 
     /**
@@ -381,7 +452,7 @@ public class SAMBridge implements Runnable, ClientApp {
      *  @since 0.9.6
      */
     public String getDisplayName() {
-        return "SAM " + Arrays.toString(_args);
+        return "SAM " + _listenHost + ':' + _listenPort;
     }
 
     ////// end ClientApp interface
@@ -421,7 +492,8 @@ public class SAMBridge implements Runnable, ClientApp {
     public static void main(String args[]) {
         try {
             Options options = getOptions(args);
-            SAMBridge bridge = new SAMBridge(options.host, options.port, options.opts, options.keyFile);
+            SAMBridge bridge = new SAMBridge(options.host, options.port, options.isSSL, options.opts,
+                                             options.keyFile, options.configFile);
             bridge.startThread();
         } catch (RuntimeException e) {
             e.printStackTrace();
@@ -459,9 +531,13 @@ public class SAMBridge implements Runnable, ClientApp {
         private final String host, keyFile;
         private final int port;
         private final Properties opts;
+        private final boolean isSSL;
+        private final File configFile;
 
-        public Options(String host, int port, Properties opts, String keyFile) {
+        public Options(String host, int port, boolean isSSL, Properties opts, String keyFile, File configFile) {
             this.host = host; this.port = port; this.opts = opts; this.keyFile = keyFile;
+            this.isSSL = isSSL;
+            this.configFile = configFile;
         }
     }
     
@@ -476,73 +552,162 @@ public class SAMBridge implements Runnable, ClientApp {
      * depth, etc.
      * @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ]
      * @return non-null Options or throws Exception
+     * @throws HelpRequestedException on command line problems
+     * @throws IllegalArgumentException if specified config file does not exist
+     * @throws IOException if specified config file cannot be read, or on SSL keystore problems
      * @since 0.9.6
      */
     private static Options getOptions(String args[]) throws Exception {
-        String keyfile = DEFAULT_SAM_KEYFILE;
-        int port = SAM_LISTENPORT;
-        String host = DEFAULT_TCP_HOST;
-        Properties opts = null;
-        if (args.length > 0) {
-       		opts = parseOptions(args, 0);
-       		keyfile = args[0];
-       		int portIndex = 1;
-       		try {
-       			if (args.length>portIndex) port = Integer.parseInt(args[portIndex]);
-       		} catch (NumberFormatException nfe) {
-       			host = args[portIndex];
-       			portIndex++;
-       			try {
-       				if (args.length>portIndex) port = Integer.parseInt(args[portIndex]);
-       			} catch (NumberFormatException nfe1) {
-       				port = Integer.parseInt(opts.getProperty(SAMBridge.PROP_TCP_PORT, SAMBridge.DEFAULT_TCP_PORT));
-       				host = opts.getProperty(SAMBridge.PROP_TCP_HOST, SAMBridge.DEFAULT_TCP_HOST);
-       			}
-       		}
+        String keyfile = null;
+        int port = -1;
+        String host = null;
+        boolean isSSL = false;
+        String cfile = null;
+        Getopt g = new Getopt("SAM", args, "hsc:");
+        int c;
+        while ((c = g.getopt()) != -1) {
+          switch (c) {
+            case 's':
+                isSSL = true;
+                break;
+
+            case 'c':
+                cfile = g.getOptarg();
+                break;
+
+            case 'h':
+            case '?':
+            case ':':
+            default:
+                throw new HelpRequestedException();
+          }  // switch
+        } // while
+
+        int startArgs = g.getOptind();
+        // possible args before ones containing '=';
+        // (none)
+        // key port
+        // key host port
+        int startOpts;
+        for (startOpts = startArgs; startOpts < args.length; startOpts++) {
+            if (args[startOpts].contains("="))
+                break;
+        }
+        int numArgs = startOpts - startArgs;
+        switch (numArgs) {
+            case 0:
+                break;
+
+            case 2:
+                keyfile = args[startArgs];
+                try {
+                    port = Integer.parseInt(args[startArgs + 1]);
+                } catch (NumberFormatException nfe) {
+                    throw new HelpRequestedException();
+                }
+                break;
+
+            case 3:
+                keyfile = args[startArgs];
+                host = args[startArgs + 1];
+                try {
+                    port = Integer.parseInt(args[startArgs + 2]);
+                } catch (NumberFormatException nfe) {
+                    throw new HelpRequestedException();
+                }
+                break;
+
+            default:
+                throw new HelpRequestedException();
+        }
+
+        String scfile = cfile != null ? cfile : DEFAULT_SAM_CONFIGFILE;
+        File file = new File(scfile);
+        if (!file.isAbsolute())
+            file = new File(I2PAppContext.getGlobalContext().getConfigDir(), scfile);
+
+        Properties opts = new Properties();
+        if (file.exists()) {
+            DataHelper.loadProps(opts, file);
+        } else if (cfile != null) {
+            // only throw if specified on command line
+            throw new IllegalArgumentException("Config file not found: " + file);
+        }
+        // command line trumps config file trumps defaults
+        if (host == null)
+            host = opts.getProperty(PROP_TCP_HOST, DEFAULT_TCP_HOST);
+        if (port < 0) {
+            try {
+                port = Integer.parseInt(opts.getProperty(PROP_TCP_PORT, DEFAULT_TCP_PORT));
+            } catch (NumberFormatException nfe) {
+                throw new HelpRequestedException();
+            }
+        }
+        if (keyfile == null)
+            keyfile = opts.getProperty(PROP_SAM_KEYFILE, DEFAULT_SAM_KEYFILE);
+        if (!isSSL)
+            isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL));
+        if (isSSL) {
+            // must do this before we add command line opts since we may be writing them back out
+            boolean shouldSave = SSLUtil.verifyKeyStore(opts);
+            if (shouldSave)
+                DataHelper.storeProps(opts, file);
         }
-        return new Options(host, port, opts, keyfile);
+
+        int remaining = args.length - startOpts;
+        if (remaining > 0) {
+       		parseOptions(args, startOpts, opts);
+        }
+        return new Options(host, port, isSSL, opts, keyfile, file);
     }
 
-    private static Properties parseOptions(String args[], int startArgs) throws HelpRequestedException {
-        Properties props = new Properties();
-        // skip over first few options
+    /**
+     *  Parse key=value options starting at startArgs.
+     *  @param props out parameter, any options found are added
+     *  @throws HelpRequestedException on any item not of the form key=value.
+     */
+    private static void parseOptions(String args[], int startArgs, Properties props) throws HelpRequestedException {
         for (int i = startArgs; i < args.length; i++) {
-        	if (args[i].equals("-h")) throw new HelpRequestedException();
             int eq = args[i].indexOf('=');
-            if (eq <= 0) continue;
-            if (eq >= args[i].length()-1) continue;
+            if (eq <= 0)
+                throw new HelpRequestedException();
+            if (eq >= args[i].length()-1)
+                throw new HelpRequestedException();
             String key = args[i].substring(0, eq);
             String val = args[i].substring(eq+1);
             key = key.trim();
             val = val.trim();
             if ( (key.length() > 0) && (val.length() > 0) )
                 props.setProperty(key, val);
+            else
+                throw new HelpRequestedException();
         }
-        return props;
     }
     
     private static void usage() {
-        System.err.println("Usage: SAMBridge [keyfile [listenHost] listenPortNum[ name=val]*]");
-        System.err.println("or:");
-        System.err.println("       SAMBridge [ name=val ]*");
-        System.err.println(" keyfile: location to persist private keys (default sam.keys)");
-        System.err.println(" listenHost: interface to listen on (0.0.0.0 for all interfaces)");
-        System.err.println(" listenPort: port to listen for SAM connections on (default 7656)");
-        System.err.println(" name=val: options to pass when connecting via I2CP, such as ");
-        System.err.println("           i2cp.host=localhost and i2cp.port=7654");
-        System.err.println("");
-        System.err.println("Host and ports of the SAM bridge can be specified with the alternate");
-        System.err.println("form by specifying options "+SAMBridge.PROP_TCP_HOST+" and/or "+
-        		SAMBridge.PROP_TCP_PORT);
-        System.err.println("");
-        System.err.println("Options "+SAMBridge.PROP_DATAGRAM_HOST+" and "+SAMBridge.PROP_DATAGRAM_PORT+
-        		" specify the listening ip");
-        System.err.println("range and the port of SAM datagram server. This server is");
-        System.err.println("only launched after a client creates the first SAM datagram");
-        System.err.println("or raw session, after a handshake with SAM version >= 3.0.");
-        System.err.println("");
-        System.err.println("The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used");
-        System.err.println("for tuning the log verbosity.\n");
+        System.err.println("Usage: SAMBridge [-s] [-c sam.config] [keyfile [listenHost] listenPortNum[ name=val]*]\n" +
+                           "or:\n" +
+                           "       SAMBridge [ name=val ]*\n" +
+                           " -s: Use SSL\n" +
+                           " -c sam.config: Specify config file\n" +
+                           " keyfile: location to persist private keys (default sam.keys)\n" +
+                           " listenHost: interface to listen on (0.0.0.0 for all interfaces)\n" +
+                           " listenPort: port to listen for SAM connections on (default 7656)\n" +
+                           " name=val: options to pass when connecting via I2CP, such as \n" +
+                           "           i2cp.host=localhost and i2cp.port=7654\n" +
+                           "\n" +
+                           "Host and ports of the SAM bridge can be specified with the alternate\n" +
+                           "form by specifying options "+SAMBridge.PROP_TCP_HOST+" and/or "+
+                           SAMBridge.PROP_TCP_PORT +
+                           "\n" +
+                           "Options "+SAMBridge.PROP_DATAGRAM_HOST+" and "+SAMBridge.PROP_DATAGRAM_PORT+
+                           " specify the listening ip\n" +
+                           "range and the port of SAM datagram server. This server is\n" +
+                           "only launched after a client creates the first SAM datagram\n" +
+                           "or raw session, after a handshake with SAM version >= 3.0.\n" +
+                           "\n" +
+                           "The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used\n" +
+                           "for tuning the log verbosity.");
     }
     
     public void run() {
@@ -621,4 +786,9 @@ public class SAMBridge implements Runnable, ClientApp {
             changeState(STOPPED);
         }
     }
+
+    /** @since 0.9.24 */
+    public void saveConfig() throws IOException {
+        DataHelper.storeProps(i2cpProps, _configFile);
+    }
 }
diff --git a/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java
index 68971af31a46a208277ab550064144c2fd325a6b..b07ff8fbed71097a33238aed793137c7a917870d 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java
@@ -22,9 +22,12 @@ interface SAMDatagramReceiver {
      *
      * @param sender Destination
      * @param data Byte array to be received
+     * @param proto I2CP protocol
+     * @param fromPort I2CP from port
+     * @param toPort I2CP to port
      * @throws IOException 
      */
-    public void receiveDatagramBytes(Destination sender, byte data[]) throws IOException;
+    public void receiveDatagramBytes(Destination sender, byte data[], int proto, int fromPort, int toPort) throws IOException;
 
     /**
      * Stop receiving data.
diff --git a/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java b/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java
index ce3c1803c31b326e3c057109d0c73d9b74ee95b8..b25d6ad0b3b1911b11f7a75c5f8422f1a855b2b5 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java
@@ -78,24 +78,24 @@ class SAMDatagramSession extends SAMMessageSession {
      *
      * @param dest Destination
      * @param data Bytes to be sent
+     * @param proto ignored, will always use PROTO_DATAGRAM (17)
      *
      * @return True if the data was sent, false otherwise
      * @throws DataFormatException on unknown / bad dest
      * @throws I2PSessionException on serious error, probably session closed
      */
-    public boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException {
+    public boolean sendBytes(String dest, byte[] data, int proto,
+                             int fromPort, int toPort) throws DataFormatException, I2PSessionException {
         if (data.length > DGRAM_SIZE_MAX)
             throw new DataFormatException("Datagram size exceeded (" + data.length + ")");
         byte[] dgram ;
         synchronized (dgramMaker) {
         	dgram = dgramMaker.makeI2PDatagram(data);
         }
-        // TODO pass ports through
-        return sendBytesThroughMessageSession(dest, dgram, I2PSession.PROTO_DATAGRAM,
-                                              I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
+        return sendBytesThroughMessageSession(dest, dgram, I2PSession.PROTO_DATAGRAM, fromPort, toPort);
     }
 
-    protected void messageReceived(byte[] msg) {
+    protected void messageReceived(byte[] msg, int proto, int fromPort, int toPort) {
         byte[] payload;
         Destination sender;
         try {
@@ -106,18 +106,18 @@ class SAMDatagramSession extends SAMMessageSession {
             }
         } catch (DataFormatException e) {
             if (_log.shouldLog(Log.DEBUG)) {
-                _log.debug("Dropping ill-formatted I2P repliable datagram");
+                _log.debug("Dropping ill-formatted I2P repliable datagram", e);
             }
             return;
         } catch (I2PInvalidDatagramException e) {
             if (_log.shouldLog(Log.DEBUG)) {
-                _log.debug("Dropping ill-signed I2P repliable datagram");
+                _log.debug("Dropping ill-signed I2P repliable datagram", e);
             }
             return;
         }
 
         try {
-            recv.receiveDatagramBytes(sender, payload);
+            recv.receiveDatagramBytes(sender, payload, proto, fromPort, toPort);
         } catch (IOException e) {
             _log.error("Error forwarding message to receiver", e);
             close();
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandler.java b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
index 56d68878c612ec998fdc6a80f56e5b298f03f7e0..e2bcb84fb709d4ad2d3005fa0a9c57c02f5d3df9 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMHandler.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
@@ -35,8 +35,8 @@ abstract class SAMHandler implements Runnable, Handler {
     private final Object socketWLock = new Object(); // Guards writings on socket
     protected final SocketChannel socket;
 
-    protected final int verMajor;
-    protected final int verMinor;
+    public final int verMajor;
+    public final int verMinor;
     
     /** I2CP options configuring the I2CP connection (port, host, numHops, etc) */
     protected final Properties i2cpProps;
@@ -102,7 +102,10 @@ abstract class SAMHandler implements Runnable, Handler {
         }
     }
     
-    static public void writeBytes(ByteBuffer data, SocketChannel out) throws IOException {
+    /**
+     *  Caller must synch
+     */
+    private static void writeBytes(ByteBuffer data, SocketChannel out) throws IOException {
         while (data.hasRemaining()) out.write(data);           
         out.socket().getOutputStream().flush();
     }
@@ -132,7 +135,10 @@ abstract class SAMHandler implements Runnable, Handler {
         }
     }
 
-    /** @return success */
+    /**
+     * Unsynchronized, use with caution
+     * @return success
+     */
     public static boolean writeString(String str, SocketChannel out)
     {
     	try {
@@ -158,6 +164,8 @@ abstract class SAMHandler implements Runnable, Handler {
      * unregister with the bridge.
      */
     public void stopHandling() {
+        if (_log.shouldInfo())
+            _log.info("Stopping: " + this, new Exception("I did it"));
         synchronized (stopLock) {
             stopHandler = true;
         }
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
index 582854d870617970e18ea5efbeb4dba2c11c7bb6..71a617e25fc8ad436f19e304277cae3487c12577 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
@@ -18,6 +18,7 @@ import java.util.StringTokenizer;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.util.Log;
+import net.i2p.util.PasswordManager;
 import net.i2p.util.VersionComparator;
 
 /**
@@ -25,7 +26,7 @@ import net.i2p.util.VersionComparator;
  */
 class SAMHandlerFactory {
 
-    private static final String VERSION = "3.1";
+    private static final String VERSION = "3.2";
 
     private static final int HELLO_TIMEOUT = 60*1000;
 
@@ -45,20 +46,17 @@ class SAMHandlerFactory {
 
         try {
             Socket sock = s.socket();
-            sock.setSoTimeout(HELLO_TIMEOUT);
             sock.setKeepAlive(true);
-            String line = DataHelper.readLine(sock.getInputStream());
+            StringBuilder buf = new StringBuilder(128);
+            ReadLine.readLine(sock, buf, HELLO_TIMEOUT);
+            String line = buf.toString();
             sock.setSoTimeout(0);
-            if (line == null) {
-                log.debug("Connection closed by client");
-                return null;
-            }
             tok = new StringTokenizer(line.trim(), " ");
         } catch (SocketTimeoutException e) {
             throw new SAMException("Timeout waiting for HELLO VERSION", e);
         } catch (IOException e) {
             throw new SAMException("Error reading from socket", e);
-        } catch (Exception e) {
+        } catch (RuntimeException e) {
             throw new SAMException("Unexpected error", e);
         }
 
@@ -93,6 +91,20 @@ class SAMHandlerFactory {
             SAMHandler.writeString("HELLO REPLY RESULT=NOVERSION\n", s);
             return null;
         }
+
+        if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) {
+            String user = props.getProperty("USER");
+            String pw = props.getProperty("PASSWORD");
+            if (user == null || pw == null)
+                throw new SAMException("USER and PASSWORD required");
+            String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX);
+            if (savedPW == null)
+                throw new SAMException("Authorization failed");
+            PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
+            if (!pm.checkHash(savedPW, pw))
+                throw new SAMException("Authorization failed");
+        }
+
         // Let's answer positively
         if (!SAMHandler.writeString("HELLO REPLY RESULT=OK VERSION=" + ver + "\n", s))
             throw new SAMException("Error writing to socket");       
@@ -131,6 +143,9 @@ class SAMHandlerFactory {
         if (VersionComparator.comp(VERSION, minVer) >= 0 &&
             VersionComparator.comp(VERSION, maxVer) <= 0)
             return VERSION;
+        if (VersionComparator.comp("3.1", minVer) >= 0 &&
+            VersionComparator.comp("3.1", maxVer) <= 0)
+            return "3.1";
         // in VersionComparator, "3" < "3.0" so
         // use comparisons carefully
         if (VersionComparator.comp("3.0", minVer) >= 0 &&
diff --git a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java
index e8e502da8b2c5b5cf51cbc5b01343f6b68fd52d2..c95b6a9ab22c8faff38daeb7c99d507096662ec8 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java
@@ -19,11 +19,12 @@ import net.i2p.client.I2PClient;
 import net.i2p.client.I2PClientFactory;
 import net.i2p.client.I2PSession;
 import net.i2p.client.I2PSessionException;
-import net.i2p.client.I2PSessionListener;
+import net.i2p.client.I2PSessionMuxedListener;
+import net.i2p.client.SendMessageOptions;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
-import net.i2p.util.HexDump;
+//import net.i2p.util.HexDump;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 
@@ -97,7 +98,8 @@ abstract class SAMMessageSession implements Closeable {
      * @throws DataFormatException on unknown / bad dest
      * @throws I2PSessionException on serious error, probably session closed
      */
-    public abstract boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException;
+    public abstract boolean sendBytes(String dest, byte[] data, int proto,
+                                      int fromPort, int toPort) throws DataFormatException, I2PSessionException;
 
     /**
      * Actually send bytes through the SAM message-based session I2PSession
@@ -125,6 +127,40 @@ abstract class SAMMessageSession implements Closeable {
 	return session.sendMessage(d, data, proto, fromPort, toPort);
     }
 
+    /**
+     * Actually send bytes through the SAM message-based session I2PSession.
+     * TODO unused, umimplemented in the sessions and handlers
+     *
+     * @param dest Destination
+     * @param data Bytes to be sent
+     * @param proto I2CP protocol
+     * @param fromPort I2CP from port
+     * @param toPort I2CP to port
+     *
+     * @return True if the data was sent, false otherwise
+     * @throws DataFormatException on unknown / bad dest
+     * @throws I2PSessionException on serious error, probably session closed
+     * @since 0.9.24
+     */
+    protected boolean sendBytesThroughMessageSession(String dest, byte[] data,
+                                        int proto, int fromPort, int toPort,
+                                        boolean sendLeaseSet, int sendTags,
+                                        int tagThreshold, long expires)
+                                        throws DataFormatException, I2PSessionException {
+	Destination d = SAMUtils.getDest(dest);
+
+	if (_log.shouldLog(Log.DEBUG)) {
+	    _log.debug("Sending " + data.length + " bytes to " + dest);
+	}
+	SendMessageOptions opts = new SendMessageOptions();
+	opts.setSendLeaseSet(sendLeaseSet);
+	opts.setTagsToSend(sendTags);
+	opts.setTagThreshold(tagThreshold);
+	opts.setDate(expires);
+
+	return session.sendMessage(d, data, 0, data.length, proto, fromPort, toPort, opts);
+    }
+
     /**
      * Close a SAM message-based session.
      */
@@ -136,7 +172,7 @@ abstract class SAMMessageSession implements Closeable {
      * Handle a new received message
      * @param msg Message payload
      */
-    protected abstract void messageReceived(byte[] msg);
+    protected abstract void messageReceived(byte[] msg, int proto, int fromPort, int toPort);
     
     /**
      * Do whatever is needed to shutdown the SAM session
@@ -158,7 +194,7 @@ abstract class SAMMessageSession implements Closeable {
      *
      * @author human
      */
-    class SAMMessageSessionHandler implements Runnable, I2PSessionListener {
+    class SAMMessageSessionHandler implements Runnable, I2PSessionMuxedListener {
 
         private final Object runningLock = new Object();
         private volatile boolean stillRunning = true;
@@ -187,7 +223,7 @@ abstract class SAMMessageSession implements Closeable {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("I2P session connected");
 
-            session.setSessionListener(this);
+            session.addMuxedSessionListener(this, I2PSession.PROTO_ANY, I2PSession.PORT_ANY);
         }
 
         /**
@@ -218,6 +254,7 @@ abstract class SAMMessageSession implements Closeable {
                 _log.debug("Shutting down SAM message-based session handler");
             
             shutDown();
+            session.removeListener(I2PSession.PROTO_ANY, I2PSession.PORT_ANY);
             
             try {
                 if (_log.shouldLog(Log.DEBUG))
@@ -243,7 +280,15 @@ abstract class SAMMessageSession implements Closeable {
             stopRunning();
         }
             
-        public void messageAvailable(I2PSession session, int msgId, long size){
+        public void messageAvailable(I2PSession session, int msgId, long size) {
+            messageAvailable(session, msgId, size, I2PSession.PROTO_UNSPECIFIED,
+                             I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
+        }
+
+        /** @since 0.9.24 */
+        public void messageAvailable(I2PSession session, int msgId, long size,
+                                     int proto, int fromPort, int toPort) {
+
             if (_log.shouldLog(Log.DEBUG)) {
                 _log.debug("I2P message available (id: " + msgId
                            + "; size: " + size + ")");
@@ -252,12 +297,12 @@ abstract class SAMMessageSession implements Closeable {
                 byte msg[] = session.receiveMessage(msgId);
                 if (msg == null)
                     return;
-                if (_log.shouldLog(Log.DEBUG)) {
-                    _log.debug("Content of message " + msgId + ":\n"
-                               + HexDump.dump(msg));
-                }
+                //if (_log.shouldLog(Log.DEBUG)) {
+                //    _log.debug("Content of message " + msgId + ":\n"
+                //               + HexDump.dump(msg));
+                //}
                 
-                messageReceived(msg);
+                messageReceived(msg, proto, fromPort, toPort);
             } catch (I2PSessionException e) {
                 _log.error("Error fetching I2P message", e);
                 stopRunning();
diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
index 96ebe45d082102ffd98e4a35d1062a498fcfd5cd..564f95c3d8b39e53b2a20c07f3f4b1d5eac95d1f 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
@@ -20,9 +20,12 @@ interface SAMRawReceiver {
      * regarding the sender.
      *
      * @param data Byte array to be received
+     * @param proto I2CP protocol
+     * @param fromPort I2CP from port
+     * @param toPort I2CP to port
      * @throws IOException 
      */
-    public void receiveRawBytes(byte data[]) throws IOException;
+    public void receiveRawBytes(byte data[], int proto, int fromPort, int toPort) throws IOException;
 
     /**
      * Stop receiving data.
diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java
index e3c42160834a7f46fd736f33d779b4472ce9bae4..ab1cd76322e36d6ca305510d14b9f0b1f82fffb9 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java
@@ -67,22 +67,24 @@ class SAMRawSession extends SAMMessageSession {
      * Send bytes through a SAM RAW session.
      *
      * @param data Bytes to be sent
+     * @param proto if 0, will use PROTO_DATAGRAM_RAW (18)
      *
      * @return True if the data was sent, false otherwise
      * @throws DataFormatException on unknown / bad dest
      * @throws I2PSessionException on serious error, probably session closed
      */
-    public boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException {
+    public boolean sendBytes(String dest, byte[] data, int proto,
+                             int fromPort, int toPort) throws DataFormatException, I2PSessionException {
         if (data.length > RAW_SIZE_MAX)
             throw new DataFormatException("Data size limit exceeded (" + data.length + ")");
-        // TODO pass ports through
-        return sendBytesThroughMessageSession(dest, data, I2PSession.PROTO_DATAGRAM_RAW,
-                                              I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
+        if (proto == I2PSession.PROTO_UNSPECIFIED)
+            proto = I2PSession.PROTO_DATAGRAM_RAW;
+        return sendBytesThroughMessageSession(dest, data, proto, fromPort, toPort);
     }
 
-    protected void messageReceived(byte[] msg) {
+    protected void messageReceived(byte[] msg, int proto, int fromPort, int toPort) {
         try {
-            recv.receiveRawBytes(msg);
+            recv.receiveRawBytes(msg, proto, fromPort, toPort);
         } catch (IOException e) {
             _log.error("Error forwarding message to receiver", e);
             close();
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
index 588d5f5e5786d0c6d3e228ec1949cd84f4abcaf5..5b594701686bea2de347ec5dba1471d466117c4e 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
@@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicLong;
 
 import net.i2p.I2PException;
 import net.i2p.client.I2PClient;
+import net.i2p.client.I2PSession;
 import net.i2p.client.I2PSessionException;
 import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
@@ -186,7 +187,9 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
         } catch (IOException e) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Caught IOException for message [" + msg + "]", e);
-        } catch (Exception e) {
+        } catch (SAMException e) {
+            _log.error("Unexpected exception for message [" + msg + "]", e);
+        } catch (RuntimeException e) {
             _log.error("Unexpected exception for message [" + msg + "]", e);
         } finally {
             if (_log.shouldLog(Log.DEBUG))
@@ -438,25 +441,44 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
             }
 
             int size;
-            {
-                String strsize = props.getProperty("SIZE");
-                if (strsize == null) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Size not specified in DATAGRAM SEND message");
-                    return false;
-                }
+            String strsize = props.getProperty("SIZE");
+            if (strsize == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Size not specified in DATAGRAM SEND message");
+                return false;
+            }
+            try {
+                size = Integer.parseInt(strsize);
+            } catch (NumberFormatException e) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Invalid DATAGRAM SEND size specified: " + strsize);
+                return false;
+            }
+            if (!checkDatagramSize(size)) {
+                if (_log.shouldLog(Log.WARN))
+                     _log.warn("Specified size (" + size
+                           + ") is out of protocol limits");
+                return false;
+            }
+            int proto = I2PSession.PROTO_DATAGRAM;
+            int fromPort = I2PSession.PORT_UNSPECIFIED;
+            int toPort = I2PSession.PORT_UNSPECIFIED;
+            String s = props.getProperty("FROM_PORT");
+            if (s != null) {
                 try {
-                    size = Integer.parseInt(strsize);
+                    fromPort = Integer.parseInt(s);
                 } catch (NumberFormatException e) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Invalid DATAGRAM SEND size specified: " + strsize);
-                    return false;
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid DATAGRAM SEND port specified: " + s);
                 }
-                if (!checkDatagramSize(size)) {
-                    if (_log.shouldLog(Log.DEBUG))
-                         _log.debug("Specified size (" + size
-                               + ") is out of protocol limits");
-                    return false;
+            }
+            s = props.getProperty("TO_PORT");
+            if (s != null) {
+                try {
+                    toPort = Integer.parseInt(s);
+                } catch (NumberFormatException e) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid RAW SEND port specified: " + s);
                 }
             }
 
@@ -466,7 +488,7 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
 
                 in.readFully(data);
 
-                if (!getDatagramSession().sendBytes(dest, data)) {
+                if (!getDatagramSession().sendBytes(dest, data, proto, fromPort, toPort)) {
                     _log.error("DATAGRAM SEND failed");
                     // a message send failure is no reason to drop the SAM session
                     // for raw and repliable datagrams, just carry on our merry way
@@ -523,25 +545,53 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
             }
 
             int size;
-            {
-                String strsize = props.getProperty("SIZE");
-                if (strsize == null) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Size not specified in RAW SEND message");
-                    return false;
+            String strsize = props.getProperty("SIZE");
+            if (strsize == null) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Size not specified in RAW SEND message");
+                return false;
+            }
+            try {
+                size = Integer.parseInt(strsize);
+            } catch (NumberFormatException e) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Invalid RAW SEND size specified: " + strsize);
+                return false;
+            }
+            if (!checkSize(size)) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Specified size (" + size
+                           + ") is out of protocol limits");
+                return false;
+            }
+            int proto = I2PSession.PROTO_DATAGRAM_RAW;
+            int fromPort = I2PSession.PORT_UNSPECIFIED;
+            int toPort = I2PSession.PORT_UNSPECIFIED;
+            String s = props.getProperty("PROTOCOL");
+            if (s != null) {
+                try {
+                    proto = Integer.parseInt(s);
+                } catch (NumberFormatException e) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid RAW SEND protocol specified: " + s);
                 }
+            }
+            s = props.getProperty("FROM_PORT");
+            if (s != null) {
                 try {
-                    size = Integer.parseInt(strsize);
+                    fromPort = Integer.parseInt(s);
                 } catch (NumberFormatException e) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Invalid RAW SEND size specified: " + strsize);
-                    return false;
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid RAW SEND port specified: " + s);
                 }
-                if (!checkSize(size)) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Specified size (" + size
-                               + ") is out of protocol limits");
-                    return false;
+            }
+            s = props.getProperty("TO_PORT");
+            if (s != null) {
+                try {
+                    toPort = Integer.parseInt(s);
+                } catch (NumberFormatException e) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid RAW SEND port specified: " + s);
                 }
             }
 
@@ -551,7 +601,7 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
 
                 in.readFully(data);
 
-                if (!getRawSession().sendBytes(dest, data)) {
+                if (!getRawSession().sendBytes(dest, data, proto, fromPort, toPort)) {
                     _log.error("RAW SEND failed");
                     // a message send failure is no reason to drop the SAM session
                     // for raw and repliable datagrams, just carry on our merry way
@@ -796,16 +846,21 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
     }
     
     // SAMRawReceiver implementation
-    public void receiveRawBytes(byte data[]) throws IOException {
+    public void receiveRawBytes(byte data[], int proto, int fromPort, int toPort) throws IOException {
         if (getRawSession() == null) {
             _log.error("BUG! Received raw bytes, but session is null!");
             return;
         }
 
-        ByteArrayOutputStream msg = new ByteArrayOutputStream();
+        ByteArrayOutputStream msg = new ByteArrayOutputStream(64 + data.length);
 
-        String msgText = "RAW RECEIVED SIZE=" + data.length + "\n";
+        String msgText = "RAW RECEIVED SIZE=" + data.length;
         msg.write(DataHelper.getASCII(msgText));
+        if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) {
+            msgText = " PROTOCOL=" + proto + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort;
+            msg.write(DataHelper.getASCII(msgText));
+        }
+        msg.write((byte) '\n');
         msg.write(data);
         
         if (_log.shouldLog(Log.DEBUG))
@@ -832,17 +887,23 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
     }
 
     // SAMDatagramReceiver implementation
-    public void receiveDatagramBytes(Destination sender, byte data[]) throws IOException {
+    public void receiveDatagramBytes(Destination sender, byte data[], int proto,
+                                     int fromPort, int toPort) throws IOException {
         if (getDatagramSession() == null) {
             _log.error("BUG! Received datagram bytes, but session is null!");
             return;
         }
 
-        ByteArrayOutputStream msg = new ByteArrayOutputStream();
+        ByteArrayOutputStream msg = new ByteArrayOutputStream(100 + data.length);
 
         String msgText = "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64()
-                         + " SIZE=" + data.length + "\n";
+                         + " SIZE=" + data.length;
         msg.write(DataHelper.getASCII(msgText));
+        if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) {
+            msgText = " FROM_PORT=" + fromPort + " TO_PORT=" + toPort;
+            msg.write(DataHelper.getASCII(msgText));
+        }
+        msg.write((byte) '\n');
         
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("sending to client: " + msgText);
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..a482318fa66b69cea6d94e28b179747747eeb81e
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java
@@ -0,0 +1,223 @@
+package net.i2p.sam;
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by human in 2004 and released into the public domain
+ * with no warranty of any kind, either expressed or implied.
+ * It probably won't  make your computer catch on fire, or eat
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.I2PSession;
+import net.i2p.data.DataHelper;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+
+/**
+ *  This is the thread listening on 127.0.0.1:7655 or as specified by
+ *  sam.udp.host and sam.udp.port properties.
+ *  This is used for both repliable and raw datagrams.
+ *
+ *  @since 0.9.24 moved from SAMv3Handler
+ */
+class SAMv3DatagramServer implements Handler {
+	
+	private final DatagramChannel _server;
+	private final Thread _listener;
+	private final SAMBridge _parent;
+	private final String _host;
+	private final int _port;
+	
+	/**
+	 *  Does not start listener.
+	 *  Caller must call start().
+	 *
+	 *  @param parent may be null
+	 *  @param props ignored for now
+	 */
+	public SAMv3DatagramServer(SAMBridge parent, String host, int port, Properties props) throws IOException {
+		_parent = parent;
+		_server = DatagramChannel.open();
+		
+		_server.socket().bind(new InetSocketAddress(host, port));
+		_listener = new I2PAppThread(new Listener(_server), "SAM DatagramListener " + port);
+		_host = host;
+		_port = port;
+	}
+	
+	/**
+	 *  Only call once.
+	 *  @since 0.9.22
+	 */
+	public synchronized void start() {
+		_listener.start();
+		if (_parent != null)
+			_parent.register(this);
+	}
+	
+	/**
+	 *  Cannot be restarted.
+	 *  @since 0.9.22
+	 */
+	public synchronized void stopHandling() {
+		try {
+			_server.close();
+		} catch (IOException ioe) {}
+		_listener.interrupt();
+		if (_parent != null)
+			_parent.unregister(this);
+	}
+	
+	public void send(SocketAddress addr, ByteBuffer msg) throws IOException {
+		_server.send(msg, addr);
+	}
+
+	/** @since 0.9.24 */
+	public String getHost() { return _host; }
+
+	/** @since 0.9.24 */
+	public int getPort() { return _port; }
+
+	private static class Listener implements Runnable {
+		
+		private final DatagramChannel server;
+		
+		public Listener(DatagramChannel server)
+		{
+			this.server = server ;
+		}
+		public void run()
+		{
+			ByteBuffer inBuf = ByteBuffer.allocateDirect(SAMRawSession.RAW_SIZE_MAX+1024);
+			
+			while (!Thread.interrupted())
+			{
+				inBuf.clear();
+				try {
+					server.receive(inBuf);
+				} catch (IOException e) {
+					break ;
+				}
+				inBuf.flip();
+				ByteBuffer outBuf = ByteBuffer.wrap(new byte[inBuf.remaining()]);
+				outBuf.put(inBuf);
+				outBuf.flip();
+				// A new thread for every message is wildly inefficient...
+				//new I2PAppThread(new MessageDispatcher(outBuf.array()), "MessageDispatcher").start();
+				// inline
+				// Even though we could be sending messages through multiple sessions,
+				// that isn't a common use case, and blocking should be rare.
+				// Inside router context, I2CP drops on overflow.
+				(new MessageDispatcher(outBuf.array())).run();
+			}
+		}
+	}
+
+	private static class MessageDispatcher implements Runnable {
+		private final ByteArrayInputStream is;
+	
+		public MessageDispatcher(byte[] buf) {
+			this.is = new ByteArrayInputStream(buf) ;
+		}
+	
+		public void run() {
+			try {
+				String header = DataHelper.readLine(is).trim();
+				StringTokenizer tok = new StringTokenizer(header, " ");
+				if (tok.countTokens() < 3) {
+					// This is not a correct message, for sure
+					warn("Bad datagram header received");
+					return;
+				}
+				String version = tok.nextToken();
+				if (!version.startsWith("3.")) {
+					warn("Bad datagram header received");
+					return;
+				}
+				String nick = tok.nextToken();
+				String dest = tok.nextToken();
+
+				SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
+				if (rec!=null) {
+					Properties sprops = rec.getProps();
+					String pr = sprops.getProperty("PROTOCOL");
+					String fp = sprops.getProperty("FROM_PORT");
+					String tp = sprops.getProperty("TO_PORT");
+					while (tok.hasMoreTokens()) {
+						String t = tok.nextToken();
+						if (t.startsWith("PROTOCOL="))
+							pr = t.substring("PROTOCOL=".length());
+						else if (t.startsWith("FROM_PORT="))
+							fp = t.substring("FROM_PORT=".length());
+						else if (t.startsWith("TO_PORT="))
+							tp = t.substring("TO_PORT=".length());
+					}
+
+					int proto = I2PSession.PROTO_UNSPECIFIED;
+					int fromPort = I2PSession.PORT_UNSPECIFIED;
+					int toPort = I2PSession.PORT_UNSPECIFIED;
+					if (pr != null) {
+						try {
+							proto = Integer.parseInt(pr);
+						} catch (NumberFormatException nfe) {
+							warn("Bad datagram header received");
+							return;
+						}
+					}
+					if (fp != null) {
+						try {
+							fromPort = Integer.parseInt(fp);
+						} catch (NumberFormatException nfe) {
+							warn("Bad datagram header received");
+							return;
+						}
+					}
+					if (tp != null) {
+						try {
+							toPort = Integer.parseInt(tp);
+						} catch (NumberFormatException nfe) {
+							warn("Bad datagram header received");
+							return;
+						}
+					}
+					// TODO too many allocations and copies. One here and one in Listener above.
+					byte[] data = new byte[is.available()];
+					is.read(data);
+					SAMv3Handler.Session sess = rec.getHandler().getSession();
+					if (sess != null)
+						sess.sendBytes(dest, data, proto, fromPort, toPort);
+					else
+						warn("Dropping datagram, no session for " + nick);
+				} else {
+					warn("Dropping datagram, no session for " + nick);
+				}
+			} catch (Exception e) {
+				warn("Error handling datagram", e);
+			}
+		}
+
+		/** @since 0.9.22 */
+		private static void warn(String s) {
+			warn(s, null);
+		}
+
+		/** @since 0.9.22 */
+		private static void warn(String s, Throwable t) {
+			Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3DatagramServer.class);
+			if (log.shouldLog(Log.WARN))
+				log.warn(s, t);
+		}
+	}
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java
index a16c92327fe23cc2f055d850a3ab48cd08a546a9..ee2782559266ceb1bb820069d796155a1fd02666 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java
@@ -21,7 +21,7 @@ import java.nio.ByteBuffer;
 class SAMv3DatagramSession extends SAMDatagramSession implements SAMv3Handler.Session, SAMDatagramReceiver {
 	
 	private final SAMv3Handler handler;
-	private final SAMv3Handler.DatagramServer server;
+	private final SAMv3DatagramServer server;
 	private final String nick;
 	private final SocketAddress clientAddress;
 	
@@ -30,52 +30,58 @@ class SAMv3DatagramSession extends SAMDatagramSession implements SAMv3Handler.Se
 	/**
 	 *   build a DatagramSession according to informations registered
 	 *   with the given nickname
+	 *
 	 * @param nick nickname of the session
 	 * @throws IOException
 	 * @throws DataFormatException
 	 * @throws I2PSessionException
 	 */
-	public SAMv3DatagramSession(String nick) 
-	throws IOException, DataFormatException, I2PSessionException, SAMException {
-		
+	public SAMv3DatagramSession(String nick, SAMv3DatagramServer dgServer) 
+			throws IOException, DataFormatException, I2PSessionException, SAMException {
 		super(SAMv3Handler.sSessionsHash.get(nick).getDest(),
 				SAMv3Handler.sSessionsHash.get(nick).getProps(),
 				null  // to be replaced by this
 				);
-		this.nick = nick ;
-		this.recv = this ;  // replacement
-		this.server = SAMv3Handler.DatagramServer.getInstance() ;
+		this.nick = nick;
+		this.recv = this;  // replacement
+		this.server = dgServer;
 
 		SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
-        if ( rec==null ) throw new SAMException("Record disappeared for nickname : \""+nick+"\"") ;
+		if (rec == null)
+			throw new SAMException("Record disappeared for nickname : \""+nick+"\"");
 
-        this.handler = rec.getHandler();
+		this.handler = rec.getHandler();
 		
-        Properties props = rec.getProps();
-    	String portStr = props.getProperty("PORT") ;
-    	if ( portStr==null ) {
-    		_log.debug("receiver port not specified. Current socket will be used.");
-    		this.clientAddress = null;
-    	}
-    	else {
-    		int port = Integer.parseInt(portStr);
-    	
-    		String host = props.getProperty("HOST");
-    		if ( host==null ) {    		
-    			host = rec.getHandler().getClientIP();
-    			_log.debug("no host specified. Taken from the client socket : " + host+':'+port);
-    		}
-
-    	
-    		this.clientAddress = new InetSocketAddress(host,port);
-    	}
+		Properties props = rec.getProps();
+		String portStr = props.getProperty("PORT");
+		if (portStr == null) {
+			if (_log.shouldDebug())
+				_log.debug("receiver port not specified. Current socket will be used.");
+			this.clientAddress = null;
+		} else {
+			int port = Integer.parseInt(portStr);
+			String host = props.getProperty("HOST");
+			if (host == null) {    		
+				host = rec.getHandler().getClientIP();
+				if (_log.shouldDebug())
+					_log.debug("no host specified. Taken from the client socket : " + host+':'+port);
+			}
+			this.clientAddress = new InetSocketAddress(host, port);
+		}
 	}
 
-	public void receiveDatagramBytes(Destination sender, byte[] data) throws IOException {
+	public void receiveDatagramBytes(Destination sender, byte[] data, int proto,
+	                                 int fromPort, int toPort) throws IOException {
 		if (this.clientAddress==null) {
-			this.handler.receiveDatagramBytes(sender, data);
+			this.handler.receiveDatagramBytes(sender, data, proto, fromPort, toPort);
 		} else {
-			String msg = sender.toBase64()+"\n";
+			StringBuilder buf = new StringBuilder(600);
+			buf.append(sender.toBase64());
+			if ((handler.verMajor == 3 && handler.verMinor >= 2) || handler.verMajor > 3) {
+				buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort);
+			}
+			buf.append('\n');
+			String msg = buf.toString();
 			ByteBuffer msgBuf = ByteBuffer.allocate(msg.length()+data.length);
 			msgBuf.put(DataHelper.getASCII(msg));
 			msgBuf.put(data);
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java
index 5e75dcf661d485c1fa1b94e8a54683b122dfa849..9cc4a500c752a710125ec09d1db5d3ab601e3fed 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java
@@ -10,15 +10,16 @@ package net.i2p.sam;
 
 
 import java.io.ByteArrayOutputStream;
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.net.ConnectException;
 import java.net.InetSocketAddress;
+import java.net.Socket;
 import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
 import java.net.NoRouteToHostException;
-import java.nio.channels.DatagramChannel;
 import java.nio.channels.SocketChannel;
 import java.nio.ByteBuffer;
 import java.util.Properties;
@@ -28,6 +29,7 @@ import java.util.StringTokenizer;
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
 import net.i2p.client.I2PClient;
+import net.i2p.client.I2PSession;
 import net.i2p.client.I2PSessionException;
 import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
@@ -36,6 +38,7 @@ import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.util.Log;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.PasswordManager;
 
 /**
  * Class able to handle a SAM version 3 client connection.
@@ -50,12 +53,15 @@ class SAMv3Handler extends SAMv1Handler
 	public static final SessionsDB sSessionsHash = new SessionsDB();
 	private volatile boolean stolenSocket;
 	private volatile boolean streamForwardingSocket;
-
+	private final boolean sendPorts;
+	private long _lastPing;
+	private static final int READ_TIMEOUT = 3*60*1000;
 	
 	interface Session {
 		String getNick();
 		void close();
-		boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException;
+		boolean sendBytes(String dest, byte[] data, int proto,
+		                  int fromPort, int toPort) throws DataFormatException, I2PSessionException;
 	}
 	
 	/**
@@ -88,6 +94,7 @@ class SAMv3Handler extends SAMv1Handler
 	                    Properties i2cpProps, SAMBridge parent) throws SAMException, IOException
 	{
 		super(s, verMajor, verMinor, i2cpProps, parent);
+	        sendPorts = (verMajor == 3 && verMinor >= 2) || verMajor > 3;
 		if (_log.shouldLog(Log.DEBUG))
 			_log.debug("SAM version 3 handler instantiated");
 	}
@@ -97,124 +104,6 @@ class SAMv3Handler extends SAMv1Handler
 	{
 		return (verMajor == 3);
 	}
-
-	public static class DatagramServer  {
-		
-		private static DatagramServer _instance;
-		private static DatagramChannel server;
-		
-		public static DatagramServer getInstance() throws IOException {
-			return getInstance(new Properties());
-		}
-		
-		public static DatagramServer getInstance(Properties props) throws IOException {
-			synchronized(DatagramServer.class) {
-				if (_instance==null)
-					_instance = new DatagramServer(props);
-				return _instance ;
-			}
-		}
-		
-		public DatagramServer(Properties props) throws IOException {
-			synchronized(DatagramServer.class) {
-				if (server==null)
-					server = DatagramChannel.open();
-			}
-			
-			String host = props.getProperty(SAMBridge.PROP_DATAGRAM_HOST, SAMBridge.DEFAULT_DATAGRAM_HOST);
-			String portStr = props.getProperty(SAMBridge.PROP_DATAGRAM_PORT, SAMBridge.DEFAULT_DATAGRAM_PORT);
-			int port ;
-			try {
-				port = Integer.parseInt(portStr);
-			} catch (NumberFormatException e) {
-				port = Integer.parseInt(SAMBridge.DEFAULT_DATAGRAM_PORT);
-			}
-			
-			server.socket().bind(new InetSocketAddress(host, port));
-			new I2PAppThread(new Listener(server), "DatagramListener").start();
-		}
-		
-		public void send(SocketAddress addr, ByteBuffer msg) throws IOException {
-			server.send(msg, addr);
-		}
-		
-		static class Listener implements Runnable {
-			
-			private final DatagramChannel server;
-			
-			public Listener(DatagramChannel server)
-			{
-				this.server = server ;
-			}
-			public void run()
-			{
-				ByteBuffer inBuf = ByteBuffer.allocateDirect(SAMRawSession.RAW_SIZE_MAX+1024);
-				
-				while (!Thread.interrupted())
-				{
-					inBuf.clear();
-					try {
-						server.receive(inBuf);
-					} catch (IOException e) {
-						break ;
-					}
-					inBuf.flip();
-					ByteBuffer outBuf = ByteBuffer.wrap(new byte[inBuf.remaining()]);
-					outBuf.put(inBuf);
-					outBuf.flip();
-					// A new thread for every message is wildly inefficient...
-					//new I2PAppThread(new MessageDispatcher(outBuf.array()), "MessageDispatcher").start();
-					// inline
-					// Even though we could be sending messages through multiple sessions,
-					// that isn't a common use case, and blocking should be rare.
-					// Inside router context, I2CP drops on overflow.
-					(new MessageDispatcher(outBuf.array())).run();
-				}
-			}
-		}
-	}
-
-	private static class MessageDispatcher implements Runnable
-	{
-		private final ByteArrayInputStream is;
-		
-		public MessageDispatcher(byte[] buf)
-		{
-			this.is = new java.io.ByteArrayInputStream(buf) ;
-		}
-		
-		public void run() {
-			try {
-				String header = DataHelper.readLine(is).trim();
-				StringTokenizer tok = new StringTokenizer(header, " ");
-				if (tok.countTokens() != 3) {
-					// This is not a correct message, for sure
-					//_log.debug("Error in message format");
-					// FIXME log? throw?
-					return;
-				}
-				String version = tok.nextToken();
-				if (!"3.0".equals(version)) return ;
-				String nick = tok.nextToken();
-				String dest = tok.nextToken();
-
-				byte[] data = new byte[is.available()];
-				is.read(data);
-				SessionRecord rec = sSessionsHash.get(nick);
-				if (rec!=null) {
-					rec.getHandler().session.sendBytes(dest,data);
-				} else {
-					Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3Handler.class);
-					if (log.shouldLog(Log.WARN))
-						log.warn("Dropping datagram, no session for " + nick);
-				}
-			} catch (Exception e) {
-				Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3Handler.class);
-				if (log.shouldLog(Log.WARN))
-					log.warn("Error handling datagram", e);
-			}
-		}
-	}
 	
 	/**
 	 *  The values in the SessionsDB
@@ -342,6 +231,11 @@ class SAMv3Handler extends SAMv1Handler
 	public void stealSocket()
 	{
 		stolenSocket = true ;
+		if (sendPorts) {
+			try {
+			       socket.socket().setSoTimeout(0);
+			} catch (SocketException se) {}
+		}
 		this.stopHandling();
 	}
 	
@@ -353,6 +247,16 @@ class SAMv3Handler extends SAMv1Handler
 		return bridge;
 	}
 	
+	/**
+	 *  For SAMv3DatagramServer
+	 *  @return may be null
+	 *  @since 0.9.24
+	 */
+	Session getSession() {
+		return session;
+	}
+	
+	@Override
 	public void handle() {
 		String msg = null;
 		String domain = null;
@@ -366,15 +270,72 @@ class SAMv3Handler extends SAMv1Handler
 			_log.debug("SAMv3 handling started");
 
 		try {
-			InputStream in = getClientSocket().socket().getInputStream();
+			Socket socket = getClientSocket().socket();
+			InputStream in = socket.getInputStream();
 
+			StringBuilder buf = new StringBuilder(1024);
 			while (true) {
 				if (shouldStop()) {
 					if (_log.shouldLog(Log.DEBUG))
 						_log.debug("Stop request found");
 					break;
 				}
-				String line = DataHelper.readLine(in) ;
+				String line;
+				if (sendPorts) {
+					// client supports PING
+					try {
+						ReadLine.readLine(socket, buf, READ_TIMEOUT);
+						line = buf.toString();
+						buf.setLength(0);					
+					} catch (SocketTimeoutException ste) {
+						long now = System.currentTimeMillis();
+						if (buf.length() <= 0) {
+							if (_lastPing > 0) {
+								if (now - _lastPing >= READ_TIMEOUT) {
+									if (_log.shouldWarn())
+										_log.warn("Failed to respond to PING");
+									writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"PONG timeout\"\n");
+									break;
+								}
+							} else {
+								if (_log.shouldDebug())
+									_log.debug("Sendng PING " + now);
+								_lastPing = now;
+								if (!writeString("PING " + now + '\n'))
+									break;
+							}
+						} else {
+							if (_lastPing > 0) {
+								if (now - _lastPing >= 2*READ_TIMEOUT) {
+									if (_log.shouldWarn())
+										_log.warn("Failed to respond to PING");
+									writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"PONG timeout\"\n");
+									break;
+								}
+							} else if (_lastPing < 0) {
+								if (_log.shouldWarn())
+									_log.warn("2nd timeout");
+								writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"command timeout, bye\"\n");
+								break;
+							} else {
+								// don't clear buffer, don't send ping,
+								// go around again
+								_lastPing = -1;
+								if (_log.shouldWarn())
+									_log.warn("timeout after partial: " + buf);
+							}
+						}
+						if (_log.shouldDebug())
+							_log.debug("loop after timeout");
+						continue;
+					}
+				} else {
+					buf.setLength(0);					
+					if (DataHelper.readLine(in, buf))
+						line = buf.toString();
+					else
+						line = null;
+				}
 				if (line==null) {
 					if (_log.shouldLog(Log.DEBUG))
 						_log.debug("Connection closed by client (line read : null)");
@@ -394,13 +355,33 @@ class SAMv3Handler extends SAMv1Handler
 				}
 
 				tok = new StringTokenizer(msg, " ");
-				if (tok.countTokens() < 2) {
+				int count = tok.countTokens();
+				if (count <= 0) {
 					// This is not a correct message, for sure
 					if (_log.shouldLog(Log.DEBUG))
-						_log.debug("Error in message format");
-					break;
+						_log.debug("Ignoring whitespace");
+					continue;
 				}
 				domain = tok.nextToken();
+				// these may not have a second token
+				if (domain.equals("PING")) {
+					execPingMessage(tok);
+					continue;
+				} else if (domain.equals("PONG")) {
+					execPongMessage(tok);
+					continue;
+				} else if (domain.equals("QUIT") || domain.equals("STOP") ||
+				           domain.equals("EXIT")) {
+					writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n");
+					break;
+				}
+				if (count <= 1) {
+					// This is not a correct message, for sure
+					if (writeString(domain + " STATUS RESULT=I2P_ERROR MESSAGE=\"command not specified\"\n"))
+						continue;
+					else
+						break;
+				}
 				opcode = tok.nextToken();
 				if (_log.shouldLog(Log.DEBUG)) {
 					_log.debug("Parsing (domain: \"" + domain
@@ -424,6 +405,8 @@ class SAMv3Handler extends SAMv1Handler
 				} else if (domain.equals("RAW")) {
 					// TODO not yet overridden, ID is ignored, most recent RAW session is used
 					canContinue = execRawMessage(opcode, props);
+				} else if (domain.equals("AUTH")) {
+					canContinue = execAuthMessage(opcode, props);
 				} else {
 					if (_log.shouldLog(Log.DEBUG))
 						_log.debug("Unrecognized message domain: \""
@@ -434,12 +417,14 @@ class SAMv3Handler extends SAMv1Handler
 				if (!canContinue) {
 					break;
 				}
-			}
+			} // while
 		} catch (IOException e) {
 			if (_log.shouldLog(Log.DEBUG))
-				_log.debug("Caught IOException for message [" + msg + "]", e);
-		} catch (Exception e) {
-			_log.error("Unexpected exception for message [" + msg + "]", e);
+				_log.debug("Caught IOException in handler", e);
+		} catch (SAMException e) {
+			_log.error("Unexpected exception for message [" + msg + ']', e);
+		} catch (RuntimeException e) {
+			_log.error("Unexpected exception for message [" + msg + ']', e);
 		} finally {
 			if (_log.shouldLog(Log.DEBUG))
 				_log.debug("Stopping handler");
@@ -481,6 +466,8 @@ class SAMv3Handler extends SAMv1Handler
 	 */
 	@Override
 	public void stopHandling() {
+            if (_log.shouldInfo())
+                _log.info("Stopping (stolen? " + stolenSocket + "): " + this, new Exception("I did it"));
 	    synchronized (stopLock) {
 	        stopHandler = true;
 	    }
@@ -609,13 +596,13 @@ class SAMv3Handler extends SAMv1Handler
 				// Create the session
 
 				if (style.equals("RAW")) {
-					DatagramServer.getInstance(i2cpProps);
-					SAMv3RawSession v3 = newSAMRawSession(nick);
+					SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
+					SAMv3RawSession v3 = new SAMv3RawSession(nick, dgs);
                                         rawSession = v3;
 					this.session = v3;
 				} else if (style.equals("DATAGRAM")) {
-					DatagramServer.getInstance(i2cpProps);
-					SAMv3DatagramSession v3 = newSAMDatagramSession(nick);
+					SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props);
+					SAMv3DatagramSession v3 = new SAMv3DatagramSession(nick, dgs);
 					datagramSession = v3;
 					this.session = v3;
 				} else if (style.equals("STREAM")) {
@@ -669,18 +656,6 @@ class SAMv3Handler extends SAMv1Handler
 		return new SAMv3StreamSession( login ) ;
 	}
 
-	private static SAMv3RawSession newSAMRawSession(String login )
-			throws IOException, DataFormatException, SAMException, I2PSessionException
-	{
-		return new SAMv3RawSession( login ) ;
-	}
-
-	private static SAMv3DatagramSession newSAMDatagramSession(String login )
-			throws IOException, DataFormatException, SAMException, I2PSessionException
-	{
-		return new SAMv3DatagramSession( login ) ;
-	}
-
 	/* Parse and execute a STREAM message */
 	@Override
 	protected boolean execStreamMessage ( String opcode, Properties props )
@@ -757,14 +732,16 @@ class SAMv3Handler extends SAMv1Handler
 
 	@Override
 	protected boolean execStreamConnect( Properties props) {
+		// Messages are NOT sent if SILENT=true,
+		// The specs said that they were.
+	    	boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT"));
 		try {
 			if (props.isEmpty()) {
-				notifyStreamResult(true,"I2P_ERROR","No parameters specified in STREAM CONNECT message");
+				notifyStreamResult(verbose, "I2P_ERROR","No parameters specified in STREAM CONNECT message");
 				if (_log.shouldLog(Log.DEBUG))
 					_log.debug("No parameters specified in STREAM CONNECT message");
 				return false;
 			}
-			boolean verbose = props.getProperty("SILENT","false").equals("false");
 		
 			String dest = props.getProperty("DESTINATION");
 			if (dest == null) {
@@ -804,11 +781,14 @@ class SAMv3Handler extends SAMv1Handler
 		return false ;
 	}
 
-	protected boolean execStreamForwardIncoming( Properties props ) {
+	private boolean execStreamForwardIncoming( Properties props ) {
+		// Messages ARE sent if SILENT=true,
+		// which is different from CONNECT and ACCEPT.
+		// But this matched the specs.
 		try {
 			try {
 				streamForwardingSocket = true ;
-				((SAMv3StreamSession)streamSession).startForwardingIncoming(props);
+				((SAMv3StreamSession)streamSession).startForwardingIncoming(props, sendPorts);
 				notifyStreamResult( true, "OK", null );
 				return true ;
 			} catch (SAMException e) {
@@ -821,9 +801,11 @@ class SAMv3Handler extends SAMv1Handler
 		return false ;		
 	}
 
-	protected boolean execStreamAccept( Properties props )
+	private boolean execStreamAccept( Properties props )
 	{
-		boolean verbose = props.getProperty( "SILENT", "false").equals("false");
+		// Messages are NOT sent if SILENT=true,
+		// The specs said that they were.
+	    	boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT"));
 		try {
 			try {
 				notifyStreamResult(verbose, "OK", null);
@@ -858,13 +840,18 @@ class SAMv3Handler extends SAMv1Handler
 		}
 	}
 
-	public void notifyStreamIncomingConnection(Destination d) throws IOException {
+	public void notifyStreamIncomingConnection(Destination d, int fromPort, int toPort) throws IOException {
 	    if (getStreamSession() == null) {
 	        _log.error("BUG! Received stream connection, but session is null!");
 	        throw new NullPointerException("BUG! STREAM session is null!");
 	    }
-
-	    if (!writeString(d.toBase64() + "\n")) {
+	    StringBuilder buf = new StringBuilder(600);
+	    buf.append(d.toBase64());
+	    if (sendPorts) {
+		buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort);
+	    }
+	    buf.append('\n');
+	    if (!writeString(buf.toString())) {
 	        throw new IOException("Error notifying connection to SAM client");
 	    }
 	}
@@ -874,6 +861,91 @@ class SAMv3Handler extends SAMv1Handler
 	        throw new IOException("Error notifying connection to SAM client");
 	    }
 	}
+	
+	/** @since 0.9.24 */
+	public static void notifyStreamIncomingConnection(SocketChannel client, Destination d,
+	                                                  int fromPort, int toPort) throws IOException {
+	    if (!writeString(d.toBase64() + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort + '\n', client)) {
+	        throw new IOException("Error notifying connection to SAM client");
+	    }
+	}
 
+	/** @since 0.9.24 */
+	private boolean execAuthMessage(String opcode, Properties props) {
+		if (opcode.equals("ENABLE")) {
+			i2cpProps.setProperty(SAMBridge.PROP_AUTH, "true");
+		} else if (opcode.equals("DISABLE")) {
+			i2cpProps.setProperty(SAMBridge.PROP_AUTH, "false");
+		} else if (opcode.equals("ADD")) {
+			String user = props.getProperty("USER");
+			String pw = props.getProperty("PASSWORD");
+			if (user == null || pw == null)
+				return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"USER and PASSWORD required\"\n");
+			String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX;
+			if (i2cpProps.containsKey(prop))
+				return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"user " + user + " already exists\"\n");
+			PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
+			String shash = pm.createHash(pw);
+			i2cpProps.setProperty(prop, shash);
+		} else if (opcode.equals("REMOVE")) {
+			String user = props.getProperty("USER");
+			if (user == null)
+				return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"USER required\"\n");
+			String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX;
+			if (!i2cpProps.containsKey(prop))
+				return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"user " + user + " not found\"\n");
+			i2cpProps.remove(prop);
+		} else {
+			return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"Unknown AUTH command\"\n");
+		}
+		try {
+			bridge.saveConfig();
+			return writeString("AUTH STATUS RESULT=OK\n");
+		} catch (IOException ioe) {
+			return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"Config save failed: " + ioe + "\"\n");
+		}
+	}
+
+	/**
+	 * Handle a PING.
+	 * Send a PONG.
+	 * @since 0.9.24
+	 */
+	private void execPingMessage(StringTokenizer tok) {
+		StringBuilder buf = new StringBuilder();
+		buf.append("PONG");
+		while (tok.hasMoreTokens()) {
+			buf.append(' ').append(tok.nextToken());
+		}
+		buf.append('\n');
+		writeString(buf.toString());
+	}
+
+	/**
+	 * Handle a PONG.
+	 * @since 0.9.24
+	 */
+	private void execPongMessage(StringTokenizer tok) {
+		String s;
+		if (tok.hasMoreTokens()) {
+			s = tok.nextToken();
+		} else {
+			s = "";
+		}
+		if (_lastPing > 0) {
+			String expected = Long.toString(_lastPing);
+			if (expected.equals(s)) {
+				_lastPing = 0;
+				if (_log.shouldInfo())
+					_log.warn("Got expected pong: " + s);
+			} else {
+				if (_log.shouldInfo())
+					_log.warn("Got unexpected pong: " + s);
+			}
+		} else {
+			if (_log.shouldWarn())
+				_log.warn("Pong received without a ping: " + s);
+		}
+	}
 }
 
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java
index 90eae76f9315f5e71bf4e7d1574bb1b332c78b1f..b68f3a74a151f7d5126b68e3be8234fc18906002 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java
@@ -12,6 +12,7 @@ import java.util.Properties;
 
 import net.i2p.client.I2PSessionException;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
 import net.i2p.util.Log;
 
 /**
@@ -22,8 +23,9 @@ class SAMv3RawSession extends SAMRawSession  implements SAMv3Handler.Session, SA
 	
 	private final String nick;
 	private final SAMv3Handler handler;
-	private final SAMv3Handler.DatagramServer server;
+	private final SAMv3DatagramServer server;
 	private final SocketAddress clientAddress;
+	private final boolean _sendHeader;
 
 	public String getNick() { return nick; }
 
@@ -36,52 +38,57 @@ class SAMv3RawSession extends SAMRawSession  implements SAMv3Handler.Session, SA
 	 * @throws DataFormatException
 	 * @throws I2PSessionException
 	 */
-	public SAMv3RawSession(String nick) 
-	throws IOException, DataFormatException, I2PSessionException {
-		
+	public SAMv3RawSession(String nick, SAMv3DatagramServer dgServer) 
+			throws IOException, DataFormatException, I2PSessionException {
 		super(SAMv3Handler.sSessionsHash.get(nick).getDest(),
-				SAMv3Handler.sSessionsHash.get(nick).getProps(),
-				SAMv3Handler.sSessionsHash.get(nick).getHandler()  // to be replaced by this
-				);
+		      SAMv3Handler.sSessionsHash.get(nick).getProps(),
+		      SAMv3Handler.sSessionsHash.get(nick).getHandler()  // to be replaced by this
+		);
 		this.nick = nick ;
 		this.recv = this ;  // replacement
-		this.server = SAMv3Handler.DatagramServer.getInstance() ;
+		this.server = dgServer;
 
 		SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
-        if ( rec==null ) throw new InterruptedIOException() ;
-
-        this.handler = rec.getHandler();
-		
-        Properties props = rec.getProps();
-        
-        
-    	String portStr = props.getProperty("PORT") ;
-    	if ( portStr==null ) {
-		if (_log.shouldLog(Log.DEBUG))
-    			_log.debug("receiver port not specified. Current socket will be used.");
-    		this.clientAddress = null;
-    	}
-    	else {
-    		int port = Integer.parseInt(portStr);
-    	
-    		String host = props.getProperty("HOST");
-    		if ( host==null ) {
-    			host = rec.getHandler().getClientIP();
-
+		if (rec == null)
+			throw new InterruptedIOException() ;
+		this.handler = rec.getHandler();
+		Properties props = rec.getProps();
+		String portStr = props.getProperty("PORT") ;
+		if (portStr == null) {
 			if (_log.shouldLog(Log.DEBUG))
-	    			_log.debug("no host specified. Taken from the client socket : " + host +':'+port);
-    		}
-
-    	
-    		this.clientAddress = new InetSocketAddress(host,port);
-    	}
+				_log.debug("receiver port not specified. Current socket will be used.");
+			this.clientAddress = null;
+		} else {
+			int port = Integer.parseInt(portStr);
+			String host = props.getProperty("HOST");
+			if ( host==null ) {
+				host = rec.getHandler().getClientIP();
+				if (_log.shouldLog(Log.DEBUG))
+		    			_log.debug("no host specified. Taken from the client socket : " + host +':'+port);
+			}
+			this.clientAddress = new InetSocketAddress(host, port);
+		}
+		_sendHeader = ((handler.verMajor == 3 && handler.verMinor >= 2) || handler.verMajor > 3) &&
+                              Boolean.parseBoolean(props.getProperty("HEADER"));
 	}
 	
-	public void receiveRawBytes(byte[] data) throws IOException {
+	public void receiveRawBytes(byte[] data, int proto, int fromPort, int toPort) throws IOException {
 		if (this.clientAddress==null) {
-			this.handler.receiveRawBytes(data);
+			this.handler.receiveRawBytes(data, proto, fromPort, toPort);
 		} else {
-			ByteBuffer msgBuf = ByteBuffer.allocate(data.length);
+			ByteBuffer msgBuf;
+			if (_sendHeader) {
+				StringBuilder buf = new StringBuilder(64);
+				buf.append("PROTOCOL=").append(proto)
+				   .append(" FROM_PORT=").append(fromPort)
+				   .append(" TO_PORT=").append(toPort)
+				   .append('\n');
+				String msg = buf.toString();
+				msgBuf = ByteBuffer.allocate(msg.length()+data.length);
+				msgBuf.put(DataHelper.getASCII(msg));
+			} else {
+				msgBuf = ByteBuffer.allocate(data.length);
+			}
 			msgBuf.put(data);
 			msgBuf.flip();
 			this.server.send(this.clientAddress, msgBuf);
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java
index 7770d4075e251436712742e20721df201e535ad8..54f58054f6b7e74e66bafed57f05ae60fb4dbe37 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java
@@ -11,9 +11,22 @@ package net.i2p.sam;
 import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.net.ConnectException;
+import java.net.InetSocketAddress;
 import java.net.NoRouteToHostException;
+import java.net.SocketTimeoutException;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.security.GeneralSecurityException;
 import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
 
+import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
 import net.i2p.client.streaming.I2PServerSocket;
 import net.i2p.client.streaming.I2PSocket;
@@ -21,12 +34,8 @@ import net.i2p.client.streaming.I2PSocketOptions;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.I2PSSLSocketFactory;
 import net.i2p.util.Log;
-import java.nio.channels.Channels;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
 
 /**
  * SAMv3 STREAM session class.
@@ -40,7 +49,12 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 		private static final int BUFFER_SIZE = 1024 ;
 		
 		private final Object socketServerLock = new Object();
+		/** this is ONLY set for FORWARD, not for ACCEPT */
 		private I2PServerSocket socketServer;
+		/** this is the count of active ACCEPT sockets */
+		private final AtomicInteger _acceptors = new AtomicInteger();
+
+		private static I2PSSLSocketFactory _sslSocketFactory;
 	
 		private final String nick ;
 		
@@ -91,12 +105,28 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	        throws I2PException, ConnectException, NoRouteToHostException, 
 	    		DataFormatException, InterruptedIOException, IOException {
 
-	    	boolean verbose = (props.getProperty("SILENT", "false").equals("false"));
+	    	boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT"));
 	        Destination d = SAMUtils.getDest(dest);
 
 	        I2PSocketOptions opts = socketMgr.buildOptions(props);
 	        if (props.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null)
 	            opts.setConnectTimeout(60 * 1000);
+	        String fromPort = props.getProperty("FROM_PORT");
+	        if (fromPort != null) {
+	            try {
+	                opts.setLocalPort(Integer.parseInt(fromPort));
+	            } catch (NumberFormatException nfe) {
+	                throw new I2PException("Bad port " + fromPort);
+	            }
+	        }
+	        String toPort = props.getProperty("TO_PORT");
+	        if (toPort != null) {
+	            try {
+	                opts.setPort(Integer.parseInt(toPort));
+	            } catch (NumberFormatException nfe) {
+	                throw new I2PException("Bad port " + toPort);
+	            }
+	        }
 
 	        if (_log.shouldLog(Log.DEBUG))
 	            _log.debug("Connecting new I2PSocket...");
@@ -129,6 +159,8 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 
 	    /**
 	     * Accept a single incoming STREAM on the socket stolen from the handler.
+	     * As of version 3.2 (0.9.24), multiple simultaneous accepts are allowed.
+	     * Accepts and forwarding may not be done at the same time.
 	     *
 	     * @param handler The handler that communicates with the requesting client
 	     * @param verbose If true, SAM will send the Base64-encoded peer Destination of an
@@ -145,30 +177,30 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	    public void accept(SAMv3Handler handler, boolean verbose) 
 	    	throws I2PException, InterruptedIOException, IOException, SAMException {
 
-	    	synchronized( this.socketServerLock )
-	    	{
-	    		if (this.socketServer!=null) {
-	                	if (_log.shouldLog(Log.DEBUG))
-	   				_log.debug("a socket server is already defined for this destination");
-	    			throw new SAMException("a socket server is already defined for this destination");
-	    		}
-	    		this.socketServer = this.socketMgr.getServerSocket();
-	    	}
-	    	
-		I2PSocket i2ps = this.socketServer.accept();
+		synchronized(this.socketServerLock) {
+			if (this.socketServer != null) {
+				if (_log.shouldWarn())
+					_log.warn("a forwarding server is already defined for this destination");
+				throw new SAMException("a forwarding server is already defined for this destination");
+			}
+		}
+
+		I2PSocket i2ps;
+		_acceptors.incrementAndGet();
+		try {
+			i2ps = socketMgr.getServerSocket().accept();
+		} finally {
+			_acceptors.decrementAndGet();
+		}
 
-	    	synchronized( this.socketServerLock )
-	    	{
-	    		this.socketServer = null ;
-	    	}
-	    	
 	    	SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
 
 		if ( rec==null || i2ps==null ) throw new InterruptedIOException() ;
 
-		if (verbose)
-			handler.notifyStreamIncomingConnection(i2ps.getPeerDestination()) ;
-
+		if (verbose) {
+			handler.notifyStreamIncomingConnection(i2ps.getPeerDestination(),
+			                                       i2ps.getPort(), i2ps.getLocalPort());
+		}
 	        handler.stealSocket() ;
 	        ReadableByteChannel fromClient = handler.getClientSocket();
 	        ReadableByteChannel fromI2P    = Channels.newChannel(i2ps.getInputStream());
@@ -185,10 +217,14 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	    }
 
 	    
-	    public void startForwardingIncoming( Properties props ) throws SAMException, InterruptedIOException
+	    /**
+	     *  Forward sockets from I2P to the host/port provided.
+	     *  Accepts and forwarding may not be done at the same time.
+	     */
+	    public void startForwardingIncoming(Properties props, boolean sendPorts) throws SAMException, InterruptedIOException
 	    {
 	    	SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
-	    	boolean verbose = props.getProperty("SILENT", "false").equals("false");
+	    	boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT"));
 	        
 	        if ( rec==null ) throw new InterruptedIOException() ;
 	        
@@ -206,34 +242,43 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	                if (_log.shouldLog(Log.DEBUG))
 		    		_log.debug("no host specified. Taken from the client socket : " + host +':'+port);
 	    	}
-
-	    	
-	    	synchronized( this.socketServerLock )
-	    	{
-	    		if (this.socketServer!=null) {
-		                if (_log.shouldLog(Log.DEBUG))
-		    			_log.debug("a socket server is already defined for this destination");
-	    			throw new SAMException("a socket server is already defined for this destination");
-    			}
+		boolean isSSL = Boolean.parseBoolean(props.getProperty("SSL"));
+		if (_acceptors.get() > 0) {
+			if (_log.shouldWarn())
+				_log.warn("an accepting server is already defined for this destination");
+			throw new SAMException("an accepting server is already defined for this destination");
+		}
+		synchronized(this.socketServerLock) {
+			if (this.socketServer!=null) {
+				if (_log.shouldWarn())
+					_log.warn("a forwarding server is already defined for this destination");
+				throw new SAMException("a forwarding server is already defined for this destination");
+			}
 	    		this.socketServer = this.socketMgr.getServerSocket();
 	    	}
 	    	
-	    	SocketForwarder forwarder = new SocketForwarder(host, port, this, verbose);
+	    	SocketForwarder forwarder = new SocketForwarder(host, port, isSSL, this, verbose, sendPorts);
 	    	(new I2PAppThread(rec.getThreadGroup(), forwarder, "SAMV3StreamForwarder")).start();
 	    }
 	    
+	    /**
+	     *  Forward sockets from I2P to the host/port provided
+	     */
 	    private static class SocketForwarder implements Runnable
 	    {
 	    	private final String host;
 	    	private final int port;
 	    	private final SAMv3StreamSession session;
-	    	private final boolean verbose;
+	    	private final boolean isSSL, verbose, sendPorts;
 	    	
-	    	SocketForwarder(String host, int port, SAMv3StreamSession session, boolean verbose) {
+	    	SocketForwarder(String host, int port, boolean isSSL,
+		                SAMv3StreamSession session, boolean verbose, boolean sendPorts) {
 	    		this.host = host ;
 	    		this.port = port ;
 	    		this.session = session ;
 	    		this.verbose = verbose ;
+	    		this.sendPorts = sendPorts;
+			this.isSSL = isSSL;
 	    	}
 	    	
 	    	public void run()
@@ -241,32 +286,77 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	    		while (session.getSocketServer()!=null) {
 	    			
 	    			// wait and accept a connection from I2P side
-	    			I2PSocket i2ps = null ;
+	    			I2PSocket i2ps;
 	    			try {
 	    				i2ps = session.getSocketServer().accept();
-	    			} catch (Exception e) {}
-	    			
-	    			if (i2ps==null) {
-	    				continue ;
-	    			}
+	    				if (i2ps == null)
+		    				continue;
+				} catch (SocketTimeoutException ste) {
+					continue;
+				} catch (ConnectException ce) {
+					Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class);
+					if (log.shouldLog(Log.WARN))
+						log.warn("Error accepting", ce);
+					try { Thread.sleep(50); } catch (InterruptedException ie) {}
+					continue;
+				} catch (I2PException ipe) {
+					Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class);
+					if (log.shouldLog(Log.WARN))
+						log.warn("Error accepting", ipe);
+					break;
+				}
 
 	    			// open a socket towards client
-	    			java.net.InetSocketAddress addr = new java.net.InetSocketAddress(host,port);
 	    			
-	    			SocketChannel clientServerSock = null ;
+	    			SocketChannel clientServerSock;
 	    			try {
-	    				clientServerSock = SocketChannel.open(addr) ;
-	    			}
-	    			catch ( IOException e ) {
-	    				continue ;
+					if (isSSL) {
+						I2PAppContext ctx =  I2PAppContext.getGlobalContext();
+						synchronized(SAMv3StreamSession.class) {
+							if (_sslSocketFactory == null) {
+								try {
+									_sslSocketFactory = new I2PSSLSocketFactory(
+									    ctx, true, "certificates/sam");
+								} catch (GeneralSecurityException gse) {
+									Log log = ctx.logManager().getLog(SAMv3StreamSession.class);
+									log.error("SSL error", gse);
+									try {
+										i2ps.close();
+									} catch (IOException ee) {}
+									throw new RuntimeException("SSL error", gse);
+								}
+							}
+						}
+						SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(host, port);
+						I2PSSLSocketFactory.verifyHostname(ctx, sock, host);
+		    				clientServerSock = new SSLSocketChannel(sock);
+		    			} else {
+		    				InetSocketAddress addr = new InetSocketAddress(host, port);
+		    				clientServerSock = SocketChannel.open(addr) ;
+		    			}
+	    			} catch (IOException ioe) {
+					Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class);
+					if (log.shouldLog(Log.WARN))
+						log.warn("Error forwarding", ioe);
+					try {
+						i2ps.close();
+					} catch (IOException ee) {}
+					continue;
 	    			}
 
 	    			// build pipes between both sockets
 	    			try {
 					clientServerSock.socket().setKeepAlive(true);
-	    				if (this.verbose)
-	    					SAMv3Handler.notifyStreamIncomingConnection(
+	    				if (this.verbose) {
+						if (sendPorts) {
+	    					       SAMv3Handler.notifyStreamIncomingConnection(
+	    							clientServerSock, i2ps.getPeerDestination(),
+								i2ps.getPort(), i2ps.getLocalPort());
+						} else {
+	    					       SAMv3Handler.notifyStreamIncomingConnection(
 	    							clientServerSock, i2ps.getPeerDestination());
+						}
+					}
 	    				ReadableByteChannel fromClient = clientServerSock ;
 	    				ReadableByteChannel fromI2P    = Channels.newChannel(i2ps.getInputStream());
 	    				WritableByteChannel toClient   = clientServerSock ;
@@ -347,7 +437,7 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 		}
 	    }
 	    
-	    public I2PServerSocket getSocketServer()
+	    private I2PServerSocket getSocketServer()
 	    {
 	    	synchronized ( this.socketServerLock ) {
 	    		return this.socketServer ;
@@ -390,7 +480,11 @@ class SAMv3StreamSession  extends SAMStreamSession implements SAMv3Handler.Sessi
 	        socketMgr.destroySocketManager();
 	    }
 
-	    public boolean sendBytes(String s, byte[] b) throws DataFormatException
+	    /**
+	     *  Unsupported
+	     *  @throws DataFormatException always
+	     */
+	    public boolean sendBytes(String s, byte[] b, int pr, int fp, int tp) throws DataFormatException
 	    {
 	    	throw new DataFormatException(null);
 	    }
diff --git a/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java b/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..546955ceefb56375b2f49c49fe666164ba2b40e8
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java
@@ -0,0 +1,77 @@
+package net.i2p.sam;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.SocketAddress;
+/* requires Java 7 */
+import java.net.SocketOption;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.Collections;
+import java.util.Set;
+
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Simple wrapper for a SSLServerSocket.
+ * Cannot be used for asynch ops.
+ *
+ * @since 0.9.24
+ */
+class SSLServerSocketChannel extends ServerSocketChannel {
+
+    private final SSLServerSocket _socket;
+
+    public SSLServerSocketChannel(SSLServerSocket socket) {
+        super(SelectorProvider.provider());
+        _socket = socket;
+    }
+
+    //// ServerSocketChannel abstract methods
+
+    public SocketChannel accept() throws IOException {
+        return new SSLSocketChannel((SSLSocket)_socket.accept());
+    }
+
+    public ServerSocket socket() {
+        return _socket;
+    }
+
+    /** requires Java 7 */
+    public ServerSocketChannel bind(SocketAddress local, int backlog) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** requires Java 7 */
+    public <T> ServerSocketChannel setOption(SocketOption<T> name, T value) {
+        return this;
+    }
+
+    //// AbstractSelectableChannel abstract methods
+
+    public void implCloseSelectableChannel() throws IOException {
+        _socket.close();
+    }
+
+    public void implConfigureBlocking(boolean block) throws IOException {
+        if (!block)
+            throw new UnsupportedOperationException();
+    }
+
+    //// NetworkChannel interface methods
+
+    public SocketAddress getLocalAddress() {
+        return _socket.getLocalSocketAddress();
+    }
+
+    public <T> T getOption(SocketOption<T> name) {
+        return null;
+    }
+
+    public Set<SocketOption<?>> supportedOptions() {
+        return Collections.emptySet();
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java b/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b956a16e3d1381c388801e2d288e0dc2db676f6
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java
@@ -0,0 +1,140 @@
+package net.i2p.sam;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+/* requires Java 7 */
+import java.net.SocketOption;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.Collections;
+import java.util.Set;
+
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Simple wrapper for a SSLSocket.
+ * Cannot be used for asynch ops.
+ *
+ * @since 0.9.24
+ */
+class SSLSocketChannel extends SocketChannel {
+
+    private final SSLSocket _socket;
+
+    public SSLSocketChannel(SSLSocket socket) {
+        super(SelectorProvider.provider());
+        _socket = socket;
+    }
+
+    //// SocketChannel abstract methods
+
+    public Socket socket() {
+        return _socket;
+    }
+
+    public boolean connect(SocketAddress remote) {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean finishConnect() {
+        return true;
+    }
+
+    public boolean isConnected() {
+        return _socket.isConnected();
+    }
+
+    public boolean isConnectionPending() {
+        return false;
+    }
+
+    /** new in Java 7 */
+    public SocketAddress getRemoteAddress() {
+        return _socket.getRemoteSocketAddress();
+    }
+
+    /** new in Java 7 */
+    public SocketChannel shutdownInput() throws IOException {
+        _socket.getInputStream().close();
+        return this;
+    }
+
+    /** new in Java 7 */
+    public SocketChannel shutdownOutput() throws IOException {
+        _socket.getOutputStream().close();
+        return this;
+    }
+
+    /** requires Java 7 */
+    public <T> SocketChannel setOption(SocketOption<T> name, T value) {
+        return this;
+    }
+
+    /** requires Java 7 */
+    public SocketChannel bind(SocketAddress local) {
+        throw new UnsupportedOperationException();
+    }
+
+    //// SocketChannel abstract methods
+
+    public int read(ByteBuffer src) throws IOException {
+        if (!src.hasArray())
+            throw new UnsupportedOperationException();
+       int pos = src.position();
+       int len = src.remaining();
+       int read = _socket.getInputStream().read(src.array(), src.arrayOffset() + pos, len);
+       if (read > 0)
+           src.position(pos + read);
+       return read;
+    }
+
+    public long read(ByteBuffer[] srcs, int offset, int length) {
+        throw new UnsupportedOperationException();
+    }
+
+    public int write(ByteBuffer src) throws IOException {
+        if (!src.hasArray())
+            throw new UnsupportedOperationException();
+       int pos = src.position();
+       int len = src.remaining();
+       _socket.getOutputStream().write(src.array(), src.arrayOffset() + pos, len);
+       src.position(pos + len);
+       return len;
+    }
+
+    public long write(ByteBuffer[] srcs, int offset, int length) {
+        throw new UnsupportedOperationException();
+    }
+
+    //// AbstractSelectableChannel abstract methods
+
+    public void implCloseSelectableChannel() throws IOException {
+        _socket.close();
+    }
+
+    public void implConfigureBlocking(boolean block) throws IOException {
+        if (!block)
+            throw new UnsupportedOperationException();
+    }
+
+
+    //// NetworkChannel interface methods
+
+    public SocketAddress getLocalAddress() {
+        return _socket.getLocalSocketAddress();
+    }
+
+    public <T> T getOption(SocketOption<T> name) {
+        return null;
+    }
+
+    public Set<SocketOption<?>> supportedOptions() {
+        return Collections.emptySet();
+    }
+
+
+
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/SSLUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf4ebf1e42b3fa1b80f64295f38d4d629193266f
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SSLUtil.java
@@ -0,0 +1,188 @@
+package net.i2p.sam;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.GeneralSecurityException;
+import java.util.Properties;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLContext;
+
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.KeyStoreUtil;
+import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+
+/**
+ * Utilities for SAM SSL server sockets.
+ *
+ * @since 0.9.24 adopted from net.i2p.i2ptunnel.SSLClientUtil
+ */
+class SSLUtil {
+
+    private static final String PROP_KEYSTORE_PASSWORD = "sam.keystorePassword";
+    private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
+    private static final String PROP_KEY_PASSWORD = "sam.keyPassword";
+    private static final String PROP_KEY_ALIAS = "sam.keyAlias";
+    private static final String ASCII_KEYFILE_SUFFIX = ".local.crt";
+    private static final String PROP_KS_NAME = "sam.keystoreFile";
+    private static final String KS_DIR = "keystore";
+    private static final String PREFIX = "sam-";
+    private static final String KS_SUFFIX = ".ks";
+    private static final String CERT_DIR = "certificates/sam";
+
+    /**
+     *  Create a new selfsigned cert and keystore and pubkey cert if they don't exist.
+     *  May take a while.
+     *
+     *  @param opts in/out, updated if rv is true
+     *  @return false if it already exists; if true, caller must save opts
+     *  @throws IOException on creation fail
+     */
+    public static boolean verifyKeyStore(Properties opts) throws IOException {
+        String name = opts.getProperty(PROP_KEY_ALIAS);
+        if (name == null) {
+            name = KeyStoreUtil.randomString();
+            opts.setProperty(PROP_KEY_ALIAS, name);
+        }
+        String ksname = opts.getProperty(PROP_KS_NAME);
+        if (ksname == null) {
+            ksname = PREFIX + name + KS_SUFFIX;
+            opts.setProperty(PROP_KS_NAME, ksname);
+        }
+        File ks = new File(ksname);
+        if (!ks.isAbsolute()) {
+            ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR);
+            ks = new File(ks, ksname);
+        }
+        if (ks.exists())
+            return false;
+        File dir = ks.getParentFile();
+        if (!dir.exists()) {
+            File sdir = new SecureDirectory(dir.getAbsolutePath());
+            if (!sdir.mkdirs())
+                throw new IOException("Unable to create keystore " + ks);
+        }
+        boolean rv = createKeyStore(ks, name, opts);
+        if (!rv)
+            throw new IOException("Unable to create keystore " + ks);
+
+        // Now read it back out of the new keystore and save it in ascii form
+        // where the clients can get to it.
+        // Failure of this part is not fatal.
+        exportCert(ks, name, opts);
+        return true;
+    }
+
+
+    /**
+     *  Call out to keytool to create a new keystore with a keypair in it.
+     *
+     *  @param name used in CNAME
+     *  @param opts in/out, updated if rv is true, must contain PROP_KEY_ALIAS
+     *  @return success, if true, opts will have password properties added to be saved
+     */
+    private static boolean createKeyStore(File ks, String name, Properties opts) {
+        // make a random 48 character password (30 * 8 / 5)
+        String keyPassword = KeyStoreUtil.randomString();
+        // and one for the cname
+        String cname = name + ".sam.i2p.net";
+
+        String keyName = opts.getProperty(PROP_KEY_ALIAS);
+        boolean success = KeyStoreUtil.createKeys(ks, keyName, cname, "SAM", keyPassword);
+        if (success) {
+            success = ks.exists();
+            if (success) {
+                opts.setProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+                opts.setProperty(PROP_KEY_PASSWORD, keyPassword);
+            }
+        }
+        if (success) {
+            logAlways("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
+                           "The certificate name was generated randomly, and is not associated with your " +
+                           "IP address, host name, router identity, or destination keys.");
+        } else {
+            error("Failed to create SAM SSL keystore.\n" +
+                       "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD +
+                       " to " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath());
+        }
+        return success;
+    }
+
+    /** 
+     *  Pull the cert back OUT of the keystore and save it as ascii
+     *  so the clients can get to it.
+     *
+     *  @param name used to generate output file name
+     *  @param opts must contain PROP_KEY_ALIAS
+     */
+    private static void exportCert(File ks, String name, Properties opts) {
+        File sdir = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), CERT_DIR);
+        if (sdir.exists() || sdir.mkdirs()) {
+            String keyAlias = opts.getProperty(PROP_KEY_ALIAS);
+            String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+            File out = new File(sdir, PREFIX + name + ASCII_KEYFILE_SUFFIX);
+            boolean success = KeyStoreUtil.exportCert(ks, ksPass, keyAlias, out);
+            if (!success)
+                error("Error getting SSL cert to save as ASCII");
+        } else {
+            error("Error saving ASCII SSL keys");
+        }
+    }
+
+    /** 
+     *  Sets up the SSLContext and sets the socket factory.
+     *  No option prefix allowed.
+     *
+     * @throws IOException; GeneralSecurityExceptions are wrapped in IOE for convenience
+     * @return factory, throws on all errors
+     */
+    public static SSLServerSocketFactory initializeFactory(Properties opts) throws IOException {
+        String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+        String keyPass = opts.getProperty(PROP_KEY_PASSWORD);
+        if (keyPass == null) {
+            throw new IOException("No key password, set " + PROP_KEY_PASSWORD + " in " +
+                       (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath());
+        }
+        String ksname = opts.getProperty(PROP_KS_NAME);
+        if (ksname == null) {
+            throw new IOException("No keystore, set " + PROP_KS_NAME + " in " +
+                       (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath());
+        }
+        File ks = new File(ksname);
+        if (!ks.isAbsolute()) {
+            ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR);
+            ks = new File(ks, ksname);
+        }
+
+        InputStream fis = null;
+        try {
+            SSLContext sslc = SSLContext.getInstance("TLS");
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            fis = new FileInputStream(ks);
+            keyStore.load(fis, ksPass.toCharArray());
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(keyStore, keyPass.toCharArray());
+            sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random());
+            return sslc.getServerSocketFactory();
+        } catch (GeneralSecurityException gse) {
+            IOException ioe = new IOException("keystore error");
+            ioe.initCause(gse);
+            throw ioe;
+        } finally {
+            if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    private static void error(String s) {
+        I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).error(s);
+    }
+
+    private static void logAlways(String s) {
+        I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).logAlways(Log.INFO, s);
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java b/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java
index 5388f5dc2336f831aca586628ca1a6105553bcd2..1b7cdc99adffc516863998a11943d2bfc297d59c 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java
@@ -7,12 +7,16 @@ import java.util.Properties;
  */
 public class SAMClientEventListenerImpl implements SAMReader.SAMClientEventListener {
     public void destReplyReceived(String publicKey, String privateKey) {}
-    public void helloReplyReceived(boolean ok) {}
+    public void helloReplyReceived(boolean ok, String version) {}
     public void namingReplyReceived(String name, String result, String value, String message) {}
     public void sessionStatusReceived(String result, String destination, String message) {}
-    public void streamClosedReceived(String result, int id, String message) {}
-    public void streamConnectedReceived(String remoteDestination, int id) {}
-    public void streamDataReceived(int id, byte[] data, int offset, int length) {}
-    public void streamStatusReceived(String result, int id, String message) {}
+    public void streamClosedReceived(String result, String id, String message) {}
+    public void streamConnectedReceived(String remoteDestination, String id) {}
+    public void streamDataReceived(String id, byte[] data, int offset, int length) {}
+    public void streamStatusReceived(String result, String id, String message) {}
+    public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort) {}
+    public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol) {}
+    public void pingReceived(String data) {}
+    public void pongReceived(String data) {}
     public void unknownMessageReceived(String major, String minor, Properties params) {}
 }
diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java b/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java
index 2d1a5b63b90a770b227e132a5e6c9cb2968e21e1..dc90d3fb2423dd31688e7e7cbcef8a0411158617 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java
@@ -13,31 +13,35 @@ import net.i2p.util.Log;
  */
 public class SAMEventHandler extends SAMClientEventListenerImpl {
     //private I2PAppContext _context;
-    private Log _log;
+    private final Log _log;
     private Boolean _helloOk;
-    private Object _helloLock = new Object();
+    private String _version;
+    private final Object _helloLock = new Object();
     private Boolean _sessionCreateOk;
-    private Object _sessionCreateLock = new Object();
-    private Object _namingReplyLock = new Object();
-    private Map<String,String> _namingReplies = new HashMap<String,String>();
+    private Boolean _streamStatusOk;
+    private final Object _sessionCreateLock = new Object();
+    private final Object _namingReplyLock = new Object();
+    private final Object _streamStatusLock = new Object();
+    private final Map<String,String> _namingReplies = new HashMap<String,String>();
 
     public SAMEventHandler(I2PAppContext ctx) {
         //_context = ctx;
         _log = ctx.logManager().getLog(getClass());
     }
     
-	@Override
-    public void helloReplyReceived(boolean ok) {
+    @Override
+    public void helloReplyReceived(boolean ok, String version) {
         synchronized (_helloLock) {
             if (ok)
                 _helloOk = Boolean.TRUE;
             else
                 _helloOk = Boolean.FALSE;
+            _version = version;
             _helloLock.notifyAll();
         }
     }
 
-	@Override
+    @Override
     public void sessionStatusReceived(String result, String destination, String msg) {
         synchronized (_sessionCreateLock) {
             if (SAMReader.SAMClientEventListener.SESSION_STATUS_OK.equals(result))
@@ -48,7 +52,7 @@ public class SAMEventHandler extends SAMClientEventListenerImpl {
         }
     }
 
-	@Override
+    @Override
     public void namingReplyReceived(String name, String result, String value, String msg) {
         synchronized (_namingReplyLock) {
             if (SAMReader.SAMClientEventListener.NAMING_REPLY_OK.equals(result)) 
@@ -59,9 +63,20 @@ public class SAMEventHandler extends SAMClientEventListenerImpl {
         }
     }
 
-	@Override
+    @Override
+    public void streamStatusReceived(String result, String id, String message) {
+        synchronized (_streamStatusLock) {
+            if (SAMReader.SAMClientEventListener.SESSION_STATUS_OK.equals(result))
+                _streamStatusOk = Boolean.TRUE;
+            else 
+                _streamStatusOk = Boolean.FALSE;
+            _streamStatusLock.notifyAll();
+        }
+    }
+
+    @Override
     public void unknownMessageReceived(String major, String minor, Properties params) {
-        _log.error("wrt, [" + major + "] [" + minor + "] [" + params + "]");
+        _log.error("Unhandled message: [" + major + "] [" + minor + "] [" + params + "]");
     }
 
     
@@ -70,20 +85,20 @@ public class SAMEventHandler extends SAMClientEventListenerImpl {
     //
 
     /**
-     * Wait for the connection to be established, returning true if everything 
+     * Wait for the connection to be established, returning the server version if everything 
      * went ok
-     * @return true if everything ok
+     * @return SAM server version if everything ok, or null on failure
      */
-    public boolean waitForHelloReply() {
+    public String waitForHelloReply() {
         while (true) {
             try {
                 synchronized (_helloLock) {
                     if (_helloOk == null)
                         _helloLock.wait();
                     else 
-                        return _helloOk.booleanValue();
+                        return _helloOk.booleanValue() ? _version : null;
                 }
-            } catch (InterruptedException ie) {}
+            } catch (InterruptedException ie) { return null; }
         }
     }
 
@@ -101,7 +116,25 @@ public class SAMEventHandler extends SAMClientEventListenerImpl {
                     else
                         return _sessionCreateOk.booleanValue();
                 }
-            } catch (InterruptedException ie) {}
+            } catch (InterruptedException ie) { return false; }
+        }
+    }
+
+    /**
+     * Wait for the stream to be created, returning true if everything went ok
+     *
+     * @return true if everything ok
+     */
+    public boolean waitForStreamStatusReply() {
+        while (true) {
+            try {
+                synchronized (_streamStatusLock) {
+                    if (_streamStatusOk == null)
+                        _streamStatusLock.wait();
+                    else
+                        return _streamStatusOk.booleanValue();
+                }
+            } catch (InterruptedException ie) { return false; }
         }
     }
     
@@ -128,7 +161,7 @@ public class SAMEventHandler extends SAMClientEventListenerImpl {
                             return val;
                     }
                 }
-            } catch (InterruptedException ie) {}
+            } catch (InterruptedException ie) { return null; }
         }
     }
 }
diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMReader.java b/apps/sam/java/src/net/i2p/sam/client/SAMReader.java
index f39394901754eae7c1dbbb8cb6453a8722b84866..c82dff7219ea995ff26686cf284adce8d84d11d1 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMReader.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMReader.java
@@ -16,10 +16,11 @@ import net.i2p.util.Log;
  *
  */
 public class SAMReader {
-    private Log _log;
-    private InputStream _inRaw;
-    private SAMClientEventListener _listener;
-    private boolean _live;
+    private final Log _log;
+    private final InputStream _inRaw;
+    private final SAMClientEventListener _listener;
+    private volatile boolean _live;
+    private Thread _thread;
     
     public SAMReader(I2PAppContext context, InputStream samIn, SAMClientEventListener listener) {
         _log = context.logManager().getLog(SAMReader.class);
@@ -27,12 +28,23 @@ public class SAMReader {
         _listener = listener;
     }
     
-    public void startReading() {
+    public synchronized void startReading() {
+        if (_live)
+            throw new IllegalStateException();
         _live = true;
         I2PAppThread t = new I2PAppThread(new Runner(), "SAM reader");
         t.start();
+        _thread = t;
+    }
+
+    public synchronized void stopReading() {
+        _live = false;
+        if (_thread != null) {
+            _thread.interrupt();
+            _thread = null;
+            try { _inRaw.close(); } catch (IOException ioe) {}
+        }
     }
-    public void stopReading() { _live = false; }
     
     /**
      * Async event notification interface for SAM clients
@@ -60,14 +72,18 @@ public class SAMReader {
         public static final String NAMING_REPLY_INVALID_KEY = "INVALID_KEY";
         public static final String NAMING_REPLY_KEY_NOT_FOUND = "KEY_NOT_FOUND";
         
-        public void helloReplyReceived(boolean ok);
+        public void helloReplyReceived(boolean ok, String version);
         public void sessionStatusReceived(String result, String destination, String message);
-        public void streamStatusReceived(String result, int id, String message);
-        public void streamConnectedReceived(String remoteDestination, int id);
-        public void streamClosedReceived(String result, int id, String message);
-        public void streamDataReceived(int id, byte data[], int offset, int length);
+        public void streamStatusReceived(String result, String id, String message);
+        public void streamConnectedReceived(String remoteDestination, String id);
+        public void streamClosedReceived(String result, String id, String message);
+        public void streamDataReceived(String id, byte data[], int offset, int length);
         public void namingReplyReceived(String name, String result, String value, String message);
         public void destReplyReceived(String publicKey, String privateKey);
+        public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort);
+        public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol);
+        public void pingReceived(String data);
+        public void pongReceived(String data);
         
         public void unknownMessageReceived(String major, String minor, Properties params);
     }
@@ -87,33 +103,29 @@ public class SAMReader {
                         baos.write(c);
                     }
                     if (c == -1) {
-                        _log.error("Error reading from the SAM bridge");
-                        return;
+                        _log.info("EOF reading from the SAM bridge");
+                        break;
                     }
                 } catch (IOException ioe) {
                     _log.error("Error reading from SAM", ioe);
+                    break;
                 }
                 
                 String line = DataHelper.getUTF8(baos.toByteArray());
                 baos.reset();
                 
-                if (line == null) {
-                    _log.info("No more data from the SAM bridge");
-                    break;
-                }
-                
-                _log.debug("Line read from the bridge: " + line);
+                if (_log.shouldDebug())
+                    _log.debug("Line read from the bridge: " + line);
                 
                 StringTokenizer tok = new StringTokenizer(line);
                 
-                if (tok.countTokens() < 2) {
+                if (tok.countTokens() <= 0) {
                     _log.error("Invalid SAM line: [" + line + "]");
-                    _live = false;
-                    return;
+                    break;
                 }
                 
                 String major = tok.nextToken();
-                String minor = tok.nextToken();
+                String minor = tok.hasMoreTokens() ? tok.nextToken() : "";
                 
                 params.clear();
                 while (tok.hasMoreTokens()) {
@@ -132,6 +144,9 @@ public class SAMReader {
                 
                 processEvent(major, minor, params);
             }
+            _live = false;
+            if (_log.shouldWarn())
+                _log.warn("SAMReader exiting");
         }
     }
     
@@ -144,10 +159,11 @@ public class SAMReader {
         if ("HELLO".equals(major)) {
             if ("REPLY".equals(minor)) {
                 String result = params.getProperty("RESULT");
-                if ("OK".equals(result))
-                    _listener.helloReplyReceived(true);
+                String version= params.getProperty("VERSION");
+                if ("OK".equals(result) && version != null)
+                    _listener.helloReplyReceived(true, version);
                 else
-                    _listener.helloReplyReceived(false);
+                    _listener.helloReplyReceived(false, version);
             } else {
                 _listener.unknownMessageReceived(major, minor, params);
             }
@@ -165,24 +181,17 @@ public class SAMReader {
                 String result = params.getProperty("RESULT");
                 String id = params.getProperty("ID");
                 String msg = params.getProperty("MESSAGE");
-                if (id != null) {
-                    try {
-                        _listener.streamStatusReceived(result, Integer.parseInt(id), msg);
-                    } catch (NumberFormatException nfe) {
-                        _listener.unknownMessageReceived(major, minor, params);
-                    }
-                } else {
-                    _listener.unknownMessageReceived(major, minor, params);
-                }
+                // id is null in v3, so pass it through regardless
+                //if (id != null) {
+                    _listener.streamStatusReceived(result, id, msg);
+                //} else {
+                //    _listener.unknownMessageReceived(major, minor, params);
+                //}
             } else if ("CONNECTED".equals(minor)) {
                 String dest = params.getProperty("DESTINATION");
                 String id = params.getProperty("ID");
                 if (id != null) {
-                    try {
-                        _listener.streamConnectedReceived(dest, Integer.parseInt(id));
-                    } catch (NumberFormatException nfe) {
-                        _listener.unknownMessageReceived(major, minor, params);
-                    }
+                    _listener.streamConnectedReceived(dest, id);
                 } else {
                     _listener.unknownMessageReceived(major, minor, params);
                 }
@@ -191,11 +200,7 @@ public class SAMReader {
                 String id = params.getProperty("ID");
                 String msg = params.getProperty("MESSAGE");
                 if (id != null) {
-                    try {
-                        _listener.streamClosedReceived(result, Integer.parseInt(id), msg);
-                    } catch (NumberFormatException nfe) {
-                        _listener.unknownMessageReceived(major, minor, params);
-                    }
+                    _listener.streamClosedReceived(result, id, msg);
                 } else {
                     _listener.unknownMessageReceived(major, minor, params);
                 }
@@ -204,7 +209,6 @@ public class SAMReader {
                 String size = params.getProperty("SIZE");
                 if (id != null) {
                     try {
-                        int idVal = Integer.parseInt(id);
                         int sizeVal = Integer.parseInt(size);
                         
                         byte data[] = new byte[sizeVal];
@@ -212,7 +216,7 @@ public class SAMReader {
                         if (read != sizeVal) {
                             _listener.unknownMessageReceived(major, minor, params);
                         } else {
-                            _listener.streamDataReceived(idVal, data, 0, sizeVal);
+                            _listener.streamDataReceived(id, data, 0, sizeVal);
                         }
                     } catch (NumberFormatException nfe) {
                         _listener.unknownMessageReceived(major, minor, params);
@@ -226,6 +230,73 @@ public class SAMReader {
             } else {
                 _listener.unknownMessageReceived(major, minor, params);
             }
+        } else if ("DATAGRAM".equals(major)) {
+            if ("RECEIVED".equals(minor)) {
+                String dest = params.getProperty("DESTINATION");
+                String size = params.getProperty("SIZE");
+                String fp = params.getProperty("FROM_PORT");
+                String tp = params.getProperty("TO_PORT");
+                int fromPort = 0;
+                int toPort = 0;
+                if (dest != null) {
+                    try {
+                      if (fp != null)
+                          fromPort = Integer.parseInt(fp);
+                      if (tp != null)
+                          toPort = Integer.parseInt(tp);
+                        int sizeVal = Integer.parseInt(size);
+                        byte data[] = new byte[sizeVal];
+                        int read = DataHelper.read(_inRaw, data);
+                        if (read != sizeVal) {
+                            _listener.unknownMessageReceived(major, minor, params);
+                        } else {
+                            _listener.datagramReceived(dest, data, 0, sizeVal, fromPort, toPort);
+                        }
+                    } catch (NumberFormatException nfe) {
+                        _listener.unknownMessageReceived(major, minor, params);
+                    } catch (IOException ioe) {
+                        _live = false;
+                        _listener.unknownMessageReceived(major, minor, params);
+                    }
+                } else {
+                    _listener.unknownMessageReceived(major, minor, params);
+                }
+            } else {
+                _listener.unknownMessageReceived(major, minor, params);
+            }
+        } else if ("RAW".equals(major)) {
+            if ("RECEIVED".equals(minor)) {
+                String size = params.getProperty("SIZE");
+                String fp = params.getProperty("FROM_PORT");
+                String tp = params.getProperty("TO_PORT");
+                String pr = params.getProperty("PROTOCOL");
+                int fromPort = 0;
+                int toPort = 0;
+                int protocol = 18;
+                try {
+                    if (fp != null)
+                        fromPort = Integer.parseInt(fp);
+                    if (tp != null)
+                        toPort = Integer.parseInt(tp);
+                    if (pr != null)
+                        protocol = Integer.parseInt(pr);
+                    int sizeVal = Integer.parseInt(size);
+                    byte data[] = new byte[sizeVal];
+                    int read = DataHelper.read(_inRaw, data);
+                    if (read != sizeVal) {
+                        _listener.unknownMessageReceived(major, minor, params);
+                    } else {
+                        _listener.rawReceived(data, 0, sizeVal, fromPort, toPort, protocol);
+                    }
+                } catch (NumberFormatException nfe) {
+                    _listener.unknownMessageReceived(major, minor, params);
+                } catch (IOException ioe) {
+                    _live = false;
+                    _listener.unknownMessageReceived(major, minor, params);
+                }
+            } else {
+                _listener.unknownMessageReceived(major, minor, params);
+            }
         } else if ("NAMING".equals(major)) {
             if ("REPLY".equals(minor)) {
                 String name = params.getProperty("NAME");
@@ -244,6 +315,12 @@ public class SAMReader {
             } else {
                 _listener.unknownMessageReceived(major, minor, params);
             }
+        } else if ("PING".equals(major)) {
+            // this omits anything after a space
+            _listener.pingReceived(minor);
+        } else if ("PONG".equals(major)) {
+            // this omits anything after a space
+            _listener.pongReceived(minor);
         } else {
             _listener.unknownMessageReceived(major, minor, params);
         }
diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java
index 2ce8b3b02f81f86ee9a48c9f5dd2dbf290559a0c..d8f7432086fcfd53dbce375a8322f4640cd411df 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java
@@ -1,50 +1,141 @@
 package net.i2p.sam.client;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.security.GeneralSecurityException;
 import java.util.HashMap;
 import java.util.Map;
+import javax.net.ssl.SSLSocket;
+
+import gnu.getopt.Getopt;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
 import net.i2p.data.DataHelper;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.I2PSSLSocketFactory;
 import net.i2p.util.Log;
+import net.i2p.util.VersionComparator;
 
 /**
- * Send a file to a peer
+ * Swiss army knife tester.
+ * Sends a file (datafile) to a peer (b64 dest in peerDestFile).
+ *
+ * Usage: SAMStreamSend [options] peerDestFile dataFile
  *
- * Usage: SAMStreamSend samHost samPort peerDestFile dataFile
+ * See apps/sam/doc/README-test.txt for info on test setup.
+ * Sends data in one of 5 modes.
+ * Optionally uses SSL.
+ * Configurable SAM client version.
  *
  */
 public class SAMStreamSend {
-    private I2PAppContext _context;
-    private Log _log;
-    private String _samHost;
-    private String _samPort;
-    private String _destFile;
-    private String _dataFile;
+    private final I2PAppContext _context;
+    private final Log _log;
+    private final String _samHost;
+    private final String _samPort;
+    private final String _destFile;
+    private final String _dataFile;
     private String _conOptions;
-    private Socket _samSocket;
-    private OutputStream _samOut;
-    private InputStream _samIn;
-    private SAMReader _reader;
+    private SAMReader _reader, _reader2;
+    private boolean _isV3;
+    private boolean _isV32;
+    private String _v3ID;
     //private boolean _dead;
-    private SAMEventHandler _eventHandler;
     /** Connection id (Integer) to peer (Flooder) */
-    private Map<Integer, Sender> _remotePeers;
+    private final Map<String, Sender> _remotePeers;
+    private static I2PSSLSocketFactory _sslSocketFactory;
     
+    private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4;
+    private static final String USAGE = "Usage: SAMStreamSend [-s] [-m mode] [-v version] [-b samHost] [-p samPort] [-o opt=val] [-u user] [-w password] peerDestFile dataDir\n" +
+                                        "       modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4\n" +
+                                        "       -s: use SSL\n" +
+                                        "       multiple -o session options are allowed";
+
     public static void main(String args[]) {
-        if (args.length < 4) {
-            System.err.println("Usage: SAMStreamSend samHost samPort peerDestFile dataFile");
+        Getopt g = new Getopt("SAM", args, "sb:m:o:p:u:v:w:");
+        boolean isSSL = false;
+        int mode = STREAM;
+        String version = "1.0";
+        String host = "127.0.0.1";
+        String port = "7656";
+        String user = null;
+        String password = null;
+        String opts = "";
+        int c;
+        while ((c = g.getopt()) != -1) {
+          switch (c) {
+            case 's':
+                isSSL = true;
+                break;
+
+            case 'm':
+                mode = Integer.parseInt(g.getOptarg());
+                if (mode < 0 || mode > V1RAW) {
+                    System.err.println(USAGE);
+                    return;
+                }
+                break;
+
+            case 'v':
+                version = g.getOptarg();
+                break;
+
+            case 'b':
+                host = g.getOptarg();
+                break;
+
+            case 'o':
+                opts = opts + ' ' + g.getOptarg();
+                break;
+
+            case 'p':
+                port = g.getOptarg();
+                break;
+
+            case 'u':
+                user = g.getOptarg();
+                break;
+
+            case 'w':
+                password = g.getOptarg();
+                break;
+
+            case 'h':
+            case '?':
+            case ':':
+            default:
+                System.err.println(USAGE);
+                return;
+          }  // switch
+        } // while
+
+        int startArgs = g.getOptind();
+        if (args.length - startArgs != 2) {
+            System.err.println(USAGE);
+            return;
+        }
+        if ((user == null && password != null) ||
+            (user != null && password == null)) {
+            System.err.println("both user and password or neither");
+            return;
+        }
+        if (user != null && password != null && VersionComparator.comp(version, "3.2") < 0) {
+            System.err.println("user/password require 3.2");
             return;
         }
-        I2PAppContext ctx = new I2PAppContext();
-        //String files[] = new String[args.length - 3];
-        SAMStreamSend sender = new SAMStreamSend(ctx, args[0], args[1], args[2], args[3]);
-        sender.startup();
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        SAMStreamSend sender = new SAMStreamSend(ctx, host, port,
+                                                      args[startArgs], args[startArgs + 1]);
+        sender.startup(version, isSSL, mode, user, password, opts);
     }
     
     public SAMStreamSend(I2PAppContext ctx, String samHost, String samPort, String destFile, String dataFile) {
@@ -56,86 +147,141 @@ public class SAMStreamSend {
         _destFile = destFile;
         _dataFile = dataFile;
         _conOptions = "";
-        _eventHandler = new SendEventHandler(_context);
-        _remotePeers = new HashMap<Integer,Sender>();
+        _remotePeers = new HashMap<String, Sender>();
     }
     
-    public void startup() {
+    public void startup(String version, boolean isSSL, int mode, String user, String password, String sessionOpts) {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Starting up");
-        boolean ok = connect();
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Connected: " + ok);
-        if (ok) {
-            _reader = new SAMReader(_context, _samIn, _eventHandler);
+        try {
+            Socket sock = connect(isSSL);
+            SAMEventHandler eventHandler = new SendEventHandler(_context);
+            _reader = new SAMReader(_context, sock.getInputStream(), eventHandler);
             _reader.startReading();
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Reader created");
-            String ourDest = handshake();
+            OutputStream out = sock.getOutputStream();
+            String ourDest = handshake(out, version, true, eventHandler, mode, user, password, sessionOpts);
+            if (ourDest == null)
+                throw new IOException("handshake failed");
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Handshake complete.  we are " + ourDest);
-            if (ourDest != null) {
-                send();
+            if (_isV3 && mode == STREAM) {
+                Socket sock2 = connect(isSSL);
+                eventHandler = new SendEventHandler(_context);
+                _reader2 = new SAMReader(_context, sock2.getInputStream(), eventHandler);
+                _reader2.startReading();
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Reader2 created");
+                out = sock2.getOutputStream();
+                String ok = handshake(out, version, false, eventHandler, mode, user, password, "");
+                if (ok == null)
+                    throw new IOException("2nd handshake failed");
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Handshake2 complete.");
             }
+            if (mode == DG || mode == RAW)
+                out = null;
+            send(out, eventHandler, mode);
+        } catch (IOException e) {
+            _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
+            if (_reader != null)
+                _reader.stopReading();
+            if (_reader2 != null)
+                _reader2.stopReading();
         }
     }
     
     private class SendEventHandler extends SAMEventHandler {
         public SendEventHandler(I2PAppContext ctx) { super(ctx); }
-        public void streamClosedReceived(String result, int id, String message) {
+
+        @Override
+        public void streamClosedReceived(String result, String id, String message) {
             Sender sender = null;
             synchronized (_remotePeers) {
-                sender = _remotePeers.remove(Integer.valueOf(id));
+                sender = _remotePeers.remove(id);
             }
             if (sender != null) {
                 sender.closed();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Connection " + sender.getConnectionId() + " closed to " + sender.getDestination());
             } else {
-                _log.error("wtf, not connected to " + id + " but we were just closed?");
+                _log.error("not connected to " + id + " but we were just closed?");
             }
         }
     }
     
-    private boolean connect() {
-        try {
-            _samSocket = new Socket(_samHost, Integer.parseInt(_samPort));
-            _samOut = _samSocket.getOutputStream();
-            _samIn = _samSocket.getInputStream();
-            return true;
-        } catch (Exception e) {
-            _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
-            return false;
+    private Socket connect(boolean isSSL) throws IOException {
+        int port = Integer.parseInt(_samPort);
+        if (!isSSL)
+            return new Socket(_samHost, port);
+        synchronized(SAMStreamSink.class) {
+            if (_sslSocketFactory == null) {
+                try {
+                    _sslSocketFactory = new I2PSSLSocketFactory(
+                        _context, true, "certificates/sam");
+                } catch (GeneralSecurityException gse) {
+                    throw new IOException("SSL error", gse);
+                }
+            }
         }
+        SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(_samHost, port);
+        I2PSSLSocketFactory.verifyHostname(_context, sock, _samHost);
+        return sock;
     }
     
-    private String handshake() {
-        synchronized (_samOut) {
+    /** @return our b64 dest or null */
+    private String handshake(OutputStream samOut, String version, boolean isMaster,
+                             SAMEventHandler eventHandler, int mode, String user, String password,
+                             String opts) {
+        synchronized (samOut) {
             try {
-                _samOut.write("HELLO VERSION MIN=1.0 MAX=1.0\n".getBytes());
-                _samOut.flush();
+                if (user != null && password != null)
+                    samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + " USER=" + user + " PASSWORD=" + password + '\n').getBytes());
+                else
+                    samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + '\n').getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Hello sent");
-                boolean ok = _eventHandler.waitForHelloReply();
+                String hisVersion = eventHandler.waitForHelloReply();
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Hello reply found: " + ok);
-                if (!ok) 
-                    throw new IOException("wtf, hello failed?");
-                String req = "SESSION CREATE STYLE=STREAM DESTINATION=TRANSIENT " + _conOptions + "\n";
-                _samOut.write(req.getBytes());
-                _samOut.flush();
+                    _log.debug("Hello reply found: " + hisVersion);
+                if (hisVersion == null) 
+                    throw new IOException("Hello failed");
+                if (!isMaster)
+                    return "OK";
+                _isV3 = VersionComparator.comp(hisVersion, "3") >= 0;
+                if (_isV3) {
+                    _isV32 = VersionComparator.comp(hisVersion, "3.2") >= 0;
+                    byte[] id = new byte[5];
+                    _context.random().nextBytes(id);
+                    _v3ID = Base32.encode(id);
+                    _conOptions = "ID=" + _v3ID;
+                }
+                String style;
+                if (mode == STREAM)
+                    style = "STREAM";
+                else if (mode == DG || mode == V1DG)
+                    style = "DATAGRAM";
+                else
+                    style = "RAW";
+                String req = "SESSION CREATE STYLE=" + style + " DESTINATION=TRANSIENT " + _conOptions + ' ' + opts + '\n';
+                samOut.write(req.getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Session create sent");
-                ok = _eventHandler.waitForSessionCreateReply();
+                boolean ok = eventHandler.waitForSessionCreateReply();
+                if (!ok) 
+                    throw new IOException("Session create failed");
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Session create reply found: " + ok);
 
                 req = "NAMING LOOKUP NAME=ME\n";
-                _samOut.write(req.getBytes());
-                _samOut.flush();
+                samOut.write(req.getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Naming lookup sent");
-                String destination = _eventHandler.waitForNamingReply("ME");
+                String destination = eventHandler.waitForNamingReply("ME");
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Naming lookup reply found: " + destination);
                 if (destination == null) {
@@ -145,32 +291,56 @@ public class SAMStreamSend {
                     _log.info("We are " + destination);
                 }
                 return destination;
-            } catch (Exception e) {
+            } catch (IOException e) {
                 _log.error("Error handshaking", e);
                 return null;
             }
         }
     }
     
-    private void send() {
-        Sender sender = new Sender();
+    private void send(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException {
+        Sender sender = new Sender(samOut, eventHandler, mode);
         boolean ok = sender.openConnection();
         if (ok) {
             I2PAppThread t = new I2PAppThread(sender, "Sender");
             t.start();
+        } else {
+            throw new IOException("Sender failed to connect");
         }
     }
     
     private class Sender implements Runnable {
-        private int _connectionId; 
+        private final String _connectionId; 
         private String _remoteDestination;
         private InputStream _in;
-        private boolean _closed;
+        private volatile boolean _closed;
         private long _started;
         private long _totalSent;
+        private final OutputStream _samOut;
+        private final SAMEventHandler _eventHandler;
+        private final int _mode;
+        private final DatagramSocket _dgSock;
+        private final InetSocketAddress _dgSAM;
         
-        public Sender() {
-            _closed = false;
+        public Sender(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException {
+            _samOut = samOut;
+            _eventHandler = eventHandler;
+            _mode = mode;
+            if (mode == DG || mode == RAW) {
+                // samOut will be null
+                _dgSock = new DatagramSocket();
+                _dgSAM = new InetSocketAddress(_samHost, 7655);
+            } else {
+                _dgSock = null;
+                _dgSAM = null;
+            }
+            synchronized (_remotePeers) {
+                if (_v3ID != null)
+                    _connectionId = _v3ID;
+                else
+                    _connectionId = Integer.toString(_remotePeers.size() + 1);
+                _remotePeers.put(_connectionId, Sender.this);
+            }
         }
         
         public boolean openConnection() {
@@ -181,19 +351,27 @@ public class SAMStreamSend {
                 int read = DataHelper.read(fin, dest);
 
                 _remoteDestination = DataHelper.getUTF8(dest, 0, read);
-                synchronized (_remotePeers) {
-                    _connectionId = _remotePeers.size() + 1;
-                    _remotePeers.put(Integer.valueOf(_connectionId), Sender.this);
-                }
 
                 _context.statManager().createRateStat("send." + _connectionId + ".totalSent", "Data size sent", "swarm", new long[] { 30*1000, 60*1000, 5*60*1000 });
                 _context.statManager().createRateStat("send." + _connectionId + ".started", "When we start", "swarm", new long[] { 5*60*1000 });
                 _context.statManager().createRateStat("send." + _connectionId + ".lifetime", "How long we talk to a peer", "swarm", new long[] { 5*60*1000 });
                 
-                byte msg[] = ("STREAM CONNECT ID=" + _connectionId + " DESTINATION=" + _remoteDestination + "\n").getBytes();
-                synchronized (_samOut) {
-                    _samOut.write(msg);
-                    _samOut.flush();
+                if (_mode == STREAM) {
+                    StringBuilder buf = new StringBuilder(1024);
+                    buf.append("STREAM CONNECT ID=").append(_connectionId).append(" DESTINATION=").append(_remoteDestination);
+                    // not supported until 3.2 but 3.0-3.1 will ignore
+                    if (_isV3)
+                        buf.append(" FROM_PORT=1234 TO_PORT=5678");
+                    buf.append('\n');
+                    byte[] msg = DataHelper.getASCII(buf.toString());
+                    synchronized (_samOut) {
+                        _samOut.write(msg);
+                        _samOut.flush();
+                    }
+                    _log.debug("STREAM CONNECT sent, waiting for STREAM STATUS...");
+                    boolean ok = _eventHandler.waitForStreamStatusReply();
+                    if (!ok)
+                        throw new IOException("STREAM CONNECT failed");
                 }
 
                 _in = new FileInputStream(_dataFile);
@@ -210,7 +388,7 @@ public class SAMStreamSend {
             }
         }
         
-        public int getConnectionId() { return _connectionId; }
+        public String getConnectionId() { return _connectionId; }
         public String getDestination() { return _remoteDestination; }
         
         public void closed() {
@@ -224,7 +402,8 @@ public class SAMStreamSend {
         public void run() {
             _started = _context.clock().now();
             _context.statManager().addRateData("send." + _connectionId + ".started", 1, 0);
-            byte data[] = new byte[1024];
+            final long toSend = (new File(_dataFile)).length();
+            byte data[] = new byte[8192];
             long lastSend = _context.clock().now();
             while (!_closed) {
                 try {
@@ -239,11 +418,45 @@ public class SAMStreamSend {
                             _log.debug("Sending " + read + " on " + _connectionId + " after " + (now-lastSend));
                         lastSend = now;
                         
-                        byte msg[] = ("STREAM SEND ID=" + _connectionId + " SIZE=" + read + "\n").getBytes();
-                        synchronized (_samOut) {
-                            _samOut.write(msg);
-                            _samOut.write(data, 0, read);
-                            _samOut.flush();
+                        if (_samOut != null) {
+                            synchronized (_samOut) {
+                                if (!_isV3 || _mode == V1DG || _mode == V1RAW) {
+                                    String m;
+                                    if (_mode == STREAM) {
+                                        m = "STREAM SEND ID=" + _connectionId + " SIZE=" + read + "\n";
+                                    } else if (_mode == V1DG) {
+                                        m = "DATAGRAM SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n";
+                                    } else if (_mode == V1RAW) {
+                                        m = "RAW SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n";
+                                    } else {
+                                        throw new IOException("unsupported mode " + _mode);
+                                    }
+                                    byte msg[] = DataHelper.getASCII(m);
+                                    _samOut.write(msg);
+                                }
+                                _samOut.write(data, 0, read);
+                                _samOut.flush();
+                            }
+                        } else {
+                            // real datagrams
+                            ByteArrayOutputStream baos = new ByteArrayOutputStream(read + 1024);
+                            baos.write(DataHelper.getASCII("3.0 "));
+                            baos.write(DataHelper.getASCII(_v3ID));
+                            baos.write((byte) ' ');
+                            baos.write(DataHelper.getASCII(_remoteDestination));
+                            if (_isV32) {
+                                // only set TO_PORT to test session setting of FROM_PORT
+                                if (_mode == RAW)
+                                    baos.write(DataHelper.getASCII(" PROTOCOL=123 TO_PORT=5678"));
+                                else
+                                    baos.write(DataHelper.getASCII(" TO_PORT=5678"));
+                            }
+                            baos.write((byte) '\n');
+                            baos.write(data, 0, read);
+                            byte[] pkt = baos.toByteArray();
+                            DatagramPacket p = new DatagramPacket(pkt, pkt.length, _dgSAM);
+                            _dgSock.send(p);
+                            try { Thread.sleep(25); } catch (InterruptedException ie) {}
                         }
                         
                         _totalSent += read;
@@ -251,20 +464,49 @@ public class SAMStreamSend {
                     }
                 } catch (IOException ioe) {
                     _log.error("Error sending", ioe);
+                    break;
                 }
             }
             
-            byte msg[] = ("STREAM CLOSE ID=" + _connectionId + "\n").getBytes();
-            try {
-                synchronized (_samOut) {
-                    _samOut.write(msg);
-                    _samOut.flush();
+            if (_samOut != null) {
+                if (_isV3) {
+                    try {
+                        _samOut.close();
+                    } catch (IOException ioe) {
+                        _log.info("Error closing", ioe);
+                    }
+                } else {
+                    byte msg[] = ("STREAM CLOSE ID=" + _connectionId + "\n").getBytes();
+                    try {
+                        synchronized (_samOut) {
+                            _samOut.write(msg);
+                            _samOut.flush();
+                            _samOut.close();
+                        }
+                    } catch (IOException ioe) {
+                        _log.info("Error closing", ioe);
+                    }
                 }
-            } catch (IOException ioe) {
-                _log.error("Error closing", ioe);
+            } else if (_dgSock != null) { 
+                _dgSock.close();
             }
             
             closed();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Runner exiting");
+            if (toSend != _totalSent)
+                _log.error("Only sent " + _totalSent + " of " + toSend + " bytes");
+            if (_reader2 != null)
+                _reader2.stopReading();
+            // stop the reader, since we're only doing this once for testing
+            // you wouldn't do this in a real application
+            if (_isV3) {
+                // closing the master socket too fast will kill the data socket flushing through
+                try {
+                    Thread.sleep(10000);
+                } catch (InterruptedException ie) {}
+            }
+            _reader.stopReading();
         }
     }
 }
diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java
index 666b7116d8bf4fb3a7022981c6ec1102a3579467..ef865afcc7067b7fd4fc11a7c8684bc38882846a 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java
@@ -1,184 +1,636 @@
 package net.i2p.sam.client;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.ServerSocket;
 import java.net.Socket;
+import java.security.GeneralSecurityException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLServerSocket;
+
+import gnu.getopt.Getopt;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
+import net.i2p.data.DataHelper;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.I2PSSLSocketFactory;
 import net.i2p.util.Log;
+import net.i2p.util.VersionComparator;
 
 /**
- * Sit around on a SAM destination, receiving lots of data and 
- * writing it to disk
+ * Swiss army knife tester.
+ * Saves our transient b64 destination to myKeyFile where SAMStreamSend can get it.
+ * Saves received data to a file (in sinkDir).
+ *
+ * Usage: SAMStreamSink [options] myKeyFile sinkDir
  *
- * Usage: SAMStreamSink samHost samPort myKeyFile sinkDir
+ * See apps/sam/doc/README-test.txt for info on test setup.
+ * Receives data in one of 7 modes.
+ * Optionally uses SSL.
+ * Configurable SAM client version.
  *
  */
 public class SAMStreamSink {
-    private I2PAppContext _context;
-    private Log _log;
-    private String _samHost;
-    private String _samPort;
-    private String _destFile;
-    private String _sinkDir;
+    private final I2PAppContext _context;
+    private final Log _log;
+    private final String _samHost;
+    private final String _samPort;
+    private final String _destFile;
+    private final String _sinkDir;
     private String _conOptions;
-    private Socket _samSocket;
-    private OutputStream _samOut;
-    private InputStream _samIn;
-    private SAMReader _reader;
-    //private boolean _dead;
-    private SAMEventHandler _eventHandler;
+    private SAMReader _reader, _reader2;
+    private boolean _isV3;
+    private boolean _isV32;
+    private String _v3ID;
     /** Connection id (Integer) to peer (Flooder) */
-    private Map<Integer, Sink> _remotePeers;
+    private final Map<String, Sink> _remotePeers;
+    private static I2PSSLSocketFactory _sslSocketFactory;
     
+    private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4, RAWHDR = 5, FORWARD = 6;
+    private static final String USAGE = "Usage: SAMStreamSink [-s] [-m mode] [-v version] [-b samHost] [-p samPort] [-o opt=val] [-u user] [-w password] myDestFile sinkDir\n" +
+                                        "       modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4; raw-with-headers: 5; stream-forward: 6\n" +
+                                        "       -s: use SSL\n" +
+                                        "       multiple -o session options are allowed";
+    private static final int V3FORWARDPORT=9998;
+    private static final int V3DGPORT=9999;
+
     public static void main(String args[]) {
-        if (args.length < 4) {
-            System.err.println("Usage: SAMStreamSink samHost samPort myDestFile sinkDir");
+        Getopt g = new Getopt("SAM", args, "sb:m:p:u:v:w:");
+        boolean isSSL = false;
+        int mode = STREAM;
+        String version = "1.0";
+        String host = "127.0.0.1";
+        String port = "7656";
+        String user = null;
+        String password = null;
+        String opts = "";
+        int c;
+        while ((c = g.getopt()) != -1) {
+          switch (c) {
+            case 's':
+                isSSL = true;
+                break;
+
+            case 'm':
+                mode = Integer.parseInt(g.getOptarg());
+                if (mode < 0 || mode > FORWARD) {
+                    System.err.println(USAGE);
+                    return;
+                }
+                break;
+
+            case 'v':
+                version = g.getOptarg();
+                break;
+
+            case 'b':
+                host = g.getOptarg();
+                break;
+
+            case 'o':
+                opts = opts + ' ' + g.getOptarg();
+                break;
+
+            case 'p':
+                port = g.getOptarg();
+                break;
+
+            case 'u':
+                user = g.getOptarg();
+                break;
+
+            case 'w':
+                password = g.getOptarg();
+                break;
+
+            case 'h':
+            case '?':
+            case ':':
+            default:
+                System.err.println(USAGE);
+                return;
+          }  // switch
+        } // while
+
+        int startArgs = g.getOptind();
+        if (args.length - startArgs != 2) {
+            System.err.println(USAGE);
             return;
         }
-        I2PAppContext ctx = new I2PAppContext();
-        SAMStreamSink sink = new SAMStreamSink(ctx, args[0], args[1], args[2], args[3]);
-        sink.startup();
+        if ((user == null && password != null) ||
+            (user != null && password == null)) {
+            System.err.println("both user and password or neither");
+            return;
+        }
+        if (user != null && password != null && VersionComparator.comp(version, "3.2") < 0) {
+            System.err.println("user/password require 3.2");
+            return;
+        }
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        SAMStreamSink sink = new SAMStreamSink(ctx, host, port,
+                                                    args[startArgs], args[startArgs + 1]);
+        sink.startup(version, isSSL, mode, user, password, opts);
     }
     
     public SAMStreamSink(I2PAppContext ctx, String samHost, String samPort, String destFile, String sinkDir) {
         _context = ctx;
         _log = ctx.logManager().getLog(SAMStreamSink.class);
-        //_dead = false;
         _samHost = samHost;
         _samPort = samPort;
         _destFile = destFile;
         _sinkDir = sinkDir;
         _conOptions = "";
-        _eventHandler = new SinkEventHandler(_context);
-        _remotePeers = new HashMap<Integer,Sink>();
+        _remotePeers = new HashMap<String, Sink>();
     }
     
-    public void startup() {
+    public void startup(String version, boolean isSSL, int mode, String user, String password, String sessionOpts) {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Starting up");
-        boolean ok = connect();
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Connected: " + ok);
-        if (ok) {
-            _reader = new SAMReader(_context, _samIn, _eventHandler);
+        try {
+            Socket sock = connect(isSSL);
+            OutputStream out = sock.getOutputStream();
+            SAMEventHandler eventHandler = new SinkEventHandler(_context, out);
+            _reader = new SAMReader(_context, sock.getInputStream(), eventHandler);
             _reader.startReading();
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Reader created");
-            String ourDest = handshake();
+            String ourDest = handshake(out, version, true, eventHandler, mode, user, password, sessionOpts);
+            if (ourDest == null)
+                throw new IOException("handshake failed");
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Handshake complete.  we are " + ourDest);
-            if (ourDest != null) {
-                //boolean written = 
-               	writeDest(ourDest);
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Dest written");
+            if (_isV32) {
+                _log.debug("Starting pinger");
+                Thread t = new Pinger(out);
+                t.start();
+            }
+            if (_isV3 && (mode == STREAM || mode == FORWARD)) {
+                // test multiple acceptors, only works in 3.2
+                int acceptors = (_isV32 && mode == STREAM) ? 4 : 1;
+                for (int i = 0; i < acceptors; i++) {
+                    Socket sock2 = connect(isSSL);
+                    out = sock2.getOutputStream();
+                    eventHandler = new SinkEventHandler2(_context, sock2.getInputStream(), out);
+                    _reader2 = new SAMReader(_context, sock2.getInputStream(), eventHandler);
+                    _reader2.startReading();
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Reader " + (2 + i) + " created");
+                    String ok = handshake(out, version, false, eventHandler, mode, user, password, "");
+                    if (ok == null)
+                        throw new IOException("handshake " + (2 + i) + " failed");
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Handshake " + (2 + i) + " complete.");
+                }
+                if (mode == FORWARD) {
+                    // set up a listening ServerSocket
+                    (new FwdRcvr(isSSL)).start();
+                }
+            } else if (_isV3 && (mode == DG || mode == RAW || mode == RAWHDR)) {
+                // set up a listening DatagramSocket
+                (new DGRcvr(mode)).start();
+            }
+            writeDest(ourDest);
+        } catch (IOException e) {
+            _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
+        }
+    }
+
+    private class DGRcvr extends I2PAppThread {
+        private final int _mode;
+
+        public DGRcvr(int mode) { _mode = mode; }
+
+        public void run() {
+            byte[] buf = new byte[32768];
+            try {
+                Sink sink = new Sink("FAKE", "FAKEFROM");
+                DatagramSocket dg = new DatagramSocket(V3DGPORT);
+                while (true) {
+                    DatagramPacket p = new DatagramPacket(buf, 32768);
+                    dg.receive(p);
+                    int len = p.getLength();
+                    int off = p.getOffset();
+                    byte[] data = p.getData();
+                    _log.info("Got datagram length " + len);
+                    if (_mode == DG || _mode == RAWHDR) {
+                        ByteArrayInputStream bais = new ByteArrayInputStream(data, off, len);
+                        String line = DataHelper.readLine(bais);
+                        if (line == null) {
+                            _log.error("DGRcvr no header line");
+                            continue;
+                        }
+                        if (_mode == DG && line.length() < 516) {
+                            _log.error("DGRcvr line too short: \"" + line + '\n');
+                            continue;
+                        }
+                        String[] parts = line.split(" ");
+                        int i = 0;
+                        if (_mode == DG) {
+                            String dest = parts[0];
+                            _log.info("DG is from " + dest);
+                            i++;
+                        }
+                        for ( ; i < parts.length; i++) {
+                            _log.info("Parameter: " + parts[i]);
+                        }
+                        int left = bais.available();
+                        sink.received(data, off + len - left, left);
+                    } else {
+                        sink.received(data, off, len);
+                    }
+                }
+            } catch (IOException ioe) {
+                _log.error("DGRcvr", ioe);
+            }
+        }
+    }
+
+    private class FwdRcvr extends I2PAppThread {
+        private final boolean _isSSL;
+
+        public FwdRcvr(boolean isSSL) {
+            if (isSSL)
+                throw new UnsupportedOperationException("TODO");
+            _isSSL = isSSL;
+        }
+
+        public void run() {
+            try {
+                ServerSocket ss;
+                if (_isSSL) {
+                    throw new UnsupportedOperationException("TODO");
+                } else {
+                    ss = new ServerSocket(V3FORWARDPORT);
+                }
+                while (true) {
+                    Socket s = ss.accept();
+                    Sink sink = new Sink("FAKE", "FAKEFROM");
+                    try {
+                        InputStream in = s.getInputStream();
+                        byte[] buf = new byte[32768];
+                        int len;
+                        while((len = in.read(buf)) >= 0) {
+                            sink.received(buf, 0, len);
+                        }
+                        sink.closed();
+                    } catch (IOException ioe) {
+                        _log.error("Fwdcvr", ioe);
+                    }
+                }
+            } catch (IOException ioe) {
+                _log.error("Fwdcvr", ioe);
+            }
+        }
+    }
+
+    private static class Pinger extends I2PAppThread {
+        private final OutputStream _out;
+
+        public Pinger(OutputStream out) {
+            super("SAM Sink Pinger");
+            setDaemon(true);
+            _out = out;
+        }
+
+        public void run() {
+            while (true) {
+                try {
+                    Thread.sleep(127*1000);
+                    synchronized(_out) {
+                        _out.write(DataHelper.getASCII("PING " + System.currentTimeMillis() + '\n'));
+                        _out.flush();
+                    }
+                } catch (InterruptedException ie) {
+                    break;
+                } catch (IOException ioe) {
+                    break;
+                }
             }
         }
     }
     
     private class SinkEventHandler extends SAMEventHandler {
 
-        public SinkEventHandler(I2PAppContext ctx) { super(ctx); }
+        protected final OutputStream _out;
+
+        public SinkEventHandler(I2PAppContext ctx, OutputStream out) {
+            super(ctx);
+            _out = out;
+        }
 
         @Override
-        public void streamClosedReceived(String result, int id, String message) {
-            Sink sink = null;
+        public void streamClosedReceived(String result, String id, String message) {
+            Sink sink;
             synchronized (_remotePeers) {
-                sink = _remotePeers.remove(Integer.valueOf(id));
+                sink = _remotePeers.remove(id);
             }
             if (sink != null) {
                 sink.closed();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Connection " + sink.getConnectionId() + " closed to " + sink.getDestination());
             } else {
-                _log.error("wtf, not connected to " + id + " but we were just closed?");
+                _log.error("not connected to " + id + " but we were just closed?");
             }
         }
 
         @Override
-        public void streamDataReceived(int id, byte data[], int offset, int length) {
-            Sink sink = null;
+        public void streamDataReceived(String id, byte data[], int offset, int length) {
+            Sink sink;
             synchronized (_remotePeers) {
-                sink = _remotePeers.get(Integer.valueOf(id));
+                sink = _remotePeers.get(id);
             }
             if (sink != null) {
                 sink.received(data, offset, length);
             } else {
-                _log.error("wtf, not connected to " + id + " but we received " + length + "?");
+                _log.error("not connected to " + id + " but we received " + length + "?");
             }
         }
 
         @Override
-        public void streamConnectedReceived(String dest, int id) {  
+        public void streamConnectedReceived(String dest, String id) {  
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Connection " + id + " received from " + dest);
 
             try {
                 Sink sink = new Sink(id, dest);
                 synchronized (_remotePeers) {
-                    _remotePeers.put(Integer.valueOf(id), sink);
+                    _remotePeers.put(id, sink);
                 }
             } catch (IOException ioe) {
                 _log.error("Error creating a new sink", ioe);
             }
         }
+
+        @Override
+        public void pingReceived(String data) {
+            if (_log.shouldInfo())
+                _log.info("Got PING " + data + ", sending PONG " + data);
+            synchronized (_out) {
+                try {
+                    _out.write(("PONG " + data + '\n').getBytes());
+                    _out.flush();
+                } catch (IOException ioe) {
+                    _log.error("PONG fail", ioe);
+                }
+            }
+        }
+
+        @Override
+        public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort) {
+            // just get the first
+            Sink sink;
+            synchronized (_remotePeers) {
+                if (_remotePeers.isEmpty()) {
+                    _log.error("not connected but we received datagram " + length + "?");
+                    return;
+                }
+                sink = _remotePeers.values().iterator().next();
+            }
+            sink.received(data, offset, length);
+        }
+
+        @Override
+        public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol) {
+            // just get the first
+            Sink sink;
+            synchronized (_remotePeers) {
+                if (_remotePeers.isEmpty()) {
+                    _log.error("not connected but we received raw " + length + "?");
+                    return;
+                }
+                sink = _remotePeers.values().iterator().next();
+            }
+            sink.received(data, offset, length);
+        }
+    }
+
+    private class SinkEventHandler2 extends SinkEventHandler {
+
+        private final InputStream _in;
+
+        public SinkEventHandler2(I2PAppContext ctx, InputStream in, OutputStream out) {
+            super(ctx, out);
+            _in = in;
+        }
+
+        @Override
+        public void streamStatusReceived(String result, String id, String message) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("got STREAM STATUS, result=" + result);
+            super.streamStatusReceived(result, id, message);
+            Sink sink = null;
+            try {
+                String dest = "TODO_if_not_silent";
+                sink = new Sink(_v3ID, dest);
+                synchronized (_remotePeers) {
+                    _remotePeers.put(_v3ID, sink);
+                }
+            } catch (IOException ioe) {
+                _log.error("Error creating a new sink", ioe);
+                try { _in.close(); } catch (IOException ioe2) {}
+                if (sink != null)
+                    sink.closed();
+                return;
+            }
+            // inline so the reader doesn't grab the data
+            try {
+                boolean gotDest = false;
+                byte[] dest = new byte[1024];
+                int dlen = 0;
+                byte buf[] = new byte[4096];
+                int len;
+                while((len = _in.read(buf)) >= 0) {
+                    if (!gotDest) {
+                        // eat the dest line
+                        for (int i = 0; i < len; i++) {
+                            byte b = buf[i];
+                            if (b == (byte) '\n') {
+                                gotDest = true;
+                                if (_log.shouldInfo()) {
+                                    try {
+                                        _log.info("Got incoming accept from: \"" + new String(dest, 0, dlen, "ISO-8859-1") + '"');
+                                    } catch (IOException uee) {}
+                                }
+                                // feed any remaining to the sink
+                                i++;
+                                if (i < len)
+                                    sink.received(buf, i, len - i);
+                                break;
+                            } else {
+                                if (dlen < dest.length) {
+                                    dest[dlen++] = b;
+                                } else if (dlen == dest.length) {
+                                    dlen++;
+                                    _log.error("first line overflow on accept");
+                                }
+                            }
+                        }
+                    } else {
+                        sink.received(buf, 0, len);
+                    }
+                }
+                sink.closed();
+            } catch (IOException ioe) {
+                _log.error("Error reading", ioe);
+            } finally {
+                try { _in.close(); } catch (IOException ioe) {}
+            }
+        }
     }
     
-    private boolean connect() {
-        try {
-            _samSocket = new Socket(_samHost, Integer.parseInt(_samPort));
-            _samOut = _samSocket.getOutputStream();
-            _samIn = _samSocket.getInputStream();
-            return true;
-        } catch (Exception e) {
-            _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
-            return false;
+    private Socket connect(boolean isSSL) throws IOException {
+        int port = Integer.parseInt(_samPort);
+        if (!isSSL)
+            return new Socket(_samHost, port);
+        synchronized(SAMStreamSink.class) {
+            if (_sslSocketFactory == null) {
+                try {
+                    _sslSocketFactory = new I2PSSLSocketFactory(
+                        _context, true, "certificates/sam");
+                } catch (GeneralSecurityException gse) {
+                    throw new IOException("SSL error", gse);
+                }
+            }
         }
+        SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(_samHost, port);
+        I2PSSLSocketFactory.verifyHostname(_context, sock, _samHost);
+        return sock;
     }
     
-    private String handshake() {
-        synchronized (_samOut) {
+    /** @return our b64 dest or null */
+    private String handshake(OutputStream samOut, String version, boolean isMaster,
+                             SAMEventHandler eventHandler, int mode, String user, String password,
+                             String sopts) {
+        synchronized (samOut) {
             try {
-                _samOut.write("HELLO VERSION MIN=1.0 MAX=1.0\n".getBytes());
-                _samOut.flush();
+                if (user != null && password != null)
+                    samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + " USER=" + user + " PASSWORD=" + password + '\n').getBytes());
+                else
+                    samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + '\n').getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Hello sent");
-                boolean ok = _eventHandler.waitForHelloReply();
+                String hisVersion = eventHandler.waitForHelloReply();
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Hello reply found: " + ok);
-                if (!ok) 
-                    throw new IOException("wtf, hello failed?");
-                String req = "SESSION CREATE STYLE=STREAM DESTINATION=" + _destFile + " " + _conOptions + "\n";
-                _samOut.write(req.getBytes());
-                _samOut.flush();
+                    _log.debug("Hello reply found: " + hisVersion);
+                if (hisVersion == null) 
+                    throw new IOException("Hello failed");
+                if (!isMaster) {
+                    // only for v3
+                    //String req = "STREAM ACCEPT SILENT=true ID=" + _v3ID + "\n";
+                    // TO_PORT not supported until 3.2 but 3.0-3.1 will ignore
+                    String req;
+                    if (mode == STREAM)
+                        req = "STREAM ACCEPT SILENT=false TO_PORT=5678 ID=" + _v3ID + "\n";
+                    else if (mode == FORWARD)
+                        req = "STREAM FORWARD ID=" + _v3ID + " PORT=" + V3FORWARDPORT + '\n';
+                    else
+                        throw new IllegalStateException("mode " + mode);
+                    samOut.write(req.getBytes());
+                    samOut.flush();
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("STREAM ACCEPT/FORWARD sent");
+                    if (mode == FORWARD) {
+                        // docs were wrong, we do not get a STREAM STATUS if SILENT=true for ACCEPT
+                        boolean ok = eventHandler.waitForStreamStatusReply();
+                        if (!ok) 
+                            throw new IOException("Stream status failed");
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("got STREAM STATUS, awaiting connection");
+                    }
+                    return "OK";
+                }
+                _isV3 = VersionComparator.comp(hisVersion, "3") >= 0;
+                String dest;
+                if (_isV3) {
+                    _isV32 = VersionComparator.comp(hisVersion, "3.2") >= 0;
+                    // we use the filename as the name in sam.keys
+                    // and read it in ourselves
+                    File keys = new File("sam.keys");
+                    if (keys.exists()) {
+                        Properties opts = new Properties();
+                        DataHelper.loadProps(opts, keys);
+                        String s = opts.getProperty(_destFile);
+                        if (s != null) {
+                            dest = s;
+                        } else {
+                            dest = "TRANSIENT";
+                            (new File(_destFile)).delete();
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug("Requesting new transient destination");
+                        }
+                    } else {
+                        dest = "TRANSIENT";
+                        (new File(_destFile)).delete();
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Requesting new transient destination");
+                    }
+                    if (isMaster) {
+                        byte[] id = new byte[5];
+                        _context.random().nextBytes(id);
+                        _v3ID = Base32.encode(id);
+                        _conOptions = "ID=" + _v3ID;
+                    }
+                } else {
+                    // we use the filename as the name in sam.keys
+                    // and give it to the SAM server
+                    dest = _destFile;
+                }
+                String style;
+                if (mode == STREAM || mode == FORWARD)
+                    style = "STREAM";
+                else if (mode == V1DG)
+                    style = "DATAGRAM";
+                else if (mode == DG)
+                    style = "DATAGRAM PORT=" + V3DGPORT;
+                else if (mode == V1RAW)
+                    style = "RAW";
+                else if (mode == RAW)
+                    style = "RAW PORT=" + V3DGPORT;
+                else
+                    style = "RAW HEADER=true PORT=" + V3DGPORT;
+                String req = "SESSION CREATE STYLE=" + style + " DESTINATION=" + dest + ' ' + _conOptions + ' ' + sopts + '\n';
+                samOut.write(req.getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Session create sent");
-                ok = _eventHandler.waitForSessionCreateReply();
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Session create reply found: " + ok);
-
+                if (mode == STREAM) {
+                    boolean ok = eventHandler.waitForSessionCreateReply();
+                    if (!ok) 
+                        throw new IOException("Session create failed");
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Session create reply found: " + ok);
+                }
                 req = "NAMING LOOKUP NAME=ME\n";
-                _samOut.write(req.getBytes());
-                _samOut.flush();
+                samOut.write(req.getBytes());
+                samOut.flush();
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Naming lookup sent");
-                String destination = _eventHandler.waitForNamingReply("ME");
+                String destination = eventHandler.waitForNamingReply("ME");
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Naming lookup reply found: " + destination);
                 if (destination == null) {
                     _log.error("No naming lookup reply found!");
                     return null;
-                } else {
+                }
+                if (_log.shouldInfo())
                     _log.info(_destFile + " is located at " + destination);
+                if (mode == V1DG || mode == V1RAW) {
+                    // fake it so the sink starts
+                    eventHandler.streamConnectedReceived(destination, "FAKE");
                 }
                 return destination;
-            } catch (Exception e) {
+            } catch (IOException e) {
                 _log.error("Error handshaking", e);
                 return null;
             }
@@ -186,11 +638,21 @@ public class SAMStreamSink {
     }
     
     private boolean writeDest(String dest) {
+        File f = new File(_destFile);
+/*
+        if (f.exists()) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Destination file exists, not overwriting: " + _destFile);
+            return false;
+        }
+*/
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(_destFile);
+            fos = new FileOutputStream(f);
             fos.write(dest.getBytes());
-        } catch (Exception e) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("My destination written to " + _destFile);
+        } catch (IOException e) {
             _log.error("Error writing to " + _destFile, e);
             return false;
         } finally {
@@ -200,14 +662,14 @@ public class SAMStreamSink {
     }
     
     private class Sink {
-        private int _connectionId; 
-        private String _remoteDestination;
-        private boolean _closed;
-        private long _started;
+        private final String _connectionId; 
+        private final String _remoteDestination;
+        private volatile boolean _closed;
+        private final long _started;
         private long _lastReceivedOn;
-        private OutputStream _out;
+        private final OutputStream _out;
         
-        public Sink(int conId, String remDest) throws IOException {
+        public Sink(String conId, String remDest) throws IOException {
             _connectionId = conId;
             _remoteDestination = remDest;
             _closed = false;
@@ -221,10 +683,13 @@ public class SAMStreamSink {
                 sinkDir.mkdirs();
             
             File out = File.createTempFile("sink", ".dat", sinkDir);
+            if (_log.shouldWarn())
+                _log.warn("outputting to " + out);
             _out = new FileOutputStream(out);
+            _started = _context.clock().now();
         }
         
-        public int getConnectionId() { return _connectionId; }
+        public String getConnectionId() { return _connectionId; }
         public String getDestination() { return _remoteDestination; }
         
         public void closed() {
@@ -235,7 +700,7 @@ public class SAMStreamSink {
             try { 
                 _out.close();
             } catch (IOException ioe) {
-                _log.error("Error closing", ioe);
+                _log.info("Error closing", ioe);
             }
         }
         public void received(byte data[], int offset, int len) {
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java
index b92648d0b86a6dbddd0e34db0e1de2432dcc852e..8af6de367e5663ea3cecce6887e714745ecc14d3 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java
@@ -294,9 +294,12 @@ class ConnectionHandler {
     private static class PoisonPacket extends Packet {
         public static final int POISON_MAX_DELAY_REQUEST = Packet.MAX_DELAY_REQUEST + 1;
 
-        public PoisonPacket() {
-            super(null);
-            setOptionalDelay(POISON_MAX_DELAY_REQUEST);
+        @Override
+        public int getOptionalDelay() { return POISON_MAX_DELAY_REQUEST; }
+
+        @Override
+        public String toString() {
+            return "POISON";
         }
     }
 }
diff --git a/build.properties b/build.properties
index 9e219c33359db5a3fd9f83a27177963bb41e0adc..3bd2548bf8403d248485699441ce08b0c8afd2f6 100644
--- a/build.properties
+++ b/build.properties
@@ -46,8 +46,11 @@ javac.version=1.6
 #javac.classpath=/PATH/TO/pack200.jar
 
 # Optional compiler args
+# This one is for subsystems requiring Java 6
 # This one keeps gcj a lot quieter
 #javac.compilerargs=-warn:-unchecked,raw,unused,serial
+# This one is for subsystems requiring Java 7
+#javac.compilerargs7=
 
 #
 # Note to packagers, embedders, distributors:
diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java
index 5e51dcb7dfb9bc8d95747f9525bc56d54cae7ca4..5ca812b4e77bf07efd458d8213dc2e311ecef681 100644
--- a/core/java/src/net/i2p/util/PasswordManager.java
+++ b/core/java/src/net/i2p/util/PasswordManager.java
@@ -99,6 +99,18 @@ public class PasswordManager {
         String shash = _context.getProperty(pfx + PROP_SHASH);
         if (shash == null)
             return false;
+        return checkHash(shash, pw);
+    }
+    
+    /**
+     *  Check pw against b64 salt+hash, as generated by createHash()
+     *
+     *  @param shash b64 string
+     *  @param pw plain text non-null, already trimmed
+     *  @return if pw verified
+     *  @since 0.9.24
+     */
+    public boolean checkHash(String shash, String pw) {
         byte[] shashBytes = Base64.decode(shash);
         if (shashBytes == null || shashBytes.length != SHASH_LENGTH)
             return false;
@@ -110,6 +122,23 @@ public class PasswordManager {
         return DataHelper.eq(hash, pwHash);
     }
     
+    /**
+     *  Create a salt+hash, to be saved and verified later by verifyHash().
+     *
+     *  @param pw plain text non-null, already trimmed
+     *  @return salted+hash b64 string
+     *  @since 0.9.24
+     */
+    public String createHash(String pw) {
+        byte[] salt = new byte[SALT_LENGTH];
+        _context.random().nextBytes(salt);
+        byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData();
+        byte[] shashBytes = new byte[SHASH_LENGTH];
+        System.arraycopy(salt, 0, shashBytes, 0, SALT_LENGTH);
+        System.arraycopy(pwHash, 0, shashBytes, SALT_LENGTH, SessionKey.KEYSIZE_BYTES);
+        return Base64.encode(shashBytes);
+    }
+
     /**
      *  Either plain or b64
      *
diff --git a/installer/install.xml b/installer/install.xml
index ba8a195c48b965d94663532dbc69a536cca4649a..3a54086185f35c9912430760cfb57a7369ebc650 100644
--- a/installer/install.xml
+++ b/installer/install.xml
@@ -9,7 +9,7 @@
             <author name="I2P" email="https://geti2p.net/"/>
         </authors>
         <url>https://geti2p.net/</url>
-        <javaversion>1.6</javaversion>
+        <javaversion>1.7</javaversion>
 
         <!-- use pack200 compression, saves about 33%
              see http://java.sun.com/j2se/1.5.0/docs/guide/deployment/deployment-guide/pack200.html
diff --git a/router/java/src/net/i2p/router/util/RouterPasswordManager.java b/router/java/src/net/i2p/router/util/RouterPasswordManager.java
index 97ef2468dbd815a7c70346e31f7e06d3f29406a7..3c2fb9b7d687cb5ff81c0f64d929fe7555147bbd 100644
--- a/router/java/src/net/i2p/router/util/RouterPasswordManager.java
+++ b/router/java/src/net/i2p/router/util/RouterPasswordManager.java
@@ -158,13 +158,7 @@ public class RouterPasswordManager extends PasswordManager {
         String pfx = realm;
         if (user != null && user.length() > 0)
             pfx += '.' + user;
-        byte[] salt = new byte[SALT_LENGTH];
-        _context.random().nextBytes(salt);
-        byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData();
-        byte[] shashBytes = new byte[SHASH_LENGTH];
-        System.arraycopy(salt, 0, shashBytes, 0, SALT_LENGTH);
-        System.arraycopy(pwHash, 0, shashBytes, SALT_LENGTH, SessionKey.KEYSIZE_BYTES);
-        String shash = Base64.encode(shashBytes);
+        String shash = createHash(pw);
         Map<String, String> toAdd = Collections.singletonMap(pfx + PROP_SHASH, shash);
         List<String> toDel = new ArrayList<String>(4);
         toDel.add(pfx + PROP_PW);