forked from tempesta-tech/tempesta
-
Notifications
You must be signed in to change notification settings - Fork 0
/
http_limits.c
1672 lines (1459 loc) · 45.1 KB
/
http_limits.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
/**
* Tempesta FW
*
* Interface to classification modules.
*
* Copyright (C) 2014 NatSys Lab. (info@natsys-lab.com).
* Copyright (C) 2015-2021 Tempesta Technologies, Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/*
* TODO: #488 add socket/connection options adjusting to change client QoS
*/
#include <linux/ctype.h>
#include <linux/spinlock.h>
#include "lib/fsm.h"
#include "tdb.h"
#include "tempesta_fw.h"
#include "addr.h"
#include "http_limits.h"
#include "client.h"
#include "connection.h"
#include "filter.h"
#include "gfsm.h"
#include "http_msg.h"
#include "vhost.h"
#include "log.h"
#include "hash.h"
/*
* ------------------------------------------------------------------------
* Generic classifier functionality.
* ------------------------------------------------------------------------
*/
static struct {
__be16 ports[DEF_MAX_PORTS];
unsigned int count;
} tfw_inports __read_mostly;
static TfwClassifier __rcu *classifier = NULL;
/**
* Shrink client connections hash and/or reduce QoS for blocked clients to
* lower back-end servers or local system load.
*/
void
tfw_classify_shrink(void)
{
/* TODO: delete a connection from the LRU */
}
int
tfw_classify_ipv4(struct sk_buff *skb)
{
int r;
TfwClassifier *clfr;
rcu_read_lock();
clfr = rcu_dereference(classifier);
r = (clfr && clfr->classify_ipv4)
? clfr->classify_ipv4(skb)
: TFW_PASS;
rcu_read_unlock();
return r;
}
int
tfw_classify_ipv6(struct sk_buff *skb)
{
int r;
TfwClassifier *clfr;
rcu_read_lock();
clfr = rcu_dereference(classifier);
r = (clfr && clfr->classify_ipv6)
? clfr->classify_ipv6(skb)
: TFW_PASS;
rcu_read_unlock();
return r;
}
void
tfw_classifier_add_inport(__be16 port)
{
BUG_ON(tfw_inports.count == DEF_MAX_PORTS - 1);
tfw_inports.ports[tfw_inports.count++] = port;
}
void
tfw_classifier_cleanup_inport(void)
{
memset(&tfw_inports, 0, sizeof(tfw_inports));
}
static int
tfw_classify_conn_estab(struct sock *sk)
{
int i;
unsigned short sport = tfw_addr_get_sk_sport(sk);
TfwClassifier *clfr;
/* Pass the packet if it's not for us. */
for (i = 0; i < tfw_inports.count; ++i)
if (sport == tfw_inports.ports[i])
goto ours;
return TFW_PASS;
ours:
rcu_read_lock();
clfr = rcu_dereference(classifier);
i = (clfr && clfr->classify_conn_estab)
? clfr->classify_conn_estab(sk)
: TFW_PASS;
rcu_read_unlock();
return i;
}
static void
tfw_classify_conn_close(struct sock *sk)
{
TfwClassifier *clfr = rcu_dereference(classifier);
if (clfr && clfr->classify_conn_close)
clfr->classify_conn_close(sk);
}
/**
* Called from sk_filter() called from tcp_v4_rcv() and tcp_v6_rcv(),
* i.e. when IP fragments are already assembled and we can process TCP.
*/
static int
tfw_classify_tcp(struct sock *sk, struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
TfwClassifier *clfr = rcu_dereference(classifier);
return clfr && clfr->classify_tcp ? clfr->classify_tcp(th) : TFW_PASS;
}
/*
* tfw_classifier_register() and tfw_classifier_unregister()
* are called at Tempesta start/stop time. The execution is
* serialized with a mutex. There's no need for additional
* protection of rcu_assign_pointer() from concurrent use.
*/
void
tfw_classifier_register(TfwClassifier *mod)
{
T_DBG("Registering new classifier: %s\n", mod->name);
BUG_ON(classifier);
rcu_assign_pointer(classifier, mod);
}
void
tfw_classifier_unregister(void)
{
T_DBG("Un-registering classifier: %s\n", classifier->name);
rcu_assign_pointer(classifier, NULL);
synchronize_rcu();
}
static TempestaOps tempesta_ops = {
.sk_alloc = tfw_classify_conn_estab,
.sk_free = tfw_classify_conn_close,
.sock_tcp_rcv = tfw_classify_tcp,
};
/*
* ------------------------------------------------------------------------
* Frang classifier - static http limits implementation.
* ------------------------------------------------------------------------
*/
typedef struct {
unsigned long ts;
unsigned int conn_new;
unsigned int req;
unsigned int tls_sess_new;
unsigned int tls_sess_incomplete;
} FrangRates;
/**
* Response code record.
*
* @cnt - Amount of responses in a time frame part;
* @ts - Response time in seconds. Four bytes could be used to store enough
* number of seconds in the assumption that there won't be delays between user
* responses longer than 68 years;
*/
typedef struct {
unsigned int cnt;
unsigned int ts;
} __attribute__((packed)) FrangRespCodeStat;
/**
* Main descriptor of client resource accounting.
* @lock can be removed if RSS is tuned to schedule packets based on
* <proto, src_ip> tuple. However, the hashing could produce bad CPU load
* balancing so, such settings are not desirable.
*
* @conn_curr - current connections number;
* @history - bursts history organized as a ring-buffer;
* @resp_code_stat - response code record
*/
typedef struct {
unsigned int conn_curr;
spinlock_t lock;
FrangRates history[FRANG_FREQ];
FrangRespCodeStat resp_code_stat[FRANG_FREQ];
} FrangAcc;
#define FRANG_CLI2ACC(c) ((FrangAcc *)(&(c)->class_prvt))
#define FRANG_ACC2CLI(a) container_of((TfwClassifierPrvt *)a, \
TfwClient, class_prvt)
#define frang_msg(check, addr, fmt, ...) \
T_WARN_MOD_ADDR(frang, check, addr, TFW_NO_PORT, fmt, ##__VA_ARGS__)
/*
* Client actions has triggered a security event. Log the client addr and
* Frang limit name.
*/
#define frang_limmsg(lim_name, curr_val, lim, addr) \
frang_msg(lim_name " exceeded", (addr), ": %ld (lim=%ld)\n", \
(long)curr_val, (long)lim)
/*
* Local subsystem has triggered a security event, mostly it's a
* misconfiguration issue. Log the event and subsystem name.
*/
#define frang_limmsg_local(lim_name, curr_val, lim, system) \
T_WARN("frang: " lim_name " exceeded for '%s' subsystem: " \
"%ld (lim=%ld)\n", \
system, (long)curr_val, (long)lim)
#ifdef DEBUG
#define frang_dbg(fmt_msg, addr, ...) \
do { \
char abuf[TFW_ADDR_STR_BUF_SIZE] = {0}; \
tfw_addr_fmt(addr, TFW_NO_PORT, abuf); \
T_DBG("frang: " fmt_msg, abuf, ##__VA_ARGS__); \
} while (0)
#else
#define frang_dbg(...)
#endif
static int
frang_conn_limit(FrangAcc *ra, FrangGlobCfg *conf)
{
unsigned long ts = (jiffies * FRANG_FREQ) / HZ;
unsigned int csum = 0;
int i = ts % FRANG_FREQ;
if (ra->history[i].ts != ts) {
bzero_fast(&ra->history[i], sizeof(ra->history[i]));
ra->history[i].ts = ts;
}
/*
* Increment connection counters even when we return TFW_BLOCK.
* Linux will call sk_free() from inet_csk_clone_lock(), so our
* frang_conn_close() is also called. @conn_curr is decremented
* there, but @conn_new is not changed. We count both failed
* connection attempts and connections that were successfully
* established.
*/
ra->history[i].conn_new++;
ra->conn_curr++;
if (conf->conn_max && ra->conn_curr > conf->conn_max) {
frang_limmsg("connections max num.", ra->conn_curr,
conf->conn_max, &FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
if (conf->conn_burst && ra->history[i].conn_new > conf->conn_burst) {
frang_limmsg("new connections burst", ra->history[i].conn_new,
conf->conn_burst, &FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
/* Collect current connection sum. */
for (i = 0; i < FRANG_FREQ; i++)
if (ra->history[i].ts + FRANG_FREQ >= ts)
csum += ra->history[i].conn_new;
if (conf->conn_rate && csum > conf->conn_rate) {
frang_limmsg("new connections rate", csum, conf->conn_rate,
&FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
return TFW_PASS;
}
static void
__frang_init_acc(void *data)
{
TfwClient *cli = (TfwClient *)data;
FrangAcc *ra = FRANG_CLI2ACC(cli);
spin_lock_init(&ra->lock);
}
static int
frang_conn_new(struct sock *sk)
{
int r = TFW_BLOCK;
FrangAcc *ra;
TfwClient *cli;
TfwAddr addr;
TfwVhost *dflt_vh = tfw_vhost_lookup_default();
/*
* Default vhost configuration stores global frang settings, it's always
* available even on reload under heavy load. But the pointer comes
* from other module, take care of probable null-dereferences.
*/
if (WARN_ON_ONCE(!dflt_vh))
return TFW_BLOCK;
ss_getpeername(sk, &addr);
cli = tfw_client_obtain(addr, NULL, NULL, __frang_init_acc);
if (unlikely(!cli)) {
T_ERR("can't obtain a client for frang accounting\n");
tfw_vhost_put(dflt_vh);
return TFW_BLOCK;
}
ra = FRANG_CLI2ACC(cli);
spin_lock(&ra->lock);
/*
* sk->sk_user_data references TfwConn{} which in turn references
* TfwPeer, so basically we can get FrangAcc from TfwConn{}.
* However, socket can live (for a short period of time, when kernel
* just allocated the socket and called tempesta_new_clntsk()) w/o
* TfwConn{} and vise versa - TfwConn{} can leave w/o socket
* (e.g. server connections during failover). Thus to keep design
* consistent we have two references to TfwPeer: from socket and
* TfwConn{}.
*/
sk->sk_security = ra;
/*
* TBD: Since we can determine vhost by SNI field in TLS headers, there
* will be a desire to make all frang limits per-vhost. After some
* thinking I have found this counter-intuitive: The way, how the
* frang limits work will depend on too many conditions. E.g.
* TLS-enabled clients will use higher limits than non-TLS clients;
* the same client can break security rules for one vhost, but be a
* legitimate client for other vhosts, so it's a big question how to
* block him by IP or by connection resets; if multi-domain certificate
* (SAN) is configured, TLS clients will behave as non-TLS. Too
* complicated for administrator to understand how client is blocked
* and to configure it, while making some of the limits to be global
* for a single client is absolutely straight-forward.
*/
r = frang_conn_limit(ra, dflt_vh->frang_gconf);
if (r == TFW_BLOCK && dflt_vh->frang_gconf->ip_block) {
tfw_filter_block_ip(&cli->addr);
tfw_client_put(cli);
}
spin_unlock(&ra->lock);
tfw_vhost_put(dflt_vh);
return r;
}
/**
* Just update current connection count for a user.
*/
static void
frang_conn_close(struct sock *sk)
{
FrangAcc *ra = sk->sk_security;
if (unlikely(!sk->sk_user_data))
return;
BUG_ON(!ra);
spin_lock(&ra->lock);
BUG_ON(!ra->conn_curr);
ra->conn_curr--;
sk->sk_security = NULL;
spin_unlock(&ra->lock);
tfw_client_put(FRANG_ACC2CLI(ra));
}
static int
frang_time_in_frame(const unsigned long tcur, const unsigned long tprev)
{
return tprev + FRANG_FREQ > tcur;
}
static int
frang_req_limit(FrangAcc *ra, unsigned int req_burst, unsigned int req_rate)
{
unsigned long ts = jiffies * FRANG_FREQ / HZ;
unsigned int rsum = 0;
int i = ts % FRANG_FREQ;
if (!req_burst && !req_rate)
return TFW_PASS;
if (ra->history[i].ts != ts) {
ra->history[i].ts = ts;
ra->history[i].conn_new = 0;
ra->history[i].req = 0;
}
ra->history[i].req++;
if (req_burst && ra->history[i].req > req_burst) {
frang_limmsg("requests burst", ra->history[i].req,
req_burst, &FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
/* Collect current request sum. */
for (i = 0; i < FRANG_FREQ; i++)
if (frang_time_in_frame(ts, ra->history[i].ts))
rsum += ra->history[i].req;
if (req_rate && rsum > req_rate) {
frang_limmsg("request rate", rsum, req_rate,
&FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
return TFW_PASS;
}
static int
frang_http_uri_len(const TfwHttpReq *req, FrangAcc *ra, unsigned int uri_len)
{
if (req->uri_path.len > uri_len) {
frang_limmsg("HTTP URI length", req->uri_path.len,
uri_len, &FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
return TFW_PASS;
}
/**
* Check all parsed headers in request headers table.
* We observe all headers many times, actually on each data chunk.
* However, the check is relatively fast, so that should be Ok.
* It's necessary to run the checks on each data chunk to prevent memory
* exhausting DoS attack on many large header fields, since we don't know
* which headers were read on each data chunk.
*
* TODO #1346: we should recognize inadequately long header fields as early
* as possible, to avoid resources wasting on malformed requests processing;
* this is especially important in case of Huffman decoding (during processing
* of HTTP/2 header fields), since Huffman decoding is an extremely expensive
* operation; thus, we need to check @http_field_len HTTP limit before we go
* to further parsing/decoding, and it seems that the most simple and effective
* way to achieve that - is to embed a hook into HTTP-parsers (or in case of
* HTTP/2, into HPACK-decoder) directly to catch the long headers immediately.
*/
static int
__frang_http_field_len(const TfwHttpReq *req, FrangAcc *ra,
unsigned int field_len, unsigned int hdr_cnt)
{
const TfwStr *field, *end, *dup, *dup_end;
if (hdr_cnt && req->h_tbl->off >= hdr_cnt) {
frang_limmsg("HTTP headers number", req->h_tbl->off,
hdr_cnt, &FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
if (!field_len)
return TFW_PASS;
FOR_EACH_HDR_FIELD(field, end, req) {
TFW_STR_FOR_EACH_DUP(dup, field, dup_end) {
if (dup->len > field_len) {
frang_limmsg("HTTP field length",
dup->len, field_len,
&FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
}
}
return TFW_PASS;
}
static int
frang_http_field_len(const TfwHttpReq *req, FrangAcc *ra, unsigned int field_len,
unsigned int hdr_cnt)
{
TfwHttpParser *parser = &req->stream->parser;
if (field_len && (parser->hdr.len > field_len)) {
frang_limmsg("HTTP in-progress field length",
parser->hdr.len, field_len,
&FRANG_ACC2CLI(ra)->addr);
return TFW_BLOCK;
}
return __frang_http_field_len(req, ra, field_len, hdr_cnt);
}
static int
frang_http_methods(const TfwHttpReq *req, FrangAcc *ra, unsigned long m_mask)
{
unsigned long mbit = (1UL << req->method);
if (!(m_mask & mbit)) {
frang_msg("restricted HTTP method", &FRANG_ACC2CLI(ra)->addr,
": %u (%#lxu)\n", req->method, mbit);
return TFW_BLOCK;
}
return TFW_PASS;
}
static int
frang_http_methods_override(const TfwHttpReq *req, FrangAcc *ra,
FrangVhostCfg *f_cfg)
{
unsigned long mbit = (1UL << req->method_override);
if (!req->method_override)
return TFW_PASS;
if (!f_cfg->http_method_override
|| (f_cfg->http_methods_mask && !(f_cfg->http_methods_mask & mbit)))
{
frang_msg("restricted overridden HTTP method",
&FRANG_ACC2CLI(ra)->addr, ": %u (%#lxu)\n",
req->method_override, mbit);
return TFW_BLOCK;
}
return TFW_PASS;
}
static int
frang_http_ct_check(const TfwHttpReq *req, FrangAcc *ra, FrangCtVals *ct_vals)
{
TfwStr field, *s;
FrangCtVal *curr;
if (req->method != TFW_HTTP_METH_POST)
return TFW_PASS;
if (TFW_STR_EMPTY(&req->h_tbl->tbl[TFW_HTTP_HDR_CONTENT_TYPE])) {
frang_msg("Content-Type header field", &FRANG_ACC2CLI(ra)->addr,
" is missed\n");
return TFW_BLOCK;
}
tfw_http_msg_clnthdr_val(req,
&req->h_tbl->tbl[TFW_HTTP_HDR_CONTENT_TYPE],
TFW_HTTP_HDR_CONTENT_TYPE, &field);
/*
* Verify that Content-Type value is on the list of allowed values.
* Use prefix match to allow parameters, see RFC 7231 3.1.1:
*
* Content-Type = media-type
* media-type = type "/" subtype *( OWS ";" OWS parameter )
*
* FIXME this matching is too permissive, e.g. we can pass
* "text/plain1", which isn't a correct subtype. Strong FSM processing
* is required. Or HTTP parser must pass only known types and Frang
* decides which of them are allowed.
* See also comment in frang_set_ct_vals().
*
* TODO: possible improvement: binary search.
* Generally binary search is more efficient, but linear search
* is usually faster for small sets of values. Perhaps we should
* switch between the two if performance is critical here,
* but benchmarks should be done to measure the impact.
*/
for (curr = ct_vals->vals; curr->str; ++curr) {
if (tfw_str_eq_cstr(&field, curr->str, curr->len,
TFW_STR_EQ_PREFIX_CASEI))
return TFW_PASS;
}
/* Take first chunk only for logging. */
s = TFW_STR_CHUNK(&field, 0);
if (s) {
frang_msg("restricted Content-Type", &FRANG_ACC2CLI(ra)->addr,
": %.*s\n", PR_TFW_STR(s));
} else {
frang_msg("restricted empty Content-Type",
&FRANG_ACC2CLI(ra)->addr, "\n");
}
return TFW_BLOCK;
}
/**
* Get Host value. Host can be defined:
* - in h2 requests in Host header, in authority pseudo header. The latter SHOULD
* be used, but RFC 7540 still allows to use Host header.
* - in http/1 requests in Host header and inside URI.
*
* @req - request handle;
* @hid - header id, -1 for req->host;
* @trimmed - trimmed host value without spaces.
* @name_only - host value without port component.
*/
static void
frang_get_host_header(const TfwHttpReq *req, int hid, TfwStr *trimmed,
TfwStr *name_only)
{
TfwStr raw_val, hdr_trim = { 0 }, hdr_name = { 0 }, *c, *end;
bool got_delim = false;
/* Parser will block duplicated headers for this hids. */
if (hid > 0) {
tfw_http_msg_clnthdr_val(req, &req->h_tbl->tbl[hid], hid,
&raw_val);
}
else {
/*
* Only used for http/1.1 requests. Always must be identical
* to Host: header, so we can keep parser cleaner and faster.
*/
*trimmed = req->host;
*name_only = req->host;
return;
}
hdr_name.chunks = hdr_trim.chunks = raw_val.chunks;
hdr_name.flags = hdr_trim.flags = raw_val.flags;
TFW_STR_FOR_EACH_CHUNK(c, &raw_val, end) {
if (c->flags & TFW_STR_VALUE) {
if (!got_delim) {
hdr_name.nchunks++;
hdr_name.len += c->len;
}
hdr_trim.nchunks++;
hdr_trim.len += c->len;
}
/*
* When host is IPv6 addr, a 1-byte long chunk with ':' may
* also represent a part of IP address, but it is marked with
* TFW_STR_VALUE and checked in condition above.
*/
else if (c->len == 1 && c->data[0] == ':') {
got_delim = true;
hdr_trim.nchunks++;
hdr_trim.len += c->len;
}
}
*trimmed = hdr_trim;
*name_only = hdr_name;
}
static bool
frang_assert_host_header(const TfwStr *l, const TfwStr *r)
{
if (TFW_STR_EMPTY(l) || TFW_STR_EMPTY(r))
return false;
return tfw_strcmp(l, r);
}
/**
* Require host header in HTTP request (RFC 7230 5.4).
* Block HTTP/1.1 requests w/o host header,
* but just print warning for older HTTP.
*/
static int
frang_http_host_check(const TfwHttpReq *req, FrangAcc *ra)
{
TfwAddr addr;
unsigned long sni_hash = 0;
unsigned short port;
unsigned short real_port;
TfwStr prim_trim = { 0 }, prim_name = { 0 }, /* primary source */
sec_trim = { 0 }, sec_name = { 0 }, /* secondary source */
*val_trim = NULL, *val_name = NULL; /* effective source */
BUG_ON(!req);
BUG_ON(!req->h_tbl);
switch (req->version) {
/*
* In h2 protocol the Host: header is not required and :authority
* pseudo-header should be used instead. But this is "SHOULD"
* requirement and a request may has missing :authority and existing
* Host: header. Block request if none defined or their values are
* not identical.
*/
case TFW_HTTP_VER_20:
frang_get_host_header(req, TFW_HTTP_HDR_H2_AUTHORITY,
&prim_trim, &prim_name);
frang_get_host_header(req, TFW_HTTP_HDR_HOST,
&sec_trim, &sec_name);
val_name = &prim_name;
val_trim = &prim_trim;
if (unlikely(TFW_STR_EMPTY(val_trim))) {
val_name = &sec_name;
val_trim = &sec_trim;
if (unlikely(TFW_STR_EMPTY(val_trim))) {
frang_msg("Request authority is unknown",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
}
else if (frang_assert_host_header(&prim_trim, &sec_trim)) {
frang_msg("Request authority differs between headers",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
break;
/*
* In http/1.1 host header and authority defined in URI must always be
* identical.
*/
case TFW_HTTP_VER_11:
frang_get_host_header(req, TFW_HTTP_HDR_HOST,
&prim_trim, &prim_name);
frang_get_host_header(req, -1,
&sec_trim, &sec_name);
val_name = &prim_name;
val_trim = &prim_trim;
if (unlikely(TFW_STR_EMPTY(val_trim))) {
frang_msg("Request authority is unknown",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
else if (frang_assert_host_header(&prim_trim, &sec_trim)) {
frang_msg("Request authority in URI differs from host header",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
break;
/*
* Old protocols may have no 'host' header, if presents it's a usual
* header with no special meaning. But in installations of servers with
* modern protocols its use for routing decisions is very common.
* It's suspicious, when modern features are used with obsoleted
* protocols, block the request to avoid possible confusion of HTTP
* routing on backends.
*/
case TFW_HTTP_VER_10:
case TFW_HTTP_VER_09:
frang_get_host_header(req, TFW_HTTP_HDR_HOST,
&prim_trim, &prim_name);
if (TFW_STR_EMPTY(&req->host) && TFW_STR_EMPTY(&prim_trim))
return TFW_PASS;
frang_msg("Host header field in protocol prior to HTTP/1.1",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
default:
return TFW_BLOCK;
}
/* Check that host header is not a IP address. */
if (!tfw_addr_pton(val_trim, &addr)) {
frang_msg("Host header field contains IP address",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
/* Check that SNI for TLS connection matches host header. */
if (TFW_CONN_TLS(req->conn)) {
sni_hash = tfw_tls_context(req->conn)->sni_hash;
port = req->host_port ? : 443;
}
else {
port = req->host_port ? : 80;
}
if (sni_hash && (tfw_hash_str(val_name) != sni_hash)) {
frang_msg("host header doesn't match SNI from TLS handshake",
&FRANG_ACC2CLI(ra)->addr, "\n");
return TFW_BLOCK;
}
/*
* TfwClient instance can be reused across multiple connections,
* check the port number of the current connection, not the first one.
*/
real_port = be16_to_cpu(inet_sk(req->conn->sk)->inet_sport);
if (port != real_port) {
frang_msg("port from host header doesn't match real port",
&FRANG_ACC2CLI(ra)->addr, ": %d (%d)\n", port,
real_port);
return TFW_BLOCK;
}
return TFW_PASS;
}
/*
* The GFSM states aren't hookable, so don't open the states definitions and
* only start and finish states are present.
*/
#define TFW_GFSM_FRANG_REQ_STATE(s) \
((TFW_FSM_FRANG_REQ << TFW_GFSM_FSM_SHIFT) | (s))
enum {
TFW_FRANG_REQ_FSM_INIT = TFW_GFSM_FRANG_REQ_STATE(0),
TFW_FRANG_REQ_FSM_DONE = TFW_GFSM_FRANG_REQ_STATE(TFW_GFSM_STATE_LAST)
};
enum {
Frang_Req_0 = 0,
Frang_Req_Hdr_Method,
Frang_Req_Hdr_UriLen,
Frang_Req_Hdr_Check,
Frang_Req_Hdr_NoState,
Frang_Req_Body_Start,
Frang_Req_Body,
Frang_Req_Body_NoState,
Frang_Req_Trailer,
Frang_Req_Done
};
#define TFW_GFSM_FRANG_RESP_STATE(s) \
((TFW_FSM_FRANG_RESP << TFW_GFSM_FSM_SHIFT) | (s))
enum {
TFW_FRANG_RESP_FSM_INIT = TFW_GFSM_FRANG_RESP_STATE(0),
TFW_FRANG_RESP_FSM_FWD = TFW_GFSM_FRANG_RESP_STATE(1),
TFW_FRANG_RESP_FSM_DONE = TFW_GFSM_FRANG_RESP_STATE(TFW_GFSM_STATE_LAST)
};
#define __FRANG_FSM_MOVE(st) T_FSM_MOVE(st, if (r) T_FSM_EXIT(); )
#define __FRANG_FSM_JUMP_EXIT(st) \
do { \
__fsm_const_state = st; /* optimized out to constant */ \
T_FSM_EXIT(); \
} while (0)
static int
frang_http_req_incomplete_hdrs_check(FrangAcc *ra, TfwFsmData *data,
FrangGlobCfg *fg_cfg)
{
TfwHttpReq *req = (TfwHttpReq *)data->req;
struct sk_buff *skb = data->skb;
struct sk_buff *head_skb = req->msg.skb_head;
unsigned int hchnk_cnt = fg_cfg->http_hchunk_cnt;
frang_dbg("check incomplete request headers for client %s, acc=%p\n",
&FRANG_ACC2CLI(ra)->addr, ra);
/*
* There's no need to check for header timeout if this is the very
* first chunk of a request (first full separate SKB with data).
* The FSM is guaranteed to go through the initial states and then
* either block or move to one of header states. Then header timeout
* is checked on each consecutive SKB with data - while we're still
* in one of header processing states.
*/
if (fg_cfg->clnt_hdr_timeout && (skb != head_skb)) {
unsigned long start = req->tm_header;
unsigned long delta = fg_cfg->clnt_hdr_timeout;
if (time_is_before_jiffies(start + delta)) {
frang_limmsg("client header timeout",
jiffies_to_msecs(jiffies - start),
jiffies_to_msecs(delta),
&FRANG_ACC2CLI(ra)->addr);
goto block;
}
}
if (hchnk_cnt && (req->chunk_cnt > hchnk_cnt)) {
frang_limmsg("HTTP header chunk count", req->chunk_cnt,
hchnk_cnt, &FRANG_ACC2CLI(ra)->addr);
goto block;
}
return TFW_PASS;
block:
return TFW_BLOCK;
}
static int
frang_http_req_incomplete_body_check(FrangAcc *ra, TfwFsmData *data,
FrangGlobCfg *fg_cfg, FrangVhostCfg *f_cfg)
{
TfwHttpReq *req = (TfwHttpReq *)data->req;
unsigned long body_len = f_cfg->http_body_len;
unsigned int bchunk_cnt = fg_cfg->http_bchunk_cnt;
unsigned long body_timeout = fg_cfg->clnt_body_timeout;
struct sk_buff *skb = data->skb;
frang_dbg("check incomplete request body for client %s, acc=%p\n",
&FRANG_ACC2CLI(ra)->addr, ra);
/* CLRF after headers was parsed, but the body didn't arrive yet. */
if (TFW_STR_EMPTY(&req->body))
return TFW_PASS;
/*
* Ensure that HTTP request body is coming without delays.
* The timeout is between chunks of the body, so reset
* the start time after each check.
*/
if (body_timeout && req->tm_bchunk && (skb != req->body.skb)) {
unsigned long start = req->tm_bchunk;
unsigned long delta = body_timeout;
if (time_is_before_jiffies(start + delta)) {
frang_limmsg("client body timeout",
jiffies_to_msecs(jiffies - start),
jiffies_to_msecs(delta),
&FRANG_ACC2CLI(ra)->addr);
goto block;
}
req->tm_bchunk = jiffies;
}
/* Limit number of chunks in request body */
if (bchunk_cnt && (req->chunk_cnt > bchunk_cnt)) {
frang_limmsg("HTTP body chunk count", req->chunk_cnt,
bchunk_cnt, &FRANG_ACC2CLI(ra)->addr);
goto block;
}
if (body_len && (req->body.len > body_len)) {
frang_limmsg("HTTP body length", req->body.len,
body_len, &FRANG_ACC2CLI(ra)->addr);
goto block;
}
return TFW_PASS;
block:
return TFW_BLOCK;
}
/*
* RFC 7230 Section-4.1.2:
* When a chunked message containing a non-empty trailer is received,
* the recipient MAY process the fields.
*
* RFC 7230 Section-4.1.2:
* ...a server SHOULD NOT
* generate trailer fields that it believes are necessary for the user
* agent to receive. Without a TE containing "trailers", the server
* ought to assume that the trailer fields might be silently discarded
* along the path to the user agent. This requirement allows
* intermediaries to forward a de-chunked message to an HTTP/1.0
* recipient without buffering the entire response.
*
* RFC doesn't prohibit trailers in request, but it always speaks about
* trailers in response context. But requests with trailer headers
* are valid http messages. Support for them is not documented,
* and implementation-dependent. E.g. Apache doesn't care about trailer
* headers, but ModSecurity for Apache does.
* https://swende.se/blog/HTTPChunked.html
* Some discussions also highlight that trailer headers are poorly
* supported on both servers and clients, while CDNs tend to add
* trailers. https://github.com/whatwg/fetch/issues/34
*
* Since RFC doesn't speak clearly about trailer headers in requests, the
* following assumptions were used:
* - Our intermediaries on client side do not care about trailers and send
* them in the manner as the body. Thus frang's body limitations are used,
* not headers ones.
* - Same header may have different values depending on how the servers work
* with the trailer. Administrator can block that behaviour.
*/
static int
frang_http_req_trailer_check(FrangAcc *ra, TfwFsmData *data,
FrangGlobCfg *fg_cfg, FrangVhostCfg *f_cfg)
{
int r = TFW_PASS;
TfwHttpReq *req = (TfwHttpReq *)data->req;
const TfwStr *field, *end, *dup, *dup_end;