[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Check in some long-pending tweaks from my laptop. Impl...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv18251/lib/mixminion

Modified Files:
	BuildMessage.py ClientDirectory.py ClientMain.py Common.py 
	Config.py MMTPClient.py Packet.py ServerInfo.py test.py 
Added Files:
	NetUtils.py 
Log Message:
Check in some long-pending tweaks from my laptop.  Implements DNS, refactors.

NetUtils (New File):
- Added new module to hold general-purpose networking code.  Currently
  includes gethostname/getaddrinfo wrappers; timeout helper code; and
  IPv6-support testing.

BuildMessage:
- Finally remove the old build*Message calls and switch to the build*Packet
  messages.  The difference is that the old ones combined payload encoding
  with packet building, while the new ones take a complete (~28K) payload
  generated by encodeMessage.

ClientDirectory:
- Debug lots of new code.
- Turn timeout from global to argument
- Use new NetUtils timeout logic.

ClientDirectory,ClientMain:
- Make DROP packets work again.

Common, MMTPClient:
- Move TimeoutError into Common

Common:
- Add function to detect strings that look like hostnames
- Debug TimeoutQueue

Config:
- Add _parseHost and _parseIP6

MMTPClient:
- Handle DNS lookup for MMTPHostInfo code and (possible) IPv6 result.
- Use new NetUtils timeout code

Packet:
- Don't allow MMTPHostInfo objects when hostname is not a possible hostname
- Force hostnames to lower case

ServerInfo, ServerKeys:
- Rename Packet-Formats to Packet-Versions
- Deprecate IP, add Hostname.
- Deprecate Key-Digest.
- Add getIP, getHostname
- Make getKeyDigest compute digest of identity key; we don't need Key-Digest
  any more.
- Refactor protocol matchup code
- Add canStartAt function to tell whether we can connect directly to a given
  server.

test:
- Add test for new packet types and config types
- Change code to use new BuildMessage interfaces

DNSFarm:
- Handle panding requests correctly.
- Move getIP functions to NetUtils.

MMTPServer,ServerMain:
- Implement DNS farm hookup for MMTPHostInfo routing.

ServerConfig:
- Add hostnames to configuration.






--- NEW FILE: NetUtils.py ---
# Copyright 2002-2003 Nick Mathewson.  See LICENSE for licensing information.
# Id: ClientMain.py,v 1.89 2003/06/05 18:41:40 nickm Exp $

"""mixminion.NetUtils

   This module holds helper code for network-related operations."""

__all__ = [ ]

import errno
import select
import signal
import socket
import time
from mixminion.Common import LOG, TimeoutError

#======================================================================
PREFER_INET4 = 1
AF_INET = socket.AF_INET
try:
    AF_INET6 = socket.AF_INET6
except AttributeError:
    AF_INET6 = "<Sorry, no IP6>"

# For windows -- list of errno values that we can expect when blocking IO
# blocks on a connect.
IN_PROGRESS_ERRNOS = [ getattr(errno, ename) 
   for ename in [ "EINPROGRESS", "WSAEWOULDBLOCK"]
   if hasattr(errno,ename) ]
del ename

#======================================================================
if hasattr(socket, 'getaddrinfo'):
    def getIPs(name):
        """DOCDOC"""
        r = []
        ai = socket.getaddrinfo(name,None)
        now = time.time()
        for family, _, _, _, addr in ai:
            if family not in (AF_INET, AF_INET6):
                continue
            r.append((family, addr[0], now))
        return r
else:
    def getIPs(name):
        addr = socket.gethostbyname(name)
        return [ (AF_INET, addr, time.time()) ]
        
def getIP(name, preferIP4=PREFER_INET4):
    try:
        r = getIPs(name)
        inet4 = [ addr for addr in r if addr[0] == AF_INET ]
        inet6 = [ addr for addr in r if addr[0] == AF_INET6 ]
        if not (inet4 or inet6):
            LOG.error("getIP returned no inet addresses!")
            return ("NOENT", "No inet addresses returned", time.time())
        if inet4: best4=inet4[0]
        if inet6: best6=inet6[0]
        if preferIP4:
            res = best4 or best6
        else:
            res = best6 or best4
        protoname = (res[0] == AF_INET) and "inet" or "inet6"
        LOG.trace("Result for getIP(%r): %s:%s (%d others dropped)",
                  name,protoname,res[1],len(r)-1)
        return res
    except socket.error, e:
        LOG.trace("Result for getIP(%r): error:%r",name,e)
        if len(e.args) == 2:
            return ("NOENT", str(e[1]), time.time())
        else:
            return ("NOENT", str(e), time.time())            
#----------------------------------------------------------------------

_SOCKETS_SUPPORT_TIMEOUT = hasattr(socket.SocketType, "settimeout")

def connectWithTimeout(sock,dest,timeout=None):
    """DOCDOC; sock must be blocking."""
    if timeout is None:
        return sock.connect(dest)
    elif _SOCKETS_SUPPORT_TIMEOUT:
        t = sock.gettimeout()
        try:
            sock.settimeout(timeout)
            try:
                return sock.connect(dest)
            except socket.error, e:
                if e[0] in IN_PROGRESS_ERRNOS:
                    raise TimeoutError()
                else:
                    raise
        finally:
            sock.settimeout(t)
    else:
        sock.setblocking(0)
        try:
            try:
                sock.connect(dest)
            except socket.error, e:
                if e[0] not in IN_PROGRESS_ERRNOS:
                    raise
            fd = sock.fileno()
            try:
                _,wfds,_ = select.select([],[fd],[], timeout)
            except select.error, e:
                raise
            if not wfds:
                raise TimeoutError()
##             try:
##                 sock.connect(dest)
##             except select.error, e:
##                 if e[0] != errno.EISCONN:
##                     raise
        finally:
            sock.setblocking(1)

#----------------------------------------------------------------------

_PREV_DEFAULT_TIMEOUT = None

def setAlarmTimeout(timeout):
    if hasattr(signal, 'alarm'):
        def sigalrmHandler(sig,_): pass
        signal.signal(signal.SIGALRM, sigalrmHandler)
        signal.alarm(timeout)

def clearAlarmTimeout(timeout):
    if hasattr(signal, 'alarm'):
        signal.alarm(0)

def setGlobalTimeout(timeout,noalarm=0):
    global _PREV_DEFAULT_TIMEOUT
    assert timeout > 0
    if _SOCKETS_SUPPORT_TIMEOUT:
        _PREV_DEFAULT_TIMEOUT = socket.getdefaulttimeout()
        socket.setdefaultimeout(timeout)
    elif not noalarm:
        setAlarmTimeout(timeout)

def exceptionIsTimeout(ex):
    if isinstance(ex, socket.error):
        if ex[0] in IN_PROGRESS_ERRNOS:
            return 1
        elif ex[0] == errno.EINTR and not _SOCKETS_SUPPORT_TIMEOUT:
            return 1
    return 0

def unsetGlobalTimeout(noalarm=0):
    global _PREV_DEFAULT_TIMEOUT
    if _SOCKETS_SUPPORT_TIMEOUT:
        socket.setdefaulttimeout(_PREV_DEFAULT_TIMEOUT)
    elif hasattr(signal, 'alarm') and not noalarm:
        signal.alarm(0)

#----------------------------------------------------------------------
_PROTOCOL_SUPPORT = None

def getProtocolSupport():
    """DOCDOC"""
    global _PROTOCOL_SUPPORT
    if _PROTOCOL_SUPPORT is not None:
        return _PROTOCOL_SUPPORT

    res = [0,0]
    for pos, familyname, loopback in ((0, "AF_INET", "127.0.0.1"),
                                      (1, "AF_INET6", "::1")):
        family = getattr(socket, familyname)
        if family is None: continue
        s = None
        try:
            s = socket.socket(family, socket.SOCK_DGRAM)
            s.connect((loopback, 9)) # discard port
            res[pos] = 1 # Everything worked, so we must have IP(foo) support.
        except socket.error:
            pass
        if s is not None:
            s.close()
            
    _PROTOCOL_SUPPORT = res
    return res

Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.59
retrieving revision 1.60
diff -u -d -r1.59 -r1.60
--- BuildMessage.py	9 Oct 2003 15:26:15 -0000	1.59
+++ BuildMessage.py	19 Oct 2003 03:12:01 -0000	1.60
@@ -20,8 +20,8 @@
 if sys.version_info[:3] < (2,2,0):
     import mixminion._zlibutil as zlibutil
 
-__all__ = ['buildForwardMessage', 'buildEncryptedMessage',
-           'buildReplyMessage', 'buildReplyBlock', 'checkPathLength',
+__all__ = ['buildForwardPacket', 'buildEncryptedForwardPacket',
+           'buildReplyPacket', 'buildReplyBlock', 'checkPathLength',
            'encodeMessage', 'decodePayload' ]
 
 def encodeMessage(message, overhead, uncompressedFragmentPrefix="",
@@ -82,26 +82,13 @@
         rawFragments[i] = None
     return fragments
 
-#XXXX006 Most of the build*Message functions here should be 'build*Packet'.
-#XXXX006 All of the build*Message functions should be replaced with their
-#XXXX006 _build*Message variants.
-def buildForwardMessage(payload, exitType, exitInfo, path1, path2,
-                        paddingPRNG=None):
-    # Compress, pad, and checksum the payload.
-    if payload is not None and exitType != DROP_TYPE:
-        payloads = encodeMessage(payload, 0, "", paddingPRNG)
-        if len(payloads) != 1:
-            raise MixError("buildForwardMessage does not support fragmented payloads")
-        payload = payloads[0]
-        LOG.debug("Encoding forward message for %s-byte payload",len(payload))
-    else:
-        payload = (paddingPRNG or Crypto.getCommonPRNG()).getBytes(PAYLOAD_LEN)
-        LOG.debug("Generating DROP message with %s bytes", PAYLOAD_LEN)
-
-    return _buildForwardMessage(payload, exitType, exitInfo, path1, path2,
-                                paddingPRNG)
+def buildRandomPayload(paddingPRNG=None):
+    """DOCDOC"""
+    if not paddingPRNG:
+        paddingPRNG = Crypto.getCommonPRNG()
+    return paddingPRNG.getBytes(PAYLOAD_LEN)
 
-def _buildForwardMessage(payload, exitType, exitInfo, path1, path2,
+def buildForwardPacket(payload, exitType, exitInfo, path1, path2,
                         paddingPRNG=None):
     """Construct a forward message.
             payload: The payload to deliver.  Must be exactly 28K.  If the
@@ -138,19 +125,11 @@
     if not suppressTag:
         tag = _getRandomTag(paddingPRNG)
         exitInfo = tag + exitInfo
-    return _buildMessage(payload, exitType, exitInfo, path1, path2,
-                         paddingPRNG)
+    return _buildPacket(payload, exitType, exitInfo, path1, path2,
+                        paddingPRNG)
 
-def buildEncryptedForwardMessage(payload, exitType, exitInfo, path1, path2,
-                                 key, paddingPRNG=None, secretRNG=None):
-    payloads = encodeMessage(payload, ENC_FWD_OVERHEAD, "", paddingPRNG)
-    if len(payloads) != 1:
-        raise UIError("No support yet for fragmented encrypted messages")
-    return _buildEncryptedForwardMessage(payloads[0], exitType, exitInfo,
-                                         path1, path2, key, paddingPRNG,
-                                         secretRNG)
 
-def _buildEncryptedForwardMessage(payload, exitType, exitInfo, path1, path2,
+def buildEncryptedForwardPacket(payload, exitType, exitInfo, path1, path2,
                                  key, paddingPRNG=None, secretRNG=None):
     """Construct a forward message encrypted with the public key of a
        given user.
@@ -208,15 +187,9 @@
     assert len(payload) == 28*1024
 
     # And now, we can finally build the message.
-    return _buildMessage(payload, exitType, exitInfo, path1, path2,paddingPRNG)
-
-def buildReplyMessage(payload, path1, replyBlock, paddingPRNG=None):
-    payloads = encodeMessage(payload, 0, "", paddingPRNG)
-    if len(payloads) != 1:
-        raise UIError("No support yet for fragmented reply messages")
-    return _buildReplyMessage(payloads[0], path1, replyBlock, paddingPRNG)
+    return _buildPacket(payload, exitType, exitInfo, path1, path2,paddingPRNG)
 
-def _buildReplyMessage(payload, path1, replyBlock, paddingPRNG=None):
+def buildReplyPacket(payload, path1, replyBlock, paddingPRNG=None):
     """Build a message using a reply block.  'path1' is a sequence of
        ServerInfo for the nodes on the first leg of the path.  'payload'
        must be exactly 28K long.
@@ -237,7 +210,7 @@
                          Crypto.PAYLOAD_ENCRYPT_MODE)
     payload = Crypto.lioness_decrypt(payload, k)
 
-    return _buildMessage(payload, None, None,
+    return _buildPacket(payload, None, None,
                          path1=path1, path2=replyBlock)
 
 def _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime=0,
@@ -285,9 +258,10 @@
     header = _buildHeader(path, headerSecrets, exitType, tag+exitInfo,
                           paddingPRNG=Crypto.getCommonPRNG())
 
+    # XXXX007 switch to Host info.
     return ReplyBlock(header, expiryTime,
                       SWAP_FWD_IPV4_TYPE,
-                      path[0].getRoutingInfo().pack(), sharedKey), secrets, tag
+                      path[0].getIPV4Info().pack(), sharedKey), secrets, tag
 
 # Maybe we shouldn't even allow this to be called with userKey==None.
 def buildReplyBlock(path, exitType, exitInfo, userKey,
@@ -482,8 +456,8 @@
     return _decodeReplyPayload(payload, secrets, check=1)
 
 #----------------------------------------------------------------------
-def _buildMessage(payload, exitType, exitInfo,
-                  path1, path2, paddingPRNG=None, paranoia=0):
+def _buildPacket(payload, exitType, exitInfo,
+                path1, path2, paddingPRNG=None, paranoia=0):
     """Helper method to create a message.
 
     The following fields must be set:

Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -d -r1.6 -r1.7
--- ClientDirectory.py	17 Oct 2003 17:23:24 -0000	1.6
+++ ClientDirectory.py	19 Oct 2003 03:12:01 -0000	1.7
@@ -15,7 +15,6 @@
 import operator
 import os
 import re
-import signal
 import socket
 import stat
 import time
@@ -25,6 +24,7 @@
 import mixminion.ClientMain #XXXX
 import mixminion.Config
 import mixminion.Crypto
+import mixminion.NetUtils
 import mixminion.Packet
 import mixminion.ServerInfo
 
@@ -54,7 +54,7 @@
     # digestMap: Map of (Digest -> 'D'|'D-'|'I:filename').
     # byNickname: Map from nickname.lower() to list of (ServerInfo, source)
     #   tuples.
-    # byKeyID: Map from desc.getKeyID() to list of ServerInfo.
+    # byKeyID: Map from desc.getKeyDigest() to list of ServerInfo.
     # byCapability: Map from capability ('mbox'/'smtp'/'relay'/None) to
     #    list of (ServerInfo, source) tuples.
     # allServers: Same as byCapability[None]
@@ -101,22 +101,15 @@
             self.downloadDirectory()
         else:
             LOG.debug("Directory is up to date.")
-    def downloadDirectory(self):
+    def downloadDirectory(self, timeout=15):
         """Download a new directory from the network, validate it, and
            rescan its servers."""
-        # FFFF Make configurable
-        DIRECTORY_TIMEOUT = 15
         # Start downloading the directory.
         url = MIXMINION_DIRECTORY_URL
         LOG.info("Downloading directory from %s", url)
 
         # XXXX Refactor download logic.
-
-        if hasattr(signal, 'alarm'):
-            def sigalrmHandler(sig, _):
-                pass
-            signal.signal(signal.SIGALRM, sigalrmHandler)
-            signal.alarm(DIRECTORY_TIMEOUT)
+        if timeout: mixminion.NetUtils.setGlobalTimeout(timeout)
         try:
             try:
                 infile = urllib2.urlopen(url)
@@ -125,14 +118,13 @@
                     ("Couldn't connect to directory server: %s.\n"
                      "Try '-D no' to run without downloading a directory.")%e)
             except socket.error, e:
-                if getattr(e,"errno",-1) == errno.EINTR:
+                if mixminion.NetUtils.exceptionIsTimeout(e):
                     raise UIError("Connection to directory server timed out")
                 else:
                     raise UIError("Error connecting: %s"%e)
-                raise UIError
         finally:
-            if hasattr(signal, 'alarm'):
-                signal.alarm(0)
+            if timeout:
+                mixminion.NetUtils.unsetGlobalTimeout()
         
         # Open a temporary output file.
         if url.endswith(".gz"):
@@ -366,7 +358,7 @@
         for info, where in self.serverList:
             nn = info.getNickname().lower()
             lists = [ self.allServers, self.byNickname.setdefault(nn, []),
-                      self.byKeyID.setdefault(info.getKeyID(), []) ]
+                      self.byKeyID.setdefault(info.getKeyDigest(), []) ]
             for c in info.getCaps():
                 lists.append( self.byCapability[c] )
             for lst in lists:
@@ -658,7 +650,8 @@
             for c in relays:
                 if ((prev and c.hasSameNicknameAs(prev)) or
                     (next and c.hasSameNicknameAs(next)) or
-                    (prev and not prev.canRelayTo(c)) or 
+                    (prev and not prev.canRelayTo(c)) or
+                    ((not prev) and not c.canStartAt()) or
                     (next and not c.canRelayTo(next))):
                     continue
                 candidates.append(c)                    
@@ -698,6 +691,13 @@
         for e in p:
             e.validate(self, startAt, endAt)
 
+        #XXXX006 make sure p can never be empty!
+
+        # If there is a 1st element, make sure we can route to it.
+        fixed = p[0].getFixedServer(self, startAt, endAt)
+        if fixed and not fixed.canStartAt():
+            raise UIError("Cannot relay messages to %s"%fixed.getNickname())
+
         # When there are 2 elements in a row, make sure each can route to
         # the next.
         prevFixed = None
@@ -796,6 +796,8 @@
     def setFragmented(self, isSSFragmented, nFragments):
         self.isSSFragmented = isSSFragmented
         self.nFragments = nFragments
+    def hasPayload(self):
+        return self.exitType not in ('drop', DROP_TYPE)
     def setExitSize(self, exitSize):
         self.exitSize = exitSize
     def setHeaders(self, headers):

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.121
retrieving revision 1.122
diff -u -d -r1.121 -r1.122
--- ClientMain.py	14 Oct 2003 00:08:27 -0000	1.121
+++ ClientMain.py	19 Oct 2003 03:12:01 -0000	1.122
@@ -7,7 +7,6 @@
    """
 
 __all__ = [ 'Address', 'ClientKeyring', 'MixminionClient' ]
-            
 
 import getopt
 import os
@@ -223,7 +222,7 @@
                fails."""
         assert not (forceQueue and forceNoQueue)
 
-        allPackets = self.generateForwardPayloads(
+        allPackets = self.generateForwardPackets(
             directory, address, pathSpec, message, startAt, endAt)
 
         for routing, packets in self._sortPackets(allPackets):
@@ -273,7 +272,7 @@
 
         return block
 
-    def generateForwardPayloads(self, directory, address, pathSpec, message,
+    def generateForwardPackets(self, directory, address, pathSpec, message,
                                startAt, endAt):
         """Generate a forward message, but do not send it.  Returns a
            list of tuples of (the packet body, a ServerInfo for the
@@ -289,11 +288,15 @@
         fragmentedMessagePrefix = address.getFragmentedMessagePrefix()
         LOG.info("Generating payload(s)...")
         r = []
-        payloads = mixminion.BuildMessage.encodeMessage(message, 0,
-                            fragmentedMessagePrefix)
-        if len(payloads) > 1:
-            address.setFragmented(1,len(payloads))
+        if address.hasPayload():
+            payloads = mixminion.BuildMessage.encodeMessage(message, 0,
+                                fragmentedMessagePrefix)
+            if len(payloads) > 1:
+                address.setFragmented(1,len(payloads))
+            else:
+                address.setFragmented(0,1)
         else:
+            payloads = [ mixminion.BuildMessage.buildRandomPayload() ]
             address.setFragmented(0,1)
         routingType, routingInfo, _ = address.getRouting()
         
@@ -302,7 +305,7 @@
         for p, (path1,path2) in zip(payloads, directory.generatePaths(
             len(payloads), pathSpec, address, startAt, endAt)):
 
-            msg = mixminion.BuildMessage._buildForwardMessage(
+            msg = mixminion.BuildMessage.buildForwardPacket(
                 p, routingType, routingInfo, path1, path2,
                 self.prng)
             r.append( (msg, path1[0]) )
@@ -341,7 +344,7 @@
                                           startAt,endAt)):
                 assert path1 and not path2
                 LOG.info("Generating packet...")
-                msg = mixminion.BuildMessage.buildReplyMessage(
+                msg = mixminion.BuildMessage.buildReplyPacket(
                     payload, path1, surb, self.prng)
                 
                 surbLog.markSURBUsed(surb)
@@ -1028,11 +1031,11 @@
 
     # Read the message.
     # XXXX Clean up this ugly control structure.
-    if address and inFile is None and address.getRouting()[0] == DROP_TYPE:
+    if address and inFile is None and not address.hasPayload():
         message = None
         LOG.info("Sending dummy message")
     else:
-        if address and address.getRouting()[0] == DROP_TYPE:
+        if address and not address.hasPayload():
             raise UIError("Cannot send a message in a DROP packet")
 
         if inFile is None:
@@ -1052,7 +1055,6 @@
         message = "%s%s" % (headerStr, message)
 
         address.setExitSize(len(message))
-
 
     if parser.exitAddress.isReply:
         client.sendReplyMessage(

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.112
retrieving revision 1.113
diff -u -d -r1.112 -r1.113
--- Common.py	13 Oct 2003 17:31:22 -0000	1.112
+++ Common.py	19 Oct 2003 03:12:01 -0000	1.113
@@ -71,6 +71,10 @@
     """Exception class for MMTP protocol violations"""
     pass
 
+class TimeoutError(MixProtocolError):
+    """Exception raised for protocol timeout."""
+    pass
+
 class MixProtocolReject(MixProtocolError):
     """Exception class for server-rejected packets."""
     pass
@@ -216,6 +220,19 @@
     else:
         return "%s%s%s %s" % (punc.join(lst[0:-1]), punc, compound, lst[-1])
 
+_HOST_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
+               "abcdefghijklmnopqrstuvwxyz"+
+               "0123456789.")
+def isPlausibleHostname(s):
+    """DOCDOC"""
+    if not s:
+        return 0
+    if s.translate(_ALLCHARS, _HOST_CHARS):
+        return 0
+    if stringContains(s, "..") or s.startswith("."):
+        return 0
+    return 1
+
 #----------------------------------------------------------------------
 # Functions to generate and parse OpenPGP-style ASCII armor
 
@@ -1565,7 +1582,7 @@
             if timeout is None:
                 return MessageQueue.get(self, blocking)
 
-            # Adapted from 'Condition
+            # Adapted from 'Condition'.
             _time = time.time
             _sleep = time.sleep
             deadline = timeout+_time()
@@ -1574,7 +1591,7 @@
                 try:
                     return MessageQueue.get(self,0)
                 except QueueEmpty:
-                    remaining = endTime-_time()
+                    remaining = deadline-_time()
                     if remaining <= 0:
                         raise
                     delay = min(delay*2,remaining,0.2)

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.60
retrieving revision 1.61
diff -u -d -r1.60 -r1.61
--- Config.py	9 Oct 2003 15:26:16 -0000	1.60
+++ Config.py	19 Oct 2003 03:12:02 -0000	1.61
@@ -56,6 +56,7 @@
 import os
 import re
 import socket # for inet_aton and error
+import string # for atoi
 try:
     import pwd
 except ImportError:
@@ -213,6 +214,61 @@
 
     return i
 
+_IP6_CHARS="01233456789ABCDEFabcdef:."
+
+def _parseIP6(ip6):
+    """DOCDOC"""
+    ip = ip6.strip()
+    bad = ip6.translate(mixminion.Common._ALLCHARS, _IP6_CHARS)
+    if bad:
+        raise ConfigError("Invalid characters %r in address %r"%(bad,ip))
+    if len(ip) < 2:
+        raise ConfigError("IPv6 address %r is too short"%ip)
+        
+    items = ip.split(":")
+    if not items:
+        raise ConfigError("Empty IPv6 address")
+    if items[:2] == ["",""]:
+        del items[0]
+    if items[-2:] == ["",""]:
+        del items[-1]
+    foundNils = 0
+    foundWords = 0 # 16-bit words
+
+    for item in items:
+        if item == "":
+            foundNils += 1
+        elif '.' in item:
+            _parseIP(item)
+            if item is not items[-1]:
+                raise ConfigError("Embedded IPv4 address %r must appear at end of IPv6 address %r"%(item,ip))
+            foundWords += 2
+        else:
+            try:
+                val = string.atoi(item,16)
+            except ValueError:
+                raise ConfigError("IPv6 word %r did not parse"%item)
+            if not (0 <= val <= 0xFFFF):
+                raise ConfigError("IPv6 word %r out of range"%item)
+            foundWords += 1
+            
+    if foundNils > 1:
+        raise ConfigError("Too many ::'s in IPv6 address %r"%ip)
+    elif foundNils == 0 and foundWords < 8:
+        raise ConfigError("IPv6 address %r is too short"%ip)
+    elif foundWords > 8:
+        raise ConfigError("IPv6 address %r is too long"%ip)
+            
+    return ip
+
+
+def _parseHost(host):
+    """DOCDOC"""
+    host = host.strip()
+    if not mixminion.Common.isPlausibleHostname(host):
+        raise ConfigError("%r doesn't look like a valid hostname",host)
+    return host
+                          
 # Regular expression to match 'address sets' as used in Allow/Deny
 # configuration lines. General format is "<IP|*> ['/'MASK] [PORT['-'PORT]]"
 _address_set_re = re.compile(r'''^(\d+\.\d+\.\d+\.\d+|\*)

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.38
retrieving revision 1.39
diff -u -d -r1.38 -r1.39
--- MMTPClient.py	24 Sep 2003 01:03:14 -0000	1.38
+++ MMTPClient.py	19 Oct 2003 03:12:02 -0000	1.39
@@ -15,17 +15,13 @@
 
 __all__ = [ "BlockingClientConnection", "sendMessages" ]
 
-import errno
-import signal
 import socket
 import mixminion._minionlib as _ml
+import mixminion.NetUtils
 from mixminion.Crypto import sha1, getCommonPRNG
 from mixminion.Common import MixProtocolError, MixProtocolReject, \
-     MixProtocolBadAuth, LOG, MixError, formatBase64
-
-class TimeoutError(MixProtocolError):
-    """Exception raised for protocol timeout."""
-    pass
+     MixProtocolBadAuth, LOG, MixError, formatBase64, TimeoutError
+from mixminion.Packet import IPV4Info, MMTPHostInfo
 
 class BlockingClientConnection:
     """A BlockingClientConnection represents a MMTP connection to a single
@@ -44,9 +40,10 @@
     # PROTOCOL_VERSIONS: (static) a list of protocol versions we allow,
     #     in decreasing order of preference.
     PROTOCOL_VERSIONS = ['0.3']
-    def __init__(self, targetIP, targetPort, targetKeyID):
+    def __init__(self, targetFamily, targetAddr, targetPort, targetKeyID):
         """Open a new connection."""
-        self.targetIP = targetIP
+        self.targetFamily = targetFamily
+        self.targetAddr = targetAddr
         self.targetPort = targetPort
         if targetKeyID != '\x00' *20:
             self.targetKeyID = targetKeyID
@@ -83,50 +80,29 @@
         else:
             tp = str(type(err))
         e = MixProtocolError("%s error while %s to %s:%s: %s" %(
-                             tp, action, self.targetIP, self.targetPort, err))
+                             tp, action, self.targetAddr, self.targetPort, err))
         e.base = err
         raise e
 
     def _connect(self, connectTimeout=None):
         """Helper method; implements _connect."""
-        # FFFF There should be a way to specify timeout for communication.
-        if not hasattr(signal, 'alarm'): #WWWW
-            connectTimeout = None
-
-        def sigalarmHandler(sig, _):
-            assert sig == signal.SIGALRM
-        if connectTimeout:
-            signal.signal(signal.SIGALRM, sigalarmHandler)
-
         # Connect to the server
-        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.sock = socket.socket(self.targetFamily, socket.SOCK_STREAM)
         self.sock.setblocking(1)
-        LOG.debug("Connecting to %s:%s", self.targetIP, self.targetPort)
+        LOG.debug("Connecting to %s:%s", self.targetAddr, self.targetPort)
 
         # Do the TLS handshaking
-        if connectTimeout:
-            signal.alarm(connectTimeout)
-        try:
-            try:
-                self.sock.connect((self.targetIP,self.targetPort))
-            except socket.error, e:
-                if e[0] == errno.EINTR:
-                    raise TimeoutError("Connection timed out")
-                else:
-                    raise MixProtocolError("Error connecting: %s" % e)
-        finally:
-            if connectTimeout:
-                signal.alarm(0)
-
-        LOG.debug("Handshaking with %s:%s",self.targetIP, self.targetPort)
+        mixminion.NetUtils.connectWithTimeout(
+            self.sock, (self.targetAddr, self.targetPort), connectTimeout)
+        
+        LOG.debug("Handshaking with %s:%s",self.targetAddr, self.targetPort)
         self.tls = self.context.sock(self.sock.fileno())
         self.tls.connect()
         LOG.debug("Connected.")
-
         # Check the public key of the server to prevent man-in-the-middle
         # attacks.
         self.certCache.check(self.tls, self.targetKeyID,
-                             "%s:%s"%(self.targetIP,self.targetPort))
+                             "%s:%s"%(self.targetAddr,self.targetPort))
 
         ####
         # Protocol negotiation
@@ -207,7 +183,7 @@
     def shutdown(self):
         """Close this connection."""
         LOG.debug("Shutting down connection to %s:%s",
-                       self.targetIP, self.targetPort)
+                  self.targetAddr, self.targetPort)
         try:
             if self.tls is not None:
                 self.tls.shutdown()
@@ -221,7 +197,8 @@
     """Sends a list of messages to a server.  Raise MixProtocolError on
        failure.
 
-       routing -- an instance of mixminion.Packet.IPV4Info.
+       routing -- an instance of mixminion.Packet.IPV4Info or
+                  mixminion.Packet.MMTPHostInfo.
                   If routing.keyinfo == '\000'*20, we ignore the server's
                   keyid.
        packetList -- a list of 32KB packets and control strings.  Control
@@ -242,7 +219,17 @@
         else:
             packets.append(("MSG", p))
 
-    con = BlockingClientConnection(routing.ip,routing.port,routing.keyinfo)
+    if isinstance(routing, IPV4Info):
+        family, addr = socket.AF_INET, routing.ip
+    else:
+        assert isinstance(routing, MMTPHostInfo)
+        LOG.trace("Looking up %s...",routing.hostname)
+        family, addr, _ = mixminion.NetUtils.getIP(routing.hostname)
+        if family == "NOENT":
+            raise MixProtocolError("Couldn't resolve hostname %s: %s",
+                                   routing.hostname, addr)
+
+    con = BlockingClientConnection(family,addr,routing.port,routing.keyinfo)
     try:
         con.connect(connectTimeout=connectTimeout)
         for idx in xrange(len(packets)):

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.62
retrieving revision 1.63
diff -u -d -r1.62 -r1.63
--- Packet.py	13 Oct 2003 17:32:04 -0000	1.62
+++ Packet.py	19 Oct 2003 03:12:02 -0000	1.63
@@ -16,12 +16,12 @@
             'FRAGMENT_MESSAGEID_LEN', 'FRAGMENT_TYPE', 
             'HEADER_LEN', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
             'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE',
-            'MIN_SUBHEADER_LEN', 'Packet',
+            'MIN_SUBHEADER_LEN', 'MMTPHostInfo', 'Packet',
             'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
             'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
             'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_IPV4_TYPE', 'SingletonPayload',
             'Subheader', 'TAG_LEN', 'TextEncodedMessage',
-            'parseHeader', 'parseIPV4Info',
+            'parseHeader', 'parseIPV4Info', 'parseMMTPHostInfo',
             'parseMBOXInfo', 'parsePacket', 'parseMessageAndHeaders',
             'parsePayload', 'parseReplyBlock',
             'parseReplyBlocks', 'parseSMTPInfo', 'parseSubheader',
@@ -37,7 +37,8 @@
 import zlib
 from socket import inet_ntoa, inet_aton
 from mixminion.Common import MixError, MixFatalError, encodeBase64, \
-     floorDiv, formatTime, isSMTPMailbox, LOG, armorText, unarmorText
+     floorDiv, formatTime, isSMTPMailbox, LOG, armorText, unarmorText, \
+     isPlausibleHostname
 from mixminion.Crypto import sha1
 
 if sys.version_info[:3] < (2,2,0):
@@ -636,14 +637,16 @@
         port, keyinfo = struct.unpack(MMTP_HOST_PAT, s[:2+DIGEST_LEN])
     except struct.error:
         raise ParseError("Misformatted routing info")
+    host = s[2+DIGEST_LEN:]
+    if not isPlausibleHostname(host):
+        raise ParseError("Nonsensical hostname")
     return MMTPHostInfo(s[2+DIGEST_LEN:], port, keyinfo)
 
 class MMTPHostInfo:
     """DOCDOC"""
     def __init__(self, hostname, port, keyinfo):
-        """Construct a new IPV4Info"""
         assert 0 <= port <= 65535
-        self.hostname = hostname
+        self.hostname = hostname.lower()
         self.port = port
         self.keyinfo = keyinfo
 

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.58
retrieving revision 1.59
diff -u -d -r1.58 -r1.59
--- ServerInfo.py	13 Oct 2003 17:30:24 -0000	1.58
+++ ServerInfo.py	19 Oct 2003 03:12:02 -0000	1.59
@@ -15,12 +15,13 @@
 
 import mixminion.Config
 import mixminion.Crypto
+import mixminion.MMTPClient
 import mixminion.Packet
 
 from mixminion.Common import IntervalSet, LOG, MixError, createPrivateDir, \
     formatBase64, formatDate, formatTime, readPossiblyGzippedFile
 from mixminion.Config import ConfigError
-from mixminion.Packet import IPV4Info
+from mixminion.Packet import IPV4Info, MMTPHostInfo
 from mixminion.Crypto import CryptoError, DIGEST_LEN, pk_check_signature
 
 # Longest allowed Contact email
@@ -64,16 +65,18 @@
                      "Packet-Key": ("REQUIRE", C._parsePublicKey, None),
                      "Contact-Fingerprint": ("ALLOW", None, None),
                      # XXXX010 change these next few to "REQUIRE".
-                     "Packet-Formats": ("ALLOW", None, None),
+                     "Packet-Formats": ("ALLOW", None, None),#XXXX007 remove
+                     "Packet-Versions": ("ALLOW", None, None),
                      "Software": ("ALLOW", None, None),
                      "Secure-Configuration": ("ALLOW", C._parseBoolean, None),
                      "Why-Insecure": ("ALLOW", None, None),
                      },
         "Incoming/MMTP" : {
                      "Version": ("REQUIRE", None, None),
-                     "IP": ("REQUIRE", C._parseIP, None),
+                     "IP": ("ALLOW", C._parseIP, None),#XXXX007 remove
+                     "Hostname": ("ALLOW", C._parseHost, None),#XXXX008 require
                      "Port": ("REQUIRE", C._parseInt, None),
-                     "Key-Digest": ("REQUIRE", C._parseBase64, None),
+                     "Key-Digest": ("ALLOW", C._parseBase64, None),#XXXX007 rmv
                      "Protocols": ("REQUIRE", None, None),
                      "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
                      "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
@@ -221,6 +224,8 @@
             if len(inMMTP['Key-Digest']) != DIGEST_LEN:
                 raise ConfigError("Invalid key digest %s"%
                                   formatBase64(inMMTP['Key-Digest']))
+            if not inMMTP['IP'] and not inMMTP['Hostname']:
+                raise ConfigError("Incoming/MMTP section has neither IP nor hostname")
 
         ## Outgoing/MMTP section
         outMMTP = self['Outgoing/MMTP']
@@ -243,9 +248,13 @@
            descriptor."""
         return self['Server']['Digest']
 
-    def getAddr(self):
+    def getIP(self):
         """Returns this server's IP address"""
-        return self['Incoming/MMTP']['IP']
+        return self['Incoming/MMTP'].get('IP')
+
+    def getHostname(self):
+        """DOCDOC"""
+        return self['Incoming/MMTP'].get("Hostname")
 
     def getPort(self):
         """Returns this server's IP port"""
@@ -255,44 +264,72 @@
         """Returns the RSA key this server uses to decrypt messages"""
         return self['Server']['Packet-Key']
 
-    def getKeyID(self):
+    def getKeyDigest(self):
         """Returns a hash of this server's MMTP key"""
-        return self['Incoming/MMTP']['Key-Digest']
+        return mixminion.Crypto.sha1(
+            mixminion.Crypto.pk_encode_public_key(self['Server']['Identity']))
+        #return self['Incoming/MMTP']['Key-Digest']
 
-    def getRoutingInfo(self):
+    def getIPV4Info(self):
         """Returns a mixminion.Packet.IPV4Info object for routing messages
            to this server."""
-        return IPV4Info(self.getAddr(), self.getPort(), self.getKeyID())
+        return IPV4Info(self.getIP(), self.getPort(), self.getKeyDigest())
+
+    def getMMTPHostInfo(self):
+        """DOCDOC"""
+        return MMTPHostInfo(get.getHostname(), self.getPort(), self.getKeyDigest())
+    
+    def getRoutingInfo(self):
+        return self.getIPV4Info()
 
     def getIdentity(self):
         return self['Server']['Identity']
 
+    def getIncomingMMTPProtocols(self):
+        inc = self['Incoming/MMTP']
+        if not inc.get("Version"):
+            return []
+        return [ s.strip() for s in inc["Protocols"].split(",") ]
+
+    def getOutgoingMMTPProtocols(self):
+        inc = self['Outgoing/MMTP']
+        if not inc.get("Version"):
+            return []
+        return [ s.strip() for s in inc["Protocols"].split(",") ]
+
     def canRelayTo(self, otherDesc):
         """DOCDOC"""
         if self.hasSameNicknameAs(otherDesc):
             return 1
-        myOut = self['Outgoing/MMTP']
-        if not myOut.get("Version"):
-            return 0
-        otherIn = otherDesc['Incoming/MMTP']
-        if not otherIn.get("Version"):
-            return 0
-        myOutProtocols = [ s.strip() for s in ",".split(myOut["Protocols"]) ]
-        otherInProtocols = [s.strip() for s in ",".split(otherIn["Protocols"])]
+        myOutProtocols = self.getOutgoingMMTPProtocols()
+        otherInProtocols = otherDesc.getIncomingMMTPProtocols()
         for out in myOutProtocols:
             if out in otherInProtocols:
                 return 1
         return 0
 
+    def canStartAt(self):
+        """DOCDOC"""
+        myInProtocols = self.getIncomingMMTPProtocols()
+        for out in mixminion.MMTPClient.BlockingClientConnection.PROTOCOL_VERSIONS:
+            if out in myInProtocols:
+                return 1
+        return 0
+
     def getRoutingFor(self, otherDesc, swap=0):
         """DOCDOC"""
-        #XXXX006 use this
+        #XXXX006 use this!
         assert self.canRelayTo(otherDesc)
-        if swap:
-            rt = mixminion.Packet.SWAP_FWD_IPV4_TYPE
+        assert 0 <= swap <= 1
+        if self.getHostname() and otherDesc.getHostname():
+            ri = otherDesc.getMMTPHostInfo().pack()
+            rt = [mixminion.Packet.FWD_HOST_TYPE,
+                  mixminion.Packet.SWAP_FWD_HOST_TYPE][swap]
         else:
-            rt = mixminion.Packet.FWD_IPV4_TYPE
-        ri = otherDesc.getRoutingInfo().pack()
+            ri = otherDesc.getIPV4Info().pack()
+            rt = [mixminion.Packet.FWD_IPV4_TYPE,
+                  mixminion.Packet.SWAP_FWD_IPV4_TYPE][swap]
+
         return rt, ri
         
     def getCaps(self):

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.157
retrieving revision 1.158
diff -u -d -r1.157 -r1.158
--- test.py	9 Oct 2003 15:26:16 -0000	1.157
+++ test.py	19 Oct 2003 03:12:02 -0000	1.158
@@ -251,7 +251,7 @@
             self.assertEquals(parse(s), t)
             if not last:
                 continue
-
+            
             if c == 'EX':
                 self.assertRaises(ValueError, cmp, last, t)
             elif c == '<':
@@ -1336,6 +1336,23 @@
         self.failUnlessRaises(ParseError, parseIPV4Info, ri[:-1])
         self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
 
+    def test_mmtphostinfo(self):
+        keyid = "zbcd"*5
+        ri = "\x30\x55"+keyid+"the.hostname.is.here"
+        inf = parseMMTPHostInfo(ri)
+        self.assertEquals(inf.hostname, "the.hostname.is.here")
+        self.assertEquals(inf.port, 0x3055)
+        self.assertEquals(inf.keyinfo, keyid)
+        self.assertEquals(inf.pack(), ri)
+        self.assertEquals(MMTPHostInfo("the.hostname.is.here", 0x3055,
+                                       keyid).pack(), ri)
+
+        self.failUnlessRaises(ParseError, parseMMTPHostInfo, "z")
+        self.failUnlessRaises(ParseError, parseMMTPHostInfo, "\x30\x55"+keyid)
+        self.failUnlessRaises(ParseError, parseMMTPHostInfo,
+                              "\x30\x55"+keyid+"_.com")
+                          
+
     def test_replyblock(self):
         # Try parsing an example 'reply block' object
         key = "\x99"*16
@@ -1657,15 +1674,15 @@
         self.keyid = keyid
 
     def getNickname(self): return "N(%s:%s)"%(self.addr,self.port)
-    def getAddr(self): return self.addr
+    def getIP(self): return self.addr
     def getPort(self): return self.port
     def getPacketKey(self): return self.key
-    def getKeyID(self): return self.keyid
+    def getKeyDigest(self): return self.keyid
 
     def getRoutingInfo(self):
-        """Returns a mixminion.Packet.IPV4Info object for routing messages
-           to this server."""
         return IPV4Info(self.addr, self.port, self.keyid)
+    def getIPV4Info(self):
+        return self.getRoutingInfo()
 
 class BuildMessageTests(TestCase):
     def setUp(self):
@@ -1999,11 +2016,11 @@
 
     def test_build_fwd_message(self):
         
-        bfm = BuildMessage.buildForwardMessage
-        befm = BuildMessage.buildEncryptedForwardMessage
-        payload = "Hello!!!!"
+        bfm = BuildMessage.buildForwardPacket
+        befm = BuildMessage.buildEncryptedForwardPacket
+        payloadF = BuildMessage.encodeMessage("Hello!!!!",0)[0]
 
-        m = bfm(payload, 500, "Goodbye",
+        m = bfm(payloadF, 500, "Goodbye",
                 [self.server1, self.server2],
                 [self.server3, self.server2])
 
@@ -2017,7 +2034,7 @@
                                (self.server2.getRoutingInfo().pack(),
                                 "Goodbye") ),
                              "Hello!!!!")
-        m = bfm(payload, 500, "Goodbye", [self.server1], [self.server3])
+        m = bfm(payloadF, 500, "Goodbye", [self.server1], [self.server3])
         
         messages = {}
 
@@ -2036,7 +2053,8 @@
                              decoder=decoder0)
         
         # Drop message gets no tag, random payload
-        m = bfm(payload, DROP_TYPE, "", [self.server1], [self.server3])
+        m = bfm(BuildMessage.buildRandomPayload(),
+                DROP_TYPE, "", [self.server1], [self.server3])
 
         def decoderDrop(p,t,self=self):
             self.assertEquals(None, t)
@@ -2053,12 +2071,12 @@
                              "",
                              decoder=decoderDrop)
 
-
         # Encrypted forward message
         rsa1, rsa2 = self.pk1, self.pk512
-        payload = "<<<<Hello>>>>" * 100
+        payloadE = BuildMessage.encodeMessage(
+            "<<<<Hello>>>>"*100,ENC_FWD_OVERHEAD)[0]
         for rsakey in rsa1,rsa2:
-            m = befm(payload, 500, "Phello",
+            m = befm(payloadE, 500, "Phello",
                      [self.server1, self.server2],
                      [self.server3, self.server2],
                      rsakey)
@@ -2075,7 +2093,7 @@
                                    (FWD_IPV4_TYPE, 500),
                                    (self.server2.getRoutingInfo().pack(),
                                     "Phello") ),
-                                 payload,
+                                 "<<<<Hello>>>>"*100,
                                  decoder=decoder)
             
         # Now do more tests on final messages: is the format as expected?
@@ -2097,7 +2115,7 @@
             ks = Keyset(sessionkey)
             msg = rsa_rest + lioness_decrypt(mrest,
                               ks.getLionessKeys("END-TO-END ENCRYPT"))
-            comp = compressData(payload)
+            comp = compressData("<<<<Hello>>>>"*100)
             self.assert_(len(comp), ord(msg[0])*256 + ord(msg[1]))
             self.assertEquals(sha1(msg[22:]), msg[2:22])
             self.assertStartsWith(msg[22:], comp)
@@ -2105,7 +2123,7 @@
     def test_buildreply(self):
         brbi = BuildMessage._buildReplyBlockImpl
         brb = BuildMessage.buildReplyBlock
-        brm = BuildMessage.buildReplyMessage
+        brm = BuildMessage.buildReplyPacket
 
         ## Stateful reply blocks.
         reply, secrets_1, tag_1 = \
@@ -2124,7 +2142,7 @@
 
         self.assert_(reply.routingInfo == self.server3.getRoutingInfo().pack())
 
-        m = brm("Information???",
+        m = brm(BuildMessage.encodeMessage("Information???",0)[0],
                 [self.server3, self.server1],
                 reply)
 
@@ -2196,12 +2214,13 @@
         self.assertEquals(sec, [ prng.getBytes(16) for _ in range(len(sec)) ])
 
         # _Gravity's Rainbow_, page 258.
-        payload = '''
+        message = '''
               "...Is it any wonder the world's gone insane, with information
             come to the be the only medium of exchange?"
               "I thought it was cigarettes."
               "You dream."
                   -- Gravity's Rainbow, p.258 ''' # " <- for emacs python-mode
+        payload = BuildMessage.encodeMessage(message,0)[0]
         m = brm(payload,
                 [self.server3, self.server1],
                 reply)
@@ -2220,7 +2239,7 @@
                              (pks_1, None,
                               (FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,MBOX_TYPE),
                               infos+("fred",)),
-                             payload,
+                             message,
                              decoder=decoder2)
 
         # Now test format of generated messages.
@@ -2243,7 +2262,7 @@
             ks = Keyset(s)
             p = lioness_encrypt(p, ks.getLionessKeys(
                                       Crypto.PAYLOAD_ENCRYPT_MODE))
-        comp = compressData(payload)
+        comp = compressData(message)
         self.assertEquals(len(comp), ord(p[0])*256 +ord(p[1]))
         self.assertStartsWith(p[22:], comp)
         self.assertEquals(sha1(p[22:]), p[2:22])
@@ -2446,10 +2465,11 @@
         return res
 
     def test_successful(self):
-        bfm = BuildMessage.buildForwardMessage
+        bfm = BuildMessage.buildForwardPacket
         # A two-hop/one-hop message.
         p = "Now is the time for all good men to come to the aid"
-        m = bfm("\n"+p, SMTP_TYPE, "nobody@invalid",
+        m = bfm(BuildMessage.encodeMessage("\n"+p,0)[0],
+                SMTP_TYPE, "nobody@invalid",
                 [self.server1, self.server2], [self.server3])
 
         self.do_test_chain(m,
@@ -2461,7 +2481,7 @@
                            p)
 
         # A one-hop/one-hop message.
-        m = bfm("\n"+p,
+        m = bfm(BuildMessage.encodeMessage("\n"+p,0)[0],
                 SMTP_TYPE, "nobody@invalid", [self.server1], [self.server3])
 
         self.do_test_chain(m,
@@ -2472,7 +2492,7 @@
                            p)
 
         # Try servers with multiple keys
-        m = bfm("\n"+p,
+        m = bfm(BuildMessage.encodeMessage("\n"+p,0)[0],
                 SMTP_TYPE, "nobody@invalid", [self.server2], [self.server3])
         self.do_test_chain(m, [self.sp2_3, self.sp2_3], [FWD_IPV4_TYPE, SMTP_TYPE],
                            [self.server3.getRoutingInfo().pack(),
@@ -2481,7 +2501,8 @@
         # A 3/3 message with a long exit header.
         for i in (100,300):
             longemail = "f"*i+"@invalid"
-            m = bfm("\n"+p, SMTP_TYPE, longemail,
+            m = bfm(BuildMessage.encodeMessage("\n"+p,0)[0],
+                    SMTP_TYPE, longemail,
                     [self.server1, self.server2, self.server1],
                     [self.server3, self.server1, self.server2])
 
@@ -2500,12 +2521,13 @@
 
     def test_deliverypacket(self):
         # Test out DeliveryPacket.*: with a plaintext ascii packet.
-        bfm = BuildMessage.buildForwardMessage
-        befm = BuildMessage.buildEncryptedForwardMessage
+        bfm = BuildMessage.buildForwardPacket
+        befm = BuildMessage.buildEncryptedForwardPacket
 
         h = "SUBJECT:cooper\n\n"
         p = "That gum you like, it's coming back in style."
-        m = bfm(h+p, SMTP_TYPE, "nobody@invalid", [self.server1],
+        payload = BuildMessage.encodeMessage(h+p,0)[0]
+        m = bfm(payload, SMTP_TYPE, "nobody@invalid", [self.server1],
                 [self.server3])
 
         pkt = self.do_test_chain(m,
@@ -2530,7 +2552,8 @@
         self.assertEquals({"SUBJECT":"cooper"}, pkt.getHeaders())
         # with a plaintext, nonascii packet.
         pbin = hexread("0123456789ABCDEFFEDCBA9876543210")
-        m = bfm("\n"+pbin, SMTP_TYPE, "nobody@invalid",
+        m = bfm(BuildMessage.encodeMessage("\n"+pbin,0)[0],
+                SMTP_TYPE, "nobody@invalid",
                 [self.server1], [self.server3])
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
@@ -2546,7 +2569,8 @@
         # with an overcompressed content
         pcomp = "          "*4096
         #      (forward, overcompressed)
-        m = bfm(pcomp, SMTP_TYPE, "nobody@invalid",
+        m = bfm(BuildMessage.encodeMessage(pcomp,0)[0],
+                SMTP_TYPE, "nobody@invalid",
                 [self.server1], [self.server3])
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
@@ -2560,7 +2584,8 @@
              encodeBase64(compressData(pcomp)))
 
         #      (enc-forward, overcompressed)
-        m = befm(p, SMTP_TYPE, "nobody@invalid", [self.server1],
+        m = befm(BuildMessage.encodeMessage(pcomp,ENC_FWD_OVERHEAD)[0],
+                 SMTP_TYPE, "nobody@invalid", [self.server1],
                  [self.server3], getRSAKey(0,1024))
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
@@ -2579,7 +2604,8 @@
         #    (ASCII msg with headers)
         h = {"FROM":'fred@foo', "SUBJECT":'Stuff'}
         p = encodeMessageHeaders(h) + "This is the message.\n"
-        m = bfm(p, SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
+        m = bfm(BuildMessage.encodeMessage(p,0)[0],
+                SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
         pkt = self.do_test_chain(m, 
                                  [self.sp1, self.sp3],
                                  [FWD_IPV4_TYPE, SMTP_TYPE],
@@ -2594,7 +2620,8 @@
         #    (binary msg with headers.)
         body = "\x01\x02\x03\x04"*10
         p = encodeMessageHeaders(h) + body
-        m = bfm(p, SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
+        m = bfm(BuildMessage.encodeMessage(p,0)[0],
+                SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
         pkt = self.do_test_chain(m, 
                                  [self.sp1, self.sp3],
                                  [FWD_IPV4_TYPE, SMTP_TYPE],
@@ -2608,8 +2635,8 @@
 
 
     def test_rejected(self):
-        bfm = BuildMessage.buildForwardMessage
-        brm = BuildMessage.buildReplyMessage
+        bfm = BuildMessage.buildForwardPacket
+        brm = BuildMessage.buildReplyPacket
         brbi = BuildMessage._buildReplyBlockImpl
 
         # A long intermediate header needs to fail.
@@ -2618,21 +2645,23 @@
             def pack(self): return "x"*200
         server1X.getRoutingInfo = lambda _packable=_packable: _packable()
 
-        m = bfm("Z", MBOX_TYPE, "hello\000bye",
+        zPayload = BuildMessage.encodeMessage("Z",0)[0]
+        m = bfm(zPayload, MBOX_TYPE, "hello\000bye",
                 [self.server2, server1X, self.server3],
                 [server1X, self.server2, self.server3])
         self.failUnlessRaises(ParseError, self.sp2.processMessage, m)
 
         # Duplicate messages need to fail.
-        m = bfm("Z", SMTP_TYPE, "nobody@invalid",
+        m = bfm(zPayload, SMTP_TYPE, "nobody@invalid",
                 [self.server1, self.server2], [self.server3])
         self.sp1.processMessage(m)
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m)
 
         # Duplicate reply blocks need to fail
         reply,s,tag = brbi([self.server3], SMTP_TYPE, "fred@invalid")
-        m = brm("Y", [self.server2], reply)
-        m2 = brm("Y", [self.server1], reply)
+        yPayload = BuildMessage.encodeMessage("Y",0)[0]
+        m = brm(yPayload, [self.server2], reply)
+        m2 = brm(yPayload, [self.server1], reply)
         m = self.sp2.processMessage(m).getPacket()
         self.sp3.processMessage(m)
         m2 = self.sp1.processMessage(m2).getPacket()
@@ -2643,21 +2672,21 @@
         reply1,s,t = brbi([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
         prng = AESCounterPRNG(" "*16)
         reply2,s,t = brbi([self.server2], MBOX_TYPE, "foo",0,prng)
-        m = brm("Y", [self.server3], reply1)
-        m2 = brm("Y", [self.server3], reply2)
+        m = brm(yPayload, [self.server3], reply1)
+        m2 = brm(yPayload, [self.server3], reply2)
         m = self.sp3.processMessage(m).getPacket()
         self.sp1.processMessage(m)
         m2 = self.sp3.processMessage(m2).getPacket()
         self.failUnlessRaises(ContentError, self.sp2.processMessage, m2)
 
         # Drop gets dropped.
-        m = bfm("Z", DROP_TYPE, "", [self.server2], [self.server2])
+        m = bfm(zPayload, DROP_TYPE, "", [self.server2], [self.server2])
         m = self.sp2.processMessage(m).getPacket()
         res = self.sp2.processMessage(m)
         self.assertEquals(res,None)
 
         # Wrong server.
-        m = bfm("Z", DROP_TYPE, "", [self.server1], [self.server2])
+        m = bfm(zPayload, DROP_TYPE, "", [self.server1], [self.server2])
         self.failUnlessRaises(CryptoError, self.sp2.processMessage, m)
         self.failUnlessRaises(CryptoError, self.sp2_3.processMessage, m)
 
@@ -2676,7 +2705,7 @@
             #  constant.)
             save = mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE
             mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = 50
-            m_x = bfm("Z", 500, "", [self.server1], [self.server2])
+            m_x = bfm(zPayload, 500, "", [self.server1], [self.server2])
         finally:
             mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = save
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
@@ -2710,7 +2739,7 @@
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Corrupt payload
-        m = bfm("Z", MBOX_TYPE, "Z", [self.server1, self.server2],
+        m = bfm(zPayload, MBOX_TYPE, "Z", [self.server1, self.server2],
                 [self.server3])
         m_x = m[:-30] + " "*30
         assert len(m_x) == len(m)
@@ -3520,8 +3549,8 @@
         return con
     if minimal:
         conFactory = conFactoryMin
-    listener = mixminion.server.MMTPServer.ListenConnection("127.0.0.1",
-                                                          port, 5, conFactory)
+    listener = mixminion.server.MMTPServer.ListenConnection(
+        socket.AF_INET, "127.0.0.1", port, 5, conFactory)
     listener.register(server)
     keyid = _getTLSContextKeyID()
 
@@ -3734,7 +3763,7 @@
                                           targetKeyID, msgFast, msgSlow):
             try:
                 con = mixminion.MMTPClient.BlockingClientConnection(
-                    targetIP,targetPort,targetKeyID)
+                    socket.AF_INET,targetIP,targetPort,targetKeyID)
                 con.connect()
                 con.sendPacket(msgFast)
                 while pausing[0] > 0:
@@ -4044,6 +4073,19 @@
         self.assertEquals(C._parseInt("99"), 99)
         # IP
         self.assertEquals(C._parseIP("192.168.0.1"), "192.168.0.1")
+        # IP6
+        self.assertEquals(C._parseIP6("::"), "::")
+        self.assertEquals(C._parseIP6("A::"), "A::")
+        self.assertEquals(C._parseIP6("::A"), "::A")
+        self.assertEquals(C._parseIP6("FE0F::A"), "FE0F::A")
+        self.assertEquals(C._parseIP6("1:2:3:4:5:6:7:8"), "1:2:3:4:5:6:7:8")
+        self.assertEquals(C._parseIP6("1:2:3::5:6:7:8"), "1:2:3::5:6:7:8")
+        self.assertEquals(C._parseIP6("1:2:3:4:5:6:1.2.3.4"),
+                          "1:2:3:4:5:6:1.2.3.4")
+        self.assertEquals(C._parseIP6("FFFF::1.2.3.4"),"FFFF::1.2.3.4")
+        self.assertEquals(C._parseIP6("::000:1.2.3.4"),"::000:1.2.3.4")
+        # Host
+        self.assertEquals(C._parseHost("foo.bar.baz "), "foo.bar.baz")
         # AddressSet
         pa = C._parseAddressSet_allow
         self.assertEquals(pa("*"), ("0.0.0.0", "0.0.0.0", 48099, 48099))
@@ -4136,6 +4178,15 @@
         fails(C._parseIP, "192.0.0")
         fails(C._parseIP, "192.0.0.0.0")
         fails(C._parseIP, "A.0.0.0")
+        fails(C._parseIP6, "100::200::300")
+        fails(C._parseIP6, "10000::")
+        fails(C._parseIP6, ":::")
+        fails(C._parseIP6, "1:2:3:4:5:6:7:8:9")
+        fails(C._parseIP6, "F:E:D:B:C:A:7:1.2.3.4")
+        fails(C._parseIP6, ":")
+        fails(C._parseIP6, "")
+        fails(C._parseHost, ".")
+        fails(C._parseHost, "foo..bar")
         fails(pa, "1/1")
         fails(pa, "192.168.0.1 50-40")
         fails(pa, "192.168.0.1 50-9999999")
@@ -4224,6 +4275,7 @@
 [Incoming/MMTP]
 Enabled = yes
 IP: 192.168.0.1
+Hostname: Theserver
 Allow: 192.168.0.16 1-1024
 Deny: 192.168.0.16
 Allow: *
@@ -4275,6 +4327,7 @@
         eq = self.assertEquals
         eq(info['Server']['Descriptor-Version'], "0.2")
         eq(info['Incoming/MMTP']['IP'], "192.168.0.1")
+        eq(info['Incoming/MMTP']['Hostname'], "Theserver")
         eq(info['Server']['Nickname'], "The_Server")
         self.failUnless(0 <= time.time()-info['Server']['Published'] <= 120)
         self.failUnless(0 <= time.time()-info['Server']['Valid-After']
@@ -4283,7 +4336,8 @@
            10*24*60*60)
         eq(info['Server']['Contact'], "a@b.c")
         eq(info['Server']['Software'], "Mixminion %s"%mixminion.__version__)
-        eq(info['Server']['Packet-Formats'], "0.3")
+        eq(info['Server']['Packet-Formats'], None)
+        eq(info['Server']['Packet-Versions'], "0.3")
         eq(info['Server']['Comments'],
            "This is a test of the emergency broadcast system")
 
@@ -4347,6 +4401,9 @@
 
         self.assertUnorderedEq(info.getCaps(), ["relay", "frag"])
 
+        self.assertEquals(info.getIncomingMMTPProtocols(), ["0.3"])
+        self.assertEquals(info.getOutgoingMMTPProtocols(), ["0.3"])
+
         # Now check whether we still validate the same after some corruption
         self.assertStartsWith(inf, "[Server]\n")
         self.assertEndsWith(inf, "\n")
@@ -4407,6 +4464,7 @@
             conf = mixminion.server.ServerConfig.ServerConfig(string=(SERVER_CONFIG_SHORT%mix_mktemp())+
                                            """[Incoming/MMTP]
 Enabled: yes
+Hostname: Theserver2
 IP: 192.168.0.99
 """)
         finally:
@@ -4465,6 +4523,7 @@
                 string=(SERVER_CONFIG_SHORT%mix_mktemp())+
                                            """[Incoming/MMTP]
 Enabled: yes
+Hostname: Theserver3
 IP: 192.168.100.3
 [Delivery/SMTP]
 Enabled: yes
@@ -4474,6 +4533,7 @@
                 string=(SERVER_CONFIG_SHORT%mix_mktemp())+
                                            """[Incoming/MMTP]
 Enabled: yes
+Hostname: Theserver4
 IP: 192.168.100.4
 """)
         finally:
@@ -4493,11 +4553,13 @@
                           key2.getMMTPKey().get_public_key())
         self.assertEquals(key3.getPacketKey().get_public_key(),
                           key2.getPacketKey().get_public_key())
+        eq(info3['Incoming/MMTP']['Hostname'], "Theserver3")
         eq(info3['Incoming/MMTP']['IP'], "192.168.100.3")
         self.assert_('smtp' in info3.getCaps())
 
         key3.regenerateServerDescriptor(conf2, identity)
         info3 = key3.getServerDescriptor()
+        eq(info3['Incoming/MMTP']['Hostname'], "Theserver4")
         eq(info3['Incoming/MMTP']['IP'], "192.168.100.4")
 
     def test_directory(self):
@@ -4863,6 +4925,7 @@
 Foo: 99
 [Incoming/MMTP]
 Enabled: yes
+Hostname: Theserver5
 IP: 1.0.0.1
 """ % (mod_dir)
 
@@ -5577,6 +5640,7 @@
 Nickname: mac-the-knife
 [Incoming/MMTP]
 Enabled: yes
+Hostname: Theserver5
 IP: 10.0.0.1
 """
 
@@ -5785,6 +5849,7 @@
 Nickname: %(nickname)s
 [Incoming/MMTP]
 Enabled: yes
+Hostname: %(nickname)s
 IP: %(ip)s
 [Outgoing/MMTP]
 Enabled: yes
@@ -6514,8 +6579,8 @@
         pathSpec1 = parsePath(usercfg, "lola,joe:alice,joe")
 
         ##  Test generateForwardMessage.
-        # We replace 'buildForwardMessage' to make this easier to test.
-        replaceFunction(mixminion.BuildMessage, "buildForwardMessage",
+        # We replace 'buildForwardPacket' to make this easier to test.
+        replaceFunction(mixminion.BuildMessage, "buildForwardPacket",
                         lambda *a, **k:"X")
         try:
             getCalls = getReplacedFunctionCallLog
@@ -6523,12 +6588,12 @@
             # First, two forward messages that end with 'joe' and go via
             # SMTP
             payload = "Hey Joe, where you goin' with that gun in your hand?"
-            client.generateForwardPayloads(
+            client.generateForwardPackets(
                 directory,
                 parseAddress("joe@cledonism.net"),
                 pathSpec1,
                 payload, time.time(), time.time()+200)
-            client.generateForwardPayloads(
+            client.generateForwardPackets(
                 directory,
                 parseAddress("smtp:joe@cledonism.net"),
                 pathSpec1,
@@ -6536,7 +6601,7 @@
                 time.time(), time.time()+200)
 
             for fn, args, kwargs in getCalls():
-                self.assertEquals(fn, "buildForwardMessage")
+                self.assertEquals(fn, "buildForwardPacket")
                 self.assertEquals(args[1:3],
                                   (SMTP_TYPE, "joe@cledonism.net"))
                 self.assert_(len(args[3]) == len(args[4]) == 2)
@@ -6546,13 +6611,13 @@
 
             # Now try an mbox message, with an explicit last hop.
             payload = "Hey, Lo', where you goin' with that pun in your hand?"
-            client.generateForwardPayloads(
+            client.generateForwardPackets(
                 directory,
                 parseAddress("mbox:granola"),
                 parsePath(usercfg, "lola,joe:alice,lola"),
                 payload, time.time(), time.time()+200)
             # And an mbox message with a last hop implicit in the address
-            client.generateForwardPayloads(
+            client.generateForwardPackets(
                 directory,
                 parseAddress("mbox:granola@Lola"),
                 parsePath(usercfg, "Lola,Joe:Alice"),
@@ -6560,9 +6625,11 @@
                 
 
             for fn, args, kwargs in getCalls():
-                self.assertEquals(fn, "buildForwardMessage")
-                self.assertEquals(args[0:3],
-                                  (payload, MBOX_TYPE, "granola"))
+                self.assertEquals(fn, "buildForwardPacket")
+                self.assertEquals(args[1:3],
+                                  (MBOX_TYPE, "granola"))
+                self.assertEquals(payload,
+                    BuildMessage.decodePayload(args[0],"Z"*20).getUncompressedContents())
                 self.assert_(len(args[3]) == len(args[4]) == 2)
                 self.assertEquals(["Lola", "Joe", "Alice", "Lola"],
                      [x.getNickname() for x in args[3]+args[4]])
@@ -6576,9 +6643,11 @@
         # Temporarily replace BlockingClientConnection so we can try the client
         # without hitting the network.
         class FakeBCC:
-            def __init__(self, addr, port, keyid):
+            PROTOCOL_VERSIONS=["0.3"]
+            def __init__(self, family, addr, port, keyid):
                 global BCC_INSTANCE
                 BCC_INSTANCE = self
+                self.family = family
                 self.addr = addr
                 self.port = port
                 self.keyid = keyid
@@ -6797,6 +6866,32 @@
         pool.close()
         
 #----------------------------------------------------------------------
+
+def initializeGlobals():
+    init_crypto()
+
+    mixminion.ClientMain.configureClientLock(mix_mktemp())
+
+    # Suppress 'files-can't-be-securely-deleted' message while testing
+    LOG.setMinSeverity("FATAL")
+    mixminion.Common.secureDelete([],1)
+
+    # Don't complain about owner on /tmp, no matter who it is.
+    mixminion.Common._VALID_DIRECTORIES["/tmp"] = 1
+    mixminion.Common._VALID_DIRECTORIES["/var/tmp"] = 1
+
+    # Disable TRACE and DEBUG log messages, unless somebody overrides from
+    # the environment.
+    LOG.setMinSeverity(os.environ.get('MM_TEST_LOGLEVEL', "WARN"))
+    #LOG.setMinSeverity(os.environ.get('MM_TEST_LOGLEVEL', "TRACE"))
+
+    # Suppress DNS resolution on fake hostnames.
+    klh = mixminion.server.ServerKeys._KNOWN_LOCAL_HOSTNAMES
+    for name in [ 'Theserver', 'Theserver2', 'Theserver3', 'Theserver4',
+                  'Theserver5', 'Alice', 'Bob', 'Fred', 'Lola', 'Joe',
+                  'Lisa' ]:
+        klh[name]=1
+
 def testSuite():
     """Return a PyUnit test suite containing all the unit test cases."""
     suite = unittest.TestSuite()
@@ -6804,7 +6899,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(ClientDirectoryTests))
+        suite.addTest(tc(PacketHandlerTests))
         return suite
     testClasses = [MiscTests,
                    MinionlibCryptoTests,
@@ -6840,20 +6935,5 @@
     return suite
 
 def testAll(name, args):
-    init_crypto()
-    mixminion.ClientMain.configureClientLock(mix_mktemp())
-
-    # Suppress 'files-can't-be-securely-deleted' message while testing
-    LOG.setMinSeverity("FATAL")
-    mixminion.Common.secureDelete([],1)
-
-    # Don't complain about owner on /tmp, no matter who it is.
-    mixminion.Common._VALID_DIRECTORIES["/tmp"] = 1
-    mixminion.Common._VALID_DIRECTORIES["/var/tmp"] = 1
-
-    # Disable TRACE and DEBUG log messages, unless somebody overrides from
-    # the environment.
-    LOG.setMinSeverity(os.environ.get('MM_TEST_LOGLEVEL', "WARN"))
-    #LOG.setMinSeverity(os.environ.get('MM_TEST_LOGLEVEL', "TRACE"))
-
+    initializeGlobals()
     unittest.TextTestRunner(verbosity=1).run(testSuite())