[comp.protocols.appletalk] KIP and AlisaTalk

cck@CUNIXA.COLUMBIA.EDU (Charlie C. Kim) (05/08/88)

Bill Greene and I have been tracking down the AlisaTalk/KIP problems
and have found the following.

The first important piece of information is that AlisaTalk has an
"AppleTalk Bridge" on the VMS hosts that all the VMS services lie
behind.

The first problem we found was that KIP was improperly using using the
hop count in the ddp header as part of the length (a bug that I
introduced I belive).  This was in the code that downsized an incoming
packet if it was longer than the ddp packet size -- this happens when
a packet on EtherTalk is shorter than the minimum ethernet size
(should be 60 bytes, but some systems insist on 64 bytes).  This
prevented macs on the localtalk network from accepting incoming
packets.  NBP worked okay because the packet sizes involved were over
43 bytes (the mimimum size ddp packet that would function without
error in the above).


The second problem found is far more serious.  In summary, KIP does
not properly handle RTMP/ZIP transactions.  It has a minimal amount of
code that allows it to accept RTMP from other bridges and send ZIP
responses to queries.  However, KIP does not attempt to find the zone
of networks found via RTMP transactions (in other words, it does not
send out ZIP queries or accept ZIP responses) -- instead it sets them
to "unknown" (0).  Because of this, servers (actually nbp lookups on
entities) behind non-KIP gateways (AlisaTalk, Hayes InterBridge)
cannot be done.  To explain this further, we simply note that NBP
lookups are done in a bridged environment by sending a "Bridge Lookup"
packet to the nearest Bridge that is responsible for sending NBP
lookups as directed broadcasts to each network in the specified zone.
Since acquired networks are never assigned a zone, NBP lookups would
never be sent to those networks.

But, why where the lookups working at all?  It was because of a bug in
the code that did not check to see that the "ALLZONES" index was
initialized before it was used.  (A network in allzones receives all
NBP lookups).  Allzones is set to zero.  The "unknown" zone value is
zero.  Thus, all the networks in "unknown" were being 'queried' on
every lookup.  (Bill, the real reason the first gateway worked was the
above.  The reason the other gateway didn't work was because the
"acquired" routes are only sent out to other KIP gateways if they are
marked "core").

Parts of this analysis may be incorrect, but I believe the majority to
be correct.  Note: clients behind the non-KIP networks should still be
able to communicate with most servers (the only exception would be in
the situation where you have:
non-kip-bridge<->kip-bridge<->non-kip-bridge).

So what is the solution?  The best possible solution is to make KIP
query unknown networks.  This is a fair amount of work.  A workaround
would be to (a) allow non-IP networks to be set in atalkatab or (b) to
write an host EtherTalk redirector.

If someone wants to do the work, they are welcome to contact me for
ideas.

Following are set of patches:
	gw.c - correct ethertalk length correction
	gw2.c - fix ddpreply to be smart about sending short vs. long
	   	ddp.  short ddp only to localtalk interface
	      - fix nbpback to ensure allzones is set before use
	      - update for modified ddpreply
	rtmp.c - update for modified ddprely: rtmp & zip packets
	      - initialize allzones
	      - make ZIP GetMyZone return zone of the network the packet
	 	came in upon, not the zone of the interface it came in upon
		(suggested by Robert Elz).  This makes it useful when host
		whose primary zone isn't that of the gw does a gmz

The ddpreply patches were the first tried to attempt to resolve the
problem -- while they turned out not to be necessary, they still bring
kip closer to specification (EtherTalk).

The GMZ patch could be used to allow CAP hosts to "automatically"
retrieve their zone (e.g. so zone name doesn't have to be specified in
atalk.local).

Charlie C. Kim
User Services
Columbia University

%%%START OF PATCHES%%%
*** /tmp/,RCSt1008421	Sun May  8 10:52:05 1988
--- gw.c	Fri May  6 17:35:54 1988
***************
*** 318,324
  		  ddp.srcNet = source_if->if_dnet;
  		if (ddp.dstNet == 0)
  		  ddp.dstNet = source_if->if_dnet;
! 		temp = ddp.length - ddpSize;
  		p->p_off += ddpSize;
  		wasddp = 1;
  		break;

--- 318,324 -----
  		  ddp.srcNet = source_if->if_dnet;
  		if (ddp.dstNet == 0)
  		  ddp.dstNet = source_if->if_dnet;
! 		temp = (ddp.length & ddpLengthMask) - ddpSize;
  		p->p_off += ddpSize;
  		wasddp = 1;
  		break;
*** /tmp/,RCSt1008421	Sun May  8 10:52:07 1988
--- gw2.c	Fri May  6 15:32:49 1988
***************
*** 199,207
  	for ( ; ar < &aroute[NAROUTE] ; ++ar) {
  		if (ar->net == 0)
  			continue;
! 		if (ar->zone != allzones &&
! 		    ar->zone != nback_zoneindex) /* wrong zone? */
! 			continue;
  		K_PGET(PT_DATA, p);
  		if (p == 0)
  			return;	/* try later */

--- 199,209 -----
  	for ( ; ar < &aroute[NAROUTE] ; ++ar) {
  		if (ar->net == 0)
  			continue;
! 		/* continue if wrong zone and the zone is not all zones */
! 		/* make sure allzones is set before using */
! 		if (ar->zone != nback_zoneindex &&
! 		    !(allzones && ar->zone == allzones))
! 		  continue;
  		K_PGET(PT_DATA, p);
  		if (p == 0)
  			return;	/* try later */
***************
*** 488,494
  	  if (p->p_off[lapSize+ddpSize] != echoRequest) 
  	    goto drop;
  	  p->p_off[lapSize+ddpSize] = echoReply;
! 	  ddpreply(p, echoSkt);
  	  return;
  	case ddpZIP:
  	  if (ddp.dstSkt != zipSkt)

--- 490,496 -----
  	  if (p->p_off[lapSize+ddpSize] != echoRequest) 
  	    goto drop;
  	  p->p_off[lapSize+ddpSize] = echoReply;
! 	  ddpreply(p, echoSkt, (ddp.length & ddpLengthMask) - ddpSize);
  	  return;
  	case ddpZIP:
  	  if (ddp.dstSkt != zipSkt)
***************
*** 527,534
  		ig->op = -1;
  		break;
  	}
- 	ddp.length = (ddpSize + sizeof *a + ipgpMinSize 
- 	    + strlen(ig->string) + 1);
  	p->p_len = ddp.length + lapSize;
  	a->control = atpRspCode + atpEOM;
  	a->bitmap = 0;

--- 529,534 -----
  		ig->op = -1;
  		break;
  	}
  	p->p_len = ddp.length + lapSize;
  	a->control = atpRspCode + atpEOM;
  	a->bitmap = 0;
***************
*** 532,538
  	p->p_len = ddp.length + lapSize;
  	a->control = atpRspCode + atpEOM;
  	a->bitmap = 0;
! 	ddpreply(p, ddpIPSkt);
  	return;
  drop:
  	K_PFREE(p);

--- 532,538 -----
  	p->p_len = ddp.length + lapSize;
  	a->control = atpRspCode + atpEOM;
  	a->bitmap = 0;
! 	ddpreply(p, ddpIPSkt, (sizeof(*a)+ipgpMinSize+strlen(ig->string)+1));
  	return;
  drop:
  	K_PFREE(p);
***************
*** 760,766
  }
  
  
! ddpreply(p, skt)
  register struct pbuf *p;
  int skt;
  {

--- 760,777 -----
  }
  
  
! /*
!  * assumes ddp.type already set and that the reply goes to the node specified
!  * by: (ddp.srcNet, ddp.srcNode, ddp.srcSkt)
!  *
!  * p should be aligned so offset points to start of lap data with room 
!  * for lap + long ddp.
!  *
!  * skt is outgoing socket
!  *
!  * len is the length of the data portion
! */
! ddpreply(p, skt, len)
  register struct pbuf *p;
  int skt;
  int len;
***************
*** 763,768
  ddpreply(p, skt)
  register struct pbuf *p;
  int skt;
  {
    register struct DDP *dp = &ddp;
  

--- 774,780 -----
  ddpreply(p, skt, len)
  register struct pbuf *p;
  int skt;
+ int len;
  {
    u_char dst;
    register struct DDP *dp = &ddp;
***************
*** 764,769
  register struct pbuf *p;
  int skt;
  {
    register struct DDP *dp = &ddp;
  
    dp->checksum = 0;

--- 776,782 -----
  int skt;
  int len;
  {
+   u_char dst;
    register struct DDP *dp = &ddp;
  
    /* if incoming was short ddp and came in from localtalk interface */
***************
*** 766,778
  {
    register struct DDP *dp = &ddp;
  
!   dp->checksum = 0;
!   dp->dstNet = dp->srcNet;
!   dp->dstNode = dp->srcNode;
!   dp->dstSkt = dp->srcSkt;
!   dp->srcNet = source_if->if_dnet;
!   dp->srcNode = source_if->if_dnode;
!   dp->srcSkt = skt;
!   bcopy((caddr_t)dp, p->p_off+lapSize, ddpSize);
!   routeddp(p, 0);
  }

--- 779,807 -----
    u_char dst;
    register struct DDP *dp = &ddp;
  
!   /* if incoming was short ddp and came in from localtalk interface */
!   /* and is going back to the localtalk interface, then send back as */
!   /* short ddp, else long ddp */
!   if (source_if == &ifab && (dp->srcNet == source_if->if_dnet)) {
!     p->p_off += (ddpSize - ddpSSize); /* move ahead to ddp short boundary */
!     p->p_len -= (ddpSize - ddpSSize);
!     dst = dp->srcNode;
!     ddps.srcSkt = skt;		/* source sockt */
!     ddps.dstSkt = dp->srcSkt;
!     ddps.type = dp->type;
!     ddps.length = len + ddpSSize;
!     bcopy((caddr_t)&ddps, p->p_off+lapSize, ddpSSize);
!     (*source_if->if_output)(source_if, p, AF_SDDP, &dst);
!   } else {
!     dp->length = len + ddpSize;
!     dp->checksum = 0;
!     dp->dstNet = dp->srcNet;
!     dp->dstNode = dp->srcNode;
!     dp->dstSkt = dp->srcSkt;
!     dp->srcNet = source_if->if_dnet;
!     dp->srcNode = source_if->if_dnode;
!     dp->srcSkt = skt;
!     bcopy((caddr_t)dp, p->p_off+lapSize, ddpSize);
!     routeddp(p, 0);
!   }
  }
*** /tmp/,RCSt1008421	Sun May  8 10:52:09 1988
--- rtmp.c	Fri May  6 15:32:49 1988
***************
*** 33,39
  	rtmp_delay = 1;
  
  	/* broadcast the current routing table */
! 	rtmpsend(0xff, rtmpSkt, 1, &ifab);
  	if (ifet.if_dnet)	/* only send routing packets if ETalk enabled*/
  	  rtmpsend(0xff, rtmpSkt, 1, &ifet);
  

--- 33,39 -----
  	rtmp_delay = 1;
  
  	/* broadcast the current routing table */
! 	rtmpsend(&ifab);
  	if (ifet.if_dnet)	/* only send routing packets if ETalk enabled*/
  	  rtmpsend(&ifet);
  
***************
*** 35,41
  	/* broadcast the current routing table */
  	rtmpsend(0xff, rtmpSkt, 1, &ifab);
  	if (ifet.if_dnet)	/* only send routing packets if ETalk enabled*/
! 	  rtmpsend(0xff, rtmpSkt, 1, &ifet);
  
  	if (rtmp_vdelay++ == 0)	/* validity timer goes off every 20 secs */
  		return;

--- 35,41 -----
  	/* broadcast the current routing table */
  	rtmpsend(&ifab);
  	if (ifet.if_dnet)	/* only send routing packets if ETalk enabled*/
! 	  rtmpsend(&ifet);
  
  	if (rtmp_vdelay++ == 0)	/* validity timer goes off every 20 secs */
  		return;
***************
*** 55,61
  
  
  /*
!  * Send an RTMP packet to dnode, dsoc.  If 'tuples' is true, 
   * include the routing tuples.
   */
  rtmpsend(dnode, dsoc, tuples, ifp)

--- 55,61 -----
  
  
  /*
!  * Send an RTMP packet.  If 'tuples' is true, 
   * include the routing tuples.
   */
  /* break into send and reply */
***************
*** 58,64
   * Send an RTMP packet to dnode, dsoc.  If 'tuples' is true, 
   * include the routing tuples.
   */
! rtmpsend(dnode, dsoc, tuples, ifp)
       struct ifnet *ifp;
  {
  	register struct pbuf *p;

--- 58,68 -----
   * Send an RTMP packet.  If 'tuples' is true, 
   * include the routing tuples.
   */
! /* break into send and reply */
! 
! /* rtmp send: used only to broadcast rtmp "here I ams" */
! /* setup for ddp reply and call rtmpreply */
! rtmpsend(ifp)
       struct ifnet *ifp;
  {
    source_if = ifp;
***************
*** 61,66
  rtmpsend(dnode, dsoc, tuples, ifp)
       struct ifnet *ifp;
  {
  	register struct pbuf *p;
  	register struct RTMP *r;
  	struct DDPS d;

--- 65,80 -----
  rtmpsend(ifp)
       struct ifnet *ifp;
  {
+   source_if = ifp;
+   ddp.srcNet = source_if->if_dnet;
+   ddp.srcNode = 0xff;		/* broadcast */
+   ddp.srcSkt = rtmpSkt;
+   rtmpreply(1);
+ }
+ 
+ /* rtmp reply - reply to last packet (was rtmp request) */
+ rtmpreply(tuples)
+ {
  	register struct pbuf *p;
  	register struct RTMP *r;
  	register i;
***************
*** 63,69
  {
  	register struct pbuf *p;
  	register struct RTMP *r;
- 	struct DDPS d;
  	register i;
  	u_char dst;
  

--- 77,82 -----
  {
  	register struct pbuf *p;
  	register struct RTMP *r;
  	register i;
  
  	K_PGET(PT_DATA, p);
***************
*** 65,71
  	register struct RTMP *r;
  	struct DDPS d;
  	register i;
- 	u_char dst;
  
  	K_PGET(PT_DATA, p);
  	if (p == 0)

--- 78,83 -----
  	register struct pbuf *p;
  	register struct RTMP *r;
  	register i;
  
  	K_PGET(PT_DATA, p);
  	if (p == 0)
***************
*** 70,77
  	K_PGET(PT_DATA, p);
  	if (p == 0)
  		return;
! 	r = (struct RTMP *)(p->p_off + lapSize + ddpSSize);
! 	r->net = ifp->if_dnet;
  	r->idLen = 8;
  	r->id = ifp->if_dnode;
  	if (tuples)

--- 82,89 -----
  	K_PGET(PT_DATA, p);
  	if (p == 0)
  		return;
! 	r = (struct RTMP *)(p->p_off + lapSize + ddpSize);
! 	r->net = source_if->if_dnet;
  	r->idLen = 8;
  	r->id = source_if->if_dnode;
  	if (tuples)
***************
*** 73,79
  	r = (struct RTMP *)(p->p_off + lapSize + ddpSSize);
  	r->net = ifp->if_dnet;
  	r->idLen = 8;
! 	r->id = ifp->if_dnode;
  	if (tuples)
  		i = rtmpsettuples((caddr_t)(r + 1));
  	else

--- 85,91 -----
  	r = (struct RTMP *)(p->p_off + lapSize + ddpSize);
  	r->net = source_if->if_dnet;
  	r->idLen = 8;
! 	r->id = source_if->if_dnode;
  	if (tuples)
  		i = rtmpsettuples((caddr_t)(r + 1));
  	else
***************
*** 78,91
  		i = rtmpsettuples((caddr_t)(r + 1));
  	else
  		i = 0;
! 	d.length = i + ddpSSize + rtmpSize;
! 	p->p_len = d.length + lapSize;
! 	d.dstSkt = dsoc;
! 	d.srcSkt = rtmpSkt;
! 	dst = dnode;
! 	d.type = ddpRTMP;
! 	bcopy((caddr_t)&d, p->p_off+lapSize, ddpSSize);
! 	(*ifp->if_output)(ifp, p, AF_SDDP, &dst);
  }
  
  

--- 90,99 -----
  		i = rtmpsettuples((caddr_t)(r + 1));
  	else
  		i = 0;
! 	/* setup for ddp send */
! 	ddp.type = ddpRTMP;	/* in case request */
! 	p->p_len = i+rtmpSize+ddpSize+lapSize;
! 	ddpreply(p, rtmpSkt, i+rtmpSize);
  }
  
  
***************
*** 201,207
  		/* RTMP request from Mac trying to get his net # */
  		if (*p->p_off != 1)
  			goto drop;	/* only opcode defined now is 1 */
! 		rtmpsend(ddp.srcNode, ddp.srcSkt, 0, source_if);
  		goto drop;
  	}
  

--- 209,215 -----
  		/* RTMP request from Mac trying to get his net # */
  		if (*p->p_off != 1)
  			goto drop;	/* only opcode defined now is 1 */
! 		rtmpreply(0);	/* reply */
  		goto drop;
  	}
  
***************
*** 462,467
  	 * net# net# ... 0 zonename
  	 * 0xFFFF
  	 */
  	for (;;) {
  		n = *cp++;
  		n <<= 8;

--- 470,476 -----
  	 * net# net# ... 0 zonename
  	 * 0xFFFF
  	 */
+ 	allzones = 0;
  	for (;;) {
  		n = *cp++;
  		n <<= 8;
***************
*** 523,529
  	}
  }
  
- 
  /* 
   * String compare, case independent;
   * returns zero if equal, one otherwise

--- 532,537 -----
  	}
  }
  
  /* 
   * String compare, case independent;
   * returns zero if equal, one otherwise
***************
*** 577,583
  
    if (ddp.type == ddpATP)
      goto get;	/* if ATP style request */
!   /* else pure ZIP in a short DDP */
    if (ddp.type != ddpZIP)
      goto drop;
    z = (struct ZIP *)ip->p_off+lapSize+ddpSize;

--- 585,591 -----
  
    if (ddp.type == ddpATP)
      goto get;	/* if ATP style request */
!   /* else pure ZIP */
    if (ddp.type != ddpZIP)
      goto drop;
    z = (struct ZIP *)ip->p_off+lapSize+ddpSize;
***************
*** 592,598
    K_PGET(PT_DATA, op);
    if (op == 0)
      goto drop;
!   po = op->p_off + lapSize + ddpSSize + sizeof(struct ZIP);
    sp = (u_short *)(z+1);
    for (count = 0, len = 0 ; count < z->count && len < 512 ; count++) {
      u.s = i = *sp++;		/* network */

--- 600,606 -----
    K_PGET(PT_DATA, op);
    if (op == 0)
      goto drop;
!   po = op->p_off + lapSize + ddpSize + sizeof(struct ZIP);
    sp = (u_short *)(z+1);
    for (count = 0, len = 0 ; count < z->count && len < 512 ; count++) {
      u.s = i = *sp++;		/* network */
***************
*** 608,614
      po += (*pi + 1);
      len += (*pi + 3);
    }
!   z = (struct ZIP *)(op->p_off + lapSize + ddpSSize);
    z->command = zipReply;
    z->count = count;
    d.length = len + sizeof *z + ddpSSize;

--- 616,622 -----
      po += (*pi + 1);
      len += (*pi + 3);
    }
!   z = (struct ZIP *)(op->p_off + lapSize + ddpSize);
    z->command = zipReply;
    z->count = count;
    op->p_len = lapSize + ddpSize + len + sizeof *z;
***************
*** 611,624
    z = (struct ZIP *)(op->p_off + lapSize + ddpSSize);
    z->command = zipReply;
    z->count = count;
!   d.length = len + sizeof *z + ddpSSize;
!   op->p_len = d.length + lapSize;
!   d.dstSkt = ddp.srcSkt;
!   d.srcSkt = zipSkt;
!   d.type = ddpZIP;
!   dst = ddp.srcNode;
!   bcopy((caddr_t)&d, op->p_off+lapSize, ddpSSize);
!   (*source_if->if_output)(source_if, op, AF_SDDP, &dst);
    goto drop;
  get:
    /* we can reuse the input packet (query needs too much out of it) */

--- 619,626 -----
    z = (struct ZIP *)(op->p_off + lapSize + ddpSize);
    z->command = zipReply;
    z->count = count;
!   op->p_len = lapSize + ddpSize + len + sizeof *z;
!   ddpreply(op, zipSkt, len + sizeof *z);
    goto drop;
  get:
    /* we can reuse the input packet (query needs too much out of it) */
***************
*** 644,649
    case GMZ:
      zap->c[4] = 0;
      /* Find the zone for the interface request came in on */
      for (ar = &aroute[0]; ar < &aroute[NAROUTE]; ++ar)
        if (ar->net == source_if->if_dnet) break;
      pi = azone[ar->zone];

--- 646,653 -----
    case GMZ:
      zap->c[4] = 0;
      /* Find the zone for the interface request came in on */
+     /* cck: modify to find the zone for the source network */
+     /* of the packet that came in */
      for (ar = &aroute[0]; ar < &aroute[NAROUTE]; ++ar)
        if (ar->net == ddp.srcNet)
  	break;
***************
*** 645,651
      zap->c[4] = 0;
      /* Find the zone for the interface request came in on */
      for (ar = &aroute[0]; ar < &aroute[NAROUTE]; ++ar)
!       if (ar->net == source_if->if_dnet) break;
      pi = azone[ar->zone];
      count = *pi + 1;
      bcopy(pi, po, count);	/* get zone */

--- 649,658 -----
      /* cck: modify to find the zone for the source network */
      /* of the packet that came in */
      for (ar = &aroute[0]; ar < &aroute[NAROUTE]; ++ar)
!       if (ar->net == ddp.srcNet)
! 	break;
!     if (ar == &aroute[NAROUTE])
!       goto drop;
      pi = azone[ar->zone];
      count = *pi + 1;
      bcopy(pi, po, count);	/* get zone */
***************
*** 678,686
      zap->s[3] = j;		/* set count */
      break;
    }
!   ddp.length = sizeof(zipatp) + len + ddpSize;
!   ip->p_len = lapSize+ddp.length;
!   ddpreply(ip, zipSkt);
    return;
  drop:
    K_PFREE(ip);

--- 685,692 -----
      zap->s[3] = j;		/* set count */
      break;
    }
!   ip->p_len = lapSize+ddpSize+sizeof(zipatp)+len;
!   ddpreply(ip, zipSkt, sizeof(zipatp) + len);
    return;
  drop:
    K_PFREE(ip);
%%%END OF PATCHES%%%

cck@CUNIXC.COLUMBIA.EDU (Charlie C. Kim) (05/12/88)

In a set of patches for KIP & AlisaTalk, modifications were made to
ddpreply to make it conform to the "ethertalk" specification better.
Unfortunately, I created a major bug in gw2.c that killed the
igpassign functions.

So, don't apply the patches to gw2.c and rtmp.c.  The patchs to gw.c
is correct.  When I fix the problems and verify the fixes, I will
resend (or post the location of) a set of diffs incorporating all
changes to be made against the baseline kip 01/88 code.

Charlie C. Kim
User Services
Columbia University

newsuser@LTH.Se (Lund Institute of Technology news server) (05/16/88)

In article <8805081514.AA06203@columbia.edu> cck@CUNIXA.COLUMBIA.EDU (Charlie C. Kim) writes:
>
>The first problem we found was that KIP was improperly using using the
>hop count in the ddp header as part of the length (a bug that I
>introduced I belive).

> [ ... ]

>The second problem found is far more serious.  In summary, KIP does
>not properly handle RTMP/ZIP transactions.  It has a minimal amount of
>code that allows it to accept RTMP from other bridges and send ZIP
>responses to queries.

> [ ... ]

>Following are set of patches:
>	gw.c - correct ethertalk length correction
>	gw2.c - fix ddpreply to be smart about sending short vs. long
>	   	ddp.  short ddp only to localtalk interface
>	      - fix nbpback to ensure allzones is set before use
>	      - update for modified ddpreply
>	rtmp.c - update for modified ddprely: rtmp & zip packets
>	      - initialize allzones
>	      - make ZIP GetMyZone return zone of the network the packet
>	 	came in upon, not the zone of the interface it came in upon
>		(suggested by Robert Elz).  This makes it useful when host
>		whose primary zone isn't that of the gw does a gmz

> [ ... ]

>Charlie C. Kim
>User Services
>Columbia University

We have a network with one Kinetics FastPath and one Hayes InterBridge. The
CAP server is not visible on the other (non-KFPS) side of the InterBridge. I
have looked at the packets on LocalTalk, and the InterBridge sends a ZIP
query every now and then, but the KFPS ignores it. This is a major problem
for us.

I don't want to introduce new bugs, and it seems to be a litte bit
difficult to compile the gateway code (find a vax, use the right compiler,
convert to s-records etc). So, if anybody (Charlie?) has already done it,
could you please upload it to the CAP (listserv) server at cuvma, post
it, or mail it to me?

Thanks!


-- 
Roland Mansson, Dept of Comp Sc, Lund University, Box 118, S-221 00 Lund, Sweden
Phone +46-46109640 (work), +46-46111539 (home)
USENET:roland@dna.lth.se   BITNET:LTHLIB@SELDC52   AppleLink:IT0073