Date: Tue, 15 Apr 2003 19:17:57 +0200 From: Pawel Jakub Dawidek <nick@garage.freebsd.pl> To: freebsd-hackers@freebsd.org Cc: Poul-Henning Kamp <phk@FreeBSD.org> Subject: Multiple ip-numbers in jails (fixed INADDR_ANY behaviour). Message-ID: <20030415171757.GU52293@garage.freebsd.pl>
next in thread | raw e-mail | index | archive | help
--BcZrms9gUsdgyR6a Content-Type: multipart/mixed; boundary="V4yrq4dHtCqH+JvC" Content-Disposition: inline --V4yrq4dHtCqH+JvC Content-Type: text/plain; charset=iso-8859-2 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Hello hackers... I've just finished patch for multiple ip-numbers inside jails. There was a problem with handling INADDR_ANY correctly in multiple ips implementations, but I think I solved this problem. Another thing are priorities. When port X is opened on main host and in jail as INADDR_ANY, current implementation of jail converts INADDR_ANY to jail's IP. When we're connecting to this port we will connect to jail's daemon, because "exactly match" is there. In my solution looking for opened port is in this order: 1. non-jailed, non-wild. 2. non-jailed, wild. 3. jailed, non-wild. 4. jailed, wild. Please, review it. Thanks. PS. Patch is against FreeBSD-CURRENT. --=20 Pawel Jakub Dawidek pawel@dawidek.net UNIX Systems Programmer/Administrator http://garage.freebsd.pl Am I Evil? Yes, I Am! http://cerber.sourceforge.net --V4yrq4dHtCqH+JvC Content-Type: text/plain; charset=iso-8859-2 Content-Disposition: attachment; filename="mijail5.patch" Content-Transfer-Encoding: quoted-printable diff -ru /usr/src/sys/kern/kern_jail.c src/sys/kern/kern_jail.c --- /usr/src/sys/kern/kern_jail.c Wed Apr 9 04:55:17 2003 +++ src/sys/kern/kern_jail.c Tue Apr 15 12:39:34 2003 @@ -92,7 +92,7 @@ error =3D copyin(uap->jail, &j, sizeof j); if (error) return (error); - if (j.version !=3D 0) + if (j.version !=3D 1) return (EINVAL); =20 MALLOC(pr, struct prison *, sizeof *pr , M_PRISON, M_WAITOK | M_ZERO); @@ -115,7 +115,14 @@ error =3D copyinstr(j.hostname, &pr->pr_host, sizeof pr->pr_host, 0); if (error) goto e_dropvnref; - pr->pr_ip =3D j.ip_number; + if (j.nips >=3D JAIL_MAX_IPS) + goto e_dropvnref; + MALLOC(pr->pr_ips, u_int32_t *, sizeof(u_int32_t) * j.nips, + M_PRISON, M_WAITOK); + error =3D copyin(j.ips, pr->pr_ips, sizeof(u_int32_t) * j.nips); + if (error) + goto e_dropips; + pr->pr_nips =3D j.nips; pr->pr_linux =3D NULL; pr->pr_securelevel =3D securelevel; =20 @@ -131,7 +138,7 @@ if (tryprid =3D=3D JAIL_MAX) { mtx_unlock(&allprison_mtx); error =3D EAGAIN; - goto e_dropvnref; + goto e_dropips; } goto next; } @@ -154,6 +161,8 @@ LIST_REMOVE(pr, pr_list); prisoncount--; mtx_unlock(&allprison_mtx); +e_dropips: + FREE(pr->pr_ips, M_PRISON); e_dropvnref: mtx_lock(&Giant); vrele(pr->pr_root); @@ -270,6 +279,7 @@ mtx_destroy(&pr->pr_mtx); if (pr->pr_linux !=3D NULL) FREE(pr->pr_linux, M_PRISON); + FREE(pr->pr_ips, M_PRISON); FREE(pr, M_PRISON); return; } @@ -286,13 +296,6 @@ mtx_unlock(&pr->pr_mtx); } =20 -u_int32_t -prison_getip(struct ucred *cred) -{ - - return (cred->cr_prison->pr_ip); -} - int prison_ip(struct ucred *cred, int flag, u_int32_t *ip) { @@ -304,23 +307,16 @@ tmp =3D *ip; else tmp =3D ntohl(*ip); - if (tmp =3D=3D INADDR_ANY) { - if (flag)=20 - *ip =3D cred->cr_prison->pr_ip; - else - *ip =3D htonl(cred->cr_prison->pr_ip); - return (0); - } if (tmp =3D=3D INADDR_LOOPBACK) { if (flag) - *ip =3D cred->cr_prison->pr_ip; + *ip =3D cred->cr_prison->pr_ips[0]; else - *ip =3D htonl(cred->cr_prison->pr_ip); + *ip =3D htonl(cred->cr_prison->pr_ips[0]); return (0); } - if (cred->cr_prison->pr_ip !=3D tmp) - return (1); - return (0); + if (tmp =3D=3D INADDR_ANY || jailed_ip(cred, tmp)) + return (0); + return (1); } =20 void @@ -336,9 +332,9 @@ tmp =3D ntohl(*ip); if (tmp =3D=3D INADDR_LOOPBACK) { if (flag) - *ip =3D cred->cr_prison->pr_ip; + *ip =3D cred->cr_prison->pr_ips[0]; else - *ip =3D htonl(cred->cr_prison->pr_ip); + *ip =3D htonl(cred->cr_prison->pr_ips[0]); return; } return; @@ -354,13 +350,31 @@ ok =3D 1; else if (sai->sin_family !=3D AF_INET) ok =3D 0; - else if (cred->cr_prison->pr_ip !=3D ntohl(sai->sin_addr.s_addr)) + else if (!jailed_ip(cred, ntohl(sai->sin_addr.s_addr))) ok =3D 1; else ok =3D 0; return (ok); } =20 +int +jailed_ip(struct ucred *cred, u_int32_t ip) +{ + register struct prison *pr; + register u_int i; + + if (!jailed(cred)) + return(1); + + pr =3D cred->cr_prison; + for (i =3D 0; i < pr->pr_nips; ++i) { + if (pr->pr_ips[i] =3D=3D ip) + return (1); + } + + return (0); +} + /* * Return 0 if jails permit p1 to frob p2, otherwise ESRCH. */ @@ -439,7 +453,8 @@ xp->pr_id =3D pr->pr_id; strlcpy(xp->pr_path, pr->pr_path, sizeof(xp->pr_path)); strlcpy(xp->pr_host, pr->pr_host, sizeof(xp->pr_host)); - xp->pr_ip =3D pr->pr_ip; + memcpy(xp->pr_ips, pr->pr_ips, sizeof(u_int32_t) * pr->pr_nips); + xp->pr_nips =3D pr->pr_nips; mtx_unlock(&pr->pr_mtx); xp++; } diff -ru /usr/src/sys/netinet/in_pcb.c src/sys/netinet/in_pcb.c --- /usr/src/sys/netinet/in_pcb.c Fri Feb 21 06:28:27 2003 +++ src/sys/netinet/in_pcb.c Tue Apr 15 18:58:34 2003 @@ -249,7 +249,7 @@ struct in_addr laddr; u_short lport =3D 0; int wild =3D 0, reuseport =3D (so->so_options & SO_REUSEPORT); - int error, prison =3D 0; + int error; =20 if (TAILQ_EMPTY(&in_ifaddrhead)) /* XXX broken! */ return (EADDRNOTAVAIL); @@ -270,9 +270,8 @@ if (sin->sin_family !=3D AF_INET) return (EAFNOSUPPORT); #endif - if (sin->sin_addr.s_addr !=3D INADDR_ANY) - if (prison_ip(td->td_ucred, 0, &sin->sin_addr.s_addr)) - return(EINVAL); + if (prison_ip(td->td_ucred, 0, &sin->sin_addr.s_addr)) + return(EINVAL); if (sin->sin_port !=3D *lportp) { /* Don't allow the port to change. */ if (*lportp !=3D 0) @@ -304,13 +303,11 @@ ntohs(lport) >=3D ipport_reservedlow && td && suser_cred(td->td_ucred, PRISON_ROOT)) return (EACCES); - if (td && jailed(td->td_ucred)) - prison =3D 1; if (so->so_cred->cr_uid !=3D 0 && !IN_MULTICAST(ntohl(sin->sin_addr.s_addr))) { - t =3D in_pcblookup_local(inp->inp_pcbinfo, - sin->sin_addr, lport, - prison ? 0 : INPLOOKUP_WILDCARD); + t =3D in_pcblookup_local(td->td_ucred, + inp->inp_pcbinfo, sin->sin_addr, lport, + INPLOOKUP_WILDCARD); /* * XXX * This entire block sorely needs a rewrite. @@ -340,11 +337,10 @@ return (EADDRINUSE); } } - if (prison && - prison_ip(td->td_ucred, 0, &sin->sin_addr.s_addr)) + if (prison_ip(td->td_ucred, 0, &sin->sin_addr.s_addr)) return (EADDRNOTAVAIL); - t =3D in_pcblookup_local(pcbinfo, sin->sin_addr, - lport, prison ? 0 : wild); + t =3D in_pcblookup_local(td->td_ucred, pcbinfo, + sin->sin_addr, lport, wild); if (t && (t->inp_vflag & INP_TIMEWAIT)) { if ((reuseport & intotw(t)->tw_so_options) =3D=3D 0) return (EADDRINUSE); @@ -369,9 +365,8 @@ ushort first, last; int count; =20 - if (laddr.s_addr !=3D INADDR_ANY) - if (prison_ip(td->td_ucred, 0, &laddr.s_addr)) - return (EINVAL); + if (prison_ip(td->td_ucred, 0, &laddr.s_addr)) + return (EINVAL); =20 if (inp->inp_flags & INP_HIGHPORT) { first =3D ipport_hifirstauto; /* sysctl */ @@ -409,8 +404,8 @@ if (*lastport > first || *lastport < last) *lastport =3D first; lport =3D htons(*lastport); - } while (in_pcblookup_local(pcbinfo, laddr, lport, - wild)); + } while (in_pcblookup_local(td->td_ucred, pcbinfo, + laddr, lport, wild)); } else { /* * counting up @@ -424,8 +419,8 @@ if (*lastport < first || *lastport > last) *lastport =3D first; lport =3D htons(*lastport); - } while (in_pcblookup_local(pcbinfo, laddr, lport, - wild)); + } while (in_pcblookup_local(td->td_ucred, pcbinfo, + laddr, lport, wild)); } } if (prison_ip(td->td_ucred, 0, &laddr.s_addr)) @@ -529,16 +524,6 @@ faddr =3D sin->sin_addr; fport =3D sin->sin_port; cred =3D inp->inp_socket->so_cred; - if (laddr.s_addr =3D=3D INADDR_ANY && jailed(cred)) { - bzero(&sa, sizeof(sa)); - sa.sin_addr.s_addr =3D htonl(prison_getip(cred)); - sa.sin_len =3D sizeof(sa); - sa.sin_family =3D AF_INET; - error =3D in_pcbbind_setup(inp, (struct sockaddr *)&sa, - &laddr.s_addr, &lport, td); - if (error) - return (error); - } =20 if (!TAILQ_EMPTY(&in_ifaddrhead)) { /* @@ -548,9 +533,12 @@ * and the primary interface supports broadcast, * choose the broadcast address for that interface. */ - if (faddr.s_addr =3D=3D INADDR_ANY) - faddr =3D IA_SIN(TAILQ_FIRST(&in_ifaddrhead))->sin_addr; - else if (faddr.s_addr =3D=3D (u_long)INADDR_BROADCAST && + if (faddr.s_addr =3D=3D INADDR_ANY) { + if (jailed(cred)) + faddr.s_addr =3D htonl(cred->cr_prison->pr_ips[0]); + else + faddr =3D IA_SIN(TAILQ_FIRST(&in_ifaddrhead))->sin_addr; + } else if (faddr.s_addr =3D=3D (u_long)INADDR_BROADCAST && (TAILQ_FIRST(&in_ifaddrhead)->ia_ifp->if_flags & IFF_BROADCAST)) faddr =3D satosin(&TAILQ_FIRST( @@ -907,7 +895,8 @@ * Lookup a PCB based on the local address and port. */ struct inpcb * -in_pcblookup_local(pcbinfo, laddr, lport_arg, wild_okay) +in_pcblookup_local(cred, pcbinfo, laddr, lport_arg, wild_okay) + struct ucred *cred; struct inpcbinfo *pcbinfo; struct in_addr laddr; u_int lport_arg; @@ -931,7 +920,9 @@ #endif if (inp->inp_faddr.s_addr =3D=3D INADDR_ANY && inp->inp_laddr.s_addr =3D=3D laddr.s_addr && - inp->inp_lport =3D=3D lport) { + inp->inp_lport =3D=3D lport && + inp->inp_socket->so_cred->cr_prison =3D=3D + cred->cr_prison) { /* * Found. */ @@ -969,6 +960,10 @@ if ((inp->inp_vflag & INP_IPV4) =3D=3D 0) continue; #endif + if (inp->inp_socket->so_cred->cr_prison !=3D + cred->cr_prison) { + continue; + } if (inp->inp_faddr.s_addr !=3D INADDR_ANY) wildcard++; if (inp->inp_laddr.s_addr !=3D INADDR_ANY) { @@ -997,8 +992,7 @@ * Lookup PCB in hash list. */ struct inpcb * -in_pcblookup_hash(pcbinfo, faddr, fport_arg, laddr, lport_arg, wildcard, - ifp) +in_pcblookup_hash(pcbinfo, faddr, fport_arg, laddr, lport_arg, wildcard, i= fp) struct inpcbinfo *pcbinfo; struct in_addr faddr, laddr; u_int fport_arg, lport_arg; @@ -1006,7 +1000,7 @@ struct ifnet *ifp; { struct inpcbhead *head; - register struct inpcb *inp; + register struct inpcb *inp, *jinp =3D NULL; u_short fport =3D fport_arg, lport =3D lport_arg; =20 /* @@ -1025,15 +1019,31 @@ /* * Found. */ - return (inp); + if (inp->inp_socket->so_cred->cr_prison =3D=3D NULL) + return (inp); + else { + if (jinp =3D=3D NULL) + jinp =3D inp; + } } } + if (jinp !=3D NULL) + return (jinp); if (wildcard) { struct inpcb *local_wild =3D NULL; #if defined(INET6) struct inpcb *local_wild_mapped =3D NULL; #endif /* defined(INET6) */ + struct inpcb *jinp_wild =3D NULL; + struct ucred *cred; =20 + /* + * Order of socket selection: + * 1. non-jailed, non-wild. + * 2. non-jailed, wild. + * 3. jailed, non-wild. + * 4. jailed, wild. + */ head =3D &pcbinfo->hashbase[INP_PCBHASH(INADDR_ANY, lport, 0, pcbinfo->h= ashmask)]; LIST_FOREACH(inp, head, inp_hash) { #ifdef INET6 @@ -1045,24 +1055,43 @@ if (ifp && ifp->if_type =3D=3D IFT_FAITH && (inp->inp_flags & INP_FAITH) =3D=3D 0) continue; - if (inp->inp_laddr.s_addr =3D=3D laddr.s_addr) - return (inp); - else if (inp->inp_laddr.s_addr =3D=3D INADDR_ANY) { + cred =3D inp->inp_socket->so_cred; + if (jailed(cred)) { + if (jinp !=3D NULL) + continue; + if (!jailed_ip(cred, + ntohl(laddr.s_addr))) { + continue; + } + } + if (inp->inp_laddr.s_addr =3D=3D laddr.s_addr) { + if (jailed(cred)) + jinp =3D inp; + else + return (inp); + } else if (inp->inp_laddr.s_addr =3D=3D INADDR_ANY) { #if defined(INET6) if (INP_CHECK_SOCKAF(inp->inp_socket, AF_INET6)) local_wild_mapped =3D inp; else #endif /* defined(INET6) */ - local_wild =3D inp; + if (jailed(cred)) + jinp_wild =3D inp; + else + local_wild =3D inp; } } } + if (local_wild !=3D NULL) + return (local_wild); #if defined(INET6) - if (local_wild =3D=3D NULL) + if (local_wild_mapped !=3D NULL) return (local_wild_mapped); #endif /* defined(INET6) */ - return (local_wild); + if (jinp !=3D NULL) + return (jinp); + return (jinp_wild); } =20 /* @@ -1176,7 +1205,7 @@ { if (!jailed(td->td_ucred)) return (0); - if (ntohl(inp->inp_laddr.s_addr) =3D=3D prison_getip(td->td_ucred)) + if (jailed_ip(td->td_ucred, ntohl(inp->inp_laddr.s_addr))) return (0); return (1); } diff -ru /usr/src/sys/netinet/in_pcb.h src/sys/netinet/in_pcb.h --- /usr/src/sys/netinet/in_pcb.h Wed Apr 2 22:14:43 2003 +++ src/sys/netinet/in_pcb.h Tue Apr 15 11:47:57 2003 @@ -40,6 +40,7 @@ #include <sys/queue.h> #include <sys/_lock.h> #include <sys/_mutex.h> +#include <sys/ucred.h> =20 #include <net/route.h> =20 @@ -340,7 +341,7 @@ void in_pcbdisconnect(struct inpcb *); int in_pcbinshash(struct inpcb *); struct inpcb * - in_pcblookup_local(struct inpcbinfo *, + in_pcblookup_local(struct ucred *, struct inpcbinfo *, struct in_addr, u_int, int); struct inpcb * in_pcblookup_hash(struct inpcbinfo *, struct in_addr, u_int, diff -ru /usr/src/sys/netinet6/in6_pcb.c src/sys/netinet6/in6_pcb.c --- /usr/src/sys/netinet6/in6_pcb.c Wed Feb 19 23:32:42 2003 +++ src/sys/netinet6/in6_pcb.c Tue Apr 15 18:00:45 2003 @@ -211,9 +211,9 @@ struct sockaddr_in sin; =20 in6_sin6_2_sin(&sin, sin6); - t =3D in_pcblookup_local(pcbinfo, - sin.sin_addr, lport, - INPLOOKUP_WILDCARD); + t =3D in_pcblookup_local(td->td_ucred, + pcbinfo, sin.sin_addr, lport, + INPLOOKUP_WILDCARD); if (t && (so->so_cred->cr_uid !=3D t->inp_socket->so_cred->cr_uid) && @@ -233,8 +233,8 @@ struct sockaddr_in sin; =20 in6_sin6_2_sin(&sin, sin6); - t =3D in_pcblookup_local(pcbinfo, sin.sin_addr, - lport, wild); + t =3D in_pcblookup_local(td->td_ucred, pcbinfo, + sin.sin_addr, lport, wild); if (t && (reuseport & t->inp_socket->so_options) =3D=3D 0 && diff -ru /usr/src/sys/sys/jail.h src/sys/sys/jail.h --- /usr/src/sys/sys/jail.h Wed Apr 9 04:55:18 2003 +++ src/sys/sys/jail.h Tue Apr 15 12:38:23 2003 @@ -17,17 +17,21 @@ u_int32_t version; char *path; char *hostname; - u_int32_t ip_number; + u_int32_t *ips; + u_int nips; }; =20 +#define JAIL_MAX_IPS 256 + struct xprison { - int pr_version; - int pr_id; - char pr_path[MAXPATHLEN]; - char pr_host[MAXHOSTNAMELEN]; - u_int32_t pr_ip; + int pr_version; + int pr_id; + char pr_path[MAXPATHLEN]; + char pr_host[MAXHOSTNAMELEN]; + u_int32_t pr_ips[JAIL_MAX_IPS]; + u_int pr_nips; }; -#define XPRISON_VERSION 1 +#define XPRISON_VERSION 2 =20 #ifndef _KERNEL =20 @@ -65,7 +69,8 @@ char pr_path[MAXPATHLEN]; /* (c) chroot path */ struct vnode *pr_root; /* (c) vnode to rdir */ char pr_host[MAXHOSTNAMELEN]; /* (p) jail hostname */ - u_int32_t pr_ip; /* (c) ip addr host */ + u_int32_t *pr_ips; /* (c) ip addr host */ + u_int pr_nips; void *pr_linux; /* (p) linux abi */ int pr_securelevel; /* (p) securelevel */ struct mtx pr_mtx; @@ -92,11 +97,11 @@ void getcredhostname(struct ucred *cred, char *, size_t); int prison_check(struct ucred *cred1, struct ucred *cred2); void prison_free(struct prison *pr); -u_int32_t prison_getip(struct ucred *cred); void prison_hold(struct prison *pr); int prison_if(struct ucred *cred, struct sockaddr *sa); int prison_ip(struct ucred *cred, int flag, u_int32_t *ip); void prison_remote_ip(struct ucred *cred, int flags, u_int32_t *ip); +int jailed_ip(struct ucred *cred, u_int32_t ip); =20 #endif /* !_KERNEL */ #endif /* !_SYS_JAIL_H_ */ diff -ru /usr/src/usr.sbin/jail/jail.c src/usr.sbin/jail/jail.c --- /usr/src/usr.sbin/jail/jail.c Wed Apr 9 05:04:12 2003 +++ src/usr.sbin/jail/jail.c Tue Apr 15 12:30:01 2003 @@ -36,6 +36,8 @@ struct in_addr in; int ch, groups[NGROUPS], i, iflag, ngroups; char *username; + int c; + char *ip; =20 iflag =3D 0; username =3D NULL; @@ -71,12 +73,25 @@ if (chdir(argv[0]) !=3D 0) err(1, "chdir: %s", argv[0]); memset(&j, 0, sizeof(j)); - j.version =3D 0; + j.version =3D 1; j.path =3D argv[0]; j.hostname =3D argv[1]; - if (inet_aton(argv[2], &in) =3D=3D 0) - errx(1, "Could not make sense of ip-number: %s", argv[2]); - j.ip_number =3D ntohl(in.s_addr); + for (c =3D 1, ip =3D argv[2]; *ip; ++ip) { + if (*ip =3D=3D ',') + ++c; + } + if ((j.ips =3D (u_int32_t *)malloc(sizeof(u_int32_t) * c)) =3D=3D NULL) + errx(1, "malloc()"); + for (c =3D 0, ip =3D strtok(argv[2], ","); ip; + ++c, ip =3D strtok(NULL, ",")) { + i =3D inet_aton(ip, &in); + if (!i) { + free(j.ips); + errx(1, "Couldn't make sense of ip-number\n"); + } + j.ips[c] =3D ntohl(in.s_addr); + } + j.nips =3D c; i =3D jail(&j); if (i =3D=3D -1) err(1, "jail"); @@ -102,6 +117,6 @@ { =20 (void)fprintf(stderr, - "usage: jail [-i] [-u username] path hostname ip-number command ...\n"); + "usage: jail [-i] [-u username] path hostname ip1[,ip2[...]] command ...\= n"); exit(1); } diff -ru /usr/src/usr.sbin/jls/jls.c src/usr.sbin/jls/jls.c --- /usr/src/usr.sbin/jls/jls.c Wed Apr 9 05:04:12 2003 +++ src/usr.sbin/jls/jls.c Tue Apr 15 12:47:58 2003 @@ -43,7 +43,7 @@ {=20 struct xprison *sxp, *xp; struct in_addr in; - size_t i, len; + size_t i, j, len; =20 if (sysctlbyname("security.jail.list", NULL, &len, NULL, 0) =3D=3D -1) err(1, "sysctlbyname(): security.jail.list"); @@ -67,9 +67,13 @@ =20 printf(" JID IP Address Hostname Path\n"); for (i =3D 0; i < len / sizeof(*xp); i++) { - in.s_addr =3D ntohl(xp->pr_ip); + in.s_addr =3D ntohl(xp->pr_ips[0]); printf("%6d %-12.12s %-29.29s %.77s\n", xp->pr_id, inet_ntoa(in), xp->pr_host, xp->pr_path); + for (j =3D 1; j < xp->pr_nips; ++j) { + in.s_addr =3D ntohl(xp->pr_ips[j]); + printf(" %-12.12s\n", inet_ntoa(in)); + } xp++; } free(sxp); --V4yrq4dHtCqH+JvC-- --BcZrms9gUsdgyR6a Content-Type: application/pgp-signature Content-Disposition: inline -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.0.7 (FreeBSD) iQCVAwUBPpw+xT/PhmMH/Mf1AQEavwQAmaG3u/QiwAfUG5PMPAMWfROPzl+SGHgA CfN5jx3S5ZPMI3awwASNYNF1WutHH++na/85tYU/zL6BKr/mKSZC2zW2WN/PFNul orRmBoYKXahige2JBYn8yc+IHbNwNf8AGNsqnEjWWDD6PRP9FFDySop7kymXcoSM qRmPircf0yo= =1/2N -----END PGP SIGNATURE----- --BcZrms9gUsdgyR6a--
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20030415171757.GU52293>