Browse code

FreshClam: mirrors.dat, User-Agent UUID, cool-down

Add back the mirrors.dat file to the database directory.
This new version of mirros.dat will store:
- A randomly generated UUID for the FreshClam User-Agent.
- A retry-after timestamp that so FreshClam won't try to update after
having received an HTTP 429 response until the Retry-After timeout has
expired.

Also: FreshClam will now exit with a failure in daemon mode if an HTTP
403 (Forbidden) was received, because retrying later won't help any.
The FreshClam user will have to take actions to get unblocked.

Micah Snyder authored on 2021/03/22 11:47:21
Showing 4 changed files
... ...
@@ -1932,7 +1932,7 @@ int main(int argc, char **argv)
1932 1932
             alarm(0);
1933 1933
 #endif
1934 1934
 
1935
-            if (ret > 1) {
1935
+            if (ret > FC_UPTODATE) {
1936 1936
                 if ((opt = optget(opts, "OnErrorExecute"))->enabled)
1937 1937
                     arg = opt->strarg;
1938 1938
 
... ...
@@ -1940,6 +1940,14 @@ int main(int argc, char **argv)
1940 1940
                     execute("OnErrorExecute", arg, optget(opts, "daemon")->enabled);
1941 1941
 
1942 1942
                 arg = NULL;
1943
+
1944
+                if (FC_EFORBIDDEN == ret) {
1945
+                    /* We're being actively blocked, which is a fatal error. Exit. */
1946
+                    logg("^Freshclam was forbidden from downloading a database.\n");
1947
+                    logg("^This is fatal. Retrying later won't help. Exiting now.\n");
1948
+                    status = ret;
1949
+                    goto done;
1950
+                }
1943 1951
             }
1944 1952
 
1945 1953
             logg("#--------------------------------------\n");
... ...
@@ -250,6 +250,17 @@ fc_error_t fc_initialize(fc_config *fcConfig)
250 250
 
251 251
     g_bCompressLocalDatabase = fcConfig->bCompressLocalDatabase;
252 252
 
253
+    /* Load or create mirrors.dat */
254
+    if (FC_SUCCESS != load_mirrors_dat()) {
255
+        logg("*Failed to load mirrors.dat; will create a new mirrors.dat\n");
256
+
257
+        if (FC_SUCCESS != new_mirrors_dat()) {
258
+            logg("^Failed to create a new mirrors.dat!\n");
259
+            status = FC_EINIT;
260
+            goto done;
261
+        }
262
+    }
263
+
253 264
     status = FC_SUCCESS;
254 265
 
255 266
 done:
... ...
@@ -667,6 +678,10 @@ fc_error_t fc_update_database(
667 667
                     break;
668 668
                 }
669 669
                 case FC_ERETRYLATER: {
670
+                    char retry_after_string[26];
671
+                    struct tm *tm_info;
672
+                    tm_info = localtime(&g_mirrorsDat->retry_after);
673
+                    strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
670 674
                     logg("^FreshClam received error code 429 from the ClamAV Content Delivery Network (CDN).\n");
671 675
                     logg("This means that you have been rate limited by the CDN.\n");
672 676
                     logg(" 1. Run FreshClam no more than once an hour to check for updates.\n");
... ...
@@ -677,6 +692,7 @@ fc_error_t fc_update_database(
677 677
                     logg("    CDN and your own network.\n");
678 678
                     logg(" 3. Please do not open a ticket asking for an exemption from the rate limit,\n");
679 679
                     logg("    it will not be granted.\n");
680
+                    logg("^You are on cool-down until after: %s\n", retry_after_string);
680 681
                     goto success;
681 682
                     break;
682 683
                 }
... ...
@@ -726,6 +742,33 @@ fc_error_t fc_update_databases(
726 726
 
727 727
     *nUpdated = 0;
728 728
 
729
+    if (g_mirrorsDat->retry_after > 0) {
730
+        if (g_mirrorsDat->retry_after > time(NULL)) {
731
+            /* We're on cool-down, try again later. */
732
+            char retry_after_string[26];
733
+            struct tm *tm_info;
734
+            tm_info = localtime(&g_mirrorsDat->retry_after);
735
+            strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
736
+            logg("^FreshClam previously received error code 429 from the ClamAV Content Delivery Network (CDN).\n");
737
+            logg("This means that you have been rate limited by the CDN.\n");
738
+            logg(" 1. Run FreshClam no more than once an hour to check for updates.\n");
739
+            logg("    Freshclam should check DNS first to see if an update is needed.\n");
740
+            logg(" 2. If you have more than 10 hosts on your network attempting to download,\n");
741
+            logg("    it is recommended that you set up a private mirror on your network using\n");
742
+            logg("    cvdupdate (https://pypi.org/project/cvdupdate/) to save bandwidth on the\n");
743
+            logg("    CDN and your own network.\n");
744
+            logg(" 3. Please do not open a ticket asking for an exemption from the rate limit,\n");
745
+            logg("    it will not be granted.\n");
746
+            logg("^You are still on cool-down until after: %s\n", retry_after_string);
747
+            status = FC_SUCCESS;
748
+            goto done;
749
+        } else {
750
+            g_mirrorsDat->retry_after = 0;
751
+            logg("^Cool-down expired, ok to try again.\n");
752
+            save_mirrors_dat();
753
+        }
754
+    }
755
+
729 756
     for (i = 0; i < nDatabases; i++) {
730 757
         if (FC_SUCCESS != (ret = fc_update_database(
731 758
                                databaseList[i],
... ...
@@ -814,6 +857,41 @@ fc_error_t fc_download_url_database(
814 814
                 }
815 815
                 break;
816 816
             }
817
+            case FC_EFORBIDDEN: {
818
+                logg("^FreshClam received error code 403 from the ClamAV Content Delivery Network (CDN).\n");
819
+                logg("This could mean several things:\n");
820
+                logg(" 1. You are running an out of date version of ClamAV / FreshClam.\n");
821
+                logg("    Ensure you are the most updated version by visiting https://www.clamav.net/downloads\n");
822
+                logg(" 2. Your network is explicitly denied by the FreshClam CDN.\n");
823
+                logg("    In order to rectify this please check that you are:\n");
824
+                logg("   a. Running an up to date version of FreshClam\n");
825
+                logg("   b. Running FreshClam no more than once an hour\n");
826
+                logg("   c. If you have checked (a) and (b), please open a ticket at\n");
827
+                logg("      https://bugzilla.clamav.net under the “Mirrors” component\n");
828
+                logg("      and we will investigate why your network is blocked.\n");
829
+                status = ret;
830
+                goto done;
831
+                break;
832
+            }
833
+            case FC_ERETRYLATER: {
834
+                char retry_after_string[26];
835
+                struct tm *tm_info;
836
+                tm_info = localtime(&g_mirrorsDat->retry_after);
837
+                strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
838
+                logg("^FreshClam received error code 429 from the ClamAV Content Delivery Network (CDN).\n");
839
+                logg("This means that you have been rate limited by the CDN.\n");
840
+                logg(" 1. Run FreshClam no more than once an hour to check for updates.\n");
841
+                logg("    Freshclam should check DNS first to see if an update is needed.\n");
842
+                logg(" 2. If you have more than 10 hosts on your network attempting to download,\n");
843
+                logg("    it is recommended that you set up a private mirror on your network using\n");
844
+                logg("    cvdupdate (https://pypi.org/project/cvdupdate/) to save bandwidth on the\n");
845
+                logg("    CDN and your own network.\n");
846
+                logg(" 3. Please do not open a ticket asking for an exemption from the rate limit,\n");
847
+                logg("    it will not be granted.\n");
848
+                logg("^You are on cool-down until after: %s\n", retry_after_string);
849
+                goto success;
850
+                break;
851
+            }
817 852
             default: {
818 853
                 logg("Unexpected error when attempting to update from custom database URL: %s\n", urlDatabase);
819 854
                 status = ret;
... ...
@@ -66,6 +66,7 @@
66 66
 #include <math.h>
67 67
 
68 68
 #include <curl/curl.h>
69
+#include <openssl/rand.h>
69 70
 
70 71
 #include "target.h"
71 72
 
... ...
@@ -115,6 +116,242 @@ uint32_t g_requestTimeout = 0;
115 115
 
116 116
 uint32_t g_bCompressLocalDatabase = 0;
117 117
 
118
+mirrors_dat_v1_t *g_mirrorsDat = NULL;
119
+
120
+/** @brief Generate a Version 4 UUID according to RFC-4122
121
+ *
122
+ * Uses the openssl RAND_bytes function to generate a Version 4 UUID.
123
+ *
124
+ * Copyright 2021 Karthik Velakur
125
+ * License: MIT
126
+ * From: https://gist.github.com/kvelakur/9069c9896577c3040030
127
+ *
128
+ * @param buffer A buffer that is SIZEOF_UUID_V4
129
+ * @retval 1 on success, 0 otherwise.
130
+ */
131
+static int uuid_v4_gen(char *buffer)
132
+{
133
+    union {
134
+        struct
135
+        {
136
+            uint32_t time_low;
137
+            uint16_t time_mid;
138
+            uint16_t time_hi_and_version;
139
+            uint8_t clk_seq_hi_res;
140
+            uint8_t clk_seq_low;
141
+            uint8_t node[6];
142
+        };
143
+        uint8_t __rnd[16];
144
+    } uuid;
145
+
146
+    int rc = RAND_bytes(uuid.__rnd, sizeof(uuid));
147
+
148
+    // Refer Section 4.2 of RFC-4122
149
+    // https://tools.ietf.org/html/rfc4122#section-4.2
150
+    uuid.clk_seq_hi_res      = (uint8_t)((uuid.clk_seq_hi_res & 0x3F) | 0x80);
151
+    uuid.time_hi_and_version = (uint16_t)((uuid.time_hi_and_version & 0x0FFF) | 0x4000);
152
+
153
+    snprintf(buffer, SIZEOF_UUID_V4, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
154
+             uuid.time_low, uuid.time_mid, uuid.time_hi_and_version,
155
+             uuid.clk_seq_hi_res, uuid.clk_seq_low,
156
+             uuid.node[0], uuid.node[1], uuid.node[2],
157
+             uuid.node[3], uuid.node[4], uuid.node[5]);
158
+    buffer[SIZEOF_UUID_V4 - 1] = 0;
159
+
160
+    return rc;
161
+}
162
+
163
+fc_error_t load_mirrors_dat(void)
164
+{
165
+    fc_error_t status      = FC_EINIT;
166
+    int handle             = -1;
167
+    ssize_t bread          = 0;
168
+    mirrors_dat_v1_t *mdat = NULL;
169
+    uint32_t version       = 0;
170
+    char magic[13]         = {0};
171
+
172
+    /* Change directory to database directory */
173
+    if (chdir(g_databaseDirectory)) {
174
+        logg("!Can't change dir to %s\n", g_databaseDirectory);
175
+        status = FC_EDIRECTORY;
176
+        goto done;
177
+    }
178
+    logg("*Current working dir is %s\n", g_databaseDirectory);
179
+
180
+    if (-1 == (handle = safe_open("mirrors.dat", O_RDONLY | O_BINARY))) {
181
+        char currdir[PATH_MAX];
182
+
183
+        if (getcwd(currdir, sizeof(currdir)))
184
+            logg("*Can't open mirrors.dat in %s\n", currdir);
185
+        else
186
+            logg("*Can't open mirrors.dat in the current directory\n");
187
+
188
+        logg("*It probably doesn't exist yet. That's ok.\n");
189
+        status = FC_EFILE;
190
+        goto done;
191
+    }
192
+
193
+    if (strlen(MIRRORS_DAT_MAGIC) != (bread = read(handle, &magic, strlen(MIRRORS_DAT_MAGIC)))) {
194
+        char error_message[260];
195
+        cli_strerror(errno, error_message, 260);
196
+        logg("!Can't read version from mirrors.dat. Bytes read: %zi, error: %s\n", bread, error_message);
197
+        goto done;
198
+    }
199
+    if (0 != strncmp(magic, MIRRORS_DAT_MAGIC, strlen(MIRRORS_DAT_MAGIC))) {
200
+        logg("*Magic bytes for mirrors.dat did not match expectations.\n");
201
+        goto done;
202
+    }
203
+
204
+    if (sizeof(uint32_t) != (bread = read(handle, &version, sizeof(uint32_t)))) {
205
+        char error_message[260];
206
+        cli_strerror(errno, error_message, 260);
207
+        logg("!Can't read version from mirrors.dat. Bytes read: %zi, error: %s\n", bread, error_message);
208
+        goto done;
209
+    }
210
+
211
+    switch (version) {
212
+        case 1: {
213
+            /* Verify that file size is as expected. */
214
+            off_t file_size = lseek(handle, 0L, SEEK_END);
215
+
216
+            if (strlen(MIRRORS_DAT_MAGIC) + sizeof(mirrors_dat_v1_t) != (size_t)file_size) {
217
+                logg("*mirrors.dat is bigger than expected: %zu != %ld\n", sizeof(mirrors_dat_v1_t), file_size);
218
+                goto done;
219
+            }
220
+
221
+            /* Rewind to just after the magic bytes and read data struct */
222
+            lseek(handle, strlen(MIRRORS_DAT_MAGIC), SEEK_SET);
223
+
224
+            mdat = malloc(sizeof(mirrors_dat_v1_t));
225
+            if (NULL == mdat) {
226
+                logg("!Failed to allocate memory for mirrors.dat\n");
227
+                status = FC_EMEM;
228
+                goto done;
229
+            }
230
+
231
+            if (sizeof(mirrors_dat_v1_t) != (bread = read(handle, mdat, sizeof(mirrors_dat_v1_t)))) {
232
+                char error_message[260];
233
+                cli_strerror(errno, error_message, 260);
234
+                logg("!Can't read from mirrors.dat. Bytes read: %zi, error: %s\n", bread, error_message);
235
+                goto done;
236
+            }
237
+
238
+            /* Got it.*/
239
+            close(handle);
240
+
241
+            /* This is the latest version.
242
+               If we change the format in the future, we may wish to create a new
243
+               mirrors dat struct, import the relevant bits to the new format,
244
+               and then save (overwrite) mirrors.dat with the new data. */
245
+            g_mirrorsDat = mdat;
246
+            break;
247
+        }
248
+        default: {
249
+            logg("*mirrors.dat version is different than expected: %u != %u\n", 1, g_mirrorsDat->version);
250
+            goto done;
251
+        }
252
+    }
253
+
254
+    logg("*Loaded mirrors.dat:\n");
255
+    logg("*  version:    %d\n", g_mirrorsDat->version);
256
+    logg("*  uuid:       %s\n", g_mirrorsDat->uuid);
257
+    if (g_mirrorsDat->retry_after > 0) {
258
+        char retry_after_string[26];
259
+        struct tm *tm_info = localtime(&g_mirrorsDat->retry_after);
260
+        strftime(retry_after_string, 26, "%Y-%m-%d %H:%M:%S", tm_info);
261
+        logg("*  retry-after: %s\n", retry_after_string);
262
+    }
263
+
264
+    status = FC_SUCCESS;
265
+
266
+done:
267
+    if (-1 != handle) {
268
+        close(handle);
269
+    }
270
+    if (FC_SUCCESS != status) {
271
+        free(mdat);
272
+    }
273
+
274
+    return status;
275
+}
276
+
277
+fc_error_t save_mirrors_dat(void)
278
+{
279
+    fc_error_t status = FC_EINIT;
280
+    int handle        = -1;
281
+
282
+    if (-1 == (handle = safe_open("mirrors.dat", O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644))) {
283
+        char currdir[PATH_MAX];
284
+
285
+        if (getcwd(currdir, sizeof(currdir)))
286
+            logg("!Can't create mirrors.dat in %s\n", currdir);
287
+        else
288
+            logg("!Can't create mirrors.dat in the current directory\n");
289
+
290
+        logg("Hint: The database directory must be writable for UID %d or GID %d\n", getuid(), getgid());
291
+        status = FC_EDBDIRACCESS;
292
+        goto done;
293
+    }
294
+    if (-1 == write(handle, MIRRORS_DAT_MAGIC, strlen(MIRRORS_DAT_MAGIC))) {
295
+        logg("!Can't write to mirrors.dat\n");
296
+    }
297
+    if (-1 == write(handle, g_mirrorsDat, sizeof(mirrors_dat_v1_t))) {
298
+        logg("!Can't write to mirrors.dat\n");
299
+    }
300
+
301
+    logg("*Saved mirrors.dat\n");
302
+
303
+    status = FC_SUCCESS;
304
+done:
305
+    if (-1 != handle) {
306
+        close(handle);
307
+    }
308
+
309
+    return status;
310
+}
311
+
312
+fc_error_t new_mirrors_dat(void)
313
+{
314
+    fc_error_t status = FC_EINIT;
315
+
316
+    mirrors_dat_v1_t *mdat = malloc(sizeof(mirrors_dat_v1_t));
317
+    if (NULL == mdat) {
318
+        logg("!Failed to allocate memory for mirrors.dat\n");
319
+        status = FC_EMEM;
320
+        goto done;
321
+    }
322
+
323
+    mdat->version     = 1;
324
+    mdat->retry_after = 0;
325
+    if (0 == uuid_v4_gen(mdat->uuid)) {
326
+        /* Failed to create UUID */
327
+        status = FC_EINIT;
328
+        logg("!Failed to create random UUID!\n");
329
+        goto done;
330
+    }
331
+
332
+    g_mirrorsDat = mdat;
333
+
334
+    logg("*Creating new mirrors.dat\n");
335
+
336
+    if (FC_SUCCESS != save_mirrors_dat()) {
337
+        logg("!Failed to save mirrors.dat!\n");
338
+        status = FC_EFILE;
339
+        goto done;
340
+    }
341
+
342
+    status = FC_SUCCESS;
343
+
344
+done:
345
+    if (FC_SUCCESS != status) {
346
+        if (NULL != mdat) {
347
+            free(mdat);
348
+        }
349
+        g_mirrorsDat = NULL;
350
+    }
351
+    return status;
352
+}
353
+
118 354
 /**
119 355
  * @brief Get DNS text record field # for official databases.
120 356
  *
... ...
@@ -328,12 +565,19 @@ static fc_error_t create_curl_handle(
328 328
         goto done;
329 329
     }
330 330
 
331
-    if (g_userAgent)
331
+    if (g_userAgent) {
332 332
         strncpy(userAgent, g_userAgent, sizeof(userAgent));
333
-    else
333
+    } else {
334
+        /*
335
+         * Use a randomly generated UUID in the User-Agent
336
+         * We'll try to load it from a file in the database directory.
337
+         * If none exists, we'll create a new one and save it to said file.
338
+         */
334 339
         snprintf(userAgent, sizeof(userAgent),
335
-                 PACKAGE "/%s (OS: " TARGET_OS_TYPE ", ARCH: " TARGET_ARCH_TYPE ", CPU: " TARGET_CPU_TYPE ")",
336
-                 get_version());
340
+                 PACKAGE "/%s (OS: " TARGET_OS_TYPE ", ARCH: " TARGET_ARCH_TYPE ", CPU: " TARGET_CPU_TYPE ", UUID: %s)",
341
+                 get_version(),
342
+                 g_mirrorsDat->uuid);
343
+    }
337 344
     userAgent[sizeof(userAgent) - 1] = 0;
338 345
 
339 346
     if (mprintf_verbose) {
... ...
@@ -730,6 +974,27 @@ static fc_error_t remote_cvdhead(
730 730
             status = FC_UPTODATE;
731 731
             goto done;
732 732
         }
733
+        case 403: {
734
+            status = FC_EFORBIDDEN;
735
+            break;
736
+        }
737
+        case 429: {
738
+            status = FC_ERETRYLATER;
739
+            curl_off_t retry_after;
740
+
741
+            /* Find out how long we should wait before allowing a retry. */
742
+            curl_easy_getinfo(curl, CURLINFO_RETRY_AFTER, &retry_after);
743
+            if (retry_after > 0) {
744
+                /* The response gave us a Retry-After date. Use that. */
745
+                g_mirrorsDat->retry_after = time(NULL) + (time_t)retry_after;
746
+            } else {
747
+                /* Try again in no less than 4 hours if the response didn't specify. */
748
+                g_mirrorsDat->retry_after = time(NULL) + 60 * 60 * 4;
749
+            }
750
+            (void)save_mirrors_dat();
751
+
752
+            break;
753
+        }
733 754
         case 404: {
734 755
             if (g_proxyServer)
735 756
                 logg("^remote_cvdhead: file not found: %s (Proxy: %s:%u)\n", url, g_proxyServer, g_proxyPort);
... ...
@@ -1004,6 +1269,19 @@ static fc_error_t downloadFile(
1004 1004
         }
1005 1005
         case 429: {
1006 1006
             status = FC_ERETRYLATER;
1007
+            curl_off_t retry_after;
1008
+
1009
+            /* Find out how long we should wait before allowing a retry. */
1010
+            curl_easy_getinfo(curl, CURLINFO_RETRY_AFTER, &retry_after);
1011
+            if (retry_after > 0) {
1012
+                /* The response gave us a Retry-After date. Use that. */
1013
+                g_mirrorsDat->retry_after = time(NULL) + (time_t)retry_after;
1014
+            } else {
1015
+                /* Try again in no less than 4 hours if the response didn't specify. */
1016
+                g_mirrorsDat->retry_after = time(NULL) + 60 * 60 * 4;
1017
+            }
1018
+            (void)save_mirrors_dat();
1019
+
1007 1020
             break;
1008 1021
         }
1009 1022
         case 404: {
... ...
@@ -1792,7 +2070,7 @@ static fc_error_t check_for_new_database_version(
1792 1792
                      database, localver, remotever);
1793 1793
                 break;
1794 1794
             }
1795
-            // else: Fall-through to Up-to-date case.
1795
+            /* fall-through */
1796 1796
         }
1797 1797
         case FC_UPTODATE: {
1798 1798
             if (NULL == local_database) {
... ...
@@ -1812,6 +2090,12 @@ static fc_error_t check_for_new_database_version(
1812 1812
             remotever = localver;
1813 1813
             break;
1814 1814
         }
1815
+        case FC_EFORBIDDEN: {
1816
+            /* We tried to look up the version using HTTP and were actively blocked. */
1817
+            logg("!check_for_new_database_version: Blocked from using server %s.\n", server);
1818
+            status = FC_EFORBIDDEN;
1819
+            goto done;
1820
+        }
1815 1821
         default: {
1816 1822
             logg("!check_for_new_database_version: Failed to find %s database using server %s.\n", database, server);
1817 1823
             status = FC_EFAILEDGET;
... ...
@@ -32,6 +32,14 @@
32 32
 #define DNS_EXTRADBINFO_RECORDTIME      1
33 33
 // clang-format on
34 34
 
35
+#define SIZEOF_UUID_V4 37                 /** For uuid_v4_gen(), includes NULL byte */
36
+#define MIRRORS_DAT_MAGIC "FreshClamData" /** Magic bytes for mirrors.dat found before mirrors_dat_v1_t */
37
+typedef struct _mirrors_dat_v1 {
38
+    uint32_t version;          /** version of this dat format */
39
+    char uuid[SIZEOF_UUID_V4]; /** uuid to be used in user-agent */
40
+    time_t retry_after;        /** retry date. If > 0, don't update until after this date */
41
+} mirrors_dat_v1_t;
42
+
35 43
 /* ----------------------------------------------------------------------------
36 44
  * Internal libfreshclam globals
37 45
  */
... ...
@@ -55,6 +63,12 @@ extern uint32_t g_requestTimeout;
55 55
 
56 56
 extern uint32_t g_bCompressLocalDatabase;
57 57
 
58
+extern mirrors_dat_v1_t *g_mirrorsDat;
59
+
60
+fc_error_t load_mirrors_dat(void);
61
+fc_error_t save_mirrors_dat(void);
62
+fc_error_t new_mirrors_dat(void);
63
+
58 64
 fc_error_t updatedb(
59 65
     const char *database,
60 66
     const char *dnsUpdateInfo,