-
-
Notifications
You must be signed in to change notification settings - Fork 199
/
Copy pathgravity-db.c
2797 lines (2466 loc) · 88.2 KB
/
gravity-db.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Gravity database routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "FTL.h"
#include "sqlite3.h"
#include "gravity-db.h"
// struct config
#include "config/config.h"
// logging routines
#include "log.h"
// getstr()
#include "shmem.h"
// SQLite3 prepared statement vectors
#include "vector.h"
// log_subnet_warning()
// logg_inaccessible_adlist
#include "message-table.h"
// getMACfromIP()
#include "network-table.h"
// struct DNSCacheData
#include "datastructure.h"
// reset_aliasclient()
#include "aliasclients.h"
// Definition of struct regexData
#include "regex_r.h"
// file_readable()
#include "files.h"
// Prefix of interface names in the client table
#define INTERFACE_SEP ":"
// Process-private prepared statements are used to support multiple forks (might
// be TCP workers) to use the database simultaneously without corrupting the
// gravity database
sqlite3_stmt_vec *whitelist_stmt = NULL;
sqlite3_stmt_vec *gravity_stmt = NULL;
sqlite3_stmt_vec *antigravity_stmt = NULL;
sqlite3_stmt_vec *blacklist_stmt = NULL;
// Private variables
static sqlite3 *gravity_db = NULL;
static sqlite3_stmt* table_stmt = NULL;
bool gravityDB_opened = false;
static bool gravity_abp_format = false;
// Variables memorizing the parent gravity database connection and prepared
// statements to avoid valgrind warnings about memory leaks
static sqlite3 *parent_gravity_db = NULL;
sqlite3_stmt_vec *parent_whitelist_stmt = NULL;
sqlite3_stmt_vec *parent_gravity_stmt = NULL;
sqlite3_stmt_vec *parent_antigravity_stmt = NULL;
sqlite3_stmt_vec *parent_blacklist_stmt = NULL;
// Private prototypes
static bool gravityDB_open(void);
// Table names corresponding to the enum defined in gravity-db.h
static const char* tablename[] = { "vw_gravity", "vw_blacklist", "vw_whitelist", "vw_regex_blacklist", "vw_regex_whitelist" , "client", "group", "adlist", "denied_domains", "allowed_domains", "" };
// Prototypes from functions in dnsmasq's source
extern void rehash(int size);
// Initialize gravity subroutines
void gravityDB_forked(void)
{
// See "How To Corrupt An SQLite Database File"
// (https://www.sqlite.org/howtocorrupt.html):
// 2.6. Carrying an open database connection across a fork()
//
// Do not open an SQLite database connection, then fork(), then try to
// use that database connection in the child process. All kinds of
// locking problems will result and you can easily end up with a corrupt
// database. SQLite is not designed to support that kind of behavior.
// Any database connection that is used in a child process must be
// opened in the child process, not inherited from the parent.
//
// Do not even call sqlite3_close() on a database connection from a
// child process if the connection was opened in the parent. It is safe
// to close the underlying file descriptor, but the sqlite3_close()
// interface might invoke cleanup activities that will delete content
// out from under the parent, leading to errors and perhaps even
// database corruption.
//
// Hence, we pretend that we did not open the database so far
// NOTE: Yes, this will leak memory into the forks, however, there isn't
// much we can do about this. The "proper" solution would be to close
// the finalize the prepared gravity database statements and close the
// database connection *before* forking and re-open and re-prepare them
// afterwards (independently once in the parent, once in the fork). It
// is clear that this in not what we want to do as this is a slow
// process and many TCP queries could lead to a DoS attack.
gravityDB_opened = false;
parent_gravity_db = gravity_db;
gravity_db = NULL;
// Also pretend we have not yet prepared the list statements
parent_whitelist_stmt = whitelist_stmt;
whitelist_stmt = NULL;
parent_blacklist_stmt = blacklist_stmt;
blacklist_stmt = NULL;
parent_gravity_stmt = gravity_stmt;
gravity_stmt = NULL;
parent_antigravity_stmt = antigravity_stmt;
antigravity_stmt = NULL;
// Open the database
gravityDB_open();
}
static void gravity_check_ABP_format(void)
{
// Check if we have a valid ABP format
// We do this by checking the "abp_domains" property in the "info" table
// Prepare statement
sqlite3_stmt *stmt = NULL;
int rc = sqlite3_prepare_v2(gravity_db,
"SELECT value FROM info WHERE property = 'abp_domains';",
-1, &stmt, NULL);
if( rc != SQLITE_OK )
{
log_warn("gravity_check_ABP_format() - SQL error prepare: %s", sqlite3_errstr(rc));
return;
}
// Execute statement
rc = sqlite3_step(stmt);
if( rc != SQLITE_ROW )
{
// No result
gravity_abp_format = false;
sqlite3_finalize(stmt);
return;
}
// Get result (SQLite3 stores 1 for TRUE, 0 for FALSE)
gravity_abp_format = sqlite3_column_int(stmt, 0) != 0;
// Finalize statement
sqlite3_finalize(stmt);
}
// Open gravity database
static bool gravityDB_open(void)
{
struct stat st;
if(stat(config.files.gravity.v.s, &st) != 0)
{
// File does not exist
log_warn("gravityDB_open(): %s does not exist", config.files.gravity.v.s);
return false;
}
if(gravityDB_opened && gravity_db != NULL)
{
log_debug(DEBUG_DATABASE, "gravityDB_open(): Database already connected");
return true;
}
log_debug(DEBUG_DATABASE, "gravityDB_open(): Trying to open %s in read-only mode", config.files.gravity.v.s);
int rc = sqlite3_open_v2(config.files.gravity.v.s, &gravity_db, SQLITE_OPEN_READWRITE, NULL);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open() - SQL error: %s", sqlite3_errstr(rc));
gravityDB_close();
return false;
}
// Database connection is now open
gravityDB_opened = true;
// Tell SQLite3 to store temporary tables in memory. This speeds up read operations on
// temporary tables, indices, and views.
log_debug(DEBUG_DATABASE, "gravityDB_open(): Setting location for temporary object to MEMORY");
char *zErrMsg = NULL;
rc = sqlite3_exec(gravity_db, "PRAGMA temp_store = MEMORY", NULL, NULL, &zErrMsg);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open(PRAGMA temp_store) - SQL error (%i): %s", rc, zErrMsg);
sqlite3_free(zErrMsg);
gravityDB_close();
return false;
}
// Set SQLite3 busy timeout to a user-defined value (defaults to 1 second)
// to avoid immediate failures when the gravity database is still busy
// writing the changes to disk
log_debug(DEBUG_DATABASE, "gravityDB_open(): Setting busy timeout to %d", DATABASE_BUSY_TIMEOUT);
sqlite3_busy_timeout(gravity_db, DATABASE_BUSY_TIMEOUT);
// Prepare private vector of statements for this process (might be a TCP fork!)
if(whitelist_stmt == NULL)
whitelist_stmt = new_sqlite3_stmt_vec(counters->clients);
if(blacklist_stmt == NULL)
blacklist_stmt = new_sqlite3_stmt_vec(counters->clients);
if(gravity_stmt == NULL)
gravity_stmt = new_sqlite3_stmt_vec(counters->clients);
if(antigravity_stmt == NULL)
antigravity_stmt = new_sqlite3_stmt_vec(counters->clients);
// Explicitly set busy handler to zero milliseconds
log_debug(DEBUG_DATABASE, "gravityDB_open(): Setting busy timeout to zero");
rc = sqlite3_busy_timeout(gravity_db, 0);
if(rc != SQLITE_OK)
log_err("gravityDB_open() - Cannot set busy handler: %s", sqlite3_errstr(rc));
// Check (and remember in global variable) if there are any ABP-style
// entries in the database
gravity_check_ABP_format();
log_debug(DEBUG_DATABASE, "gravityDB_open(): Successfully opened gravity.db");
return true;
}
bool gravityDB_reopen(void)
{
// We call this routine when reloading the cache.
gravityDB_close();
// Re-open gravity database
return gravityDB_open();
}
static char* get_client_querystr(const char *table, const char *column, const char *groups)
{
// Build query string with group filtering
char *querystr = NULL;
if(asprintf(&querystr, "SELECT %s from %s WHERE domain = ? AND group_id IN (%s);", column, table, groups) < 1)
{
log_err("get_client_querystr(%s, %s) - asprintf() error", table, groups);
return NULL;
}
log_debug(DEBUG_DATABASE, "get_client_querystr: %s", querystr);
return querystr;
}
// Determine whether to show IP or hardware address
static const char *show_client_string(const char *hwaddr, const char *hostname,
const char *ip)
{
if(hostname != NULL && strlen(hostname) > 0)
{
// Valid hostname address, display it
return hostname;
}
else if(hwaddr != NULL && strncasecmp(hwaddr, "ip-", 3) != 0)
{
// Valid hardware address and not a mock-device
return hwaddr;
}
// Fallback: display IP address
return ip;
}
// Get associated groups for this client (if defined)
static bool get_client_groupids(clientsData *client)
{
const char *ip = getstr(client->ippos);
client->flags.found_group = false;
client->groupspos = 0u;
// Do not proceed when database is not available
if(!gravityDB_opened && !gravityDB_open())
{
log_warn("get_client_groupids(): Gravity database not available");
return false;
}
log_debug(DEBUG_DATABASE, "Querying gravity database for client with IP %s...", ip);
// Check if client is configured through the client table
// This will return nothing if the client is unknown/unconfigured
const char *querystr = "SELECT count(id) matching_count, "
"max(id) chosen_match_id, "
"ip chosen_match_text, "
"group_concat(id) matching_ids, "
"subnet_match(ip,?) matching_bits FROM client "
"WHERE matching_bits > 0 "
"GROUP BY matching_bits "
"ORDER BY matching_bits DESC LIMIT 1;";
// Prepare query
int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\") - SQL error prepare: %s",
ip, sqlite3_errstr(rc));
return false;
}
// Bind ipaddr to prepared statement
if((rc = sqlite3_bind_text(table_stmt, 1, ip, -1, SQLITE_STATIC)) != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\"): Failed to bind ip: %s",
ip, sqlite3_errstr(rc));
sqlite3_reset(table_stmt);
sqlite3_finalize(table_stmt);
return false;
}
// Perform query
rc = sqlite3_step(table_stmt);
int matching_count = 0, chosen_match_id = -1, matching_bits = 0;
char *matching_ids = NULL, *chosen_match_text = NULL;
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database,
// extract the result (there can be at most one line)
matching_count = sqlite3_column_int(table_stmt, 0);
chosen_match_id = sqlite3_column_int(table_stmt, 1);
chosen_match_text = strdup((const char*)sqlite3_column_text(table_stmt, 2));
matching_ids = strdup((const char*)sqlite3_column_text(table_stmt, 3));
matching_bits = sqlite3_column_int(table_stmt, 4);
if(matching_count == 1)
// Case matching_count > 1 handled below using logg_subnet_warning()
log_debug(DEBUG_CLIENTS, "--> Found record for %s in the client table (group ID %d)", ip, chosen_match_id);
}
else if(rc == SQLITE_DONE)
{
log_debug(DEBUG_CLIENTS, "--> No record for %s in the client table", ip);
}
else
{
// Error
log_err("get_client_groupids(\"%s\") - SQL error step: %s",
ip, sqlite3_errstr(rc));
gravityDB_finalizeTable();
return false;
}
// Finalize statement
gravityDB_finalizeTable();
if(matching_count > 1)
{
// There is more than one configured subnet that matches to current device
// with the same number of subnet mask bits. This is likely unintended by
// the user so we issue a warning so they can address it.
// Example:
// Device 10.8.0.22
// Client 1: 10.8.0.0/24
// Client 2: 10.8.1.0/24
logg_subnet_warning(ip, matching_count, matching_ids, matching_bits, chosen_match_text, chosen_match_id);
}
// Free memory if applicable
if(matching_ids != NULL)
{
free(matching_ids);
matching_ids = NULL;
}
if(chosen_match_text != NULL)
{
free(chosen_match_text);
chosen_match_text = NULL;
}
// If we didn't find an IP address match above, try with MAC address matches
// 1. Look up MAC address of this client
// 1.1. Look up IP address in network_addresses table
// 1.2. Get MAC address from this network_id
// 2. If found -> Get groups by looking up MAC address in client table
char *hwaddr = NULL;
if(chosen_match_id < 0)
{
log_debug(DEBUG_CLIENTS, "Querying gravity database for MAC address of %s...", ip);
// Do the lookup
hwaddr = getMACfromIP(NULL, ip);
if(hwaddr == NULL)
{
log_debug(DEBUG_CLIENTS, "--> No result.");
}
else if(strlen(hwaddr) > 3 && strncasecmp(hwaddr, "ip-", 3) == 0)
{
free(hwaddr);
hwaddr = NULL;
log_debug(DEBUG_CLIENTS, "Skipping mock-device hardware address lookup");
}
// Set MAC address from database information if available and the MAC address is not already set
else if(client->hwlen != 6)
{
// Proper MAC parsing
unsigned char data[6];
const int n = sscanf(hwaddr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&data[0], &data[1], &data[2],
&data[3], &data[4], &data[5]);
// Set hwlen only if we got data
if(n == 6)
{
memcpy(client->hwaddr, data, sizeof(data));
client->hwlen = sizeof(data);
}
}
// MAC address fallback: Try to synthesize MAC address from internal buffer
if(hwaddr == NULL && client->hwlen == 6)
{
hwaddr = calloc(18, sizeof(char)); // 18 == sizeof("AA:BB:CC:DD:EE:FF")
snprintf(hwaddr, 18, "%02X:%02X:%02X:%02X:%02X:%02X",
client->hwaddr[0], client->hwaddr[1], client->hwaddr[2],
client->hwaddr[3], client->hwaddr[4], client->hwaddr[5]);
log_debug(DEBUG_CLIENTS, "--> Obtained %s from internal ARP cache", hwaddr);
}
}
// Check if we received a valid MAC address
// This ensures we skip mock hardware addresses such as "ip-127.0.0.1"
if(hwaddr != NULL)
{
log_debug(DEBUG_CLIENTS, "--> Querying client table for %s", hwaddr);
// Check if client is configured through the client table
// This will return nothing if the client is unknown/unconfigured
// We use COLLATE NOCASE to ensure the comparison is done case-insensitive
querystr = "SELECT id FROM client WHERE ip = ? COLLATE NOCASE;";
// Prepare query
rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("get_client_groupids(%s) - SQL error prepare: %s",
querystr, sqlite3_errstr(rc));
free(hwaddr); // hwaddr != NULL -> memory has been allocated
return false;
}
// Bind hwaddr to prepared statement
if((rc = sqlite3_bind_text(table_stmt, 1, hwaddr, -1, SQLITE_STATIC)) != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\", \"%s\"): Failed to bind hwaddr: %s",
ip, hwaddr, sqlite3_errstr(rc));
sqlite3_reset(table_stmt);
sqlite3_finalize(table_stmt);
free(hwaddr); // hwaddr != NULL -> memory has been allocated
return false;
}
// Perform query
rc = sqlite3_step(table_stmt);
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database,
// extract the result (there can be at most one line)
chosen_match_id = sqlite3_column_int(table_stmt, 0);
log_debug(DEBUG_CLIENTS, "--> Found record for %s in the client table (group ID %d)", hwaddr, chosen_match_id);
}
else if(rc == SQLITE_DONE)
{
log_debug(DEBUG_CLIENTS, "--> There is no record for %s in the client table", hwaddr);
}
else
{
// Error
log_err("get_client_groupids(\"%s\", \"%s\") - SQL error step: %s",
ip, hwaddr, sqlite3_errstr(rc));
gravityDB_finalizeTable();
free(hwaddr); // hwaddr != NULL -> memory has been allocated
return false;
}
// Finalize statement and free allocated memory
gravityDB_finalizeTable();
}
// If we did neither find an IP nor a MAC address match above, we try to look
// up the client using its host name
// 1. Look up host name address of this client
// 2. If found -> Get groups by looking up host name in client table
char *hostname = NULL;
if(chosen_match_id < 0)
{
log_debug(DEBUG_CLIENTS, "Querying gravity database for host name of %s...", ip);
// Do the lookup
hostname = getNameFromIP(NULL, ip);
if(hostname == NULL)
log_debug(DEBUG_CLIENTS, "--> No result.");
if(hostname != NULL && strlen(hostname) == 0)
{
free(hostname);
hostname = NULL;
log_debug(DEBUG_CLIENTS, "Skipping empty host name lookup");
}
}
// Check if we received a valid MAC address
// This ensures we skip mock hardware addresses such as "ip-127.0.0.1"
if(hostname != NULL)
{
log_debug(DEBUG_CLIENTS, "--> Querying client table for %s", hostname);
// Check if client is configured through the client table
// This will return nothing if the client is unknown/unconfigured
// We use COLLATE NOCASE to ensure the comparison is done case-insensitive
querystr = "SELECT id FROM client WHERE ip = ? COLLATE NOCASE;";
// Prepare query
rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("get_client_groupids(%s) - SQL error prepare: %s",
querystr, sqlite3_errstr(rc));
if(hwaddr) free(hwaddr);
free(hostname); // hostname != NULL -> memory has been allocated
return false;
}
// Bind hostname to prepared statement
if((rc = sqlite3_bind_text(table_stmt, 1, hostname, -1, SQLITE_STATIC)) != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\", \"%s\"): Failed to bind hostname: %s",
ip, hostname, sqlite3_errstr(rc));
sqlite3_reset(table_stmt);
sqlite3_finalize(table_stmt);
if(hwaddr) free(hwaddr);
free(hostname); // hostname != NULL -> memory has been allocated
return false;
}
// Perform query
rc = sqlite3_step(table_stmt);
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database,
// extract the result (there can be at most one line)
chosen_match_id = sqlite3_column_int(table_stmt, 0);
log_debug(DEBUG_CLIENTS, "--> Found record for %s in the client table (group ID %d)", hostname, chosen_match_id);
}
else if(rc == SQLITE_DONE)
{
log_debug(DEBUG_CLIENTS, "--> There is no record for %s in the client table", hostname);
}
else
{
// Error
log_err("get_client_groupids(\"%s\", \"%s\") - SQL error step: %s",
ip, hostname, sqlite3_errstr(rc));
gravityDB_finalizeTable();
if(hwaddr) free(hwaddr);
free(hostname); // hostname != NULL -> memory has been allocated
return false;
}
// Finalize statement and free allocated memory
gravityDB_finalizeTable();
}
// If we did neither find an IP nor a MAC address and also no host name
// match above, we try to look up the client using its interface
// 1. Look up the interface of this client (FTL isn't aware of it
// when creating the client from history data!)
// 2. If found -> Get groups by looking up interface in client table
char *interface = NULL;
if(chosen_match_id < 0)
{
log_debug(DEBUG_CLIENTS, "Querying gravity database for interface of %s...", ip);
// Do the lookup
interface = getIfaceFromIP(NULL, ip);
if(interface == NULL)
log_debug(DEBUG_CLIENTS, "--> No result.");
if(interface != NULL && strlen(interface) == 0)
{
free(interface);
interface = 0;
log_debug(DEBUG_CLIENTS, "Skipping empty interface lookup");
}
}
// Check if we received a valid interface
if(interface != NULL)
{
log_debug(DEBUG_CLIENTS, "Querying client table for interface "INTERFACE_SEP"%s", interface);
// Check if client is configured through the client table using its interface
// This will return nothing if the client is unknown/unconfigured
// We use the SQLite concatenate operator || to prepace the queried interface by ":"
// We use COLLATE NOCASE to ensure the comparison is done case-insensitive
querystr = "SELECT id FROM client WHERE ip = '"INTERFACE_SEP"' || ? COLLATE NOCASE;";
// Prepare query
rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("get_client_groupids(%s) - SQL error prepare: %s",
querystr, sqlite3_errstr(rc));
if(hwaddr) free(hwaddr);
if(hostname) free(hostname);
free(interface); // interface != NULL -> memory has been allocated
return false;
}
// Bind interface to prepared statement
if((rc = sqlite3_bind_text(table_stmt, 1, interface, -1, SQLITE_STATIC)) != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\", \"%s\"): Failed to bind interface: %s",
ip, interface, sqlite3_errstr(rc));
sqlite3_reset(table_stmt);
sqlite3_finalize(table_stmt);
if(hwaddr) free(hwaddr);
if(hostname) free(hostname);
free(interface); // interface != NULL -> memory has been allocated
return false;
}
// Perform query
rc = sqlite3_step(table_stmt);
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database,
// extract the result (there can be at most one line)
chosen_match_id = sqlite3_column_int(table_stmt, 0);
log_debug(DEBUG_CLIENTS, "--> Found record for interface "INTERFACE_SEP"%s in the client table (group ID %d)", interface, chosen_match_id);
}
else if(rc == SQLITE_DONE)
{
log_debug(DEBUG_CLIENTS, "--> There is no record for interface "INTERFACE_SEP"%s in the client table", interface);
}
else
{
// Error
log_err("get_client_groupids(\"%s\", \"%s\") - SQL error step: %s",
ip, interface, sqlite3_errstr(rc));
gravityDB_finalizeTable();
if(hwaddr) free(hwaddr);
if(hostname) free(hostname);
free(interface); // interface != NULL -> memory has been allocated
return false;
}
// Finalize statement and free allocated memory
gravityDB_finalizeTable();
}
// We use the default group and return early here
// if above lookups didn't return any results
// (the client is not configured through the client table)
if(chosen_match_id < 0)
{
log_debug(DEBUG_CLIENTS, "Gravity database: Client %s not found. Using default group.\n",
show_client_string(hwaddr, hostname, ip));
client->groupspos = addstr("0");
client->flags.found_group = true;
if(hwaddr != NULL)
{
free(hwaddr);
hwaddr = NULL;
}
if(hostname != NULL)
{
free(hostname);
hostname = NULL;
}
if(interface != NULL)
{
free(interface);
interface = NULL;
}
return true;
}
// Build query string to get possible group associations for this particular client
// The SQL GROUP_CONCAT() function returns a string which is the concatenation of all
// non-NULL values of group_id separated by ','. The order of the concatenated elements
// is arbitrary, however, is of no relevance for your use case.
// We check using a possibly defined subnet and use the first result
querystr = "SELECT GROUP_CONCAT(group_id) FROM client_by_group "
"WHERE client_id = ?;";
log_debug(DEBUG_CLIENTS, "Querying gravity database for client %s (getting groups)", ip);
// Prepare query
rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\", \"%s\", %d) - SQL error prepare: %s",
ip, hwaddr, chosen_match_id, sqlite3_errstr(rc));
sqlite3_finalize(table_stmt);
return false;
}
// Bind hwaddr to prepared statement
if((rc = sqlite3_bind_int(table_stmt, 1, chosen_match_id)) != SQLITE_OK)
{
log_err("get_client_groupids(\"%s\", \"%s\", %d): Failed to bind chosen_match_id: %s",
ip, hwaddr, chosen_match_id, sqlite3_errstr(rc));
sqlite3_reset(table_stmt);
sqlite3_finalize(table_stmt);
return false;
}
// Perform query
rc = sqlite3_step(table_stmt);
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database
const char* result = (const char*)sqlite3_column_text(table_stmt, 0);
if(result != NULL)
{
client->groupspos = addstr(result);
client->flags.found_group = true;
}
}
else if(rc == SQLITE_DONE)
{
// Found no record for this client in the database
// -> No associated groups
client->groupspos = addstr("");
client->flags.found_group = true;
}
else
{
log_err("get_client_groupids(\"%s\", \"%s\", %d) - SQL error step: %s",
ip, hwaddr, chosen_match_id, sqlite3_errstr(rc));
gravityDB_finalizeTable();
return false;
}
// Finalize statement
gravityDB_finalizeTable();
// Debug logging
if(config.debug.clients.v.b)
{
if(interface != NULL)
{
log_debug(DEBUG_CLIENTS, "Gravity database: Client %s found (identified by interface %s). Using groups (%s)\n",
show_client_string(hwaddr, hostname, ip), interface, getstr(client->groupspos));
}
else
{
log_debug(DEBUG_CLIENTS, "Gravity database: Client %s found. Using groups (%s)\n",
show_client_string(hwaddr, hostname, ip), getstr(client->groupspos));
}
}
// Free possibly allocated memory
if(hwaddr != NULL)
{
free(hwaddr);
hwaddr = NULL;
}
if(hostname != NULL)
{
free(hostname);
hostname = NULL;
}
if(interface != NULL)
{
free(interface);
interface = NULL;
}
// Return success
return true;
}
char *__attribute__ ((malloc)) get_client_names_from_ids(const char *group_ids)
{
// Build query string to get concatenated groups
char *querystr = NULL;
if(asprintf(&querystr, "SELECT GROUP_CONCAT(ip) FROM client "
"WHERE id IN (%s);", group_ids) < 1)
{
log_err("group_names(%s) - asprintf() error", group_ids);
return NULL;
}
log_debug(DEBUG_DATABASE, "Querying group names for IDs (%s)", group_ids);
// Prepare query
int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &table_stmt, NULL);
if(rc != SQLITE_OK){
log_err("get_client_groupids(%s) - SQL error prepare: %s",
querystr, sqlite3_errstr(rc));
sqlite3_finalize(table_stmt);
free(querystr);
return strdup("N/A");
}
// Perform query
char *result = NULL;
rc = sqlite3_step(table_stmt);
if(rc == SQLITE_ROW)
{
// There is a record for this client in the database
result = strdup((const char*)sqlite3_column_text(table_stmt, 0));
if(result == NULL)
result = strdup("N/A");
}
else if(rc == SQLITE_DONE)
{
// Found no record for this client in the database
// -> No associated groups
result = strdup("N/A");
}
else
{
log_err("group_names(%s) - SQL error step: %s",
querystr, sqlite3_errstr(rc));
gravityDB_finalizeTable();
free(querystr);
return strdup("N/A");
}
// Finalize statement
gravityDB_finalizeTable();
free(querystr);
return result;
}
// Prepare statements for scanning white- and blacklist as well as gravit for one client
bool gravityDB_prepare_client_statements(clientsData *client)
{
// Return early if gravity database is not available
if(!gravityDB_opened && !gravityDB_open())
return false;
const char *clientip = getstr(client->ippos);
log_debug(DEBUG_DATABASE, "Initializing gravity statements for %s", clientip);
// Get associated groups for this client (if defined)
char *querystr = NULL;
if(!client->flags.found_group && !get_client_groupids(client))
return false;
// Prepare whitelist statement
// We use SELECT EXISTS() as this is known to efficiently use the index
// We are only interested in whether the domain exists or not in the
// list but don't case about duplicates or similar. SELECT EXISTS(...)
// returns true as soon as it sees the first row from the query inside
// of EXISTS().
log_debug(DEBUG_DATABASE, "gravityDB_open(): Preparing vw_whitelist statement for client %s", clientip);
querystr = get_client_querystr("vw_whitelist", "id", getstr(client->groupspos));
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v3(gravity_db, querystr, -1, SQLITE_PREPARE_PERSISTENT, &stmt, NULL);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open(\"SELECT(... vw_whitelist ...)\") - SQL error prepare: %s", sqlite3_errstr(rc));
gravityDB_close();
return false;
}
whitelist_stmt->set(whitelist_stmt, client->id, stmt);
free(querystr);
// Prepare gravity statement
log_debug(DEBUG_DATABASE, "gravityDB_open(): Preparing vw_gravity statement for client %s", clientip);
querystr = get_client_querystr("vw_gravity", "adlist_id", getstr(client->groupspos));
rc = sqlite3_prepare_v3(gravity_db, querystr, -1, SQLITE_PREPARE_PERSISTENT, &stmt, NULL);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open(\"SELECT(... vw_gravity ...)\") - SQL error prepare: %s", sqlite3_errstr(rc));
gravityDB_close();
return false;
}
gravity_stmt->set(gravity_stmt, client->id, stmt);
free(querystr);
// Prepare antigravity statement
log_debug(DEBUG_DATABASE, "gravityDB_open(): Preparing vw_antigravity statement for client %s", clientip);
querystr = get_client_querystr("vw_antigravity", "adlist_id", getstr(client->groupspos));
rc = sqlite3_prepare_v3(gravity_db, querystr, -1, SQLITE_PREPARE_PERSISTENT, &stmt, NULL);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open(\"SELECT(... vw_antigravity ...)\") - SQL error prepare: %s", sqlite3_errstr(rc));
gravityDB_close();
return false;
}
antigravity_stmt->set(antigravity_stmt, client->id, stmt);
free(querystr);
// Prepare blacklist statement
log_debug(DEBUG_DATABASE, "gravityDB_open(): Preparing vw_blacklist statement for client %s", clientip);
querystr = get_client_querystr("vw_blacklist", "id", getstr(client->groupspos));
rc = sqlite3_prepare_v3(gravity_db, querystr, -1, SQLITE_PREPARE_PERSISTENT, &stmt, NULL);
if( rc != SQLITE_OK )
{
log_err("gravityDB_open(\"SELECT(... vw_blacklist ...)\") - SQL error prepare: %s", sqlite3_errstr(rc));
gravityDB_close();
return false;
}
blacklist_stmt->set(blacklist_stmt, client->id, stmt);
free(querystr);
return true;
}
// Finalize non-NULL prepared statements and set them to NULL for a given client
static inline void gravityDB_finalize_client_statements(clientsData *client)
{
log_debug(DEBUG_DATABASE, "Finalizing gravity statements for %s", getstr(client->ippos));
if(whitelist_stmt != NULL &&
whitelist_stmt->get(whitelist_stmt, client->id) != NULL)
{
sqlite3_finalize(whitelist_stmt->get(whitelist_stmt, client->id));
whitelist_stmt->set(whitelist_stmt, client->id, NULL);
}
if(blacklist_stmt != NULL &&
blacklist_stmt->get(blacklist_stmt, client->id) != NULL)
{
sqlite3_finalize(blacklist_stmt->get(blacklist_stmt, client->id));
blacklist_stmt->set(blacklist_stmt, client->id, NULL);
}
if(gravity_stmt != NULL &&
gravity_stmt->get(gravity_stmt, client->id) != NULL)
{
sqlite3_finalize(gravity_stmt->get(gravity_stmt, client->id));
gravity_stmt->set(gravity_stmt, client->id, NULL);
}
if(antigravity_stmt != NULL &&
antigravity_stmt->get(antigravity_stmt, client->id) != NULL)
{
sqlite3_finalize(antigravity_stmt->get(antigravity_stmt, client->id));
antigravity_stmt->set(antigravity_stmt, client->id, NULL);
}
// Unset group found property to trigger a check next time the
// client sends a query
client->flags.found_group = false;
}
// Close gravity database connection
void gravityDB_close(void)
{
// Return early if gravity database is not available
if(!gravityDB_opened)
return;
// Finalize prepared list statements for all clients
for(unsigned int clientID = 0; clientID < counters->clients; clientID++)
{
clientsData *client = getClient(clientID, true);
if(client != NULL)
gravityDB_finalize_client_statements(client);
}
// Free allocated memory for vectors of prepared client statements
free_sqlite3_stmt_vec(&whitelist_stmt);
free_sqlite3_stmt_vec(&blacklist_stmt);
free_sqlite3_stmt_vec(&gravity_stmt);
free_sqlite3_stmt_vec(&antigravity_stmt);
// Close table
log_debug(DEBUG_ANY, "Closing gravity database");
sqlite3_close(gravity_db);
gravity_db = NULL;
gravityDB_opened = false;
}
// Prepare a SQLite3 statement which can be used by gravityDB_getDomain() to get
// blocking domains from a table which is specified when calling this function
bool gravityDB_getTable(const unsigned char list)
{
if(!gravityDB_opened && !gravityDB_open())
{
log_err("gravityDB_getTable(%u): Gravity database not available", list);
return false;
}
// Checking for smaller than GRAVITY_LIST is omitted due to list being unsigned
if(list >= UNKNOWN_TABLE)
{
log_warn("gravityDB_getTable(%u): Requested list is not known!", list);
return false;
}
const char *querystr = NULL;
// Build correct query string to be used depending on list to be read
// We GROUP BY id as the view also includes the group_id leading to possible duplicates
// when domains are included in more than one group