| /* |
| * (C)Copyright March, 2000 - Darren Reed. |
| */ |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/mman.h> |
| #include <sys/socket.h> |
| #include <sys/time.h> |
| #include <sys/ioctl.h> |
| |
| #include <netinet/in.h> |
| #include <netinet/in_systm.h> |
| #include <netinet/ip.h> |
| |
| #include <net/if.h> |
| |
| #include <stdio.h> |
| #include <netdb.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include <fcntl.h> |
| #include <errno.h> |
| #include <stdlib.h> |
| |
| #include "ip_compat.h" |
| #include "ip_fil.h" |
| #include "ip_nat.h" |
| #include "ipl.h" |
| |
| #include "ipf.h" |
| |
| extern char *optarg; |
| |
| |
| typedef struct l4cfg { |
| struct l4cfg *l4_next; |
| struct ipnat l4_nat; /* NAT rule */ |
| struct sockaddr_in l4_sin; /* remote socket to connect */ |
| time_t l4_last; /* when we last connected */ |
| int l4_alive; /* 1 = remote alive */ |
| int l4_fd; |
| int l4_rw; /* 0 = reading, 1 = writing */ |
| char *l4_rbuf; /* read buffer */ |
| int l4_rsize; /* size of buffer */ |
| int l4_rlen; /* how much used */ |
| char *l4_wptr; /* next byte to write */ |
| int l4_wlen; /* length yet to be written */ |
| } l4cfg_t; |
| |
| |
| l4cfg_t *l4list = NULL; |
| char *response = NULL; |
| char *probe = NULL; |
| l4cfg_t template; |
| int frequency = 20; |
| int ctimeout = 1; |
| int rtimeout = 1; |
| size_t plen = 0; |
| size_t rlen = 0; |
| int natfd = -1; |
| int opts = 0; |
| |
| #if defined(sun) && !defined(__svr4__) && !defined(__SVR4) |
| # define strerror(x) sys_errlist[x] |
| #endif |
| |
| |
| char *copystr(dst, src) |
| char *dst, *src; |
| { |
| register char *s, *t, c; |
| register int esc = 0; |
| |
| for (s = src, t = dst; s && t && (c = *s++); ) |
| if (esc) { |
| esc = 0; |
| switch (c) |
| { |
| case 'n' : |
| *t++ = '\n'; |
| break; |
| case 'r' : |
| *t++ = '\r'; |
| break; |
| case 't' : |
| *t++ = '\t'; |
| break; |
| } |
| } else if (c != '\\') |
| *t++ = c; |
| else |
| esc = 1; |
| *t = '\0'; |
| return dst; |
| } |
| |
| void addnat(l4) |
| l4cfg_t *l4; |
| { |
| |
| ipnat_t *ipn = &l4->l4_nat; |
| |
| printf("Add NAT rule for %s/%#x,%u -> ", inet_ntoa(ipn->in_out[0].in4), |
| ipn->in_outmsk, ntohs(ipn->in_pmin)); |
| printf("%s,%u\n", inet_ntoa(ipn->in_in[0].in4), ntohs(ipn->in_pnext)); |
| if (!(opts & OPT_DONOTHING)) { |
| ipfobj_t obj; |
| |
| bzero(&obj, sizeof(obj)); |
| obj.ipfo_rev = IPFILTER_VERSION; |
| obj.ipfo_size = sizeof(*ipn); |
| obj.ipfo_ptr = ipn; |
| |
| if (ioctl(natfd, SIOCADNAT, &obj) == -1) |
| perror("ioctl(SIOCADNAT)"); |
| } |
| } |
| |
| |
| void delnat(l4) |
| l4cfg_t *l4; |
| { |
| ipnat_t *ipn = &l4->l4_nat; |
| |
| printf("Remove NAT rule for %s/%#x,%u -> ", |
| inet_ntoa(ipn->in_out[0].in4), ipn->in_outmsk, ipn->in_pmin); |
| printf("%s,%u\n", inet_ntoa(ipn->in_in[0].in4), ipn->in_pnext); |
| if (!(opts & OPT_DONOTHING)) { |
| ipfobj_t obj; |
| |
| bzero(&obj, sizeof(obj)); |
| obj.ipfo_rev = IPFILTER_VERSION; |
| obj.ipfo_size = sizeof(*ipn); |
| obj.ipfo_ptr = ipn; |
| |
| if (ioctl(natfd, SIOCRMNAT, &ipn) == -1) |
| perror("ioctl(SIOCRMNAT)"); |
| } |
| } |
| |
| |
| void connectl4(l4) |
| l4cfg_t *l4; |
| { |
| l4->l4_rw = 1; |
| l4->l4_rlen = 0; |
| l4->l4_wlen = plen; |
| if (!l4->l4_wlen) { |
| l4->l4_alive = 1; |
| addnat(l4); |
| } else |
| l4->l4_wptr = probe; |
| } |
| |
| |
| void closel4(l4, dead) |
| l4cfg_t *l4; |
| int dead; |
| { |
| close(l4->l4_fd); |
| l4->l4_fd = -1; |
| l4->l4_rw = -1; |
| if (dead && l4->l4_alive) { |
| l4->l4_alive = 0; |
| delnat(l4); |
| } |
| } |
| |
| |
| void connectfd(l4) |
| l4cfg_t *l4; |
| { |
| if (connect(l4->l4_fd, (struct sockaddr *)&l4->l4_sin, |
| sizeof(l4->l4_sin)) == -1) { |
| if (errno == EISCONN) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Connected fd %d\n", |
| l4->l4_fd); |
| connectl4(l4); |
| return; |
| } |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Connect failed fd %d: %s\n", |
| l4->l4_fd, strerror(errno)); |
| closel4(l4, 1); |
| return; |
| } |
| l4->l4_rw = 1; |
| } |
| |
| |
| void writefd(l4) |
| l4cfg_t *l4; |
| { |
| int n, i, fd; |
| |
| fd = l4->l4_fd; |
| |
| if (l4->l4_rw == -2) { |
| connectfd(l4); |
| return; |
| } |
| |
| n = l4->l4_wlen; |
| |
| i = send(fd, l4->l4_wptr, n, 0); |
| if (i == 0 || i == -1) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Send on fd %d failed: %s\n", |
| fd, strerror(errno)); |
| closel4(l4, 1); |
| } else { |
| l4->l4_wptr += i; |
| l4->l4_wlen -= i; |
| if (l4->l4_wlen == 0) |
| l4->l4_rw = 0; |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Sent %d bytes to fd %d\n", i, fd); |
| } |
| } |
| |
| |
| void readfd(l4) |
| l4cfg_t *l4; |
| { |
| char buf[80], *ptr; |
| int n, i, fd; |
| |
| fd = l4->l4_fd; |
| |
| if (l4->l4_rw == -2) { |
| connectfd(l4); |
| return; |
| } |
| |
| if (l4->l4_rsize) { |
| n = l4->l4_rsize - l4->l4_rlen; |
| ptr = l4->l4_rbuf + l4->l4_rlen; |
| } else { |
| n = sizeof(buf) - 1; |
| ptr = buf; |
| } |
| |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Read %d bytes on fd %d to %p\n", |
| n, fd, ptr); |
| i = recv(fd, ptr, n, 0); |
| if (i == 0 || i == -1) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Read error on fd %d: %s\n", |
| fd, (i == 0) ? "EOF" : strerror(errno)); |
| closel4(l4, 1); |
| } else { |
| if (ptr == buf) |
| ptr[i] = '\0'; |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "%d: Read %d bytes [%*.*s]\n", |
| fd, i, i, i, ptr); |
| if (ptr != buf) { |
| l4->l4_rlen += i; |
| if (l4->l4_rlen >= l4->l4_rsize) { |
| if (!strncmp(response, l4->l4_rbuf, |
| l4->l4_rsize)) { |
| printf("%d: Good response\n", |
| fd); |
| if (!l4->l4_alive) { |
| l4->l4_alive = 1; |
| addnat(l4); |
| } |
| closel4(l4, 0); |
| } else { |
| if (opts & OPT_VERBOSE) |
| printf("%d: Bad response\n", |
| fd); |
| closel4(l4, 1); |
| } |
| } |
| } else if (!l4->l4_alive) { |
| l4->l4_alive = 1; |
| addnat(l4); |
| closel4(l4, 0); |
| } |
| } |
| } |
| |
| |
| int runconfig() |
| { |
| int fd, opt, res, mfd, i; |
| struct timeval tv; |
| time_t now, now1; |
| fd_set rfd, wfd; |
| l4cfg_t *l4; |
| |
| mfd = 0; |
| opt = 1; |
| now = time(NULL); |
| |
| /* |
| * First, initiate connections that are closed, as required. |
| */ |
| for (l4 = l4list; l4; l4 = l4->l4_next) { |
| if ((l4->l4_last + frequency < now) && (l4->l4_fd == -1)) { |
| l4->l4_last = now; |
| fd = socket(AF_INET, SOCK_STREAM, 0); |
| if (fd == -1) |
| continue; |
| setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, |
| sizeof(opt)); |
| #ifdef O_NONBLOCK |
| if ((res = fcntl(fd, F_GETFL, 0)) != -1) |
| fcntl(fd, F_SETFL, res | O_NONBLOCK); |
| #endif |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "Connecting to %s,%d (fd %d)...", |
| inet_ntoa(l4->l4_sin.sin_addr), |
| ntohs(l4->l4_sin.sin_port), fd); |
| if (connect(fd, (struct sockaddr *)&l4->l4_sin, |
| sizeof(l4->l4_sin)) == -1) { |
| if (errno != EINPROGRESS) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "failed\n"); |
| perror("connect"); |
| close(fd); |
| fd = -1; |
| } else { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "waiting\n"); |
| l4->l4_rw = -2; |
| } |
| } else { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "connected\n"); |
| connectl4(l4); |
| } |
| l4->l4_fd = fd; |
| } |
| } |
| |
| /* |
| * Now look for fd's which we're expecting to read/write from. |
| */ |
| FD_ZERO(&rfd); |
| FD_ZERO(&wfd); |
| tv.tv_sec = MIN(rtimeout, ctimeout); |
| tv.tv_usec = 0; |
| |
| for (l4 = l4list; l4; l4 = l4->l4_next) |
| if (l4->l4_rw == 0) { |
| if (now - l4->l4_last > rtimeout) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "%d: Read timeout\n", |
| l4->l4_fd); |
| closel4(l4, 1); |
| continue; |
| } |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Wait for read on fd %d\n", |
| l4->l4_fd); |
| FD_SET(l4->l4_fd, &rfd); |
| if (l4->l4_fd > mfd) |
| mfd = l4->l4_fd; |
| } else if ((l4->l4_rw == 1 && l4->l4_wlen) || |
| l4->l4_rw == -2) { |
| if ((l4->l4_rw == -2) && |
| (now - l4->l4_last > ctimeout)) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "%d: connect timeout\n", |
| l4->l4_fd); |
| closel4(l4); |
| continue; |
| } |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Wait for write on fd %d\n", |
| l4->l4_fd); |
| FD_SET(l4->l4_fd, &wfd); |
| if (l4->l4_fd > mfd) |
| mfd = l4->l4_fd; |
| } |
| |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Select: max fd %d wait %d\n", mfd + 1, |
| tv.tv_sec); |
| i = select(mfd + 1, &rfd, &wfd, NULL, &tv); |
| if (i == -1) { |
| perror("select"); |
| return -1; |
| } |
| |
| now1 = time(NULL); |
| |
| for (l4 = l4list; (i > 0) && l4; l4 = l4->l4_next) { |
| if (l4->l4_fd < 0) |
| continue; |
| if (FD_ISSET(l4->l4_fd, &rfd)) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Ready to read on fd %d\n", |
| l4->l4_fd); |
| readfd(l4); |
| i--; |
| } |
| |
| if ((l4->l4_fd >= 0) && FD_ISSET(l4->l4_fd, &wfd)) { |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Ready to write on fd %d\n", |
| l4->l4_fd); |
| writefd(l4); |
| i--; |
| } |
| } |
| return 0; |
| } |
| |
| |
| int gethostport(str, lnum, ipp, portp) |
| char *str; |
| int lnum; |
| u_32_t *ipp; |
| u_short *portp; |
| { |
| struct servent *sp; |
| struct hostent *hp; |
| char *host, *port; |
| |
| host = str; |
| port = strchr(host, ','); |
| if (port) |
| *port++ = '\0'; |
| |
| #ifdef HAVE_INET_ATON |
| if (ISDIGIT(*host) && inet_aton(host, &ip)) |
| *ipp = ip.s_addr; |
| #else |
| if (ISDIGIT(*host)) |
| *ipp = inet_addr(host); |
| #endif |
| else { |
| if (!(hp = gethostbyname(host))) { |
| fprintf(stderr, "%d: can't resolve hostname: %s\n", |
| lnum, host); |
| return 0; |
| } |
| *ipp = *(u_32_t *)hp->h_addr; |
| } |
| |
| if (port) { |
| if (ISDIGIT(*port)) |
| *portp = htons(atoi(port)); |
| else { |
| sp = getservbyname(port, "tcp"); |
| if (sp) |
| *portp = sp->s_port; |
| else { |
| fprintf(stderr, "%d: unknown service %s\n", |
| lnum, port); |
| return 0; |
| } |
| } |
| } else |
| *portp = 0; |
| return 1; |
| } |
| |
| |
| char *mapfile(file, sizep) |
| char *file; |
| size_t *sizep; |
| { |
| struct stat sb; |
| caddr_t addr; |
| int fd; |
| |
| fd = open(file, O_RDONLY); |
| if (fd == -1) { |
| perror("open(mapfile)"); |
| return NULL; |
| } |
| |
| if (fstat(fd, &sb) == -1) { |
| perror("fstat(mapfile)"); |
| close(fd); |
| return NULL; |
| } |
| |
| addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0); |
| if (addr == (caddr_t)-1) { |
| perror("mmap(mapfile)"); |
| close(fd); |
| return NULL; |
| } |
| close(fd); |
| *sizep = sb.st_size; |
| return (char *)addr; |
| } |
| |
| |
| int readconfig(filename) |
| char *filename; |
| { |
| char c, buf[512], *s, *t, *errtxt = NULL, *line; |
| int num, err = 0; |
| ipnat_t *ipn; |
| l4cfg_t *l4; |
| FILE *fp; |
| |
| fp = fopen(filename, "r"); |
| if (!fp) { |
| perror("open(configfile)"); |
| return -1; |
| } |
| |
| bzero((char *)&template, sizeof(template)); |
| template.l4_fd = -1; |
| template.l4_rw = -1; |
| template.l4_sin.sin_family = AF_INET; |
| ipn = &template.l4_nat; |
| ipn->in_flags = IPN_TCP|IPN_ROUNDR; |
| ipn->in_redir = NAT_REDIRECT; |
| |
| for (num = 1; fgets(buf, sizeof(buf), fp); num++) { |
| s = strchr(buf, '\n'); |
| if (!s) { |
| fprintf(stderr, "%d: line too long\n", num); |
| fclose(fp); |
| return -1; |
| } |
| |
| *s = '\0'; |
| |
| /* |
| * lines which are comments |
| */ |
| s = strchr(buf, '#'); |
| if (s) |
| *s = '\0'; |
| |
| /* |
| * Skip leading whitespace |
| */ |
| for (line = buf; (c = *line) && ISSPACE(c); line++) |
| ; |
| if (!*line) |
| continue; |
| |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Parsing: [%s]\n", line); |
| t = strtok(line, " \t"); |
| if (!t) |
| continue; |
| if (!strcasecmp(t, "interface")) { |
| s = strtok(NULL, " \t"); |
| if (s) |
| t = strtok(NULL, "\t"); |
| if (!s || !t) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| |
| if (!strchr(t, ',')) { |
| fprintf(stderr, |
| "%d: local address,port missing\n", |
| num); |
| err = -1; |
| break; |
| } |
| |
| strncpy(ipn->in_ifnames[0], s, LIFNAMSIZ); |
| strncpy(ipn->in_ifnames[1], s, LIFNAMSIZ); |
| if (!gethostport(t, num, &ipn->in_outip, |
| &ipn->in_pmin)) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| ipn->in_outmsk = 0xffffffff; |
| ipn->in_pmax = ipn->in_pmin; |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "Interface %s %s/%#x port %u\n", |
| ipn->in_ifnames[0], |
| inet_ntoa(ipn->in_out[0].in4), |
| ipn->in_outmsk, ipn->in_pmin); |
| } else if (!strcasecmp(t, "remote")) { |
| if (!*ipn->in_ifnames[0]) { |
| fprintf(stderr, |
| "%d: ifname not set prior to remote\n", |
| num); |
| err = -1; |
| break; |
| } |
| s = strtok(NULL, " \t"); |
| if (s) |
| t = strtok(NULL, ""); |
| if (!s || !t || strcasecmp(s, "server")) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| |
| ipn->in_pnext = 0; |
| if (!gethostport(t, num, &ipn->in_inip, |
| &ipn->in_pnext)) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| ipn->in_inmsk = 0xffffffff; |
| if (ipn->in_pnext == 0) |
| ipn->in_pnext = ipn->in_pmin; |
| |
| l4 = (l4cfg_t *)malloc(sizeof(*l4)); |
| if (!l4) { |
| fprintf(stderr, "%d: out of memory (%d)\n", |
| num, sizeof(*l4)); |
| err = -1; |
| break; |
| } |
| bcopy((char *)&template, (char *)l4, sizeof(*l4)); |
| l4->l4_sin.sin_addr = ipn->in_in[0].in4; |
| l4->l4_sin.sin_port = ipn->in_pnext; |
| l4->l4_next = l4list; |
| l4list = l4; |
| } else if (!strcasecmp(t, "connect")) { |
| s = strtok(NULL, " \t"); |
| if (s) |
| t = strtok(NULL, "\t"); |
| if (!s || !t) { |
| errtxt = line; |
| err = -1; |
| break; |
| } else if (!strcasecmp(s, "timeout")) { |
| ctimeout = atoi(t); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "connect timeout %d\n", |
| ctimeout); |
| } else if (!strcasecmp(s, "frequency")) { |
| frequency = atoi(t); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "connect frequency %d\n", |
| frequency); |
| } else { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| } else if (!strcasecmp(t, "probe")) { |
| s = strtok(NULL, " \t"); |
| if (!s) { |
| errtxt = line; |
| err = -1; |
| break; |
| } else if (!strcasecmp(s, "string")) { |
| if (probe) { |
| fprintf(stderr, |
| "%d: probe already set\n", |
| num); |
| err = -1; |
| break; |
| } |
| t = strtok(NULL, ""); |
| if (!t) { |
| fprintf(stderr, |
| "%d: No probe string\n", num); |
| err = -1; |
| break; |
| } |
| |
| probe = malloc(strlen(t)); |
| copystr(probe, t); |
| plen = strlen(probe); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Probe string [%s]\n", |
| probe); |
| } else if (!strcasecmp(s, "file")) { |
| t = strtok(NULL, " \t"); |
| if (!t) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| if (probe) { |
| fprintf(stderr, |
| "%d: probe already set\n", |
| num); |
| err = -1; |
| break; |
| } |
| probe = mapfile(t, &plen); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "Probe file %s len %u@%p\n", |
| t, plen, probe); |
| } |
| } else if (!strcasecmp(t, "response")) { |
| s = strtok(NULL, " \t"); |
| if (!s) { |
| errtxt = line; |
| err = -1; |
| break; |
| } else if (!strcasecmp(s, "timeout")) { |
| t = strtok(NULL, " \t"); |
| if (!t) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| rtimeout = atoi(t); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "response timeout %d\n", |
| rtimeout); |
| } else if (!strcasecmp(s, "string")) { |
| if (response) { |
| fprintf(stderr, |
| "%d: response already set\n", |
| num); |
| err = -1; |
| break; |
| } |
| response = strdup(strtok(NULL, "")); |
| rlen = strlen(response); |
| template.l4_rsize = rlen; |
| template.l4_rbuf = malloc(rlen); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "Response string [%s]\n", |
| response); |
| } else if (!strcasecmp(s, "file")) { |
| t = strtok(NULL, " \t"); |
| if (!t) { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| if (response) { |
| fprintf(stderr, |
| "%d: response already set\n", |
| num); |
| err = -1; |
| break; |
| } |
| response = mapfile(t, &rlen); |
| template.l4_rsize = rlen; |
| template.l4_rbuf = malloc(rlen); |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, |
| "Response file %s len %u@%p\n", |
| t, rlen, response); |
| } |
| } else { |
| errtxt = line; |
| err = -1; |
| break; |
| } |
| } |
| |
| if (errtxt) |
| fprintf(stderr, "%d: syntax error at \"%s\"\n", num, errtxt); |
| fclose(fp); |
| return err; |
| } |
| |
| |
| void usage(prog) |
| char *prog; |
| { |
| fprintf(stderr, "Usage: %s -f <configfile>\n", prog); |
| exit(1); |
| } |
| |
| |
| int main(argc, argv) |
| int argc; |
| char *argv[]; |
| { |
| char *config = NULL; |
| int c; |
| |
| while ((c = getopt(argc, argv, "f:nv")) != -1) |
| switch (c) |
| { |
| case 'f' : |
| config = optarg; |
| break; |
| case 'n' : |
| opts |= OPT_DONOTHING; |
| break; |
| case 'v' : |
| opts |= OPT_VERBOSE; |
| break; |
| } |
| |
| if (config == NULL) |
| usage(argv[0]); |
| |
| if (readconfig(config)) |
| exit(1); |
| |
| if (!l4list) { |
| fprintf(stderr, "No remote servers, exiting."); |
| exit(1); |
| } |
| |
| if (!(opts & OPT_DONOTHING)) { |
| natfd = open(IPNAT_NAME, O_RDWR); |
| if (natfd == -1) { |
| perror("open(IPL_NAT)"); |
| exit(1); |
| } |
| } |
| |
| if (opts & OPT_VERBOSE) |
| fprintf(stderr, "Starting...\n"); |
| while (runconfig() == 0) |
| ; |
| |
| exit(1); |
| } |