#! /usr/bin/env python

##     PyRT: Python Routeing Toolkit

##     BGP module: provides the BGP listener and BGP PDU parsers

##     Copyright (C) 2001 Richard Mortier <mort@sprintlabs.com>, Sprint ATL

##     This program is free software; you can redistribute it and/or
##     modify it under the terms of the GNU General Public License as
##     published by the Free Software Foundation; either version 2 of the
##     License, or (at your option) any later version.

##     This program is distributed in the hope that it will be useful,
##     but WITHOUT ANY WARRANTY; without even the implied warranty of
##     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
##     General Public License for more details.

##     You should have received a copy of the GNU General Public License
##     along with this program; if not, write to the Free Software
##     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
##     02111-1307 USA

#
# $Id: bgp.py,v 1.22 2002/05/06 11:35:06 mort Exp $
#

import struct, socket, sys, math, getopt, string, os.path, time
from mutils import *

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

INDENT          = "    "
VERSION         = "1.0"

RCV_BUF_SZ      = 8192
BGP_LISTEN_PORT = 179
BGP_HDR_LEN     = 19
BGP_MARKER      = struct.pack(">LLLL",
                              0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff)
BGP_MARKER_LEN  = len(BGP_MARKER)

TABLE_DUMP_ENTRY_HDR_LEN = 18

################################################################################

DLIST = []

AFI_TYPES = { 1L: "IP",
              2L: "IP6",
              }
DLIST = DLIST + [AFI_TYPES]

SAFI_TYPES = { 1L: "UNICAST",
               2L: "MULTICAST",
               }
DLIST = DLIST + [SAFI_TYPES]

MSG_TYPES = { 1L: "OPEN",
              2L: "UPDATE", 
              3L: "NOTIFICATION",
              4L: "KEEPALIVE",
              5L: "ROUTE_REFRESH",
              6L: "TABLE_DUMP_ENTRY",
              }
DLIST = DLIST + [MSG_TYPES]

# NB. PAs below actually attribute numbers; only type codes 1-10, 14, 15 valid

PATH_ATTRIBUTES = { 1L:  "ORIGIN",
                    2L:  "AS_PATH",
                    3L:  "NEXT_HOP",
                    4L:  "MULTI_EXIT_DISCRIMINATOR",
                    5L:  "LOC_PREF",
                    6L:  "ATOMIC_AGGR",
                    7L:  "AGGREGATOR",
                    8L:  "COMMUNITY",
                    9L:  "ORIGINATOR_ID",
                    10L: "CLUSTER_LIST",
                    # DPA unused (only ever made draft)
                    11L: "DPA",                         
                    12L: "ADVERTISER",
                    # RCID_PATH never sent to border routers
                    13L: "RCID_PATH/CLUSTER_ID", 
                    14L: "MP_REACH_NLRI",
                    15L: "MP_UNREACH_NLRI",
                    16L: "EXT_COMMUNITIES",
                    }
DLIST = DLIST + [PATH_ATTRIBUTES]

OPT_PARAMS = { 1L: "AUTHENTICATION",
               2L: "CAPABILITY"
               }
DLIST = DLIST + [OPT_PARAMS]

CAP_CODES = { 0L: "UNDEF",
              1L: "MULTIPROTOCOL_EXT",
              2L: "ROUTE_REFRESH",
              
              # 128+ are reserved for vendor-specific applications
              128L: "ROUTE_REFRESH_Z",
              }
DLIST = DLIST + [CAP_CODES]

NLRI_SRC = { 0L: "IGP",
             1L: "EGP",
             2L: "INCOMPLETE"
             }
DLIST = DLIST + [NLRI_SRC]

AS_PATH_SEG_TYPES = { 1L: "SET",
                      2L: "SEQUENCE",
                      3L: "CONFED_SET",
                      4L: "CONFED_SEQUENCE"
                      }
DLIST = DLIST + [AS_PATH_SEG_TYPES]

for d in DLIST: 
    for k in d.keys():
        d[ d[k] ] = k

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

# index via [code][subcode]
NOTIFY_STRINGS = [
    [ "message header error",
      [ "connection not synchronized",
        "bad message length",
        "bad message type"
        ]],

    [ "OPEN message error",
      [ "unsupported version number",
        "bad peer AS",
        "bad BGP identifier",
        "unsuported optional parameter",
        "authentication failure",
        "unacceptable hold timer",
        "unsupported capability"
        ]],

    [ "UPDATE message error",
      [ "malformed attribute list",
        "unrecognized well-known attribute",
        "missing well-known attribute",
        "attribute flags error",
        "attribute length error",
        "invalid ORIGIN attribute"
        "AS routing loop",
        "invalid NEXT-HOP attribute",
        "optional attribute error",
        "invalid network field",
        "malformed AS-PATH"
        ]],

    [ "Hold Timer expired",
      [ ""]],

    [ "Finite State Machine error",
      [ ""]],

    [ "Cease",
      [ "maximum number of prefixes reached",
        "administratively shutdown",
        "peer unconfigured",            
        "administratively reset",
        "connection rejected",
        "other configuration change" ]]
    ]

################################################################################

def parseBgpPdu(msg_type, msg_len, msg, verbose=1, level=0):
    
    msg     = msg[BGP_HDR_LEN:]
    msg_len = msg_len - BGP_HDR_LEN
    if   msg_type == MSG_TYPES["OPEN"]:
        rv = parseOpen(msg_len, msg, verbose, level)

    elif msg_type == MSG_TYPES["UPDATE"]:
        rv = parseUpdate(msg_len, msg, verbose, level)
        
    elif msg_type == MSG_TYPES["NOTIFICATION"]:
        rv = parseNotify(msg_len, msg, verbose, level)
        
    elif msg_type == MSG_TYPES["KEEPALIVE"]:
        rv = parseKeepalive(msg_len, msg, verbose, level)
        
    elif msg_type == MSG_TYPES["ROUTE_REFRESH"]:
        rv = parseRouteRefresh(msg_len, msg, verbose, level)

    else:
        rv = {"T": None, "L": 0, "V": None}
        if verbose > 0:
            print level*INDENT + "[ *** UNKNOWN MESSAGE TYPE *** ]"

    return rv
            
#-------------------------------------------------------------------------------

def parseOpen(msg_len, msg, verbose=1, level=0):

    rv = {"T": MSG_TYPES["OPEN"],
          "L": msg_len,
          "V": {}
          }

    if verbose > 1:
        print prtbin(level*INDENT, msg[:msg_len])

    version, as, holdtime, bgp_id = struct.unpack(">BHHL", msg[0:9])

    if verbose > 0:
        print level*INDENT +\
              "Open (len=%d):" % (msg_len+BGP_HDR_LEN, )
        print (level+1)*INDENT +\
              "version: %d, src AS: %d, holdtime: %d, BGP id: %s" %\
              (version, as, holdtime, id2str(bgp_id))

    opts = parseBgpOpts(msg[9:msg_len], verbose, level+1)

    rv["V"] = {"VER":  version,
               "AS":   as,
               "HT":   holdtime,
               "ID":   bgp_id,
               "OPTS": opts
               }
    return rv

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

def parseBgpOpts(opts, verbose=1, level=0):

    rv = []

    opts_len = struct.unpack("B", opts[0])
    if verbose > 1:
        print prtbin(level*INDENT, opts[0])
        
    if verbose > 0:
        print level*INDENT + "optional params. len=%d" % opts_len

    opts = opts[1:]
    while len(opts) > 0:

        opt_type, opt_len = struct.unpack("BB", opts[0:2])
        trv = { "T": opt_type,
                "L": opt_len,
                "V": {}
                }
           
        if verbose > 1:
            print prtbin(level*INDENT, opts[0:2+opt_len])
        if verbose > 0:
            print level*INDENT +\
                  "option type: %s, len=%d" % (OPT_PARAMS[opt_type], opt_len)

        opts = opts[2:]

        if opt_type == OPT_PARAMS["CAPABILITY"]:

            level = level + 1            
            cap_code, cap_len = struct.unpack("BB", opts[0:2])
            
            trv["V"] = { "T": cap_code,
                         "L": cap_len,
                         "V": {}
                         }

            if verbose > 0:
                if verbose > 1:
                    print prtbin(level*INDENT, opts[0:2+cap_len])
                print level*INDENT +\
                      "capability:", CAP_CODES[cap_code], "len=" + `cap_len`

                if verbose > 1:
                    print prtbin(level*INDENT, opts[2:2+cap_len])
                if cap_code == CAP_CODES["MULTIPROTOCOL_EXT"]:
                    afi, safi = struct.unpack(">HH", opts[2:2+cap_len])
                    if verbose > 1:
                        print level*INDENT +\
                              "afi:", AFI_TYPES[afi], "safi:", SAFI_TYPES[safi]

                    trv["V"]["V"] = { "AFI": afi, "SAFI": safi }

                elif cap_code == CAP_CODES["ROUTE_REFRESH"]:
                    pass

                elif cap_code == CAP_CODES["ROUTE_REFRESH_Z"]:
                    pass
                    
                else:
                    print level*INDENT +\
                          "[ *** UNKNOWN CAPABILITY CODE: %d *** ]" % cap_code
            level = level - 1

        else:
            if verbose > 0:
                print level*INDENT +\
                      "[ *** UNKNOWN OPTIONAL PARAMETER: %d *** ]" % opt_type

        rv.append(trv)
        opts = opts[opt_len:]

    return rv

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

def parseUpdate(msg_len, msg, verbose=1, level=0):

    rv = {"T": MSG_TYPES["UPDATE"],
          "L": msg_len,
          "V": { "UNFEASIBLE": [], "PATH_ATTRS": {}, "FEASIBLE": [] }
          }
        
    curp = 0

    # "BGP4: Inter-domain routing in the Internet" John W. Stewart III.
    # Unfeasible (withdrawn) routes are a sequence of (len, pfx) pairs, where
    # len is the length of the prefix in _bits_, and pfx is the prefix, padded
    # to a whole number of octets.  All such padding must be ignored.

    (unfeasible_len, ) = struct.unpack(">H", msg[curp:curp+2])
    unfeasible_pfxs = "\n"
    if verbose > 1:
        unfeasible_pfxs = unfeasible_pfxs +\
                          prtbin((level+1)*INDENT, msg[0:2+unfeasible_len])
    unfeasible_pfxs = unfeasible_pfxs +\
                      "\n" + (level+1)*INDENT + "UNFEASIBLE ROUTES:\n"
        
    curp = curp + 2
    endp = curp + unfeasible_len

    rn   = 0
    while curp != endp:

        rn = rn + 1

        (plen, )    = struct.unpack("B", msg[curp])
        plen_octets = int(math.ceil(plen/8.0))
        curp = curp + 1

        (pfx,) = struct.unpack("%ds" % plen_octets, msg[curp:curp+plen_octets])
        unfeasible_pfxs = unfeasible_pfxs + (level+2)*INDENT +\
                           "%d: %s\n" % (rn, pfx2str(pfx, plen))

        rv["V"]["UNFEASIBLE"].append((pfx,plen))
        curp = curp + plen_octets

    # "BGP4: Inter-domain routing in the Internet" John W. Stewart III,
    # pp.37--40.  (T,L,V) encoded.  TYPE is 2 octets, split into FLAGS and
    # TYPECODE.  LENGTH is 1 or 2 octets based on EXTENDED-LENGTH field in FLAGS
    # and is in octets (bottom p.39).  VALUE is parsed as given by TYPE-CODE,
    # cf. section 2.4

    (path_attr_len, ) = struct.unpack(">H", msg[curp:curp+2])
    path_attrs = ""
    if verbose > 1:
        path_attrs = path_attrs +\
                     prtbin((level+1)*INDENT, msg[curp:curp+2+path_attr_len])
    path_attrs = path_attrs + "\n" + (level+1)*INDENT + "PATH ATTRIBUTES:\n"

    curp = curp + 2
    endp = curp + path_attr_len

    rn   = 0
    while curp != endp:

        rn = rn + 1

        aflags, atype = struct.unpack("BB", msg[curp:curp+2])
        curp = curp + 2

        flg_optional   = (aflags & (1<<7)) >> 7
        flg_transitive = (aflags & (1<<6)) >> 6
        flg_partial    = (aflags & (1<<5)) >> 5
        flg_extlen     = (aflags & (1<<4)) >> 4

        # XXX this is a bit grim, but it seems that there's no way to
        # do this more nicely :-(

        if flg_extlen+1 == 1:
            (alen, ) = struct.unpack("B", msg[curp:curp+flg_extlen+1])
        elif flg_extlen+1 == 2:
            (alen, ) = struct.unpack(">H", msg[curp:curp+flg_extlen+1])
        else:
            error('flg_extlen was neither 0 nor 1 :-(')
            sys.exit(1)

        curp  = curp + flg_extlen+1
        adata = msg[curp:curp+alen]

        (pa_str, pa_trv) = parseBgpAttr(atype, alen, adata, verbose, level+2)
        path_attrs = path_attrs + pa_str
        
        flgs_str = "%s %s %s %s" %\
                   ("optional"*flg_optional, "transitive"*flg_transitive,
                    "partial"*flg_partial,   "extended length"*flg_extlen)
	flgs_str = string.strip(flgs_str)
	flgs_str = " [ %s ]\n" % flgs_str
        path_attrs = path_attrs + flgs_str
        pa_trv["FLAGS"] = {"optional":   flg_optional,
                           "transitive": flg_transitive,
                           "partial":    flg_partial,
                           "extlen":     flg_extlen,
                           }
        rv["V"]["PATH_ATTRS"][ pa_trv["T"] ] = pa_trv

        curp = curp + alen

    # NLRI information: the prefixes to which path attributes apply
    # <len(bits),pfx>*

    nlri_pfxs = (level+1)*INDENT + "FEASIBLE ROUTES:\n"
    endp = curp + len(msg[curp:])
    rn   = 0

    while curp < endp:

        rn = rn + 1

        (plen, )    = struct.unpack("B", msg[curp])
        plen_octets = int(math.ceil(plen/8.0))
        curp = curp + 1

        if verbose > 1:
            nlri_pfxs = nlri_pfxs + prtbin((level+2)*INDENT,
                                           msg[curp-1:curp+plen_octets]) + "\n"

        (pfx,) = struct.unpack("%ds" % len(msg[curp:curp+plen_octets]),
                               msg[curp:curp+plen_octets])

        nlri_pfxs = nlri_pfxs +\
                    (level+2)*INDENT + "%d: %s %s\n" %\
                    (rn, pfx2str(pfx, plen),
                     (len(msg[curp:curp+plen_octets]) != plen_octets)*
                     '[ *** bogus NLRI field: plen_octets did not match *** ]')

        rv["V"]["FEASIBLE"].append((pfx,plen))
        curp = curp + plen_octets

    if verbose > 0:
        print level*INDENT +\
              "Update (len=%d): unfeasible_len=%d path_attr_len=%d%s%s%s" %\
              (msg_len+BGP_HDR_LEN, unfeasible_len, path_attr_len,
               unfeasible_pfxs, path_attrs, nlri_pfxs)

    return rv
            
#-------------------------------------------------------------------------------

def parseBgpAttr(atype, alen, adata, verbose=1, level=0):

    rv = {"T": atype,
          "L": alen,
          "V": None
          }

    if len(adata) == 0:
        ret = level*INDENT + PATH_ATTRIBUTES[atype] + ": null"
        return (ret, rv)
    
    if atype in PATH_ATTRIBUTES.keys():

        if atype == PATH_ATTRIBUTES["ORIGIN"]:
            (nlri_src, ) = struct.unpack("B", adata)

            ret = level*INDENT + "ORIGIN: %s" % NLRI_SRC[nlri_src]
            rv["V"] = nlri_src

        elif atype == PATH_ATTRIBUTES["AS_PATH"]:
            
            ret     = level*INDENT + "AS_PATH: " 
            rv["V"] = []

            while adata:
                asp_t, asp_l = struct.unpack("BB", adata[0:2])
                rv_cpt = { "T": asp_t, "L": asp_l, "V": [] }

                asp_v = adata[2:2+2*asp_l]
                if asp_v:
                    path = struct.unpack(">%dH" % asp_l, asp_v)
                    if(asp_t == AS_PATH_SEG_TYPES["SET"] or
                       asp_t == AS_PATH_SEG_TYPES["CONFED_SET"]):
                        
                        ret = ret + '(%s){ ' % AS_PATH_SEG_TYPES[asp_t]
                        for as in path:
                            ret = ret + "%d, " % as
                            rv_cpt["V"].append(as)
                        ret = ret + '}'

                    elif(asp_t == AS_PATH_SEG_TYPES["SEQUENCE"] or
                         asp_t == AS_PATH_SEG_TYPES["CONFED_SEQUENCE"]):

                        ret = ret + '(%s)[ ' % AS_PATH_SEG_TYPES[asp_t]
                        for as in path:
                            ret = ret + "<- %d " % as
                            rv_cpt["V"].append(as)
                        ret = ret + ']'
                        
                rv["V"].append(rv_cpt)
                adata = adata[2+(asp_l*2):]
                        
        elif atype == PATH_ATTRIBUTES["NEXT_HOP"]:
            (nh, ) = struct.unpack(">L", adata)
            ret    = level*INDENT + "NEXT_HOP: " + id2str(nh)
            rv["V"] = nh

        elif atype == PATH_ATTRIBUTES["MULTI_EXIT_DISCRIMINATOR"]:
            (med, ) = struct.unpack(">L", adata)
            ret     = level*INDENT + "MED: " + `med`
            rv["V"] = med
            
        elif atype == PATH_ATTRIBUTES["LOC_PREF"]:
            (lp, ) = struct.unpack(">L", adata)
            ret    = level*INDENT + "LOC_PREF: " + `lp`
            rv["V"] = lp

        # ATOMIC_AGGREGATOR hit by null check at start...
        elif atype == PATH_ATTRIBUTES["AGGREGATOR"]:
            (as, ip) = struct.unpack(">H L", adata)
            ret = level*INDENT +\
                  "AGGREGATOR: formed by AS %d, router %s" % (as, id2str(ip))
            rv["V"] = (as, ip)

        elif atype == PATH_ATTRIBUTES["COMMUNITY"]:

            ret = ""
            rv["V"] = []
            for i in range(alen/4):
                x,y = struct.unpack(">HH", adata[i*4:(i+1)*4])
                ret = ret + level*INDENT + "COMMUNITY %d: %d:%d\n" % (i+1, x, y)
                rv["V"].append(adata[i*4:(i+1)*4])
            ret = ret[:-1]

        elif atype == PATH_ATTRIBUTES["ORIGINATOR_ID"]:

            (id,) = struct.unpack(">L", adata)
            ret = level*INDENT + "ORIGINATOR_ID: %s" % id2str(id)
            rv["V"] = id

        elif atype == PATH_ATTRIBUTES["CLUSTER_LIST"]:

            # These are 'defined' in RFC 1966 (route reflectors).  Or so they
            # should be.  In fact, the RFC talks complete bollocks
            # re. CLUSTER_LIST -- it defines nothing and appears to be just
            # plain wrong.  However, as usual, there is magic: from Cisco, we
            # see http://www.cisco.com/networkers/nw99_pres/309.pdf, which says
            # CLUSTER_LIST is "...just a list of ORIGINATOR_IDs...".  So there
            # we go.  I have _no idea_ what the encoding of the originator ids
            # is here -- I assume the standard ">L" for convenience.
            
            ret = level*INDENT + "CLUSTER_LIST"
            rv["V"] = []
            for i in range(alen/4):
                (id,) = struct.unpack(">L", adata[:4])
                ret = ret + ": %s" % id2str(id)
                rv["V"].append(id)

        else:
            ret = level*INDENT + "[ *** %s *** ]" % PATH_ATTRIBUTES[atype]

    else:
        ret = level*INDENT +\
              "[ *** UNKNOWN BGP path attribute: %d *** ]" % atype

    return (ret, rv)
        
#-------------------------------------------------------------------------------

def parseNotify(msg_len, msg, verbose=1, level=0):

    rv = {"T": MSG_TYPES["NOTIFICATION"],
          "L": msg_len,
          "V": None
          }

    if verbose > 1:
        print prtbin(level*INDENT, msg[:msg_len])

    code, subcode, data = struct.unpack("BB %ds" % (msg_len-2, ), msg)
    code    = code - 1
    subcode = subcode - 1

    if verbose > 0:
        print level*INDENT + "Notification (len=%d): %s : %s" %\
              (msg_len,
               NOTIFY_STRINGS[code][0], NOTIFY_STRINGS[code][1][subcode])

    ## XXX data?

    return rv

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

def parseKeepalive(msg_len, msg, verbose=1, level=0):

    rv = {"T": MSG_TYPES["KEEPALIVE"],
          "L": msg_len,
          "V": None
          }
    
    if verbose > 1:
        print prtbin(level*INDENT, msg)

    if verbose > 0:
        print level*INDENT + "Keepalive (len=%d)\n" % (msg_len+BGP_HDR_LEN, )

    return rv

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

def parseRouteRefresh(msg_len, msg, verbose=1, level=0):

    rv = {"T": MSG_TYPES["ROUTE_REFRESH"],
          "L": msg_len,
          "V": None
          }
          
    if verbose > 1:
        print prtbin(level*INDENT, msg)

    if verbose > 0:
        print level*INDENT +  "RouteRefresh (len=%d)" % (msg_len+BGP_HDR_LEN, )

    return rv

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

def parseTableEntry(length, entries, verbose=1, level=0):

    rv = {"T": MSG_TYPES["TABLE_DUMP_ENTRY"],
          "L": 0,
          "V": {}
          }

    hfmt = ">LBBLLHH"
    hfmt_l = struct.calcsize(hfmt)

    pfx, plen, status, uptime, peer_addr, peer_as, elen =\
         struct.unpack(hfmt, entries[:hfmt_l])
    
    rv["V"]["PREFIX"]  = (struct.pack(">L", pfx), plen)
    rv["V"]["STATUS"]  = status
    rv["V"]["UPTIME"]  = uptime
    rv["V"]["PEER_IP"] = peer_addr
    rv["V"]["PEER_AS"] = peer_as
    
    if verbose:
        print level*INDENT +\
              "prefix: %s/%d, peer IP: %s, peer AS: %d" %\
              (id2str(pfx), plen, id2str(peer_addr), peer_as)
        print level*INDENT + "updated: '%s'" % (time.ctime(uptime),)

    entries = entries[TABLE_DUMP_ENTRY_HDR_LEN:]
    rv["V"]["ATTRS"] = entries

    # XXX This should reuse part of bgp.parseUpdate() -- need to further
    # separate out the path attr. parsing.  Cut'n'paste for now since I'm in
    # a hurry.

    if verbose:
        print level*INDENT + 'PATH ATTRIBUTES: len=%d' % elen

    rv["L"] = TABLE_DUMP_ENTRY_HDR_LEN + elen
    while elen:
        if verbose > 1:
            print prthex(level*INDENT + 'flags/type:', entries[:2])

        aflags, atype = struct.unpack(">BB", entries[0:2])

        flg_optional   = (aflags & (1<<7)) >> 7
        flg_transitive = (aflags & (1<<6)) >> 6
        flg_partial    = (aflags & (1<<5)) >> 5
        flg_extlen     = (aflags & (1<<4)) >> 4

        flgs_str = "%s %s %s %s" %\
                   ("optional"*flg_optional, "transitive"*flg_transitive,
                    "partial"*flg_partial,   "extended length"*flg_extlen)
        flgs_str = string.strip(flgs_str)
        flgs_str = " [ %s ]" % flgs_str

        if verbose > 1:
            print prthex(level*INDENT + 'length:',
                         entries[2+flg_extlen:2+flg_extlen+1])
        (alen, ) = struct.unpack("%dB" % (flg_extlen+1),
                                 entries[2:2+flg_extlen+1])
        (adata,) = struct.unpack("%ds" % alen,
                                 entries[2+flg_extlen+1:2+flg_extlen+1+alen])

        if verbose > 1:
            print prthex(level*INDENT +'value:',
                         entries[2+flg_extlen+1:2+flg_extlen+1+alen])

        (astr, arv) = parseBgpAttr(atype, alen, adata, verbose, level+1)

        rv["V"][atype] = arv

        if verbose:
            print astr + flgs_str
        entries = entries[2+flg_extlen+1+alen:]
        elen    = elen - (2+flg_extlen+1+alen)

    if verbose: print
    return rv

################################################################################

class Bgp:

    _version = 4

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


    def __init__(self, loc_name, as, rem_name, port, holdtime):

        self._bgp_id_str  = loc_name
        self._bgp_id_addr = socket.gethostbyname(loc_name)
        self._bgp_id      = str2id(self._bgp_id_addr)
        self._bgp_as      = as

        self._bgp_peer_str  = rem_name
        self._bgp_peer_addr = socket.gethostbyname(rem_name)
        self._bgp_peer_id   = str2id(self._bgp_peer_addr)
        self._bgp_peer_prt  = port
        self._bgp_peer_as   = 0

        self._holdtime = holdtime

        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._sock.bind((self._bgp_id_str, 0))
        self._sock.connect((self._bgp_peer_str, self._bgp_peer_prt))

        self._rcvd = ""
        self._mrt  = None
        
    def __repr__(self):
            
        ret = """Passive BGP speaker version %s:
        id: %s [%s] (%#0x), AS: %d
        peer: %s:%d [%s] (%#0x), AS: %d
        holdtime: %d\n""" %\
            (VERSION,
             self._bgp_id_str, self._bgp_id_addr, self._bgp_id, self._bgp_as,
             self._bgp_peer_str, self._bgp_peer_prt, self._bgp_peer_addr,
             self._bgp_peer_id, self._bgp_peer_as, self._holdtime)

        return ret

    def close(self):
        # XXX RMM XXX should possibly be a __del__() method?
        self._sock.close()
        self._mrt.close()

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

    def recvMsg(self, verbose=1, level=0):

        while 1:
        
            if len(self._rcvd) < BGP_MARKER_LEN+3:
                self._rcvd = self._rcvd + self._sock.recv(RCV_BUF_SZ)
                continue

            ## guaranteed to have a BGP-msg-header-worth of data in buffer
            
            msg_start = string.find(self._rcvd, BGP_MARKER)
            if msg_start < 0:
                # no marker in buffer -- fill buffer and continue
                self._rcvd = self._rcvd + self._sock.recv(RCV_BUF_SZ)
                continue
            
            elif msg_start > 0:
                # marker not at buffer start -- dump skipped data to debug
                sys.stderr.write(prtbin("", self._rcvd[:msg_start]) + "\n---\n")
                sys.stderr.flush()

            ## msg_start is now at the start of a message

            msg_len, msg_type =\
                     struct.unpack(">HB",
                                   self._rcvd[msg_start+BGP_MARKER_LEN :
                                              msg_start+BGP_MARKER_LEN+3])
            break

        ## message may not be completely received...

        while msg_len > len(self._rcvd):
            self._rcvd = self._rcvd + self._sock.recv(RCV_BUF_SZ)

        msg_end = msg_start + msg_len

        ## guaranteed to have the entire message in [msg_start..msg_end]

        msg        = self._rcvd[msg_start:msg_end]                        
        self._rcvd = self._rcvd[msg_end:]

        ## have now advanced buffer past current message; current
        ## message available in msg

        if verbose > 2:
            print "recvMsg: msg: type=%s (%d) len=%d%s" %\
                  (MSG_TYPES[msg_type], msg_type, msg_len,
                   prtbin(level*INDENT, msg))
            
        return msg_type, msg_len, msg
        
    def sendMsg(self, msg_type, msg_len, msg, verbose=1, level=0):

        fmt = ">LLLLH B %ds" % msg_len
        pkt = struct.pack(fmt,
                          0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 
                          msg_len+BGP_HDR_LEN, msg_type, msg)

        if DUMP_MRTD == 1:
            self._mrt.writeBgp4pyMsg(msg_type, len(pkt), pkt)
        elif DUMP_MRTD == 2:
            self._mrt.writeBgpMsg(msg_type, len(pkt), pkt)
        elif DUMP_MRTD == 3:
            self._mrt.writeBgp4mpMsg(msg_type, len(pkt), pkt)

        if verbose > 2:
            print "%ssendMsg: type=%s (%d), len=%d%s" %\
                  (level*INDENT, MSG_TYPES[msg_type], msg_type,
                   struct.calcsize(fmt), prtbin((level+1)*INDENT, pkt))

        self._sock.send(pkt)

    def parseMsg(self, verbose=1, level=0):

        msg_type, msg_len, msg = self.recvMsg()

        if DUMP_MRTD == 1:
            self._mrt.writeBgp4pyMsg(msg_type, msg_len, msg)
        elif DUMP_MRTD == 2:
            self._mrt.writeBgpMsg(msg_type, msg_len, msg)
        elif DUMP_MRTD == 3:
            self._mrt.writeBgp4mpMsg(msg_type, msg_len, msg)

        if verbose > 2:
            print "%sparseMsg: type=%s (%d) len=%d%s" %\
                  (level*INDENT, MSG_TYPES[msg_type], msg_type,
                   msg_len-BGP_HDR_LEN,
                   prtbin((level+1)*INDENT, msg[BGP_HDR_LEN:]))

        rv = parseBgpPdu(msg_type, msg_len, msg, verbose, level)

        return rv # msg_type, msg_len, msg

    #---------------------------------------------------------------------------
        
    def sendOpen(self, verbose=1, level=0):

        print `type(self._bgp_id)`, `self._bgp_id`
        
        fmt = ">BHHLB"
        msg = struct.pack(fmt, Bgp._version,
                          self._bgp_as, self._holdtime, self._bgp_id, 0)

        if verbose > 2:
            print "%ssendOpen: len=%d%s" %\
                  (level*INDENT, struct.calcsize(fmt),
                   prtbin((level+1)*INDENT, msg))

        parseOpen(len(msg), msg, verbose, level)
        self.sendMsg(MSG_TYPES["OPEN"],
                     struct.calcsize(fmt), msg, verbose, level)

    def sendKeepalive(self, verbose=1, level=0):

        fmt = ""
        msg = ""
        if verbose > 2:
            print "sendKeepalive: len=%d%s" %\
                  (struct.calcsize(fmt), prtbin(level*INDENT, msg))

        parseKeepalive(len(msg), msg, verbose, level)
        self.sendMsg(MSG_TYPES["KEEPALIVE"], 0, msg, verbose, level)

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

################################################################################

if __name__ == "__main__":

    import mrtd

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

    global VERBOSE, DUMP_MRTD

    VERBOSE   = 1
    DUMP_MRTD = 0
    
    file_pfx  = mrtd.DEFAULT_FILE
    file_sz   = mrtd.DEFAULT_SIZE
    mrtd_type = None
    loc_name  = None
    rem_name  = None
    as        = None
    port      = BGP_LISTEN_PORT
    holdtime  = 0

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

    def usage():

        print """Usage: %s [ options ] ([*] options required):
        -h|--help     : Help
        -q|--quiet    : Be quiet
        -v|--verbose  : Be verbose
        -V|--VERBOSE  : Be very verbose
        
        -f|--file     : Set file prefix for MRTd dump [def: %s]
        -y|--dump-4py : Dump MRTd::PROTOCOL_BGP4PY format [default]
        -d|--dump     : Dump MRTd::PROTOCOL_BGP format
        -m|--dump-4mp : Dump MRTd::PROTOCOL_BGP4MP format

        -p|--peer     : [*] BGP peer address/name
        -a|--as       : [*] Local AS number
        -l|--local    : Address/name for local bind
        -t|--port     : BGP peer listening port [def: %d]
        -z|--size     : Size of output file(s) [min: %d]""" %\
            (os.path.basename(sys.argv[0]), mrtd.DEFAULT_FILE,
             BGP_LISTEN_PORT, mrtd.MIN_FILE_SZ)
        sys.exit(0)

    #---------------------------------------------------------------------------
    
    if len(sys.argv) < 2:
        usage()
        
    try:
        opts, args = getopt.getopt(sys.argv[1:],
                                   "hqvVydmp:a:o:t:l:f:z:",
                                   ("help", "quiet", "verbose", "VERBOSE",
                                    "dump-4py", "dump", "dump-4mp",
                                    "file-pfx=", "peer=", "as=", "holdtime=",
                                    "port=", "local=", "size=" ))
    except (getopt.error):
        usage()

    for (x, y) in opts:        
        if x in ('-h', '--help'):
            usage()

        elif x in ('-q', '--quiet'):
            VERBOSE = 0
            
        elif x in ('-v', '--verbose'):
            VERBOSE = 2
            
        elif x in ('-V', '--VERBOSE'):
            VERBOSE = 3

        elif x in ('-y', '--dump-4py'):
            DUMP_MRTD = 1
            mrtd_type = mrtd.MSG_TYPES["PROTOCOL_BGP4PY"]
            
        elif x in ('-d', '--dump'):
            DUMP_MRTD = 2
            mrtd_type = mrtd.MSG_TYPES["PROTOCOL_BGP"]
            
        elif x in ('-m', '--dump-4mp'):
            DUMP_MRTD = 3
            mrtd_type = mrtd.MSG_TYPES["PROTOCOL_BGP4MP"]
            
        elif x in ('-p', '--peer'):
            rem_name = y
            
        elif x in ('-a', '--as'):
            as = string.atoi(y)
            
        elif x in ('-o', '--holdtime'):
            holdtime = string.atoi(y)
            
        elif x in ('-t', '--port'):
            port = string.atoi(y)
            
        elif x in ('-l', '--local'):
            loc_name = y

        elif x in ('-f', '--file-pfx'):
            file_pfx = y

        elif x in ('-z', '--file-size'):
            file_sz = max(string.atof(y), mrtd.MIN_FILE_SZ)

        else:
            usage()
    
    if not (rem_name and as):
        usage()

    if not loc_name:
        loc_name = socket.gethostname()

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

    bgp      = Bgp(loc_name, as, rem_name, port, holdtime)
    bgp._mrt = mrtd.Mrtd(file_pfx, "w+b", file_sz, mrtd_type, bgp)
        
    if VERBOSE > 0:
        print `bgp`
    
    try:

        # the wafeur-est thin state machine you ever did see :-)
        bgp.sendOpen(VERBOSE, 0)
        rv = bgp.parseMsg(VERBOSE, 0)
        bgp._bgp_peer_as = rv["V"]["AS"]
        bgp.sendKeepalive(VERBOSE, 0)

        while 1:
            msg = bgp.parseMsg(VERBOSE, 0)

    except (KeyboardInterrupt):
        bgp.close()
        sys.exit(1)
        
    #---------------------------------------------------------------------------

################################################################################
################################################################################


syntax highlighted by Code2HTML, v. 0.9.1