diff options
author | roberto <roberto@FreeBSD.org> | 2004-07-20 15:01:56 +0000 |
---|---|---|
committer | roberto <roberto@FreeBSD.org> | 2004-07-20 15:01:56 +0000 |
commit | 118e757284cbb8fc4f43a713e892b41504b50a5f (patch) | |
tree | 528d12dda44ebdc3ffcc38050f159ac553a69c17 /contrib/ntp/ntpd/ntp_restrict.c | |
parent | a85d9ae25e8e8696677bc30feb6eaf7fc150e529 (diff) | |
download | FreeBSD-src-118e757284cbb8fc4f43a713e892b41504b50a5f.zip FreeBSD-src-118e757284cbb8fc4f43a713e892b41504b50a5f.tar.gz |
Virgin import of ntpd 4.2.0
Diffstat (limited to 'contrib/ntp/ntpd/ntp_restrict.c')
-rw-r--r-- | contrib/ntp/ntpd/ntp_restrict.c | 724 |
1 files changed, 425 insertions, 299 deletions
diff --git a/contrib/ntp/ntpd/ntp_restrict.c b/contrib/ntp/ntpd/ntp_restrict.c index 0e5b9dc..ede4225 100644 --- a/contrib/ntp/ntpd/ntp_restrict.c +++ b/contrib/ntp/ntpd/ntp_restrict.c @@ -1,5 +1,5 @@ /* - * ntp_restrict.c - find out what restrictions this host is running under + * ntp_restrict.c - determine host restrictions */ #ifdef HAVE_CONFIG_H #include <config.h> @@ -14,28 +14,41 @@ /* * This code keeps a simple address-and-mask list of hosts we want - * to place restrictions on (or remove them from). The restrictions + * to place restrictions on (or remove them from). The restrictions * are implemented as a set of flags which tell you what the host - * can't do. There is a subroutine entry to return the flags. The + * can't do. There is a subroutine entry to return the flags. The * list is kept sorted to reduce the average number of comparisons * and make sure you get the set of restrictions most specific to * the address. * * The algorithm is that, when looking up a host, it is first assumed - * that the default set of restrictions will apply. It then searches - * down through the list. Whenever it finds a match it adopts the match's - * flags instead. When you hit the point where the sorted address is - * greater than the target, you return with the last set of flags you - * found. Because of the ordering of the list, the most specific match - * will provide the final set of flags. + * that the default set of restrictions will apply. It then searches + * down through the list. Whenever it finds a match it adopts the + * match's flags instead. When you hit the point where the sorted + * address is greater than the target, you return with the last set of + * flags you found. Because of the ordering of the list, the most + * specific match will provide the final set of flags. * * This was originally intended to restrict you from sync'ing to your - * own broadcasts when you are doing that, by restricting yourself - * from your own interfaces. It was also thought it would sometimes - * be useful to keep a misbehaving host or two from abusing your primary - * clock. It has been expanded, however, to suit the needs of those - * with more restrictive access policies. + * own broadcasts when you are doing that, by restricting yourself from + * your own interfaces. It was also thought it would sometimes be useful + * to keep a misbehaving host or two from abusing your primary clock. It + * has been expanded, however, to suit the needs of those with more + * restrictive access policies. */ +/* + * We will use two lists, one for IPv4 addresses and one for IPv6 + * addresses. This is not protocol-independant but for now I can't + * find a way to respect this. We'll check this later... JFB 07/2001 + */ +#define SET_IPV6_ADDR_MASK(dst, src, msk) \ + do { \ + int idx; \ + for (idx = 0; idx < 16; idx++) { \ + (dst)->s6_addr[idx] = \ + (u_char) ((src)->s6_addr[idx] & (msk)->s6_addr[idx]); \ + } \ + } while (0) /* * Memory allocation parameters. We allocate INITRESLIST entries @@ -45,43 +58,48 @@ #define INITRESLIST 10 #define INCRESLIST 5 +#define RES_AVG 8. /* interpacket averaging factor */ + /* * The restriction list */ struct restrictlist *restrictlist; -static int restrictcount; /* count of entries in the restriction list */ +struct restrictlist6 *restrictlist6; +static int restrictcount; /* count of entries in the res list */ +static int restrictcount6; /* count of entries in the res list 2*/ /* * The free list and associated counters. Also some uninteresting * stat counters. */ static struct restrictlist *resfree; +static struct restrictlist6 *resfree6; static int numresfree; /* number of structures on free list */ +static int numresfree6; /* number of structures on free list 2 */ static u_long res_calls; static u_long res_found; static u_long res_not_found; -/* static u_long res_timereset; */ /* * Parameters of the RES_LIMITED restriction option. - * client_limit is the number of hosts allowed per source net - * client_limit_period is the number of seconds after which an entry - * is no longer considered for client limit determination */ -u_long client_limit; -u_long client_limit_period; +u_long res_avg_interval = 5; /* min average interpacket interval */ +u_long res_min_interval = 1; /* min interpacket interval */ + /* - * count number of restriction entries referring to RES_LIMITED - * controls activation/deactivation of monitoring - * (with respect to RES_LIMITED control) + * Count number of restriction entries referring to RES_LIMITED controls + * activation/deactivation of monitoring (with respect to RES_LIMITED + * control) */ static u_long res_limited_refcnt; +static u_long res_limited_refcnt6; /* - * Our initial allocation of list entries. + * Our initial allocation of lists entries. */ static struct restrictlist resinit[INITRESLIST]; +static struct restrictlist6 resinit6[INITRESLIST]; /* * init_restrict - initialize the restriction data structures @@ -90,31 +108,35 @@ void init_restrict(void) { register int i; - char bp[80]; /* * Zero the list and put all but one on the free list */ resfree = 0; memset((char *)resinit, 0, sizeof resinit); - + resfree6 = 0; + memset((char *)resinit6, 0, sizeof resinit6); for (i = 1; i < INITRESLIST; i++) { resinit[i].next = resfree; + resinit6[i].next = resfree6; resfree = &resinit[i]; + resfree6 = &resinit6[i]; } - numresfree = INITRESLIST-1; + numresfree6 = INITRESLIST-1; /* - * Put the remaining item at the head of the - * list as our default entry. Everything in here - * should be zero for now. + * Put the remaining item at the head of the list as our default + * entry. Everything in here should be zero for now. */ resinit[0].addr = htonl(INADDR_ANY); resinit[0].mask = 0; + memset(&resinit6[0].addr6, 0, sizeof(struct in6_addr)); + memset(&resinit6[0].mask6, 0, sizeof(struct in6_addr)); restrictlist = &resinit[0]; + restrictlist6 = &resinit6[0]; restrictcount = 1; - + restrictcount = 2; /* * fix up stat counters @@ -122,19 +144,12 @@ init_restrict(void) res_calls = 0; res_found = 0; res_not_found = 0; - /* res_timereset = 0; */ /* * set default values for RES_LIMIT functionality */ - client_limit = 3; - client_limit_period = 3600; res_limited_refcnt = 0; - - sprintf(bp, "client_limit=%ld", client_limit); - set_sys_var(bp, strlen(bp)+1, RO); - sprintf(bp, "client_limit_period=%ld", client_limit_period); - set_sys_var(bp, strlen(bp)+1, RO); + res_limited_refcnt6 = 0; } @@ -143,161 +158,131 @@ init_restrict(void) */ int restrictions( - struct sockaddr_in *srcadr + struct sockaddr_storage *srcadr ) { - register struct restrictlist *rl; - register struct restrictlist *match; - register u_int32 hostaddr; - register int isntpport; + struct restrictlist *rl; + struct restrictlist *match = NULL; + struct restrictlist6 *rl6; + struct restrictlist6 *match6 = NULL; + struct in6_addr hostaddr6; + struct in6_addr hostservaddr6; + u_int32 hostaddr; + int flags = 0; + int isntpport; res_calls++; - /* - * We need the host address in host order. Also need to know - * whether this is from the ntp port or not. - */ - hostaddr = SRCADR(srcadr); - isntpport = (SRCPORT(srcadr) == NTP_PORT); - - /* - * Ignore any packets with a multicast source address - * (this should be done early in the receive process, later!) - */ - if (IN_CLASSD(ntohl(srcadr->sin_addr.s_addr))) - return (int)RES_IGNORE; + if (srcadr->ss_family == AF_INET) { + /* + * We need the host address in host order. Also need to + * know whether this is from the ntp port or not. + */ + hostaddr = SRCADR(srcadr); + isntpport = (SRCPORT(srcadr) == NTP_PORT); - /* - * Set match to first entry, which is default entry. Work our - * way down from there. - */ - match = restrictlist; - - for (rl = match->next; rl != 0 && rl->addr <= hostaddr; rl = rl->next) - if ((hostaddr & rl->mask) == rl->addr) { - if ((rl->mflags & RESM_NTPONLY) && !isntpport) - continue; - match = rl; - } - - match->count++; - if (match == restrictlist) - res_not_found++; - else - res_found++; - - /* - * The following implements limiting the number of clients - * accepted from a given network. The notion of "same network" - * is determined by the mask and addr fields of the restrict - * list entry. The monitor mechanism has to be enabled for - * collecting info on current clients. - * - * The policy is as follows: - * - take the list of clients recorded - * from the given "network" seen within the last - * client_limit_period seconds - * - if there are at most client_limit entries: - * --> access allowed - * - otherwise sort by time first seen - * - current client among the first client_limit seen - * hosts? - * if yes: access allowed - * else: eccess denied - */ - if (match->flags & RES_LIMITED) { - int lcnt; - struct mon_data *md, *this_client; - -#ifdef DEBUG - if (debug > 2) - printf("limited clients check: %ld clients, period %ld seconds, net is 0x%lX\n", - client_limit, client_limit_period, - (u_long)netof(hostaddr)); -#endif /*DEBUG*/ - if (mon_enabled == MON_OFF) { -#ifdef DEBUG - if (debug > 4) - printf("no limit - monitoring is off\n"); -#endif - return (int)(match->flags & ~RES_LIMITED); - } + /* + * Ignore any packets with a multicast source address + * (this should be done early in the receive process, + * later!) + */ + if (IN_CLASSD(SRCADR(srcadr))) + return (int)RES_IGNORE; /* - * How nice, MRU list provides our current client as the - * first entry in the list. - * Monitoring was verified to be active above, thus we - * know an entry for our client must exist, or some - * brain dead set the memory limit for mon entries to ZERO!!! + * Set match to first entry, which is default entry. + * Work our way down from there. */ - this_client = mon_mru_list.mru_next; - - for (md = mon_fifo_list.fifo_next,lcnt = 0; - md != &mon_fifo_list; - md = md->fifo_next) { - if ((current_time - md->lasttime) - > client_limit_period) { -#ifdef DEBUG - if (debug > 5) - printf("checking: %s: ignore: too old: %ld\n", - numtoa(md->rmtadr), - current_time - md->lasttime); -#endif - continue; - } - if (md->mode == MODE_BROADCAST || - md->mode == MODE_CONTROL || - md->mode == MODE_PRIVATE) { -#ifdef DEBUG - if (debug > 5) - printf("checking: %s: ignore mode %d\n", - numtoa(md->rmtadr), - md->mode); -#endif - continue; + match = restrictlist; + for (rl = match->next; rl != 0 && rl->addr <= hostaddr; + rl = rl->next) + if ((hostaddr & rl->mask) == rl->addr) { + if ((rl->mflags & RESM_NTPONLY) && + !isntpport) + continue; + match = rl; } - if (netof(md->rmtadr) != - netof(hostaddr)) { -#ifdef DEBUG - if (debug > 5) - printf("checking: %s: different net 0x%lX\n", - numtoa(md->rmtadr), - (u_long)netof(md->rmtadr)); -#endif - continue; - } - lcnt++; - if (lcnt > (int) client_limit || - md->rmtadr == hostaddr) { -#ifdef DEBUG - if (debug > 5) - printf("considering %s: found host\n", - numtoa(md->rmtadr)); -#endif - break; - } -#ifdef DEBUG - else { - if (debug > 5) - printf("considering %s: same net\n", - numtoa(md->rmtadr)); - } -#endif + match->count++; + if (match == restrictlist) + res_not_found++; + else + res_found++; + flags = match->flags; + } + /* IPv6 source address */ + if (srcadr->ss_family == AF_INET6) { + /* + * Need to know whether this is from the ntp port or + * not. + */ + hostaddr6 = GET_INADDR6(*srcadr); + isntpport = (ntohs(( + (struct sockaddr_in6 *)srcadr)->sin6_port) == + NTP_PORT); + + /* + * Ignore any packets with a multicast source address + * (this should be done early in the receive process, + * later!) + */ + if (IN6_IS_ADDR_MULTICAST(&hostaddr6)) + return (int)RES_IGNORE; + + /* + * Set match to first entry, which is default entry. + * Work our way down from there. + */ + match6 = restrictlist6; + for (rl6 = match6->next; rl6 != 0 && + (memcmp(&(rl6->addr6), &hostaddr6, + sizeof(hostaddr6)) <= 0); rl6 = rl6->next) { + SET_IPV6_ADDR_MASK(&hostservaddr6, &hostaddr6, + &rl6->mask6); + if (memcmp(&hostservaddr6, &(rl6->addr6), + sizeof(hostservaddr6)) == 0) { + if ((rl6->mflags & RESM_NTPONLY) && + !isntpport) + continue; + match6 = rl6; + } } -#ifdef DEBUG - if (debug > 4) - printf("this one is rank %d in list, limit is %lu: %s\n", - lcnt, client_limit, - (lcnt <= (int) client_limit) ? "ALLOW" : "REJECT"); -#endif - if (lcnt <= (int) client_limit) { - this_client->lastdrop = 0; - return (int)(match->flags & ~RES_LIMITED); - } else { - this_client->lastdrop = current_time; - } + match6->count++; + if (match6 == restrictlist6) + res_not_found++; + else + res_found++; + flags = match6->flags; + } + + /* + * The following implements a generalized call gap facility. + * Douse the RES_LIMITED bit only if the interval since the last + * packet is greater than res_min_interval and the average is + * greater thatn res_avg_interval. + */ + if (mon_enabled == MON_OFF) { + flags &= ~RES_LIMITED; + } else { + struct mon_data *md; + + /* + * At this poin the most recent arrival is first in the + * MRU list. Let the first 10 packets in for free until + * the average stabilizes. + */ + md = mon_mru_list.mru_next; + if (md->avg_interval == 0) + md->avg_interval = md->drop_count; + else + md->avg_interval += (md->drop_count - + md->avg_interval) / RES_AVG; + if (md->count < 10 || (md->drop_count > + res_min_interval && md->avg_interval > + res_avg_interval)) + flags &= ~RES_LIMITED; + md->drop_count = flags; } - return (int)match->flags; + return (flags); } @@ -307,61 +292,112 @@ restrictions( void hack_restrict( int op, - struct sockaddr_in *resaddr, - struct sockaddr_in *resmask, + struct sockaddr_storage *resaddr, + struct sockaddr_storage *resmask, int mflags, int flags ) { - register u_int32 addr; - register u_int32 mask; - register struct restrictlist *rl; - register struct restrictlist *rlprev; - int i; - - /* - * Get address and mask in host byte order - */ - addr = SRCADR(resaddr); - mask = SRCADR(resmask); - addr &= mask; /* make sure low bits are zero */ + register u_int32 addr = 0; + register u_int32 mask = 0; + struct in6_addr addr6; + struct in6_addr mask6; + register struct restrictlist *rl = NULL; + register struct restrictlist *rlprev = NULL; + register struct restrictlist6 *rl6 = NULL; + register struct restrictlist6 *rlprev6 = NULL; + int i, addr_cmp, mask_cmp; + memset(&addr6, 0, sizeof(struct in6_addr)); + memset(&mask6, 0, sizeof(struct in6_addr)); + + if (resaddr->ss_family == AF_INET) { + /* + * Get address and mask in host byte order + */ + addr = SRCADR(resaddr); + mask = SRCADR(resmask); + addr &= mask; /* make sure low bits zero */ - /* - * If this is the default address, point at first on list. Else - * go searching for it. - */ - if (addr == htonl(INADDR_ANY)) { - rlprev = 0; - rl = restrictlist; - } else { - rlprev = restrictlist; - rl = rlprev->next; - while (rl != 0) { - if (rl->addr > addr) { - rl = 0; - break; - } else if (rl->addr == addr) { - if (rl->mask == mask) { - if ((mflags & RESM_NTPONLY) - == (rl->mflags & RESM_NTPONLY)) - break; /* exact match */ - if (!(mflags & RESM_NTPONLY)) { - /* - * No flag fits before flag - */ + /* + * If this is the default address, point at first on + * list. Else go searching for it. + */ + if (addr == 0) { + rlprev = 0; + rl = restrictlist; + } else { + rlprev = restrictlist; + rl = rlprev->next; + while (rl != 0) { + if (rl->addr > addr) { + rl = 0; + break; + } else if (rl->addr == addr) { + if (rl->mask == mask) { + if ((mflags & + RESM_NTPONLY) == + (rl->mflags & + RESM_NTPONLY)) + break; + + if (!(mflags & + RESM_NTPONLY)) { + rl = 0; + break; + } + } else if (rl->mask > mask) { rl = 0; break; } - /* continue on */ - } else if (rl->mask > mask) { - rl = 0; + } + rlprev = rl; + rl = rl->next; + } + } + } + + if (resaddr->ss_family == AF_INET6) { + mask6 = GET_INADDR6(*resmask); + SET_IPV6_ADDR_MASK(&addr6, + &GET_INADDR6(*resaddr), &mask6); + if (IN6_IS_ADDR_UNSPECIFIED(&addr6)) { + rlprev6 = 0; + rl6 = restrictlist6; + } else { + rlprev6 = restrictlist6; + rl6 = rlprev6->next; + while (rl6 != 0) { + addr_cmp = memcmp(&rl6->addr6, &addr6, + sizeof(addr6)); + if (addr_cmp > 0) { + rl6 = 0; break; + } else if (addr_cmp == 0) { + mask_cmp = memcmp(&rl6->mask6, + &mask6, sizeof(mask6)); + if (mask_cmp == 0) { + if ((mflags & + RESM_NTPONLY) == + (rl6->mflags & + RESM_NTPONLY)) + break; + + if (!(mflags & + RESM_NTPONLY)) { + rl6 = 0; + break; + } + } else if (mask_cmp > 0) { + rl6 = 0; + break; + } } + rlprev6 = rl6; + rl6 = rl6->next; } - rlprev = rl; - rl = rl->next; } } + /* * In case the above wasn't clear :-), either rl now points * at the entry this call refers to, or rl is zero and rlprev @@ -372,89 +408,179 @@ hack_restrict( /* * Switch based on operation */ - switch (op) { - case RESTRICT_FLAGS: - /* - * Here we add bits to the flags. If this is a new - * restriction add it. - */ - if (rl == 0) { - if (numresfree == 0) { - rl = (struct restrictlist *) emalloc( - INCRESLIST*sizeof(struct restrictlist)); - memset((char *)rl, 0, - INCRESLIST*sizeof(struct restrictlist)); - - for (i = 0; i < INCRESLIST; i++) { - rl->next = resfree; - resfree = rl; - rl++; + if (resaddr->ss_family == AF_INET) { + switch (op) { + case RESTRICT_FLAGS: + /* + * Here we add bits to the flags. If this is a + * new restriction add it. + */ + if (rl == 0) { + if (numresfree == 0) { + rl = (struct restrictlist *) + emalloc(INCRESLIST * + sizeof(struct + restrictlist)); + memset((char *)rl, 0, + INCRESLIST * sizeof(struct + restrictlist)); + for (i = 0; i < INCRESLIST; i++) { + rl->next = resfree; + resfree = rl; + rl++; + } + numresfree = INCRESLIST; } - numresfree = INCRESLIST; - } - rl = resfree; - resfree = rl->next; - numresfree--; + rl = resfree; + resfree = rl->next; + numresfree--; - rl->addr = addr; - rl->mask = mask; - rl->mflags = (u_short)mflags; + rl->addr = addr; + rl->mask = mask; + rl->mflags = (u_short)mflags; - rl->next = rlprev->next; - rlprev->next = rl; - restrictcount++; - } - if ((rl->flags ^ (u_short)flags) & RES_LIMITED) { - res_limited_refcnt++; - mon_start(MON_RES); /* ensure data gets collected */ - } - rl->flags |= (u_short)flags; - break; - - case RESTRICT_UNFLAG: - /* - * Remove some bits from the flags. If we didn't - * find this one, just return. - */ - if (rl != 0) { - if ((rl->flags ^ (u_short)flags) & RES_LIMITED) { - res_limited_refcnt--; - if (res_limited_refcnt == 0) - mon_stop(MON_RES); + rl->next = rlprev->next; + rlprev->next = rl; + restrictcount++; } - rl->flags &= (u_short)~flags; - } - break; + if ((rl->flags ^ (u_short)flags) & + RES_LIMITED) { + res_limited_refcnt++; + mon_start(MON_RES); + } + rl->flags |= (u_short)flags; + break; + + case RESTRICT_UNFLAG: + /* + * Remove some bits from the flags. If we didn't + * find this one, just return. + */ + if (rl != 0) { + if ((rl->flags ^ (u_short)flags) & + RES_LIMITED) { + res_limited_refcnt--; + if (res_limited_refcnt == 0) + mon_stop(MON_RES); + } + rl->flags &= (u_short)~flags; + } + break; - case RESTRICT_REMOVE: - /* - * Remove an entry from the table entirely if we found one. - * Don't remove the default entry and don't remove an - * interface entry. - */ - if (rl != 0 - && rl->addr != htonl(INADDR_ANY) - && !(rl->mflags & RESM_INTERFACE)) { - rlprev->next = rl->next; - restrictcount--; - if (rl->flags & RES_LIMITED) { - res_limited_refcnt--; - if (res_limited_refcnt == 0) - mon_stop(MON_RES); + case RESTRICT_REMOVE: + /* + * Remove an entry from the table entirely if we + * found one. Don't remove the default entry and + * don't remove an interface entry. + */ + if (rl != 0 + && rl->addr != htonl(INADDR_ANY) + && !(rl->mflags & RESM_INTERFACE)) { + rlprev->next = rl->next; + restrictcount--; + if (rl->flags & RES_LIMITED) { + res_limited_refcnt--; + if (res_limited_refcnt == 0) + mon_stop(MON_RES); + } + memset((char *)rl, 0, + sizeof(struct restrictlist)); + + rl->next = resfree; + resfree = rl; + numresfree++; } - memset((char *)rl, 0, sizeof(struct restrictlist)); + break; - rl->next = resfree; - resfree = rl; - numresfree++; + default: + break; } - break; + } else if (resaddr->ss_family == AF_INET6) { + switch (op) { + case RESTRICT_FLAGS: + /* + * Here we add bits to the flags. If this is a + * new restriction add it. + */ + if (rl6 == 0) { + if (numresfree6 == 0) { + rl6 = (struct + restrictlist6 *)emalloc( + INCRESLIST * sizeof(struct + restrictlist6)); + memset((char *)rl6, 0, + INCRESLIST * sizeof(struct + restrictlist6)); + + for (i = 0; i < INCRESLIST; + i++) { + rl6->next = resfree6; + resfree6 = rl6; + rl6++; + } + numresfree6 = INCRESLIST; + } + rl6 = resfree6; + resfree6 = rl6->next; + numresfree6--; + rl6->addr6 = addr6; + rl6->mask6 = mask6; + rl6->mflags = (u_short)mflags; + rl6->next = rlprev6->next; + rlprev6->next = rl6; + restrictcount6++; + } + if ((rl6->flags ^ (u_short)flags) & + RES_LIMITED) { + res_limited_refcnt6++; + mon_start(MON_RES); + } + rl6->flags |= (u_short)flags; + break; + + case RESTRICT_UNFLAG: + /* + * Remove some bits from the flags. If we didn't + * find this one, just return. + */ + if (rl6 != 0) { + if ((rl6->flags ^ (u_short)flags) & + RES_LIMITED) { + res_limited_refcnt6--; + if (res_limited_refcnt6 == 0) + mon_stop(MON_RES); + } + rl6->flags &= (u_short)~flags; + } + break; + + case RESTRICT_REMOVE: + /* + * Remove an entry from the table entirely if we + * found one. Don't remove the default entry and + * don't remove an interface entry. + */ + if (rl6 != 0 && + !IN6_IS_ADDR_UNSPECIFIED(&rl6->addr6) + && !(rl6->mflags & RESM_INTERFACE)) { + rlprev6->next = rl6->next; + restrictcount6--; + if (rl6->flags & RES_LIMITED) { + res_limited_refcnt6--; + if (res_limited_refcnt6 == 0) + mon_stop(MON_RES); + } + memset((char *)rl6, 0, + sizeof(struct restrictlist6)); + rl6->next = resfree6; + resfree6 = rl6; + numresfree6++; + } + break; - default: - /* Oh, well */ - break; + default: + break; + } } - - /* done! */ } |