diff --git a/README.md b/README.md index 5164f2443..6878e22fe 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,11 @@ domains_being_blocked 19977 dns_queries_today 104749 ads_blocked_today 279 ads_percentage_today 1.396606 +unique_domains 6 +queries_forwarded 3 +queries_cached 2 +clients_ever_seen 3 +unique_clients 3 ``` - `>overTime` : over time data (10 min intervals) @@ -156,7 +161,7 @@ ads_percentage_today 1.396606 ``` Variant: `>top-ads (14)` to show (up to) 14 entries -- `top-clients` : get top clients (IP addresses + host names (if available)) +- `top-clients` : get recently active top clients (IP addresses + host names (if available)) ``` 0 9373 192.168.2.1 router 1 484 192.168.2.2 work-machine @@ -164,18 +169,21 @@ ads_percentage_today 1.396606 ``` Variant: `>top-clients (9)` to show (up to) 9 client entries -- `>forward-dest` : get forward destinations (IP addresses + host names (if available)) + Variant: `>top-clients withzero (15)` to show (up to) 15 clients even if they have not been active recently (see PR #124 for further details) + +- `>forward-dest` : get forward destinations (IP addresses + host names (if available)) along with the percentage ``` -0 12940 1.2.3.4 some.dns.de -1 629 5.6.7.8 some.other.dns.com +0 80.0 ::1 local +1 10.0 1.2.3.4 some.dns.de +2 10.0 5.6.7.8 some.other.dns.com +``` + Variant: `>forward-dest unsorted` to show forward destinations in unsorted order (equivalent to using `>forward-names`) ``` -- `>querytypes` : get collected query types +- `>querytypes` : get collected query types percentage ``` -A (IPv4): 7729 -AAAA (IPv6): 5880 -PTR: 12 -SRV: 0 +A (IPv4): 60.5 +AAAA (IPv6): 39.5 ``` - `>getallqueries` : get all queries that FTL has in its database diff --git a/gc.c b/gc.c index 22f38a8a8..ecb4a4007 100644 --- a/gc.c +++ b/gc.c @@ -148,6 +148,10 @@ void *GC_thread(void *val) logg("Notice: GC removed %i queries", invalidated); } + // Run reresolveHostnames at the end of GC to account for + // formally unknown and/or changed host names on the network + reresolveHostnames(); + // Release thread lock disable_thread_lock("GC_thread"); diff --git a/main.c b/main.c index a506e49dc..ff56364bd 100644 --- a/main.c +++ b/main.c @@ -90,6 +90,7 @@ int main (int argc, char* argv[]) { runDBthread = true; // Garbadge collect in regular interval, but don't do it if the threadlocks is set + // This will also run reresolveHostnames() if(runGCthread || needGC) { // Wait until we are allowed to work on the data diff --git a/parser.c b/parser.c index 738052b95..bebe5205a 100644 --- a/parser.c +++ b/parser.c @@ -13,7 +13,7 @@ char *resolveHostname(const char *addr); void extracttimestamp(const char *readbuffer, int *querytimestamp, int *overTimetimestamp); -int getforwardID(const char * str); +int getforwardID(const char * str, bool count); int findDomain(const char *domain); int findClient(const char *client); int detectStatus(const char *domain); @@ -245,19 +245,43 @@ void process_pihole_log(int file) } if(!found) { - timeidx = counters.overTime; - validate_access("overTime", timeidx, false, __LINE__, __FUNCTION__, __FILE__); - // Set magic byte - overTime[timeidx].magic = MAGICBYTE; - overTime[timeidx].timestamp = overTimetimestamp; - overTime[timeidx].total = 0; - overTime[timeidx].blocked = 0; - overTime[timeidx].cached = 0; - overTime[timeidx].forwardnum = 0; - overTime[timeidx].forwarddata = NULL; - overTime[timeidx].querytypedata = calloc(2, sizeof(int)); - memory.querytypedata += 2*sizeof(int); - counters.overTime++; + // We loop over this to fill potential data holes with zeros + int nexttimestamp = 0; + if(counters.overTime != 0) + { + validate_access("overTime", counters.overTime-1, false, __LINE__, __FUNCTION__, __FILE__); + nexttimestamp = overTime[counters.overTime-1].timestamp + 600; + } + else + { + nexttimestamp = overTimetimestamp; + } + + while(overTimetimestamp >= nexttimestamp) + { + // Check struct size + memory_check(OVERTIME); + timeidx = counters.overTime; + validate_access("overTime", timeidx, false, __LINE__, __FUNCTION__, __FILE__); + // Set magic byte + overTime[timeidx].magic = MAGICBYTE; + overTime[timeidx].timestamp = nexttimestamp; + overTime[timeidx].total = 0; + overTime[timeidx].blocked = 0; + overTime[timeidx].cached = 0; + overTime[timeidx].forwardnum = 0; + overTime[timeidx].forwarddata = NULL; + overTime[timeidx].querytypedata = calloc(2, sizeof(int)); + memory.querytypedata += 2*sizeof(int); + counters.overTime++; + + // Update time stamp for next loop interation + if(counters.overTime != 0) + { + validate_access("overTime", counters.overTime-1, false, __LINE__, __FUNCTION__, __FILE__); + nexttimestamp = overTime[counters.overTime-1].timestamp + 600; + } + } } // Get domain @@ -378,7 +402,7 @@ void process_pihole_log(int file) status = 2; // Get ID of forward destination, create new forward destination record // if not found in current data structure - forwardID = getforwardID(readbuffer2); + forwardID = getforwardID(readbuffer2, false); if(forwardID == -2) continue; break; @@ -580,9 +604,14 @@ void process_pihole_log(int file) continue; } + // Check if this is a PTR query + // if so: skip analysis of this log line + if(strstr(readbuffer,"in-addr.arpa") != NULL) + continue; + // Get ID of forward destination, create new forward destination record // if not found in current data structure - int forwardID = getforwardID(readbuffer); + int forwardID = getforwardID(readbuffer, true); if(forwardID == -2) continue; @@ -821,7 +850,7 @@ void extracttimestamp(const char *readbuffer, int *querytimestamp, int *overTime *overTimetimestamp = *querytimestamp-(*querytimestamp%600)+300; } -int getforwardID(const char * str) +int getforwardID(const char * str, bool count) { // Get forward destination // forwardstart = pointer to | in "forwarded domain.name| to www.xxx.yyy.zzz\n" @@ -866,7 +895,8 @@ int getforwardID(const char * str) if(strcmp(forwarded[i].ip, forward) == 0) { forwardID = i; - forwarded[forwardID].count++; + if(count) + forwarded[forwardID].count++; processed = true; break; } @@ -886,8 +916,11 @@ int getforwardID(const char * str) validate_access("forwarded", forwardID, false, __LINE__, __FUNCTION__, __FILE__); // Set magic byte forwarded[forwardID].magic = MAGICBYTE; - // Set its counter to 1 - forwarded[forwardID].count = 1; + // Initialize its counter + if(count) + forwarded[forwardID].count = 1; + else + forwarded[forwardID].count = 0; // Save IP forwarded[forwardID].ip = strdup(forward); memory.forwardedips += (forwardlen + 1) * sizeof(char); @@ -991,3 +1024,35 @@ void validate_access_oTfd(int timeidx, int pos, int line, const char * function, logg(" found in %s() (line %i) in %s", function, line, file); } } + +void reresolveHostnames(void) +{ + int clientID; + for(clientID = 0; clientID < counters.clients; clientID++) + { + // Memory validation + validate_access("clients", clientID, true, __LINE__, __FUNCTION__, __FILE__); + + // Process this client only if it has at least one active query in the log + if(clients[clientID].count < 1) + continue; + + // Get client hostname + char *hostname = resolveHostname(clients[clientID].ip); + if(strlen(hostname) > 0) + { + // Delete possibly already existing hostname pointer before storing new data + if(clients[clientID].name != NULL) + { + memory.clientnames -= (strlen(clients[clientID].name) + 1) * sizeof(char); + free(clients[clientID].name); + clients[clientID].name = NULL; + } + + // Store client hostname + clients[clientID].name = strdup(hostname); + memory.clientnames += (strlen(hostname) + 1) * sizeof(char); + } + free(hostname); + } +} diff --git a/request.c b/request.c index cb663a280..6f963032f 100644 --- a/request.c +++ b/request.c @@ -21,7 +21,6 @@ void getOverTime(int *sock); void getTopDomains (char *client_message, int *sock); void getTopClients(char *client_message, int *sock); void getForwardDestinations(char *client_message, int *sock); -void getForwardNames(int *sock); void getQueryTypes(int *sock); void getAllQueries(char *client_message, int *sock); void getRecentBlocked(char *client_message, int *sock); @@ -67,7 +66,7 @@ void process_request(char *client_message, int *sock) else if(command(client_message, ">forward-names")) { processed = true; - getForwardNames(sock); + getForwardDestinations(">forward-dest unsorted", sock); } else if(command(client_message, ">querytypes")) { @@ -115,13 +114,6 @@ void process_request(char *client_message, int *sock) getDBstats(sock); } - // End of queryable commands - if(processed) - { - // Send EOM - seom(server_message, *sock); - } - // Test only at the end if we want to quit or kill // so things can be processed before if(command(client_message, ">quit") || command(client_message, EOT)) @@ -142,9 +134,16 @@ void process_request(char *client_message, int *sock) if(!processed) { - sprintf(server_message,"unknown command: %s\n",client_message); + sprintf(server_message,"unknown command: %s",client_message); swrite(server_message, *sock); } + + // End of queryable commands + if(*sock != 0) + { + // Send EOM + seom(server_message, *sock); + } } bool command(char *client_message, const char* cmd) @@ -234,9 +233,24 @@ void getStats(int *sock) sprintf(server_message,"unique_domains %i\nqueries_forwarded %i\nqueries_cached %i\n", \ counters.domains,counters.forwardedqueries,counters.cached); swrite(server_message, *sock); - sprintf(server_message,"unique_clients %i\n", \ + + // clients_ever_seen: all clients ever seen by FTL + sprintf(server_message,"clients_ever_seen %i\n", \ counters.clients); swrite(server_message, *sock); + + // unique_clients: count only clients that have been active within the most recent 24 hours + int i, activeclients = 0; + for(i=0; i < counters.clients; i++) + { + validate_access("clients", i, true, __LINE__, __FUNCTION__, __FILE__); + if(clients[i].count > 0) + activeclients++; + } + sprintf(server_message,"unique_clients %i\n", \ + activeclients); + swrite(server_message, *sock); + if(debugclients) logg("Sent stats data to client, ID: %i", *sock); } @@ -404,6 +418,15 @@ void getTopClients(char *client_message, int *sock) count = num; } + // Show also clients which have not been active recently? + // This option can be combined with existing options, + // i.e. both >top-clients withzero" and ">top-clients withzero (123)" are valid + bool includezeroclients = false; + if(command(client_message, " withzero")) + { + includezeroclients = true; + } + for(i=0; i < counters.clients; i++) { validate_access("clients", i, true, __LINE__, __FUNCTION__, __FILE__); @@ -440,8 +463,10 @@ void getTopClients(char *client_message, int *sock) continue; } } - - if(clients[j].count > 0) + // Return this client if either + // - "withzero" option is set, and/or + // - the client made at least one query within the most recent 24 hours + if(includezeroclients || clients[j].count > 0) { sprintf(server_message,"%i %i %s %s\n",i,clients[j].count,clients[j].ip,clients[j].name); swrite(server_message, *sock); @@ -458,20 +483,28 @@ void getForwardDestinations(char *client_message, int *sock) { char server_message[SOCKETBUFFERLEN]; bool allocated = false, sort = true; - int i, temparray[counters.forwarded+1][2]; + int i, temparray[counters.forwarded+1][2], forwardedsum = 0, totalqueries = 0; if(command(client_message, "unsorted")) sort = false; - if(sort) + for(i=0; i < counters.forwarded; i++) { - for(i=0; i < counters.forwarded; i++) + validate_access("forwarded", i, true, __LINE__, __FUNCTION__, __FILE__); + // Compute forwardedsum + forwardedsum += forwarded[i].count; + + // If we want to print a sorted output, we fill the temporary array with + // the values we will use for sorting afterwards + if(sort) { - validate_access("forwarded", i, true, __LINE__, __FUNCTION__, __FILE__); temparray[i][0] = i; temparray[i][1] = forwarded[i].count; } + } + if(sort) + { // Add "local " forward destination temparray[counters.forwarded][0] = counters.forwarded; temparray[counters.forwarded][1] = counters.cached + counters.blocked; @@ -480,11 +513,13 @@ void getForwardDestinations(char *client_message, int *sock) qsort(temparray, counters.forwarded+1, sizeof(int[2]), cmpdesc); } + totalqueries = counters.forwardedqueries + counters.cached + counters.blocked; + // Loop over available forward destinations for(i=0; i < min(counters.forwarded+1, 10); i++) { char *name, *ip; - int count; + double percentage; // Get sorted indices int j; @@ -500,7 +535,11 @@ void getForwardDestinations(char *client_message, int *sock) strcpy(ip, "::1"); name = calloc(6,1); strcpy(name, "local"); - count = counters.cached + counters.blocked; + if(totalqueries > 0) + // Whats the percentage of (cached + blocked) queries on the total amount of queries? + percentage = 1e2 * (counters.cached + counters.blocked) / totalqueries; + else + percentage = 0.0; allocated = true; } else @@ -508,14 +547,33 @@ void getForwardDestinations(char *client_message, int *sock) validate_access("forwarded", j, true, __LINE__, __FUNCTION__, __FILE__); ip = forwarded[j].ip; name = forwarded[j].name; - count = forwarded[j].count; + // Math explanation: + // A single query may result in requests being forwarded to multiple destinations + // Hence, in order to be able to give percentages here, we have to normalize the + // number of forwards to each specific destination by the total number of forward + // events. This term is done by + // a = forwarded[j].count / forwardedsum + // + // The fraction a describes now how much share an individual forward destination + // has on the total sum of sent requests. + // We also know the share of forwarded queries on the total number of queries + // b = counters.forwardedqueries / c + // where c is the number of valid queries, + // c = counters.forwardedqueries + counters.cached + counters.blocked + // + // To get the total percentage of a specific query on the total number of queries, + // we simply have to scale b by a which is what we do in the following. + if(forwardedsum > 0 && totalqueries > 0) + percentage = 1e2 * forwarded[j].count / forwardedsum * counters.forwardedqueries / totalqueries; + else + percentage = 0.0; allocated = false; } // Send data if count > 0 - if(count > 0) + if(percentage > 0.0) { - sprintf(server_message,"%i %i %s %s\n",i,count,ip,name); + sprintf(server_message,"%i %.2f %s %s\n",i,percentage,ip,name); swrite(server_message, *sock); } @@ -530,34 +588,20 @@ void getForwardDestinations(char *client_message, int *sock) logg("Sent forward destination data to client, ID: %i", *sock); } - -void getForwardNames(int *sock) +void getQueryTypes(int *sock) { char server_message[SOCKETBUFFERLEN]; - int i; + int total = counters.IPv4 + counters.IPv6; + double percentageIPv4 = 0.0, percentageIPv6 = 0.0; - for(i=0; i < counters.forwarded; i++) + // Prevent floating point exceptions by checking if the divisor is != 0 + if(total > 0) { - validate_access("forwarded", i, true, __LINE__, __FUNCTION__, __FILE__); - // Get sorted indices - sprintf(server_message,"%i %i %s %s\n",i,forwarded[i].count,forwarded[i].ip,forwarded[i].name); - swrite(server_message, *sock); + percentageIPv4 = 1e2*counters.IPv4/total; + percentageIPv6 = 1e2*counters.IPv6/total; } - // Add "local" forward destination - sprintf(server_message,"%i %i ::1 local\n",counters.forwarded,counters.cached); - swrite(server_message, *sock); - - if(debugclients) - logg("Sent forward destination names to client, ID: %i", *sock); -} - - -void getQueryTypes(int *sock) -{ - char server_message[SOCKETBUFFERLEN]; - - sprintf(server_message,"A (IPv4): %i\nAAAA (IPv6): %i\n",counters.IPv4,counters.IPv6); + sprintf(server_message,"A (IPv4): %.2f\nAAAA (IPv6): %.2f\n", percentageIPv4, percentageIPv6); swrite(server_message, *sock); if(debugclients) logg("Sent query type data to client, ID: %i", *sock); @@ -815,6 +859,7 @@ void getForwardDestinationsOverTime(int *sock) { char server_message[SOCKETBUFFERLEN]; int i, sendit = -1; + for(i = 0; i < counters.overTime; i++) { validate_access("overTime", i, true, __LINE__, __FUNCTION__, __FILE__); @@ -828,23 +873,74 @@ void getForwardDestinationsOverTime(int *sock) { for(i = sendit; i < counters.overTime; i++) { + double percentage; + validate_access("overTime", i, true, __LINE__, __FUNCTION__, __FILE__); sprintf(server_message, "%i", overTime[i].timestamp); - int j; + int j, forwardedsum = 0; + // Compute forwardedsum used for later normalization + for(j = 0; j < overTime[i].forwardnum; j++) + { + forwardedsum += overTime[i].forwarddata[j]; + } + + // Loop over forward destinations to generate output to be sent to the client for(j = 0; j < counters.forwarded; j++) { - int k; + int thisforward = 0; + if(j < overTime[i].forwardnum) - k = overTime[i].forwarddata[j]; + { + // This forward destination does already exist at this timestamp + // -> use counter of requests sent to this destination + thisforward = overTime[i].forwarddata[j]; + } + // else + // { + // This forward destination does not yet exist at this timestamp + // -> use zero as number of requests sent to this destination + // thisforward = 0; + // } + + // Avoid floating point exceptions + if(forwardedsum > 0 && overTime[i].total > 0 && thisforward > 0) + { + // A single query may result in requests being forwarded to multiple destinations + // Hence, in order to be able to give percentages here, we have to normalize the + // number of forwards to each specific destination by the total number of forward + // events. This is done by + // a = thisforward / forwardedsum + // The fraction a describes how much share an individual forward destination + // has on the total sum of sent requests. + // + // We also know the share of forwarded queries on the total number of queries + // b = forwardedqueries/overTime[i].total + // where the number of forwarded queries in this time interval is given by + // forwardedqueries = overTime[i].total - (overTime[i].cached + // + overTime[i].blocked) + // + // To get the total percentage of a specific forward destination on the total + // number of queries, we simply have to multiply a and b as done below: + percentage = 1e2 * thisforward / forwardedsum * (overTime[i].total - (overTime[i].cached + overTime[i].blocked)) / overTime[i].total; + } else - k = 0; + { + percentage = 0.0; + } - sprintf(server_message + strlen(server_message), " %i", k); + sprintf(server_message + strlen(server_message), " %.2f", percentage); } - sprintf(server_message + strlen(server_message), " %i\n", overTime[i].cached + overTime[i].blocked); + // Avoid floating point exceptions + if(overTime[i].total > 0) + // Forward count for destination "local" is cached + blocked normalized by total: + percentage = 1e2 * (overTime[i].cached + overTime[i].blocked) / overTime[i].total; + else + percentage = 0.0; + + sprintf(server_message + strlen(server_message), " %.2f\n", percentage); swrite(server_message, *sock); } } @@ -881,7 +977,14 @@ void getQueryTypesOverTime(int *sock) for(i = sendit; i < counters.overTime; i++) { validate_access("overTime", i, true, __LINE__, __FUNCTION__, __FILE__); - sprintf(server_message, "%i %i %i\n", overTime[i].timestamp,overTime[i].querytypedata[0],overTime[i].querytypedata[1]); + double percentageIPv4 = 0.0, percentageIPv6 = 0.0; + int sum = overTime[i].querytypedata[0] + overTime[i].querytypedata[1]; + if(sum > 0) + { + percentageIPv4 = 1e2*overTime[i].querytypedata[0] / sum; + percentageIPv6 = 1e2*overTime[i].querytypedata[1] / sum; + } + sprintf(server_message, "%i %.2f %.2f\n", overTime[i].timestamp, percentageIPv4, percentageIPv6); swrite(server_message, *sock); } } diff --git a/routines.h b/routines.h index 49278ad10..f2f039830 100644 --- a/routines.h +++ b/routines.h @@ -31,6 +31,7 @@ void process_pihole_log(int file); void *pihole_log_thread(void *val); void validate_access(const char * name, int pos, bool testmagic, int line, const char * function, const char * file); void validate_access_oTfd(int timeidx, int pos, int line, const char * function, const char * file); +void reresolveHostnames(void); void pihole_log_flushed(bool message); diff --git a/test/test_suite.sh b/test/test_suite.sh index d87158ab3..8bf1b8e6d 100644 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -25,8 +25,9 @@ load 'libs/bats-support/load' [[ ${lines[5]} =~ "unique_domains 6" ]] [[ ${lines[6]} =~ "queries_forwarded 3" ]] [[ ${lines[7]} =~ "queries_cached 2" ]] - [[ ${lines[8]} == "unique_clients 3" ]] - [[ ${lines[9]} == "---EOM---" ]] + [[ ${lines[8]} == "clients_ever_seen 3" ]] + [[ ${lines[9]} == "unique_clients 3" ]] + [[ ${lines[10]} == "---EOM---" ]] } @test "Top Clients" { @@ -71,11 +72,11 @@ load 'libs/bats-support/load' run bash -c 'echo ">forward-dest" | nc -v 127.0.0.1 4711' echo "output: ${lines[@]}" [[ ${lines[0]} == "Connection to 127.0.0.1 4711 port [tcp/*] succeeded!" ]] - [[ ${lines[1]} =~ "0 4 2001:1608:10:25::9249:d69b" ]] - [[ ${lines[2]} =~ "1 4 2620:0:ccd::2 resolver2.ipv6-sandbox.opendns.com" ]] - [[ ${lines[3]} =~ "2 4 ::1 local" ]] - [[ ${lines[4]} =~ "3 2 2001:1608:10:25::1c04:b12f" ]] - [[ ${lines[5]} =~ "4 2 2620:0:ccc::2 resolver1.ipv6-sandbox.opendns.com" ]] + [[ ${lines[1]} =~ "0 57.14 ::1 local" ]] + [[ ${lines[2]} =~ "1 14.29 2620:0:ccd::2 resolver2.ipv6-sandbox.opendns.com" ]] + [[ ${lines[3]} =~ "2 9.52 2001:1608:10:25::9249:d69b" ]] + [[ ${lines[4]} =~ "3 9.52 2001:1608:10:25::1c04:b12f" ]] + [[ ${lines[5]} =~ "4 9.52 2620:0:ccc::2 resolver1.ipv6-sandbox.opendns.com" ]] [[ ${lines[6]} == "---EOM---" ]] } @@ -83,11 +84,11 @@ load 'libs/bats-support/load' run bash -c 'echo ">forward-dest unsorted" | nc -v 127.0.0.1 4711' echo "output: ${lines[@]}" [[ ${lines[0]} == "Connection to 127.0.0.1 4711 port [tcp/*] succeeded!" ]] - [[ ${lines[1]} =~ "0 4 2001:1608:10:25::9249:d69b" ]] - [[ ${lines[2]} =~ "1 2 2001:1608:10:25::1c04:b12f" ]] - [[ ${lines[3]} =~ "2 4 2620:0:ccd::2 resolver2.ipv6-sandbox.opendns.com" ]] - [[ ${lines[4]} =~ "3 2 2620:0:ccc::2 resolver1.ipv6-sandbox.opendns.com" ]] - [[ ${lines[5]} =~ "4 4 ::1 local" ]] + [[ ${lines[1]} =~ "0 9.52 2001:1608:10:25::9249:d69b" ]] + [[ ${lines[2]} =~ "1 9.52 2001:1608:10:25::1c04:b12f" ]] + [[ ${lines[3]} =~ "2 14.29 2620:0:ccd::2 resolver2.ipv6-sandbox.opendns.com" ]] + [[ ${lines[4]} =~ "3 9.52 2620:0:ccc::2 resolver1.ipv6-sandbox.opendns.com" ]] + [[ ${lines[5]} =~ "4 57.14 ::1 local" ]] [[ ${lines[6]} == "---EOM---" ]] } @@ -95,8 +96,8 @@ load 'libs/bats-support/load' run bash -c 'echo ">querytypes" | nc -v 127.0.0.1 4711' echo "output: ${lines[@]}" [[ ${lines[0]} == "Connection to 127.0.0.1 4711 port [tcp/*] succeeded!" ]] - [[ ${lines[1]} == "A (IPv4): 5" ]] - [[ ${lines[2]} == "AAAA (IPv6): 2" ]] + [[ ${lines[1]} == "A (IPv4): 71.43" ]] + [[ ${lines[2]} == "AAAA (IPv6): 28.57" ]] [[ ${lines[3]} == "---EOM---" ]] }