Repository navigation
Expand file tree
/
Copy pathP2PeerConWsa.cpp
More file actions
2819 lines (2627 loc) · 121 KB
/
Copy pathP2PeerConWsa.cpp
File metadata and controls
2819 lines (2627 loc) · 121 KB
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
// Copyright © 2002-2009, 2026 Ivyware Pty Ltd, Khrustal & Mann
// MELBOURNE, VICTORIA, AUSTRALIA, 3000
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//
//
// P2PeerConWsa implementation
// NOTES: Manages Microsoft WSA connections
//
#include "stdafx.h"
#include "MSWSock.h"
#include "Ws2tcpip.h"
#include "P2PeerConWsa.h"
#include "P2PeerioDmx.h"
#include "P2Pwin32.h"
#include <vector> // P2PeerConWsaSourceFilter
#include <cstdio> // std::snprintf - the portable form, as used by
// P2PAuthLogin.cpp and P2PIdentityStore.cpp
///////////////////////////////////////////////////////////////////////
// Accept source allow-list
// NOTES: Defined here rather than in the header for the reason
// P2PeerConSourceTally is defined in P2PeerCon.cpp - a container in a
// shipped header is a dependency every consumer inherits whether it
// uses the feature or not
// : A prefix is 16 RAW BYTES in network order plus a family and a bit
// count, and NOT the two ULONGs it was until 2026-08-28. The mask
// arithmetic that shape allowed does not reach 128 bits, and the
// alternative - a second parallel list for the second family - would
// let the two drift. One list, one comparison, and the family is a
// field of the rule rather than a property of where it is kept
// : Network order throughout, which is the order the addresses arrive in
// and the order a prefix is written in. A v4 rule kept in host order
// had to be converted at the boundary; a byte-wise prefix compare has
// no order to be in, so the conversion is gone rather than doubled
// : A rule matches only its own family. There is no v4 rule that can
// admit a v6 peer and none the other way, and the one place the
// families could have been confused - a v4 peer arriving on a
// dual-stack socket as ::ffff:a.b.c.d - is normalised to AF_INET
// before it is ever tested. Refer AcceptSourceAddress()
// : No lock. The list is written by SetListenScope()'s neighbours
// before the service is posted and read on the pump thread afterwards,
// which is the same configure-before-arm rule RequireAuth states. A
// list mutated under a live service is not supported and would need
// the tally's critical section to be
struct P2PeerConWsaSourceFilter
{
struct Prefix
{
int nFamily; // AF_INET or AF_INET6
int nBits; // 0..32 or 0..128
UCHAR ucNetwork[16]; // network order, already masked
};
std::vector<Prefix> m_oPrefixes;
// Compares the leading nBits of two network-order addresses
// NOTES: The trailing partial byte is masked rather than shifted, so a
// prefix that does not end on a byte boundary - /12 and /7 both
// appear in AllowAcceptPrivate() - is compared exactly and not
// rounded up to the next byte, which would silently narrow it, nor
// down, which would silently widen it
static bool
BitsEqual ( const UCHAR *pucLeft, const UCHAR *pucRight, int nBits )
{
const int nWhole = nBits / 8;
const int nRest = nBits % 8;
if ( nWhole && memcmp ( pucLeft, pucRight, (size_t)nWhole ) != 0 )
return false;
if ( nRest )
{
const UCHAR ucMask = (UCHAR)( 0xFF << ( 8 - nRest ) );
if ( ( pucLeft[nWhole] & ucMask ) != ( pucRight[nWhole] & ucMask ) )
return false;
}
return true;
}
void
Add ( int nFamily, const UCHAR *pucNetwork, int nBits )
{
Prefix oPrefix;
oPrefix.nFamily = nFamily;
oPrefix.nBits = nBits;
// Stored MASKED, so a rule written "10.1.2.3/8" holds 10.0.0.0 and
// reads back as the rule it is rather than as the address it was
// typed from
memset ( oPrefix.ucNetwork, 0, sizeof(oPrefix.ucNetwork) );
const int nWhole = nBits / 8;
const int nRest = nBits % 8;
if ( nWhole )
memcpy ( oPrefix.ucNetwork, pucNetwork, (size_t)nWhole );
if ( nRest )
oPrefix.ucNetwork[nWhole] =
(UCHAR)( pucNetwork[nWhole] & (UCHAR)( 0xFF << ( 8 - nRest ) ) );
m_oPrefixes.push_back ( oPrefix );
}
bool
Matches ( int nFamily, const UCHAR *pucAddress ) const
{
for ( std::vector<Prefix>::const_iterator it = m_oPrefixes.begin ( )
; it != m_oPrefixes.end ( ); ++it )
if ( it->nFamily == nFamily &&
BitsEqual ( pucAddress, it->ucNetwork, it->nBits ) )
return true;
return false;
}
};
// The longest legal prefix text, plus room for a terminator
// NOTES: "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255/128" is 49 characters
// and is the longest thing either parser below can accept. A string
// longer than this cannot be a prefix, so the bound is a rejection
// rather than a truncation - truncating "10.0.0.0/8xxxxx" to something
// that parses is exactly the class of silent misreading these parsers
// exist to refuse
#define MAX_P2PeerConPrefix 64
//
// Description: Splits "address[/bits]" into its two halves
//
// NOTES: Works on a plain char buffer and NOT on CStringA, which
// is why it exists. The Linux shim's CStringA carries
// construction, GetString(), GetLength() and IsEmpty() and
// nothing else - no Find(), Mid(), Left(), Trim() or
// operator[] - so a parser written against those compiles
// on Windows and cannot compile on Linux. This one did
// not, from the day the allow-list landed (2026-08-27)
// until the dual-stack port was measured on Linux, because
// nothing had built this file there in between
// : A parser needs no string class. Narrowing the caller's
// text once at the door and then reading bytes is smaller
// than either alternative - a second string class or a
// widened shim - and it is the version that cannot be
// broken again by whichever class the platform lends us
// : A bare address answers the family's FULL width, so
// "10.0.0.0" and "10.0.0.0/32" are the same rule and the
// bind check one screen down - which refuses anything
// narrower than a host address - reads them alike without
// having to ask which was written
//
// Returns: bool
// Whether the text is well-formed enough to hand on
//
static bool
SplitP2PeerConPrefix ( LPCTSTR lpszPrefix, char *szAddr, size_t cchAddr
, int nMaxBits, int &rnBits )
{
rnBits = nMaxBits;
if ( cchAddr )
szAddr[0] = 0;
// To be sure, to be sure
if ( lpszPrefix == 0 || cchAddr == 0 )
return false;
// Narrowed once, here, and read as bytes from this point on. An address
// is ASCII by construction, so the conversion cannot lose anything a
// prefix could legitimately contain
const CStringA csPrefix ( lpszPrefix );
const char *pszText = (LPCSTR)csPrefix;
if ( pszText == 0 )
return false;
// Leading and trailing whitespace, which a config file will have
while ( *pszText == ' ' || *pszText == '\t' )
++pszText;
size_t cchText = strlen ( pszText );
while ( cchText && ( pszText[cchText-1] == ' ' ||
pszText[cchText-1] == '\t' ||
pszText[cchText-1] == '\r' ||
pszText[cchText-1] == '\n' ) )
--cchText;
if ( cchText == 0 || cchText >= cchAddr )
return false; // empty, or too long to be a prefix
// ONE '/' at most. "1.2.3.4/8/8" is refused outright rather than read as
// its first half, which is the same rule the octet parser follows: a rule
// nobody can be sure of is not a rule to narrow a filter by
size_t cchAddrPart = cchText;
for ( size_t i = 0; i < cchText; ++i )
if ( pszText[i] == '/' )
{
if ( cchAddrPart != cchText )
return false; // a second '/' - refuse rather than pick
cchAddrPart = i;
}
if ( cchAddrPart != cchText )
{
const char *pszBits = pszText + cchAddrPart + 1;
const size_t cchBits = cchText - cchAddrPart - 1;
if ( cchBits == 0 || cchBits > 3 )
return false;
int nBits = 0;
for ( size_t i = 0; i < cchBits; ++i )
{
if ( pszBits[i] < '0' || pszBits[i] > '9' )
return false;
nBits = nBits * 10 + ( pszBits[i] - '0' );
}
if ( nBits < 0 || nBits > nMaxBits )
return false;
rnBits = nBits;
}
if ( cchAddrPart == 0 )
return false; // "/24" with nothing in front of it
memcpy ( szAddr, pszText, cchAddrPart );
szAddr[cchAddrPart] = 0;
return true;
}
//
// Description: Parses "a.b.c.d" into 4 network-order bytes
//
// NOTES: Hand-parsed rather than handed to inet_addr(), which
// accepts "10.1" as 10.0.0.1 and "0x0a000001" as an
// address. Those are conveniences for a user typing at a
// prompt and hazards for a POLICY read from a config
// file: they turn a typo in a rule into a valid rule
// about somewhere else, silently
// : inet_pton(AF_INET) would also reject those, and IS what
// the v6 half below uses. This half is kept hand-written
// because it is the half that has been shipping and
// because its rejections are the ones the gate test names
// one by one
// : Rejects everything it is not certain of. The caller
// reports the rejection and adds no prefix, so the failure
// mode of a malformed rule is a narrower filter that is
// complained about, never a wider one that is not
//
// Returns: bool
// Whether the address parsed
//
static bool
ParseP2PeerConAddr4 ( const char *pszAddr, UCHAR *pucAddr )
{
// Four octets, each present, each 0..255
// NOTES: The loop synthesises a terminating '.' one past the end so the
// last octet is closed by the same code as the first three
const size_t cchAddr = strlen ( pszAddr );
ULONG ulAddr = 0;
int nOctet = 0;
int nDigits = 0;
int nValue = 0;
for ( size_t i = 0; ; ++i )
{
const char c = ( i < cchAddr ) ? pszAddr[i] : '.';
if ( c >= '0' && c <= '9' )
{
if ( ++nDigits > 3 )
return false;
nValue = nValue * 10 + ( c - '0' );
if ( nValue > 255 )
return false;
}
else if ( c == '.' )
{
if ( nDigits == 0 || nOctet > 3 )
return false;
ulAddr = ( ulAddr << 8 ) | (ULONG)nValue;
nDigits = 0;
nValue = 0;
if ( ++nOctet == 4 && i >= cchAddr )
break;
}
else
return false;
}
if ( nOctet != 4 )
return false;
// Network order, which is the order the octets were written in
pucAddr[0] = (UCHAR)( ( ulAddr >> 24 ) & 0xFF );
pucAddr[1] = (UCHAR)( ( ulAddr >> 16 ) & 0xFF );
pucAddr[2] = (UCHAR)( ( ulAddr >> 8 ) & 0xFF );
pucAddr[3] = (UCHAR)( ulAddr & 0xFF );
return true;
}
//
// Description: Parses "xxxx::yyyy" into 16 network-order bytes
//
// NOTES: inet_pton() rather than a hand parser, and the reason is
// the opposite of the v4 case above. There is no
// shorthand for it to accept: RFC4291 text is exact, the
// platform parser is the one everything else on the host
// agrees with, and a second implementation of :: elision
// would be a second opinion about what an operator wrote
// : A zone index - "fe80::1%eth0" - is REFUSED, because
// inet_pton() does not take one and a rule that dropped
// the zone would be a rule about every link rather than
// the one named. A link-local allow-list is spelt
// "fe80::/10" and means what it says
//
// Returns: bool
// Whether the address parsed
//
static bool
ParseP2PeerConAddr6 ( const char *pszAddr, UCHAR *pucAddr )
{
// The NARROW form deliberately. InetPton is a macro naming the WIDE one
// on the Linux shim, and the text here is already narrow
return InetPtonA ( AF_INET6, pszAddr, pucAddr ) == 1;
}
//
// Description: The family a CLIENT starts in, read off the address it dials
// NOTES: An IPv6 LITERAL can only ever be dialled over IPv6. Left
// at the IPv4 default it fails every attempt at the
// resolver - AF_INET will not answer "::1" - so starting
// such a client in IPv6 changes nothing that used to work.
// A name or a dotted quad keeps the default; a name says
// nothing about the family, and asking for AAAA on its
// behalf is the widening SetFamily() exists to keep explicit
// : A zone index - "fe80::1%eth0" - is accepted HERE, unlike
// the allow-list's parser above, because getaddrinfo()
// takes one and a link-local peer is unreachable without it
// : SetFamily() after construction still decides; Dual on a
// v6 literal is legal and dials the same address
//
// Returns: P2PeerConFamily_e
//
static P2PeerConFamily_e
ClientFamilyForAddress ( LPCTSTR lpszIPaddress )
{
if ( lpszIPaddress == 0 )
return P2PeerConFamily_IPv4;
// std::string for the search: the Linux CStringA shim carries the
// conversions and not Find()/Left()
const CStringA csAddr ( lpszIPaddress );
std::string sAddr ( (LPCSTR)csAddr );
const size_t nZone = sAddr.find ( '%' );
if ( nZone != std::string::npos && nZone > 0 )
sAddr.resize ( nZone );
UCHAR aucAddr[16];
return ParseP2PeerConAddr6 ( sAddr.c_str ( ), aucAddr )
? P2PeerConFamily_IPv6 : P2PeerConFamily_IPv4;
}
//
// Description: Parses one allow-list rule of either family
//
// NOTES: A ':' anywhere decides IPv6 and its absence IPv4. It is
// not a guess - ':' cannot occur in a v4 prefix and cannot
// be absent from a v6 one - and it means a caller never
// has to say which family a rule is in, so a rule and its
// family cannot be written to disagree
// : The family has to be decided BEFORE the bit count can be
// range-checked, because /64 is legal in one family and
// not in the other. So the text is scanned for ':' first
// and split second
//
// Returns: bool
// Whether the prefix parsed
//
static bool
ParseP2PeerConPrefix ( LPCTSTR lpszPrefix, int &rnFamily
, UCHAR *pucAddr, int &rnBits )
{
rnFamily = AF_INET;
rnBits = 0;
memset ( pucAddr, 0, 16 );
// To be sure, to be sure
if ( lpszPrefix == 0 )
return false;
// Narrowed once to decide the family, then split against that family's
// width. Two conversions of the same short string is not worth an
// out-parameter to avoid
const CStringA csFamily ( lpszPrefix );
const char *pszText = (LPCSTR)csFamily;
if ( pszText == 0 )
return false;
rnFamily = ( strchr ( pszText, ':' ) != 0 ) ? AF_INET6 : AF_INET;
char szAddr[MAX_P2PeerConPrefix];
if ( !SplitP2PeerConPrefix ( lpszPrefix, szAddr, sizeof(szAddr)
, rnFamily == AF_INET6 ? 128 : 32
, rnBits ) )
return false;
return rnFamily == AF_INET6 ? ParseP2PeerConAddr6 ( szAddr, pucAddr )
: ParseP2PeerConAddr4 ( szAddr, pucAddr );
}
//
// Description: The address bytes inside a sockaddr, and how many of them
// there are
//
// NOTES: One place that knows where a family keeps its address,
// so the filter, the key and the diagnostic all read the
// same bytes. Answers 0 for a family this transport does
// not open, which every caller treats as "unnameable"
//
// Returns: int
// 4, 16, or 0
//
static int
P2PeerConAddrBytes ( const sockaddr *pName, int nNameLen
, const UCHAR *&rpucAddr )
{
rpucAddr = 0;
if ( pName == 0 )
return 0;
if ( pName->sa_family == AF_INET )
{
if ( nNameLen < (int)sizeof(sockaddr_in) )
return 0;
rpucAddr = (const UCHAR *)&((const sockaddr_in *)pName)->sin_addr;
return 4;
}
if ( pName->sa_family == AF_INET6 )
{
if ( nNameLen < (int)sizeof(sockaddr_in6) )
return 0;
rpucAddr = (const UCHAR *)
((const sockaddr_in6 *)pName)->sin6_addr.s6_addr;
return 16;
}
return 0;
}
//
// Description: Rewrites a v4-MAPPED IPv6 address in place as the AF_INET
// address it stands for
//
// NOTES: ::ffff:a.b.c.d is how a dual-stack socket reports a peer
// that arrived over IPv4, and it is the ONE place the two
// address spaces touch. Left alone it would make a v4
// peer's origin depend on which socket carried it - an
// operator's "10.0.0.0/8" would admit it on a v4 service
// and refuse it on a dual one, for no reason the operator
// can see
// : The 12-byte prefix is tested by hand rather than with
// IN6_IS_ADDR_V4MAPPED, whose argument conventions differ
// between the platform headers this builds against.
// Twelve bytes is not worth a portability question
// : The PORT is carried across. Nothing here reads it, but
// an address that lost half of itself in a normalisation
// is a trap for whatever reads it next
//
// Returns: int
// The new address length, or the one passed in if untouched
//
static int
NormaliseP2PeerConSockaddr ( SOCKADDR_STORAGE &rAddr, int nLen )
{
if ( rAddr.ss_family != AF_INET6 || nLen < (int)sizeof(sockaddr_in6) )
return nLen;
const sockaddr_in6 *p6 = (const sockaddr_in6 *)&rAddr;
const UCHAR *pucIn = (const UCHAR *)p6->sin6_addr.s6_addr;
static const UCHAR ucMappedPrefix[12] =
{ 0,0,0,0, 0,0,0,0, 0,0,0xFF,0xFF };
if ( memcmp ( pucIn, ucMappedPrefix, sizeof(ucMappedPrefix) ) != 0 )
return nLen;
UCHAR ucV4[4];
const u_short usPort = p6->sin6_port;
memcpy ( ucV4, pucIn + 12, sizeof(ucV4) );
sockaddr_in oV4;
memset ( &oV4, 0, sizeof(oV4) );
oV4.sin_family = AF_INET;
oV4.sin_port = usPort;
memcpy ( &oV4.sin_addr, ucV4, sizeof(ucV4) );
memset ( &rAddr, 0, sizeof(rAddr) );
memcpy ( &rAddr, &oV4, sizeof(oV4) );
return (int)sizeof(sockaddr_in);
}
//
// Description: Is this address on THIS machine's loopback?
//
// NOTES: The whole of 127.0.0.0/8 and ::1/128 - exactly the two
// prefixes AllowAcceptLoopback() writes into the accept
// allow-list, and deliberately the same two. v4 has a
// loopback NET and v6 has a single loopback ADDRESS, so
// the asymmetry is the standards' and not a shortcut
// : Reads the BYTES through P2PeerConAddrBytes(), so an
// address family this transport does not open answers
// false rather than being interpreted as something it is
// not. A length without a meaning is worse than none
// : The caller must have normalised first. A v4-mapped
// ::ffff:127.0.0.1 is sixteen bytes here and would NOT
// match ::1 - which is the correct reading of the bytes
// and the wrong answer about the host, and is why every
// call site runs NormaliseP2PeerConSockaddr() ahead of
// this one
//
// Parameters: const sockaddr *pName, int nNameLen
// The address, normalised
//
// Returns: bool
// Whether the kernel can reach that address without a network
//
static bool
IsP2PeerConSockaddrLoopback ( const sockaddr *pName, int nNameLen )
{
const UCHAR *pucAddr = 0;
const int nBytes = P2PeerConAddrBytes ( pName, nNameLen, pucAddr );
if ( nBytes == 4 )
return pucAddr[0] == 127; // 127.0.0.0/8
if ( nBytes == 16 )
{
static const UCHAR ucV6Loopback[16] =
{ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1 };
return memcmp ( pucAddr, ucV6Loopback, sizeof(ucV6Loopback) ) == 0;
}
return false; // unnameable - refer above
}
//
// Description: Renders a socket address as text, for a diagnostic
//
// NOTES: getnameinfo(NI_NUMERICHOST) rather than four %u and a
// shift, which cannot render sixteen bytes and cannot
// elide a run of zeroes the way every other tool on the
// host will. An operator matching a refusal line against
// an allow-list needs the two spelt the same way
// : NUMERIC deliberately - NEVER a reverse lookup. This is
// called on the refusal path for a peer that has said
// nothing and may be hostile; a DNS round trip per refused
// connection is an amplifier pointed at the operator's own
// resolver. Refer THREAT_MODEL.md B1
// : Always leaves a printable string. A diagnostic that
// failed to render its subject would report the refusal
// without the one fact it exists to carry
//
static void
RenderP2PeerConAddr ( const sockaddr *pName, int nNameLen
, char *szOut, size_t cchOut )
{
if ( cchOut == 0 )
return;
szOut[0] = 0;
if ( pName != 0 && nNameLen > 0 &&
getnameinfo ( pName, (socklen_t)nNameLen
, szOut, (DWORD)cchOut, 0, 0, NI_NUMERICHOST ) == 0 &&
szOut[0] != 0 )
return;
std::snprintf ( szOut, cchOut, "%s", "(unnameable)" );
}
//
// Description: Folds an IPv6 origin into the 64-bit accounting key
//
// NOTES: The /64 PREFIX and not the whole address, and that is
// the substantive decision here rather than the hash. A
// v6 host is routinely delegated an entire /64 - one
// subscriber line, one machine's privacy addresses - so
// keying on 128 bits would let a single source mint 2^64
// keys and walk straight through the per-source bound the
// key exists to serve. Two hosts in one /64 sharing a key
// is the intended reading of "one source"
// : FNV-1a, which is a fold and not a security primitive.
// Nothing here resists a chosen-prefix collision and
// nothing needs to: the consequence of one is that two
// subnets share an accept budget, which is the same
// consequence a /64 already has by design
// : Bit 63 is SET, unconditionally. A v4 key is the 32-bit
// address and can never reach it, so the two families
// cannot collide in the tally's map - and ::1, whose /64
// prefix is all zeroes, cannot fold to 0, which is the
// value reserved for "the kernel would not name it"
//
// Returns: P2PsourceKey
// The key, never 0
//
static P2PsourceKey
FoldP2PeerConV6Key ( const UCHAR *pucAddr )
{
unsigned long long ullHash = 14695981039346656037ull; // FNV-1a offset
for ( int i = 0; i < 8; ++i ) // the /64 prefix
{
ullHash ^= (unsigned long long)pucAddr[i];
ullHash *= 1099511628211ull; // FNV-1a prime
}
return (P2PsourceKey)( ullHash | ( 1ull << 63 ) );
}
//
// Description: The accounting key for one already-read origin
//
// NOTES: Takes an address rather than reading a socket, so the
// accept path can read the socket ONCE and derive both the
// key and the allow-list decision from the same bytes.
// Two readings of a socket that is being torn down can
// disagree, and an accept refused by one answer while
// being counted by the other is the kind of inconsistency
// that only shows up in production
// : The address is expected NORMALISED - refer
// AcceptSourceAddress(). A v4-mapped peer reaching here
// unrewritten would be keyed as a v6 /64 and so would take
// a different share from the same peer on a v4 service
//
// Returns: P2PsourceKey
// The key, or 0 for an origin that cannot be named
//
static P2PsourceKey
KeyP2PeerConSource ( const SOCKADDR_STORAGE &rAddr, int nLen )
{
const UCHAR *pucAddr = 0;
const int nBytes = P2PeerConAddrBytes ( (const sockaddr *)&rAddr
, nLen, pucAddr );
// IPv4 IS its address, in network order, exactly as it has always been.
// The key is an identity and not a number: nothing compares it for
// magnitude, so normalising the order would buy nothing
if ( nBytes == 4 )
return (P2PsourceKey)(unsigned int)
((const sockaddr_in *)&rAddr)->sin_addr.s_addr;
if ( nBytes == 16 )
return FoldP2PeerConV6Key ( pucAddr );
return 0;
}
//
// Description: Resolves a host name or an address literal to a socket address
//
// NOTES: getaddrinfo(), replacing an InetPton() call with a
// gethostbyname() fallback that COULD NOT RUN. The
// fallback was written for inet_addr(), which signals
// failure by returning INADDR_NONE; the PostVS2015 move to
// InetPton() changed the signal and not the test.
// InetPton() answers 0 for a string it cannot parse and
// WRITES NOTHING, so after the memset the address stayed
// 0.0.0.0 and `== INADDR_NONE` could only ever fire for
// the literal "255.255.255.255". Every host NAME - and
// the self-connection path, which feeds gethostname()'s
// output straight into this - therefore resolved to
// 0.0.0.0 and failed in ConnectEx with
// WSAEADDRNOTAVAIL rather than with the "Unable to
// resolve server" this code plainly meant to report.
// Nothing in the suite noticed because every test in it
// dials the literal "127.0.0.1", which parses
// : One call now does both jobs. getaddrinfo() takes a name
// OR a literal, so there is no failure signal to
// misread and no second path to leave unreachable
// : gethostbyname() is also the one API here that COULD NOT
// be carried to IPv6 - it is A-records only. Replacing it
// was the first step of the dual-stack port, taken before
// the port was
// : ai_family is the CALLER's, and since 2026-08-28 it can
// be AF_UNSPEC. It was the literal AF_INET while every
// socket this class opened was AF_INET, because resolving
// a name to an AAAA record would have answered an address
// the connect could not use. Connect() now opens the
// socket in whichever family this answers, so the
// constraint has moved off this line and onto the family
// the caller set - refer P2PeerConFamily_e
//
// Returns: int
// 0 on success; otherwise the getaddrinfo() result, which is a
// WSA error code on Windows and an EAI_ code on Linux
//
static int
ResolveP2PeerConHost ( LPCSTR lpszHost, short nIPort, int nAiFamily
, SOCKADDR_STORAGE &rAddr, int &rnLen )
{
memset ( &rAddr, 0, sizeof(rAddr) );
rnLen = 0;
if ( lpszHost == 0 || *lpszHost == 0 )
return EAI_NONAME;
// The port travels as the SERVICE argument rather than being stamped in
// afterwards, so the one structure getaddrinfo() fills is complete and
// there is no second place for the port to be forgotten
char szPort[16];
std::snprintf ( szPort, sizeof(szPort), "%u"
, (unsigned)(unsigned short)nIPort );
addrinfo oHints;
addrinfo *pResult = 0;
memset ( &oHints, 0, sizeof(oHints) );
oHints.ai_family = nAiFamily; // refer the note above
oHints.ai_socktype = SOCK_STREAM;
oHints.ai_protocol = IPPROTO_TCP;
const int nResult = getaddrinfo ( lpszHost, szPort, &oHints, &pResult );
if ( nResult != 0 )
return nResult;
if ( pResult == 0 )
return EAI_NONAME; // success with no answer (SNHappen)
// The FIRST answer, which is what the resolver ordered for us. A host
// with several records is answered in the order the platform prefers -
// under AF_UNSPEC that is RFC6724, which puts a reachable AAAA ahead of an
// A - and this connects to that one; trying the rest in turn is a
// different behaviour from the one being restored here and would change
// what a failed connect MEANS to the caller
int nAnswer = 0;
if ( ( pResult->ai_family != AF_INET && pResult->ai_family != AF_INET6 ) ||
pResult->ai_addrlen == 0 ||
pResult->ai_addrlen > (size_t)sizeof(SOCKADDR_STORAGE) )
nAnswer = EAI_FAMILY; // not a family we open (SNHappen)
else
{
memcpy ( &rAddr, pResult->ai_addr, (size_t)pResult->ai_addrlen );
rnLen = (int)pResult->ai_addrlen;
}
freeaddrinfo ( pResult );
return nAnswer;
}
///////////////////////////////////////////////////////////////////////
// Constructors, destructor and factories
const __time64_t
iLoginTimeoutSeconds = 20; // Login sequences must be
// processed within this time
P2PeerConWsa::P2PeerConWsa ( )
{
// Firstly
RenderThisSafe ( );
}
//
// Parameters: P2PaddrSTR strThisP2Paddr
// Identification address for this end
//
// P2PaddrSTR strThatP2Paddr
// Identification address for that, or the other end
//
// DWORD oSocket
// Socket upon which connection accepted
//
P2PeerConWsa::P2PeerConWsa ( P2PaddrSTR pThatP2PaddrSTR
, SOCKET oSocket )
: P2PeerCon ( pThatP2PaddrSTR )
{
// Firstly
RenderThisSafe ( );
m_oSocket = oSocket;
m_hFile = (HANDLE)oSocket;
m_eP2PeerConMode = P2PeerCon_Accept;
}
//
// LPCTSTR lpszIPaddress
// Connection address
//
// short nIPort
// Connection port.
//
P2PeerConWsa::P2PeerConWsa ( P2PaddrSTR pThatP2PaddrSTR
, LPCTSTR lpszIPaddress, short nIPort )
: P2PeerCon ( pThatP2PaddrSTR )
{
// Firstly
RenderThisSafe ( );
m_sIPaddress = lpszIPaddress;
m_nIPort = nIPort;
m_eP2PeerConMode = P2PeerCon_CLIENT;
m_eFamily = ClientFamilyForAddress ( lpszIPaddress );
}
//
// short nIPort
// Listening port
//
P2PeerConWsa::P2PeerConWsa ( P2PaddrSTR pThatP2PaddrSTR
, short nIPort )
: P2PeerCon ( pThatP2PaddrSTR )
{
// Firstly
RenderThisSafe ( );
m_nIPort = nIPort;
m_eP2PeerConMode = P2PeerCon_SERVICE;
}
P2PeerConWsa::~P2PeerConWsa ( )
{
// Resource collection
Drop ( 0 );
}
void
P2PeerConWsa::RenderThisSafe ( )
{
// Properties
m_nIPort = 0;
m_oSocket = INVALID_SOCKET;
m_oSocketAccept = INVALID_SOCKET;
m_xBacklog = DEF_P2PeerConBacklog;
// IPv4, every interface, and no source filter. The behaviour of every
// caller written before these existed, which is the point: the mechanism
// is always present, what is opt-in is the restriction - and a family the
// caller did not ask for is a restriction like any other
m_eFamily = P2PeerConFamily_IPv4;
m_eListenScope = P2PeerConScope_Any;
m_sListenAddress.Empty ( );
m_pxSourceFilter.reset ( );
}
//
// Sets the listen backlog for this SERVICE
// NOTES: Takes effect at the next Listen(); a socket already listening keeps
// the backlog it was given
// : SOMAXCONN asks the stack for its maximum
//
void
P2PeerConWsa::SetBacklog ( int nBacklog )
{
m_xBacklog = nBacklog > 0 ? nBacklog : DEF_P2PeerConBacklog;
}
//
// Chooses the address family this connection opens in
// NOTES: Takes effect at the next Listen() or Connect(), the same rule the
// backlog and the scope state. A socket already open keeps the family
// it was opened in, because a family is not something a socket can be
// talked into afterwards
// : NOT validated against the host's stack here, and there is nowhere it
// could usefully be. A machine with IPv6 disabled fails at socket() or
// at bind() with the stack's own reason, which names the problem better
// than a guess made at configure time - and a probe here would be a
// second opinion that could disagree with the one that matters
//
void
P2PeerConWsa::SetFamily ( P2PeerConFamily_e eFamily )
{
m_eFamily = eFamily;
}
//
// Description: AF_INET or AF_INET6 for the configured family
// NOTES: The two v6 modes differ by IPV6_V6ONLY and not by
// family, so they answer the same thing here. Listen()
// is where they part
//
// Returns: int
// AF_INET or AF_INET6
//
int
P2PeerConWsa::SocketFamily ( ) const
{
return IsFamilyIPv6 ( ) ? AF_INET6 : AF_INET;
}
//
// Narrows where this SERVICE's listening socket binds
// NOTES: The address is kept only for P2PeerConScope_Address and cleared for
// the other two, so GetListenAddress() cannot describe an address that
// is not the one bound
// : Not validated here. It is validated at Listen(), by
// ListenBindSockaddr(), because that is where a failure can drop the
// service - a setter that threw would put the decision in the caller's
// constructor path, where this library has no way to report it
// : Which FAMILY the address is read in is decided by SetFamily(), not by
// the text. A v6 literal on an IPv4 service is refused rather than
// quietly promoting the service to v6: a scope says which of the host's
// addresses to bind and has no business changing what is opened
//
void
P2PeerConWsa::SetListenScope ( P2PeerConScope_e eScope, LPCTSTR lpszIpAddress )
{
m_eListenScope = eScope;
if ( eScope == P2PeerConScope_Address && lpszIpAddress != 0 )
m_sListenAddress = lpszIpAddress;
else
m_sListenAddress.Empty ( );
}
//
// Adds one prefix, of either family, to the accept allow-list
// NOTES: The FIRST call is what arms the filter. Until one succeeds there is
// no list and no restriction, so a service that never calls this is
// the service that shipped before it existed
// : Reports and answers false for an unparsable prefix rather than
// throwing. A rule is configuration, usually read from a file, and a
// bad line in a config file is something an operator fixes - not
// something that should take the process with it. Answering false is
// how the caller finds out, and the caller is expected to look
// : The rule's family comes from its own text - refer
// ParseP2PeerConPrefix(). It is NOT constrained to the family this
// connection opens in, deliberately: an operator writing a policy file
// should not have to re-order it when a service is dual-stacked, and a
// rule for a family nothing opens can only ever fail to match
//
// Returns: bool
// Whether the prefix was understood and added
//
bool
P2PeerConWsa::AllowAcceptFrom ( LPCTSTR lpszPrefix )
{
int nFamily = AF_INET;
int nBits = 0;
UCHAR ucAddr[16];
if ( !ParseP2PeerConPrefix ( lpszPrefix, nFamily, ucAddr, nBits ) )
{
EVERR->Module (L"%hs[%s]", __FUNCTION__
, GetP2PaddrHub().c_wstr() )
->Message(L"Unparsable accept prefix '%s'"
, lpszPrefix ? lpszPrefix : L"(null)" )
->Advice (L"Dotted IPv4 with an optional /bits - \"192.168.1.0/24\", "
L"\"10.0.0.0/8\", or \"10.1.2.3\" for one host")
// Advice_T is a FORMAT function, so the zone suffix is named
// rather than shown - a literal per cent sign here was eaten as a
// conversion and printed the advice as nonsense
->Advice_T("Or RFC4291 IPv6 with an optional /bits - "
"\"2001:db8::/32\", \"fd00::/8\", or \"::1\" for one "
"host. A ':' is what makes it v6, and a per-cent zone "
"suffix is not accepted in a rule - a link-local "
"allow-list is spelt \"fe80::/10\"")
->Cancel ( );
return false;
}
if ( !m_pxSourceFilter )
m_pxSourceFilter = std::make_shared<P2PeerConWsaSourceFilter> ( );
m_pxSourceFilter -> Add ( nFamily, ucAddr, nBits );
return true;
}
//
// This machine only
// NOTES: The whole of 127.0.0.0/8 rather than 127.0.0.1, because the loopback
// net is entirely local on both platforms and the suite's own tests
// dial 127.0.0.2 and 127.0.0.3 to be two sources on one host
// : ::1/128 and not ::1/8, because v6 has no loopback NET - the whole of
// it is one address, and a wider v6 rule here would admit the
// unspecified address and everything else in ::/8 along with it
//
void
P2PeerConWsa::AllowAcceptLoopback ( )
{
AllowAcceptFrom ( _T("127.0.0.0/8") ); // IPv4 loopback net
AllowAcceptFrom ( _T("::1") ); // IPv6 loopback, /128
}
//
// This machine and the private address space around it
// NOTES: RFC1918 plus loopback plus link-local, and their v6 counterparts. It
// is the nearest an address test gets to "the LAN" and it is not a
// proof of locality: anything upstream that rewrites a source address -
// a NAT, a proxy, a load balancer - can present a public peer under a
// private address, and this will admit it. What it does buy is that a
// host on the open internet, addressing this service directly, is
// refused before it can speak. Refer THREAT_MODEL.md B1
// : fc00::/7 covers both halves of the unique-local space. Only fd00::/8
// is defined for self-assignment and fc00::/8 is reserved, but a
// reserved half that arrives at all has been forged, and a rule that
// admitted the defined half while a peer forged the reserved one would
// be a distinction drawn on the wrong side
// : NAT66 is rare and NPTv6 rarer, so a v6 peer's source address is far
// more likely to be the one it really holds than a v4 peer's is. That
// makes this test STRONGER in v6 and is worth knowing, but it is still
// a restriction rather than a proof
//
void
P2PeerConWsa::AllowAcceptPrivate ( )
{
AllowAcceptFrom ( _T("127.0.0.0/8") ); // this machine
AllowAcceptFrom ( _T("10.0.0.0/8") ); // RFC1918
AllowAcceptFrom ( _T("172.16.0.0/12") ); // RFC1918
AllowAcceptFrom ( _T("192.168.0.0/16") ); // RFC1918
AllowAcceptFrom ( _T("169.254.0.0/16") ); // RFC3927 link-local
AllowAcceptFrom ( _T("::1") ); // this machine, v6
AllowAcceptFrom ( _T("fc00::/7") ); // RFC4193 unique-local
AllowAcceptFrom ( _T("fe80::/10") ); // RFC4291 link-local
}
//
// Removes the filter entirely, restoring "anyone who can reach the port"
// NOTES: Distinct from a filter that matches nothing, and deliberately: there
// is no way to spell an empty allow-list that admits everybody, so
// clearing has to be its own verb
//
void
P2PeerConWsa::ClearAcceptSourceFilter ( )
{
m_pxSourceFilter.reset ( );
}