Home | History | Annotate | Download | only in snoop
      1    0  stevel /*
      2    0  stevel  * CDDL HEADER START
      3    0  stevel  *
      4    0  stevel  * The contents of this file are subject to the terms of the
      5    0  stevel  * Common Development and Distribution License, Version 1.0 only
      6    0  stevel  * (the "License").  You may not use this file except in compliance
      7    0  stevel  * with the License.
      8    0  stevel  *
      9    0  stevel  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
     10    0  stevel  * or http://www.opensolaris.org/os/licensing.
     11    0  stevel  * See the License for the specific language governing permissions
     12    0  stevel  * and limitations under the License.
     13    0  stevel  *
     14    0  stevel  * When distributing Covered Code, include this CDDL HEADER in each
     15    0  stevel  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     16    0  stevel  * If applicable, add the following below this CDDL HEADER, with the
     17    0  stevel  * fields enclosed by brackets "[]" replaced with your own identifying
     18    0  stevel  * information: Portions Copyright [yyyy] [name of copyright owner]
     19    0  stevel  *
     20    0  stevel  * CDDL HEADER END
     21    0  stevel  */
     22    0  stevel /*
     23  410  kcpoon  * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
     24    0  stevel  * Use is subject to license terms.
     25    0  stevel  */
     26    0  stevel 
     27    0  stevel 
     28    0  stevel #pragma ident	"%Z%%M%	%I%	%E% SMI"
     29    0  stevel 
     30    0  stevel #include <stdio.h>
     31    0  stevel #include <string.h>
     32    0  stevel #include <fcntl.h>
     33    0  stevel #include <string.h>
     34    0  stevel #include <sys/types.h>
     35    0  stevel #include <sys/time.h>
     36    0  stevel 
     37    0  stevel #include <sys/socket.h>
     38    0  stevel #include <net/if.h>
     39    0  stevel #include <netinet/in_systm.h>
     40    0  stevel #include <netinet/in.h>
     41    0  stevel #include <netinet/ip.h>
     42    0  stevel #include <netinet/if_ether.h>
     43    0  stevel #include <netinet/tcp.h>
     44    0  stevel #include "snoop.h"
     45    0  stevel 
     46    0  stevel extern char *dlc_header;
     47    0  stevel 
     48    0  stevel #define	TCPOPT_HEADER_LEN	2
     49    0  stevel #define	TCPOPT_TSTAMP_LEN	10
     50    0  stevel #define	TCPOPT_SACK_LEN		8
     51    0  stevel 
     52    0  stevel /*
     53    0  stevel  * Convert a network byte order 32 bit integer to a host order integer.
     54    0  stevel  * ntohl() cannot be used because option values may not be aligned properly.
     55    0  stevel  */
     56    0  stevel #define	GET_UINT32(opt)	(((uint_t)*((uchar_t *)(opt) + 0) << 24) | \
     57    0  stevel 	((uint_t)*((uchar_t *)(opt) + 1) << 16) | \
     58    0  stevel 	((uint_t)*((uchar_t *)(opt) + 2) << 8) | \
     59    0  stevel 	((uint_t)*((uchar_t *)(opt) + 3)))
     60    0  stevel 
     61    0  stevel static void print_tcpoptions_summary(uchar_t *, int, char *);
     62    0  stevel static void print_tcpoptions(uchar_t *, int);
     63    0  stevel 
     64    0  stevel static const struct {
     65    0  stevel 	unsigned int	tf_flag;
     66    0  stevel 	const char	*tf_name;
     67    0  stevel } tcp_flags[] = {
     68    0  stevel 	{ TH_SYN, 	"Syn"	},
     69    0  stevel 	{ TH_FIN, 	"Fin"	},
     70    0  stevel 	{ TH_RST, 	"Rst"	},
     71    0  stevel 	{ TH_PUSH,	"Push"	},
     72    0  stevel 	{ TH_ECE,	"ECE"	},
     73    0  stevel 	{ TH_CWR,	"CWR"	},
     74    0  stevel 	{ 0,		NULL	}
     75    0  stevel };
     76    0  stevel 
     77    0  stevel int
     78    0  stevel interpret_tcp(int flags, struct tcphdr *tcp, int iplen, int fraglen)
     79    0  stevel {
     80    0  stevel 	char *data;
     81    0  stevel 	int hdrlen, tcplen;
     82    0  stevel 	int sunrpc = 0;
     83    0  stevel 	char *pname;
     84    0  stevel 	char buff[32];
     85    0  stevel 	char *line, *endline;
     86    0  stevel 	unsigned int i;
     87    0  stevel 
     88    0  stevel 	hdrlen = tcp->th_off * 4;
     89    0  stevel 	data = (char *)tcp + hdrlen;
     90    0  stevel 	tcplen = iplen - hdrlen;
     91    0  stevel 	fraglen -= hdrlen;
     92    0  stevel 	if (fraglen < 0)
     93  410  kcpoon 		return (fraglen + hdrlen);	/* incomplete header */
     94    0  stevel 	if (fraglen > tcplen)
     95    0  stevel 		fraglen = tcplen;
     96    0  stevel 
     97    0  stevel 	if (flags & F_SUM) {
     98    0  stevel 		line = get_sum_line();
     99    0  stevel 		endline = line + MAXLINE;
    100    0  stevel 		(void) snprintf(line, endline - line, "TCP D=%d S=%d",
    101    0  stevel 		    ntohs(tcp->th_dport), ntohs(tcp->th_sport));
    102    0  stevel 		line += strlen(line);
    103    0  stevel 
    104    0  stevel 		for (i = 0; tcp_flags[i].tf_name != NULL; i++) {
    105    0  stevel 			if (tcp->th_flags & tcp_flags[i].tf_flag) {
    106    0  stevel 				(void) snprintf(line, endline - line, " %s",
    107    0  stevel 				    tcp_flags[i].tf_name);
    108    0  stevel 				line += strlen(line);
    109    0  stevel 			}
    110    0  stevel 		}
    111    0  stevel 
    112    0  stevel 		if (tcp->th_flags & TH_URG) {
    113    0  stevel 			(void) snprintf(line, endline - line, " Urg=%u",
    114    0  stevel 			    ntohs(tcp->th_urp));
    115    0  stevel 			line += strlen(line);
    116    0  stevel 		}
    117    0  stevel 		if (tcp->th_flags & TH_ACK) {
    118    0  stevel 			(void) snprintf(line, endline - line, " Ack=%u",
    119    0  stevel 				ntohl(tcp->th_ack));
    120    0  stevel 			line += strlen(line);
    121    0  stevel 		}
    122    0  stevel 		if (ntohl(tcp->th_seq)) {
    123    0  stevel 			(void) snprintf(line, endline - line, " Seq=%u Len=%d",
    124    0  stevel 				ntohl(tcp->th_seq), tcplen);
    125    0  stevel 			line += strlen(line);
    126    0  stevel 		}
    127    0  stevel 		(void) snprintf(line, endline - line, " Win=%d",
    128    0  stevel 		    ntohs(tcp->th_win));
    129    0  stevel 		print_tcpoptions_summary((uchar_t *)(tcp + 1),
    130    0  stevel 		    (int)(tcp->th_off * 4 - sizeof (struct tcphdr)), line);
    131    0  stevel 	}
    132    0  stevel 
    133    0  stevel 	sunrpc = !reservedport(IPPROTO_TCP, ntohs(tcp->th_dport)) &&
    134    0  stevel 		!reservedport(IPPROTO_TCP, ntohs(tcp->th_sport)) &&
    135    0  stevel 		valid_rpc(data + 4, fraglen - 4);
    136    0  stevel 
    137    0  stevel 	if (flags & F_DTAIL) {
    138    0  stevel 
    139    0  stevel 	show_header("TCP:  ", "TCP Header", tcplen);
    140    0  stevel 	show_space();
    141  410  kcpoon 	(void) sprintf(get_line((char *)(uintptr_t)tcp->th_sport -
    142  410  kcpoon 		dlc_header, 2),	"Source port = %d", ntohs(tcp->th_sport));
    143    0  stevel 
    144    0  stevel 	if (sunrpc) {
    145    0  stevel 		pname = "(Sun RPC)";
    146    0  stevel 	} else {
    147    0  stevel 		pname = getportname(IPPROTO_TCP, ntohs(tcp->th_dport));
    148    0  stevel 		if (pname == NULL) {
    149    0  stevel 			pname = "";
    150    0  stevel 		} else {
    151    0  stevel 			(void) sprintf(buff, "(%s)", pname);
    152    0  stevel 			pname = buff;
    153    0  stevel 		}
    154    0  stevel 	}
    155  410  kcpoon 	(void) sprintf(get_line((char *)(uintptr_t)tcp->th_dport -
    156  410  kcpoon 		dlc_header, 2), "Destination port = %d %s",
    157    0  stevel 		ntohs(tcp->th_dport), pname);
    158  410  kcpoon 	(void) sprintf(get_line((char *)(uintptr_t)tcp->th_seq -
    159  410  kcpoon 		dlc_header, 4),	"Sequence number = %u",
    160    0  stevel 		ntohl(tcp->th_seq));
    161  410  kcpoon 	(void) sprintf(get_line((char *)(uintptr_t)tcp->th_ack - dlc_header, 4),
    162    0  stevel 		"Acknowledgement number = %u",
    163    0  stevel 		ntohl(tcp->th_ack));
    164  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_ack - dlc_header) +
    165  410  kcpoon 		4, 1), "Data offset = %d bytes", tcp->th_off * 4);
    166  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    167  410  kcpoon 		dlc_header) + 4, 1), "Flags = 0x%02x", tcp->th_flags);
    168  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    169  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_CWR,
    170  410  kcpoon 		"ECN congestion window reduced",
    171  410  kcpoon 		"No ECN congestion window reduced"));
    172  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    173  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_ECE,
    174  410  kcpoon 		"ECN echo", "No ECN echo"));
    175  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    176  410  kcpoon 		dlc_header) + 4, 1), "      %s",
    177    0  stevel 		getflag(tcp->th_flags, TH_URG,
    178  410  kcpoon 		"Urgent pointer", "No urgent pointer"));
    179  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    180  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_ACK,
    181  410  kcpoon 		"Acknowledgement", "No acknowledgement"));
    182  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    183  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_PUSH,
    184  410  kcpoon 		"Push", "No push"));
    185  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    186  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_RST,
    187  410  kcpoon 		"Reset", "No reset"));
    188  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    189  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_SYN,
    190  410  kcpoon 		"Syn", "No Syn"));
    191  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_flags -
    192  410  kcpoon 		dlc_header) + 4, 1), "      %s", getflag(tcp->th_flags, TH_FIN,
    193  410  kcpoon 		"Fin", "No Fin"));
    194  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_win - dlc_header) +
    195  410  kcpoon 		4, 1), "Window = %d", ntohs(tcp->th_win));
    196    0  stevel 	/* XXX need to compute checksum and print whether correct */
    197  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_sum - dlc_header) +
    198  410  kcpoon 		4, 1), "Checksum = 0x%04x", ntohs(tcp->th_sum));
    199  410  kcpoon 	(void) sprintf(get_line(((char *)(uintptr_t)tcp->th_urp - dlc_header) +
    200  410  kcpoon 		4, 1), "Urgent pointer = %d", ntohs(tcp->th_urp));
    201    0  stevel 
    202    0  stevel 	/* Print TCP options - if any */
    203    0  stevel 
    204    0  stevel 	print_tcpoptions((uchar_t *)(tcp + 1),
    205    0  stevel 	    tcp->th_off * 4 - sizeof (struct tcphdr));
    206    0  stevel 
    207    0  stevel 	show_space();
    208    0  stevel 	}
    209    0  stevel 
    210    0  stevel 	/* go to the next protocol layer */
    211    0  stevel 
    212    0  stevel 	if (!interpret_reserved(flags, IPPROTO_TCP,
    213    0  stevel 		ntohs(tcp->th_sport),
    214    0  stevel 		ntohs(tcp->th_dport),
    215    0  stevel 		data, fraglen)) {
    216    0  stevel 		if (sunrpc && fraglen > 0)
    217    0  stevel 			interpret_rpc(flags, data, fraglen, IPPROTO_TCP);
    218    0  stevel 	}
    219    0  stevel 
    220    0  stevel 	return (tcplen);
    221    0  stevel }
    222    0  stevel 
    223    0  stevel static void
    224    0  stevel print_tcpoptions(opt, optlen)
    225    0  stevel 	uchar_t *opt;
    226    0  stevel 	int optlen;
    227    0  stevel {
    228    0  stevel 	int	 len;
    229    0  stevel 	char	 *line;
    230    0  stevel 	uchar_t	*sack_opt;
    231    0  stevel 	uchar_t	*end_opt;
    232    0  stevel 	int	sack_len;
    233    0  stevel 
    234    0  stevel 	if (optlen <= 0) {
    235    0  stevel 		(void) sprintf(get_line((char *)&opt - dlc_header, 1),
    236    0  stevel 		"No options");
    237    0  stevel 		return;
    238    0  stevel 	}
    239    0  stevel 
    240    0  stevel 	(void) sprintf(get_line((char *)&opt - dlc_header, 1),
    241    0  stevel 	"Options: (%d bytes)", optlen);
    242    0  stevel 
    243    0  stevel 	while (optlen > 0) {
    244    0  stevel 		line = get_line((char *)&opt - dlc_header, 1);
    245    0  stevel 		len = opt[1];
    246    0  stevel 		switch (opt[0]) {
    247    0  stevel 		case TCPOPT_EOL:
    248    0  stevel 			(void) strcpy(line, "  - End of option list");
    249    0  stevel 			return;
    250    0  stevel 		case TCPOPT_NOP:
    251    0  stevel 			(void) strcpy(line, "  - No operation");
    252    0  stevel 			len = 1;
    253    0  stevel 			break;
    254    0  stevel 		case TCPOPT_MAXSEG:
    255    0  stevel 			(void) sprintf(line,
    256    0  stevel 			"  - Maximum segment size = %d bytes",
    257    0  stevel 				(opt[2] << 8) + opt[3]);
    258    0  stevel 			break;
    259    0  stevel 		case TCPOPT_WSCALE:
    260    0  stevel 			(void) sprintf(line, "  - Window scale = %d", opt[2]);
    261    0  stevel 			break;
    262    0  stevel 		case TCPOPT_TSTAMP:
    263    0  stevel 			/* Sanity check. */
    264    0  stevel 			if (optlen < TCPOPT_TSTAMP_LEN) {
    265    0  stevel 				(void) sprintf(line,
    266    0  stevel 				    "  - Incomplete TS option");
    267    0  stevel 			} else {
    268    0  stevel 				(void) sprintf(line,
    269    0  stevel 				    "  - TS Val = %u, TS Echo = %u",
    270    0  stevel 				    GET_UINT32(opt + 2),
    271    0  stevel 				    GET_UINT32(opt + 6));
    272    0  stevel 			}
    273    0  stevel 			break;
    274    0  stevel 		case TCPOPT_SACK_PERMITTED:
    275    0  stevel 			(void) sprintf(line, "  - SACK permitted option");
    276    0  stevel 			break;
    277    0  stevel 		case TCPOPT_SACK:
    278    0  stevel 			/*
    279    0  stevel 			 * Sanity check.  Total length should be greater
    280    0  stevel 			 * than just the option header length.
    281    0  stevel 			 */
    282    0  stevel 			if (len <= TCPOPT_HEADER_LEN ||
    283    0  stevel 			    opt[1] <= TCPOPT_HEADER_LEN || len < opt[1]) {
    284    0  stevel 				(void) sprintf(line,
    285    0  stevel 				    "  - Incomplete SACK option");
    286    0  stevel 				break;
    287    0  stevel 			}
    288    0  stevel 			sack_len = opt[1] - TCPOPT_HEADER_LEN;
    289    0  stevel 			sack_opt = opt + TCPOPT_HEADER_LEN;
    290    0  stevel 			end_opt = opt + optlen;
    291    0  stevel 
    292    0  stevel 			(void) sprintf(line, "  - SACK blocks:");
    293    0  stevel 			line = get_line((char *)&opt - dlc_header, 1);
    294    0  stevel 			(void) sprintf(line, "        ");
    295    0  stevel 			while (sack_len > 0) {
    296    0  stevel 				char sack_blk[MAXLINE + 1];
    297    0  stevel 
    298    0  stevel 				/*
    299    0  stevel 				 * sack_len may not tell us the truth about
    300    0  stevel 				 * the real length...  Need to be careful
    301    0  stevel 				 * not to step beyond the option buffer.
    302    0  stevel 				 */
    303    0  stevel 				if (sack_opt + TCPOPT_SACK_LEN > end_opt) {
    304    0  stevel 					(void) strcat(line,
    305    0  stevel 					    "...incomplete SACK block");
    306    0  stevel 					break;
    307    0  stevel 				}
    308    0  stevel 				(void) sprintf(sack_blk, "(%u-%u) ",
    309    0  stevel 				    GET_UINT32(sack_opt),
    310    0  stevel 				    GET_UINT32(sack_opt + 4));
    311    0  stevel 				(void) strcat(line, sack_blk);
    312    0  stevel 				sack_opt += TCPOPT_SACK_LEN;
    313    0  stevel 				sack_len -= TCPOPT_SACK_LEN;
    314    0  stevel 			}
    315    0  stevel 			break;
    316    0  stevel 		default:
    317    0  stevel 			(void) sprintf(line,
    318    0  stevel 			"  - Option %d (unknown - %d bytes) %s",
    319    0  stevel 				opt[0],
    320    0  stevel 				len - 2,
    321    0  stevel 				tohex((char *)&opt[2], len - 2));
    322    0  stevel 			break;
    323    0  stevel 		}
    324    0  stevel 		if (len <= 0) {
    325    0  stevel 			(void) sprintf(line, "  - Incomplete option len %d",
    326    0  stevel 				len);
    327    0  stevel 			break;
    328    0  stevel 		}
    329    0  stevel 		opt += len;
    330    0  stevel 		optlen -= len;
    331    0  stevel 	}
    332    0  stevel }
    333    0  stevel 
    334    0  stevel /*
    335    0  stevel  * This function is basically the same as print_tcpoptions() except that
    336    0  stevel  * all options are printed on the same line.
    337    0  stevel  */
    338    0  stevel static void
    339    0  stevel print_tcpoptions_summary(uchar_t *opt, int optlen, char *line)
    340    0  stevel {
    341    0  stevel 	int	 len;
    342    0  stevel 	uchar_t	*sack_opt;
    343    0  stevel 	uchar_t	*end_opt;
    344    0  stevel 	int	sack_len;
    345    0  stevel 	char	options[MAXLINE + 1];
    346    0  stevel 
    347    0  stevel 	if (optlen <= 0) {
    348    0  stevel 		return;
    349    0  stevel 	}
    350    0  stevel 
    351    0  stevel 	(void) strcat(line, " Options=<");
    352    0  stevel 	while (optlen > 0) {
    353    0  stevel 		len = opt[1];
    354    0  stevel 		switch (opt[0]) {
    355    0  stevel 		case TCPOPT_EOL:
    356    0  stevel 			(void) strcat(line, "eol>");
    357    0  stevel 			return;
    358    0  stevel 		case TCPOPT_NOP:
    359    0  stevel 			(void) strcat(line, "nop");
    360    0  stevel 			len = 1;
    361    0  stevel 			break;
    362    0  stevel 		case TCPOPT_MAXSEG:
    363    0  stevel 			(void) sprintf(options, "mss %d",
    364    0  stevel 			    (opt[2] << 8) + opt[3]);
    365    0  stevel 			(void) strcat(line, options);
    366    0  stevel 			break;
    367    0  stevel 		case TCPOPT_WSCALE:
    368    0  stevel 			(void) sprintf(options, "wscale %d", opt[2]);
    369    0  stevel 			(void) strcat(line, options);
    370    0  stevel 			break;
    371    0  stevel 		case TCPOPT_TSTAMP:
    372    0  stevel 			/* Sanity check. */
    373    0  stevel 			if (optlen < TCPOPT_TSTAMP_LEN) {
    374    0  stevel 				(void) strcat(line, "tstamp|");
    375    0  stevel 			} else {
    376    0  stevel 				(void) sprintf(options,
    377    0  stevel 				    "tstamp %u %u", GET_UINT32(opt + 2),
    378    0  stevel 				    GET_UINT32(opt + 6));
    379    0  stevel 				(void) strcat(line, options);
    380    0  stevel 			}
    381    0  stevel 			break;
    382    0  stevel 		case TCPOPT_SACK_PERMITTED:
    383    0  stevel 			(void) strcat(line, "sackOK");
    384    0  stevel 			break;
    385    0  stevel 		case TCPOPT_SACK:
    386    0  stevel 			/*
    387    0  stevel 			 * Sanity check.  Total length should be greater
    388    0  stevel 			 * than just the option header length.
    389    0  stevel 			 */
    390    0  stevel 			if (len <= TCPOPT_HEADER_LEN ||
    391    0  stevel 			    opt[1] <= TCPOPT_HEADER_LEN || len < opt[1]) {
    392    0  stevel 				(void) strcat(line, "sack|");
    393    0  stevel 				break;
    394    0  stevel 			}
    395    0  stevel 			sack_len = opt[1] - TCPOPT_HEADER_LEN;
    396    0  stevel 			sack_opt = opt + TCPOPT_HEADER_LEN;
    397    0  stevel 			end_opt = opt + optlen;
    398    0  stevel 
    399    0  stevel 			(void) strcat(line, "sack");
    400    0  stevel 			while (sack_len > 0) {
    401    0  stevel 				/*
    402    0  stevel 				 * sack_len may not tell us the truth about
    403    0  stevel 				 * the real length...  Need to be careful
    404    0  stevel 				 * not to step beyond the option buffer.
    405    0  stevel 				 */
    406    0  stevel 				if (sack_opt + TCPOPT_SACK_LEN > end_opt) {
    407    0  stevel 					(void) strcat(line, "|");
    408    0  stevel 					break;
    409    0  stevel 				}
    410    0  stevel 				(void) sprintf(options, " %u-%u",
    411    0  stevel 				    GET_UINT32(sack_opt),
    412    0  stevel 				    GET_UINT32(sack_opt + 4));
    413    0  stevel 				(void) strcat(line, options);
    414    0  stevel 				sack_opt += TCPOPT_SACK_LEN;
    415    0  stevel 				sack_len -= TCPOPT_SACK_LEN;
    416    0  stevel 			}
    417    0  stevel 			break;
    418    0  stevel 		default:
    419    0  stevel 			(void) sprintf(options, "unknown %d", opt[0]);
    420    0  stevel 			(void) strcat(line, options);
    421    0  stevel 			break;
    422    0  stevel 		}
    423    0  stevel 		if (len <= 0) {
    424    0  stevel 			(void) sprintf(options, "optlen %d", len);
    425    0  stevel 			(void) strcat(line, options);
    426    0  stevel 			break;
    427    0  stevel 		}
    428    0  stevel 		opt += len;
    429    0  stevel 		optlen -= len;
    430    0  stevel 		if (optlen > 0) {
    431    0  stevel 			(void) strcat(line, ",");
    432    0  stevel 		}
    433    0  stevel 	}
    434    0  stevel 	(void) strcat(line, ">");
    435    0  stevel }
    436