Use QUIC as transport protocoll

Fixes: #7001
This commit is contained in:
phosit 2026-05-20 21:12:39 +02:00
parent 1ba6e49e5d
commit c9aae81eae
No known key found for this signature in database
GPG key ID: C9430B600671C268
20 changed files with 1631 additions and 373 deletions

23
certificate.pem Normal file
View file

@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDzzCCAjegAwIBAgIUYMCyNaykR4OF8u4Bq+pesqG48LQwDQYJKoZIhvcNAQEL
BQAwADAgFw0yNjA1MjAxOTA0NDFaGA85OTk5MTIzMTIzNTk1OVowADCCAaIwDQYJ
KoZIhvcNAQEBBQADggGPADCCAYoCggGBAL5uhNcLTOs4Senn8zuwy8c15HGDCBk/
tSgUFCSueBRtbZuAnPtxlkOkJDB7y/Abp+rhHe7T7sa1iYS+Q7aWK0QIaoiKJj1x
35SsCYmniAs797tsxnzfRuP2WJe8yt/kuia6d6AL12P5Yu05JEJWaiDHljfSZsGB
spCD5c2PyYZqGKNxdUHLlFDZ3RIWkym8h/wTKESlIfKcO0l9Ythtz0qAFWZbOJRR
VXUIJ6djYtc3C3cIIa6P18598CkRlmk7WSRfSrUY1USrzCL5dwBnfqLNqyWuqHWk
hJLM7Izu2ZR24V4rQpf7V32jAUkWCwRUyseqkilgYIV3MWLoLlBP5jLTRBZowIJ2
h6EnqGNfUqKuR9W/sT21lkP+XUJPNfPEjL5paG1jpyMCwvWHrfVd63wjDUaZxa52
APZlhDLoKPrMI4jBuV0yBnO8m2PKVHGbfytuCUSWRW/+YHtoIBVJ5GpPbZ7XRjan
/p/Rk++XVXlKyo9b66PDQ/qBEzTrsFie2QIDAQABoz8wPTAMBgNVHRMBAf8EAjAA
MA4GA1UdDwEB/wQEAwIFoDAdBgNVHQ4EFgQUSCH7PDVIF0erciboK/dzV1i+hC8w
DQYJKoZIhvcNAQELBQADggGBACVS1GPwXvHxs5PFV8dobDXDbzIF984YJGkfkpTI
eQiC2GdQZd+d2NQdYJYu+yFQrmfPyhH4FPH6uLUp2eNGexXlv58TNMkh69MekapJ
+XBBwwCynL4vZETtozE6Dzp5S7XXL3cenYI+0MIforEhI3dtbjg2zxRQy4YiBYuh
tecbazuNsVXw7iSN8yT0wZTfg4gGHDyzEdO4+8BcBUfzpBDOr+/TV58doIA/ZucX
utFR8o9tVmvVEef8LaEOzmn7fgpxaEiVEbFH1PUyRhMZlQRN/eTTkDTRiTzssoXA
dtIV5M1kJjkdn/aYxM8h+unJ6/MfPvuat+HcmKDXVhdoNJ2r23mUwRFPt7fwm75Y
gp8WWwP9D8MglcVHhsp+KrDKfFX7MicDPhadc6LEWveo4PRRdJcKcWedJEvnWFNN
0TbbC5xQgIB1KijxvqoBr38XQVhFry1dZRPBv642YBMdiHWGCoLE8EBssDqs5Wp+
DMqs4E5nZzbVVSM5l5J2q6Wlbw==
-----END CERTIFICATE-----

182
privatekey.pem Normal file
View file

@ -0,0 +1,182 @@
Public Key Info:
Public Key Algorithm: RSA
Key Security Level: High (3072 bits)
modulus:
00:be:6e:84:d7:0b:4c:eb:38:49:e9:e7:f3:3b:b0:cb
c7:35:e4:71:83:08:19:3f:b5:28:14:14:24:ae:78:14
6d:6d:9b:80:9c:fb:71:96:43:a4:24:30:7b:cb:f0:1b
a7:ea:e1:1d:ee:d3:ee:c6:b5:89:84:be:43:b6:96:2b
44:08:6a:88:8a:26:3d:71:df:94:ac:09:89:a7:88:0b
3b:f7:bb:6c:c6:7c:df:46:e3:f6:58:97:bc:ca:df:e4
ba:26:ba:77:a0:0b:d7:63:f9:62:ed:39:24:42:56:6a
20:c7:96:37:d2:66:c1:81:b2:90:83:e5:cd:8f:c9:86
6a:18:a3:71:75:41:cb:94:50:d9:dd:12:16:93:29:bc
87:fc:13:28:44:a5:21:f2:9c:3b:49:7d:62:d8:6d:cf
4a:80:15:66:5b:38:94:51:55:75:08:27:a7:63:62:d7
37:0b:77:08:21:ae:8f:d7:ce:7d:f0:29:11:96:69:3b
59:24:5f:4a:b5:18:d5:44:ab:cc:22:f9:77:00:67:7e
a2:cd:ab:25:ae:a8:75:a4:84:92:cc:ec:8c:ee:d9:94
76:e1:5e:2b:42:97:fb:57:7d:a3:01:49:16:0b:04:54
ca:c7:aa:92:29:60:60:85:77:31:62:e8:2e:50:4f:e6
32:d3:44:16:68:c0:82:76:87:a1:27:a8:63:5f:52:a2
ae:47:d5:bf:b1:3d:b5:96:43:fe:5d:42:4f:35:f3:c4
8c:be:69:68:6d:63:a7:23:02:c2:f5:87:ad:f5:5d:eb
7c:23:0d:46:99:c5:ae:76:00:f6:65:84:32:e8:28:fa
cc:23:88:c1:b9:5d:32:06:73:bc:9b:63:ca:54:71:9b
7f:2b:6e:09:44:96:45:6f:fe:60:7b:68:20:15:49:e4
6a:4f:6d:9e:d7:46:36:a7:fe:9f:d1:93:ef:97:55:79
4a:ca:8f:5b:eb:a3:c3:43:fa:81:13:34:eb:b0:58:9e
d9:
public exponent:
01:00:01:
private exponent:
54:d4:52:a4:a0:ca:10:f6:30:26:dc:46:83:ce:8b:d8
1f:ef:b5:89:13:30:7c:2a:ac:c1:d4:ff:4a:20:ff:a8
87:6c:ff:eb:ee:2e:79:2b:84:91:02:70:03:36:e0:7a
fa:ac:71:73:14:41:87:8f:12:c5:69:24:2c:cf:d4:52
28:15:9c:e1:3d:8b:9d:90:65:60:05:97:a6:63:79:ed
aa:bb:79:07:2a:55:23:f5:24:a5:ee:62:11:55:8f:44
45:40:47:4d:aa:38:b6:b6:3f:15:41:a1:1f:53:f3:4e
ca:d9:e5:df:fa:1a:35:36:60:1e:01:5e:82:b0:d9:09
a6:14:18:d4:8d:0c:ac:f3:1f:39:d5:76:ec:f3:68:a2
82:ed:dd:c0:46:77:4a:e5:c1:9b:49:19:a1:23:b9:75
8c:7c:fb:ed:a2:d4:9f:2e:9c:45:97:b4:7f:17:66:9b
84:a5:ef:9f:61:6d:7e:4a:e3:da:f9:d0:75:da:46:ae
f6:e9:ae:cd:66:11:11:62:5a:3e:22:55:9f:ac:2c:bd
a8:5a:62:1e:fe:c6:78:04:1d:69:f3:12:53:c7:5a:db
c3:19:a4:d9:d6:5a:25:55:f7:ba:30:d1:f4:bb:8a:b6
8a:c5:1d:4c:66:c3:e6:c1:05:4a:d8:9b:2e:f3:b9:3f
8a:b6:6e:04:cb:5f:32:65:a0:1c:32:5a:f5:b0:36:a3
ed:96:1b:cf:b0:1d:32:3e:8b:a4:53:b2:c0:6d:30:d3
93:43:d6:fe:29:fd:9e:e2:72:30:7a:01:2b:b2:ad:39
b5:6c:8b:41:67:77:bb:09:8e:41:41:e2:a9:5a:76:4b
51:56:43:72:db:02:5e:d9:ef:93:16:c7:64:18:3a:f7
73:d8:b8:d0:8f:af:e3:cd:06:26:8f:af:b0:b6:57:f6
f0:c7:4f:f0:6f:f5:49:07:96:83:ae:44:60:5b:1f:df
0f:70:07:c4:06:03:bf:47:f3:e8:a8:1e:54:ba:46:e9
prime1:
00:f8:6f:14:90:ad:01:c6:ea:8e:e0:ad:bd:27:fd:af
e5:09:6b:24:02:c5:18:aa:69:d1:d4:0f:78:06:89:c1
1e:5f:5a:3c:21:ca:1e:a3:73:0d:5e:d6:42:e9:6c:b0
81:85:85:b7:d7:57:b2:27:42:67:5b:06:fa:e7:32:64
bd:f5:fe:31:44:fb:a2:8e:b9:e8:3a:da:42:1a:13:20
47:ac:8e:e1:f0:7e:81:1d:c9:de:ae:81:f5:fc:07:be
4d:df:a1:a4:36:bb:c8:b4:f5:96:bb:bc:cf:b8:53:28
15:28:32:f8:82:0d:9e:ed:0c:75:d5:83:a6:9f:cd:d1
37:42:ad:6d:a4:75:0c:e6:9e:dd:16:6d:f9:57:56:9a
0c:9c:f8:90:86:f8:a9:92:c8:61:81:09:82:a8:89:a9
8f:c6:71:77:63:40:77:46:ed:c8:78:41:7b:ac:95:c2
23:b2:64:44:26:00:d9:6b:b1:a8:99:28:78:7e:71:ed
a3:
prime2:
00:c4:3b:39:2f:33:08:d4:22:43:0f:4a:69:a9:89:51
c9:1a:9f:a2:e2:82:bb:fd:77:76:c0:5a:36:38:d0:36
ee:f2:aa:8b:4a:2a:56:df:eb:88:7f:50:bf:55:bc:9d
cb:e2:83:58:9d:60:3e:b5:6e:6f:89:61:4c:09:a1:99
59:78:77:38:fe:b7:87:45:6e:54:5d:52:ef:18:25:e2
39:f4:04:9d:0e:bd:05:c3:a1:06:11:d8:93:8e:59:85
0e:18:bf:80:fd:aa:a8:c6:20:3e:98:ea:82:6e:2f:37
64:ec:19:e8:ab:f5:c8:b1:f6:ff:af:ef:f5:37:e0:d2
4c:2f:eb:ca:22:fc:56:7e:9e:f5:41:df:f5:c2:06:17
5a:e4:0a:c3:fb:fe:4f:00:0c:f5:36:8b:1b:33:e9:11
d2:44:ac:3e:d0:d9:e7:c7:17:e5:3a:4e:ed:5c:73:7e
73:83:4c:28:a0:1b:78:7c:82:f9:20:7b:8b:29:68:51
53:
coefficient:
00:aa:69:84:80:d9:0d:3a:12:90:d8:4e:cf:9c:24:5a
5b:40:c0:41:b4:73:db:51:4a:7a:3b:91:9c:51:01:6c
41:de:a3:33:e9:2e:ef:3b:2c:42:fd:75:98:f4:47:f0
b3:12:f6:d6:ff:88:3e:d2:68:b7:ed:10:6e:38:c6:f4
be:0a:82:17:b3:62:b5:25:d2:c2:54:b9:5b:e8:a1:6a
04:2d:6a:bb:73:b7:9d:5f:d2:7f:5d:7c:c0:50:4a:8e
af:e9:a8:68:b3:f7:a3:d9:71:0c:84:e4:c8:68:0b:10
5a:71:f0:f5:92:ee:20:2f:0c:a5:24:6b:1a:7e:c0:3c
28:4a:37:fd:ad:2f:fa:15:87:97:a1:d0:b1:62:26:f3
96:a2:33:8d:41:d8:6a:0b:ce:c9:bf:c2:0d:8e:94:84
66:27:a4:c1:00:32:64:ad:8a:3e:45:45:4b:c1:d1:c4
7d:f2:d1:3a:f6:65:36:46:2e:52:35:b9:a2:ab:87:3c
66:
exp1:
00:8c:40:f1:e4:47:b7:f4:1b:e1:f3:d8:42:2c:fc:9c
bb:fa:58:41:69:4a:ea:84:f2:de:e2:10:a5:9b:53:53
f2:98:b4:71:b4:45:ce:8c:4a:5c:e7:08:a1:97:f4:a3
a5:4b:c3:55:29:be:b9:b6:4e:57:d9:5d:14:73:47:d0
f6:29:95:8b:2d:3d:be:e5:42:f1:67:a0:66:a7:1f:db
1e:7b:bd:e2:b2:8a:48:cc:8f:76:27:20:f9:c5:82:7d
9e:ab:3d:2f:5f:33:1e:b9:82:d8:c9:3e:6c:2a:cc:cf
99:3a:2b:a4:7e:8f:c0:04:65:ff:74:3e:31:e8:90:22
a8:46:fd:70:23:e3:6d:18:19:e2:09:52:a6:ec:f6:d8
5b:7c:97:1b:c9:07:43:7c:b9:a1:ca:5e:9b:24:19:2b
e0:1e:91:5a:6c:6f:2d:a7:9e:80:89:db:b6:3d:96:02
97:72:94:06:a1:49:e3:75:58:44:2d:cb:5a:53:50:70
6f:
exp2:
25:93:05:87:21:29:8e:9d:24:e4:17:a6:95:dd:02:79
14:8c:fe:be:8a:b3:fe:7b:d2:94:50:71:d3:7d:23:17
ac:05:b5:f8:34:95:3f:f9:34:c0:d4:30:5e:f5:67:ed
b3:68:dd:1d:fd:60:e4:92:c9:ee:af:5f:c4:f4:59:8d
c5:40:66:fc:77:1e:02:d0:76:7d:0c:35:56:15:62:f5
1f:e1:86:45:5d:32:6e:5c:35:f2:52:db:26:45:c3:f1
88:11:9b:5c:77:42:2b:f5:de:a6:9f:38:ec:6a:44:1c
22:0d:6f:fd:05:6a:31:91:8f:32:1c:2b:83:50:9c:54
14:54:fc:f6:a8:04:d3:e8:12:24:54:03:15:ec:de:a9
fb:c2:87:f6:87:a2:8e:ea:ec:45:4e:6b:9e:0c:01:ea
96:55:b9:0d:7a:bc:23:e6:52:71:50:cd:a8:87:40:ee
53:74:d0:ce:9f:93:f9:9b:86:a2:8e:a5:7d:ff:48:9b
Public Key PIN:
pin-sha256:mTYJ3eMVIy4Zcqoo22QDSav9huivaJ8VbglPlInerS8=
Public Key ID:
sha256:993609dde315232e1972aa28db640349abfd86e8af689f156e094f9489dead2f
sha1:4821fb3c35481747ab7226e82bf7735758be842f
-----BEGIN RSA PRIVATE KEY-----
MIIG5AIBAAKCAYEAvm6E1wtM6zhJ6efzO7DLxzXkcYMIGT+1KBQUJK54FG1tm4Cc
+3GWQ6QkMHvL8Bun6uEd7tPuxrWJhL5DtpYrRAhqiIomPXHflKwJiaeICzv3u2zG
fN9G4/ZYl7zK3+S6Jrp3oAvXY/li7TkkQlZqIMeWN9JmwYGykIPlzY/JhmoYo3F1
QcuUUNndEhaTKbyH/BMoRKUh8pw7SX1i2G3PSoAVZls4lFFVdQgnp2Ni1zcLdwgh
ro/Xzn3wKRGWaTtZJF9KtRjVRKvMIvl3AGd+os2rJa6odaSEkszsjO7ZlHbhXitC
l/tXfaMBSRYLBFTKx6qSKWBghXcxYuguUE/mMtNEFmjAgnaHoSeoY19Soq5H1b+x
PbWWQ/5dQk8188SMvmlobWOnIwLC9Yet9V3rfCMNRpnFrnYA9mWEMugo+swjiMG5
XTIGc7ybY8pUcZt/K24JRJZFb/5ge2ggFUnkak9tntdGNqf+n9GT75dVeUrKj1vr
o8ND+oETNOuwWJ7ZAgMBAAECggGAVNRSpKDKEPYwJtxGg86L2B/vtYkTMHwqrMHU
/0og/6iHbP/r7i55K4SRAnADNuB6+qxxcxRBh48SxWkkLM/UUigVnOE9i52QZWAF
l6Zjee2qu3kHKlUj9SSl7mIRVY9ERUBHTao4trY/FUGhH1PzTsrZ5d/6GjU2YB4B
XoKw2QmmFBjUjQys8x851Xbs82iigu3dwEZ3SuXBm0kZoSO5dYx8++2i1J8unEWX
tH8XZpuEpe+fYW1+SuPa+dB12kau9umuzWYREWJaPiJVn6wsvahaYh7+xngEHWnz
ElPHWtvDGaTZ1lolVfe6MNH0u4q2isUdTGbD5sEFStibLvO5P4q2bgTLXzJloBwy
WvWwNqPtlhvPsB0yPoukU7LAbTDTk0PW/in9nuJyMHoBK7KtObVsi0Fnd7sJjkFB
4qladktRVkNy2wJe2e+TFsdkGDr3c9i40I+v480GJo+vsLZX9vDHT/Bv9UkHloOu
RGBbH98PcAfEBgO/R/PoqB5UukbpAoHBAPhvFJCtAcbqjuCtvSf9r+UJayQCxRiq
adHUD3gGicEeX1o8Icoeo3MNXtZC6WywgYWFt9dXsidCZ1sG+ucyZL31/jFE+6KO
ueg62kIaEyBHrI7h8H6BHcneroH1/Ae+Td+hpDa7yLT1lru8z7hTKBUoMviCDZ7t
DHXVg6afzdE3Qq1tpHUM5p7dFm35V1aaDJz4kIb4qZLIYYEJgqiJqY/GcXdjQHdG
7ch4QXuslcIjsmREJgDZa7GomSh4fnHtowKBwQDEOzkvMwjUIkMPSmmpiVHJGp+i
4oK7/Xd2wFo2ONA27vKqi0oqVt/riH9Qv1W8ncvig1idYD61bm+JYUwJoZlZeHc4
/reHRW5UXVLvGCXiOfQEnQ69BcOhBhHYk45ZhQ4Yv4D9qqjGID6Y6oJuLzdk7Bno
q/XIsfb/r+/1N+DSTC/ryiL8Vn6e9UHf9cIGF1rkCsP7/k8ADPU2ixsz6RHSRKw+
0NnnxxflOk7tXHN+c4NMKKAbeHyC+SB7iyloUVMCgcEAjEDx5Ee39Bvh89hCLPyc
u/pYQWlK6oTy3uIQpZtTU/KYtHG0Rc6MSlznCKGX9KOlS8NVKb65tk5X2V0Uc0fQ
9imViy09vuVC8WegZqcf2x57veKyikjMj3YnIPnFgn2eqz0vXzMeuYLYyT5sKszP
mTorpH6PwARl/3Q+MeiQIqhG/XAj420YGeIJUqbs9thbfJcbyQdDfLmhyl6bJBkr
4B6RWmxvLaeegInbtj2WApdylAahSeN1WEQty1pTUHBvAoHAJZMFhyEpjp0k5Bem
ld0CeRSM/r6Ks/570pRQcdN9IxesBbX4NJU/+TTA1DBe9Wfts2jdHf1g5JLJ7q9f
xPRZjcVAZvx3HgLQdn0MNVYVYvUf4YZFXTJuXDXyUtsmRcPxiBGbXHdCK/Xepp84
7GpEHCINb/0FajGRjzIcK4NQnFQUVPz2qATT6BIkVAMV7N6p+8KH9oeijursRU5r
ngwB6pZVuQ16vCPmUnFQzaiHQO5TdNDOn5P5m4aijqV9/0ibAoHBAKpphIDZDToS
kNhOz5wkWltAwEG0c9tRSno7kZxRAWxB3qMz6S7vOyxC/XWY9EfwsxL21v+IPtJo
t+0QbjjG9L4KghezYrUl0sJUuVvooWoELWq7c7edX9J/XXzAUEqOr+moaLP3o9lx
DITkyGgLEFpx8PWS7iAvDKUkaxp+wDwoSjf9rS/6FYeXodCxYibzlqIzjUHYagvO
yb/CDY6UhGYnpMEAMmStij5FRUvB0cR98tE69mU2Ri5SNbmiq4c8Zg==
-----END RSA PRIVATE KEY-----

View file

@ -72,7 +72,7 @@ CNetClient::CNetClient(CGame* game, std::string serverAddressOrHostname, std::ui
CNetClient{PrivateTag{}, game, std::move(serverAddressOrHostname), serverPort, username, hostJID,
std::move(hashedPassword), std::move(controllerSecret)}
{
SetupConnection(nullptr);
SetupConnection();
}
CNetClient::CNetClient(PrivateTag, CGame* game, std::string serverAddressOrHostname,
@ -169,10 +169,10 @@ CNetClient::~CNetClient()
}
void CNetClient::SetupConnection(ENetHost* enetClient)
void CNetClient::SetupConnection()
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(m_ServerAddressOrHostname, m_ServerPort, enetClient);
bool ok = session->Connect(m_ServerAddressOrHostname, m_ServerPort);
SetAndOwnSession(session);
if (ok)
m_PollingThread = std::thread(Threading::HandleExceptions<CNetClientSession::RunNetLoop>::Wrapper, m_Session);
@ -284,7 +284,7 @@ bool CNetClient::TryToConnectWithSTUN(std::string serverAddressOrHostname, std::
try
{
g_NetClient->SetupConnection(enetClient);
g_NetClient->SetupConnection();
}
catch (...)
{
@ -463,6 +463,7 @@ bool CNetClient::SendMessage(const CNetMessage* message)
void CNetClient::HandleConnect()
{
LOGMESSAGE("Net client: Connected", m_ServerAddressOrHostname, m_ServerPort);
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}

View file

@ -288,7 +288,7 @@ private:
* Set up a connection to the remote networked server.
* @return true on success, false on connection failure
*/
void SetupConnection(ENetHost* enetClient);
void SetupConnection();
/**
* Take ownership of a session object, and use it for all network communication.

View file

@ -22,8 +22,8 @@
#include "lib/code_generation.h"
#include "lib/debug.h"
#include "network/NetClient.h"
#include "network/NetEnet.h"
#include "network/NetMessage.h"
#include "network/NetProtocol.h"
#include "network/NetStats.h"
#include "ps/CLogger.h"
#include "ps/ProfileViewer.h"
@ -32,43 +32,399 @@
constexpr int NETCLIENT_POLL_TIMEOUT = 50;
constexpr int CHANNEL_COUNT = 1;
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
struct CNetClientSession::Quic
{
AddressStorage localAddress;
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> credentials;
std::unique_ptr<gnutls_session_int, SessionDeleter> session;
std::unique_ptr<ngtcp2_conn, ConnectionDeleter> quicConnection;
ngtcp2_crypto_conn_ref connectionReference;
int fd;
std::optional<Stream> streams;
};
namespace
{
struct CreateSocketResult
{
int descriptor;
AddressStorage address;
};
CreateSocketResult CreateSocket(const char* host, const std::uint16_t port)
{
addrinfo hints{};
hints.ai_flags = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
addrinfo* res;
const int rv{getaddrinfo(host, fmt::format("{}", port).c_str(), &hints, &res)};
if (rv)
throw std::runtime_error{fmt::format("getaddrinfo: {}", gai_strerror(rv))};
std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> infoList{res, &freeaddrinfo};
addrinfo* rp;
int fd{-1};
for (rp = res; rp; rp = rp->ai_next)
{
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd != -1)
break;
}
if (fd == -1)
throw std::runtime_error{"unable to create a socket"};
return CreateSocketResult{
.descriptor{fd},
.address{.address{*rp->ai_addr}, .length{rp->ai_addrlen}}
};
}
AddressStorage ConnectSocket(const int fd, const AddressStorage& remoteAddress)
{
if (connect(fd, &remoteAddress.address.sa, remoteAddress.length))
throw std::runtime_error{fmt::format("connect: {}", strerror(errno))};
ngtcp2_sockaddr_union localAddress;
ngtcp2_socklen localAddressLength{sizeof(localAddress)};
if (getsockname(fd, &localAddress.sa, &localAddressLength) == -1)
throw std::runtime_error{fmt::format("getsockname: {}", strerror(errno))};
return {localAddress, localAddressLength};
}
void ClientGnutlsInit(CNetClientSession::Quic* c)
{
gnutls_certificate_credentials_t tempCred;
const int allocRet{gnutls_certificate_allocate_credentials(&tempCred)};
if (allocRet)
{
throw std::runtime_error{fmt::format("cred init failed: {}: {}", allocRet,
gnutls_strerror(allocRet))};
}
c->credentials.reset(tempCred);
gnutls_session_t tempSession;
if (const int initRet{gnutls_init(&tempSession, GNUTLS_CLIENT | GNUTLS_ENABLE_EARLY_DATA |
GNUTLS_NO_END_OF_EARLY_DATA)})
{
throw std::runtime_error{fmt::format("gnutls_init: {}", gnutls_strerror(initRet))};
}
c->session.reset(tempSession);
if (ngtcp2_crypto_gnutls_configure_client_session(c->session.get()))
throw std::runtime_error{"ngtcp2_crypto_gnutls_configure_client_session failed"};
if (const int priorityRet{gnutls_priority_set_direct(c->session.get(), TLS_PRIORITY, nullptr)})
{
throw std::runtime_error{fmt::format("gnutls_priority_set_direct: {}",
gnutls_strerror(priorityRet))};
}
gnutls_session_set_ptr(c->session.get(), &c->connectionReference);
if (const int setRet{gnutls_credentials_set(c->session.get(), GNUTLS_CRD_CERTIFICATE,
c->credentials.get())})
{
throw std::runtime_error{fmt::format("gnutls_credentials_set: {}", gnutls_strerror(setRet))};
}
}
int OpenStream(ngtcp2_conn*, const std::int64_t streamId, void* userData)
{
CNetClientSession& session{*static_cast<CNetClientSession*>(userData)};
session.m_Connected = true;
session.m_WasConnected = true;
session.m_Quic->streams.emplace(streamId);
session.m_IncomingMessages.push(CNetClientSession::ConnectionEstablished{});
return 0;
}
int OnStreamDataReceive(ngtcp2_conn* conn, const std::uint32_t /*flags*/, const std::int64_t streamId,
const std::size_t /*offset*/, const std::uint8_t* data, std::size_t dataSize, void* userData, void*)
{
auto& session = *static_cast<CNetClientSession*>(userData);
auto message = session.m_Quic->streams.value().Receive({data, dataSize});
if (message.has_value())
session.m_IncomingMessages.push(new std::vector<std::uint8_t>{std::move(message).value()});
increaseWindow(conn, streamId, dataSize);
return 0;
}
void ClientQuicInit(CNetClientSession& session, const AddressStorage& remote, const AddressStorage& local)
{
const ngtcp2_path path{
.local{
.addr{const_cast<ngtcp2_sockaddr*>(&local.address.sa)},
.addrlen{local.length},
},
.remote{
.addr{const_cast<ngtcp2_sockaddr*>(&remote.address.sa)},
.addrlen{remote.length},
}
};
constexpr ngtcp2_callbacks callbacks{
.client_initial{&ngtcp2_crypto_client_initial_cb},
.recv_crypto_data{&ngtcp2_crypto_recv_crypto_data_cb},
.encrypt{&ngtcp2_crypto_encrypt_cb},
.decrypt{&ngtcp2_crypto_decrypt_cb},
.hp_mask{&ngtcp2_crypto_hp_mask_cb},
.recv_stream_data{&OnStreamDataReceive},
.stream_open{&OpenStream},
.recv_retry{&ngtcp2_crypto_recv_retry_cb},
.rand{&OnRandomRequest},
.get_new_connection_id{&OnNewConnectionIdRequest},
.update_key{&ngtcp2_crypto_update_key_cb},
.delete_crypto_aead_ctx{&ngtcp2_crypto_delete_crypto_aead_ctx_cb},
.delete_crypto_cipher_ctx{&ngtcp2_crypto_delete_crypto_cipher_ctx_cb},
.get_path_challenge_data{&ngtcp2_crypto_get_path_challenge_data_cb},
.version_negotiation{&ngtcp2_crypto_version_negotiation_cb}
};
ngtcp2_cid dcid;
dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN;
if (gnutls_rnd(GNUTLS_RND_RANDOM, dcid.data, dcid.datalen))
throw std::runtime_error{"gnutls_rnd failed"};
ngtcp2_cid scid;
scid.datalen = 8;
if (gnutls_rnd(GNUTLS_RND_RANDOM, scid.data, scid.datalen))
throw std::runtime_error{"gnutls_rnd failed"};
ngtcp2_settings settings;
ngtcp2_settings_default(&settings);
settings.initial_ts = timestamp();
ngtcp2_transport_params params;
ngtcp2_transport_params_default(&params);
params.initial_max_stream_data_bidi_remote = 128 * KiB;
params.initial_max_data = 1 * MiB;
params.initial_max_streams_bidi = 1;
params.max_udp_payload_size = MAX_UDP_PAYLOAD_SIZE;
params.grease_quic_bit = 1;
ngtcp2_conn* tempConn;
if (const int rv{ngtcp2_conn_client_new(&tempConn, &dcid, &scid, &path,
NGTCP2_PROTO_VER_V1, &callbacks, &settings, &params, nullptr, &session)})
{
throw std::runtime_error{fmt::format("ngtcp2_conn_client_new: {}", ngtcp2_strerror(rv))};
}
session.m_Quic->quicConnection.reset(tempConn);
ngtcp2_conn_set_tls_native_handle(session.m_Quic->quicConnection.get(),
session.m_Quic->session.get());
}
void ClientRead(CNetClientSession::Quic* c) {
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
struct sockaddr_storage addr;
iovec iov{
.iov_base = buf.data(),
.iov_len = buf.size(),
};
msghdr msg{};
msg.msg_name = &addr;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ngtcp2_pkt_info pi{};
while (true)
{
msg.msg_namelen = sizeof(addr);
const ssize_t nread{recvmsg(c->fd, &msg, MSG_DONTWAIT)};
if (nread == -1)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
LOGERROR("recvmsg: %s", strerror(errno));
break;
}
ngtcp2_path path{
.local{
.addr{&c->localAddress.address.sa},
.addrlen{c->localAddress.length}
},
.remote{
.addr{static_cast<ngtcp2_sockaddr*>(msg.msg_name)},
.addrlen{msg.msg_namelen}
}
};
const int rv{ngtcp2_conn_read_pkt(c->quicConnection.get(), &path, &pi, buf.data(),
static_cast<std::size_t>(nread), timestamp())};
if (rv != 0)
throw std::runtime_error{fmt::format("ngtcp2_conn_read_pkt: {}", ngtcp2_strerror(rv))};
}
}
void ClientSendDatagram(CNetClientSession::Quic* c, const std::span<const std::uint8_t> data)
{
iovec iov{
.iov_base = const_cast<std::uint8_t*>(data.data()),
.iov_len = data.size(),
};
msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t nwrite;
do
{
nwrite = sendmsg(c->fd, &msg, 0);
} while (nwrite == -1 && errno == EINTR);
if (nwrite == -1)
throw std::runtime_error{fmt::format("sendmsg: {}", strerror(errno))};
}
void ClientWriteStreams(CNetClientSession::Quic* c)
{
const ngtcp2_tstamp ts{timestamp()};
ngtcp2_pkt_info pi;
std::array<uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
ngtcp2_path_storage ps;
ngtcp2_path_storage_zero(&ps);
std::uint32_t flags{NGTCP2_WRITE_STREAM_FLAG_MORE};
while (true)
{
ngtcp2_vec datav;
std::int64_t streamId;
const auto bytesToSend = c->streams.has_value() ? c->streams.value().PeekData() : std::nullopt;
if (c->streams.has_value() && bytesToSend.has_value())
{
datav.base = const_cast<uint8_t*>(bytesToSend->data());
datav.len = bytesToSend->size();
streamId = c->streams.value().m_Id;
}
else
{
datav.base = nullptr;
datav.len = 0;
streamId = -1;
if (c->streams.has_value())
flags &= ~NGTCP2_WRITE_STREAM_FLAG_MORE;
}
ngtcp2_ssize wdatalen;
const ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream(c->quicConnection.get(), &ps.path, &pi,
buffer.data(), buffer.size(), &wdatalen, flags, streamId, &datav, 1, ts);
if (nwrite < 0)
{
if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED)
{
LOGWARNING("blocked");
break;
}
if (nwrite != NGTCP2_ERR_WRITE_MORE)
{
throw std::runtime_error{fmt::format("ngtcp2_conn_writev_stream: {}",
ngtcp2_strerror(static_cast<int>(nwrite)))};
}
if (c->streams.has_value() && wdatalen > 0)
c->streams.value().MarkSent(wdatalen);
continue;
}
if (nwrite == 0)
break;
if (c->streams.has_value() && wdatalen > 0)
c->streams.value().MarkSent(wdatalen);
ClientSendDatagram(c, {buffer.data(), static_cast<std::size_t>(nwrite)});
}
ngtcp2_conn_update_pkt_tx_time(c->quicConnection.get(), timestamp());
}
void ClientHandleExpiry(CNetClientSession::Quic* c)
{
if (const int rv = ngtcp2_conn_handle_expiry(c->quicConnection.get(), timestamp()))
throw std::runtime_error{fmt::format("ngtcp2_conn_handle_expiry: {}", ngtcp2_strerror(rv))};
}
ngtcp2_conn *GetConnection(ngtcp2_crypto_conn_ref* conn_ref)
{
CNetClientSession::Quic* c{static_cast<CNetClientSession::Quic*>(conn_ref->user_data)};
return c->quicConnection.get();
}
void ClientInit(CNetClientSession& session, const char* host, const std::uint16_t port)
{
*session.m_Quic = CNetClientSession::Quic{};
const auto [descriptor, remoteAddress] = CreateSocket(host, port);
session.m_Quic->fd = descriptor;
session.m_Quic->localAddress = ConnectSocket(session.m_Quic->fd, remoteAddress);
ClientGnutlsInit(session.m_Quic.get());
ClientQuicInit(session, remoteAddress, session.m_Quic->localAddress);
session.m_Quic->connectionReference.get_conn = &GetConnection;
session.m_Quic->connectionReference.user_data = session.m_Quic.get();
}
} // anonymous namespace
CNetClientSession::CNetClientSession(CNetClient& client) :
m_Client(client), m_FileTransferer(*this)
m_Client(client), m_FileTransferer(*this),
m_Quic(std::make_unique<Quic>())
{
}
CNetClientSession::~CNetClientSession()
{
ENSURE(!m_LoopRunning);
constexpr ngtcp2_ccerr reason{
.type{NGTCP2_CCERR_TYPE_TRANSPORT},
.error_code{0}
};
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
const ngtcp2_ssize amount{ngtcp2_conn_write_connection_close(m_Quic->quicConnection.get(), nullptr, nullptr, buffer.data(),
buffer.size(), &reason, timestamp())};
if (amount <= 0)
{
LOGERROR("closing connection %s", ngtcp2_strerror(amount));
}
ClientSendDatagram(m_Quic.get(), {buffer.data(), static_cast<std::size_t>(amount)});
}
bool CNetClientSession::Connect(const CStr& server, const u16 port, ENetHost* enetClient)
bool CNetClientSession::Connect(const CStr& server, const u16 port)
{
ENSURE(!m_LoopRunning);
ENSURE(!m_Host);
ENSURE(!m_Server);
// Create ENet host if necessary.
m_Host.reset(enetClient != nullptr ? enetClient : PS::Enet::CreateHost(nullptr, 1, CHANNEL_COUNT));
ClientInit(*this, server.c_str(), port);
ClientWriteStreams(m_Quic.get());
if (!m_Host)
return false;
// Bind to specified host
ENetAddress addr;
addr.port = port;
if (enet_address_set_host(&addr, server.c_str()) < 0)
return false;
// Initiate connection to server
m_Server.reset(enet_host_connect(m_Host.get(), &addr, CHANNEL_COUNT, 0));
if (!m_Server)
return false;
m_Stats = std::make_unique<CNetStatsTable>(*m_Server);
m_Stats = std::make_unique<CNetStatsTable>(m_Quic->quicConnection.get());
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats.get());
@ -84,14 +440,24 @@ void CNetClientSession::RunNetLoop(CNetClientSession* session)
while (!session->m_ShouldShutdown)
{
ENSURE(session->m_Host && session->m_Server);
// ENSURE(session->m_Host && session->m_Server);
session->m_FileTransferer.Poll();
session->Poll();
try {
session->Poll();
}
catch (std::runtime_error&)
{
// Report immediately.
LOGMESSAGE("Net client: Disconnected");
session->m_Connected = false;
session->m_IncomingMessages.push(Disconnect{});
return;
}
session->Flush();
session->m_LastReceivedTime = enet_time_get() - session->m_Server->lastReceiveTime;
session->m_MeanRTT = session->m_Server->roundTripTime;
// session->m_LastReceivedTime = timestamp() - session->m_Server->lastReceiveTime;
// session->m_MeanRTT = session->m_Server->roundTripTime;
}
session->m_LoopRunning = false;
@ -107,96 +473,77 @@ void CNetClientSession::Shutdown()
void CNetClientSession::Poll()
{
ENetEvent event;
pollfd pfd{
.fd{m_Quic->fd},
.events{POLLIN}
};
// Use the timeout to make the thread wait and save CPU time.
if (enet_host_service(m_Host.get(), &event, NETCLIENT_POLL_TIMEOUT) <= 0)
const int ret{poll(&pfd, 1, NETCLIENT_POLL_TIMEOUT)};
if (ret < 0)
{
LOGERROR("Error while waiting for poll: %s", std::strerror(errno));
return;
if (event.type == ENET_EVENT_TYPE_CONNECT)
{
ENSURE(event.peer == m_Server.get());
// Report the server address immediately.
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port);
m_Connected = true;
m_WasConnected = true;
m_IncomingMessages.push(event);
}
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
if (ret == 0)
{
ENSURE(event.peer == m_Server.get());
// Report immediately.
LOGMESSAGE("Net client: Disconnected");
m_Connected = false;
m_IncomingMessages.push(event);
ClientHandleExpiry(m_Quic.get());
ClientWriteStreams(m_Quic.get());
return;
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
m_IncomingMessages.push(event);
ClientRead(m_Quic.get());
ClientWriteStreams(m_Quic.get());
}
void CNetClientSession::Flush()
{
ENetPacket* packet;
while (m_OutgoingMessages.pop(packet))
if (enet_peer_send(m_Server.get(), CNetHost::DEFAULT_CHANNEL, packet) < 0)
{
// Report the error, but do so silently if we know we are disconnected.
if (m_Connected)
LOGERROR("NetClient: Failed to send packet to server");
else
LOGMESSAGE("NetClient: Failed to send packet to server");
}
enet_host_flush(m_Host.get());
std::vector<std::uint8_t>* message;
while (m_OutgoingMessages.pop(message))
{
std::unique_ptr<std::vector<std::uint8_t>> data{message};
if (m_Quic->streams.has_value())
m_Quic->streams.value().PushData(std::move(*data));
else
LOGERROR("no stream to send message");
}
}
void CNetClientSession::ProcessPolledMessages()
{
ENetEvent event;
while(m_IncomingMessages.pop(event))
IncommingMessage query{};
while(m_IncomingMessages.pop(query))
{
if (event.type == ENET_EVENT_TYPE_CONNECT)
m_Client.HandleConnect();
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
std::visit([&]<typename Message>(Message message)
{
// This deletes the session, so we must break;
if (event.data == 0 && !m_WasConnected)
m_Client.HandleDisconnect(NDR_CONNECTION_REQUEST_TIMED_OUT);
else
m_Client.HandleDisconnect(event.data);
break;
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
{
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, m_Client.GetScriptInterface());
if (msg)
if constexpr (std::same_as<Message, ConnectionEstablished>)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
m_Client.HandleConnect();
}
// Thread-safe
enet_packet_destroy(event.packet);
}
else if constexpr (std::same_as<Message, Disconnect>)
{
m_Client.HandleDisconnect(NDR_UNKNOWN);
}
else
{
static_assert(std::same_as<Message, std::vector<std::uint8_t>*>);
std::unique_ptr<std::vector<std::uint8_t>> data{message};
CNetMessage* msg = CNetMessageFactory::CreateMessage(*data, m_Client.GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
}
}
}, query);
}
}
bool CNetClientSession::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host && m_Server);
// ENSURE(m_Host && m_Server);
// Thread-safe.
ENetPacket* packet = CNetHost::CreatePacket(message);
if (!packet)
return false;
if (!m_OutgoingMessages.push(packet))
if (!m_OutgoingMessages.push(new std::vector{CNetHost::CreatePacket(message)}))
{
LOGERROR("NetClient: Failed to push message on the outgoing queue.");
return false;
@ -207,17 +554,11 @@ bool CNetClientSession::SendMessage(const CNetMessage* message)
u32 CNetClientSession::GetLastReceivedTime() const
{
if (!m_Server)
return 0;
return m_LastReceivedTime;
}
u32 CNetClientSession::GetMeanRTT() const
{
if (!m_Server)
return 0;
return m_MeanRTT;
}

View file

@ -19,7 +19,6 @@
#define NETSESSION_H
#include "lib/code_annotation.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
@ -32,8 +31,6 @@ class CNetMessage;
class CNetStatsTable;
class CStr;
typedef struct _ENetHost ENetHost;
/**
* @file
* Network client/server sessions.
@ -53,10 +50,11 @@ class CNetClientSession
NONCOPYABLE(CNetClientSession);
public:
struct Quic;
CNetClientSession(CNetClient& client);
~CNetClientSession();
bool Connect(const CStr& server, const u16 port, ENetHost* enetClient);
bool Connect(const CStr& server, const u16 port);
/**
* The client NetSession is threaded to avoid getting timeouts if the main thread hangs.
@ -104,11 +102,14 @@ private:
CNetClient& m_Client;
CNetFileTransferer m_FileTransferer;
public:
// Net messages received and waiting for fetching.
boost::lockfree::queue<ENetEvent> m_IncomingMessages{16};
struct ConnectionEstablished{};
struct Disconnect{};
using IncommingMessage = std::variant<ConnectionEstablished, std::vector<std::uint8_t>*, Disconnect>;
boost::lockfree::queue<IncommingMessage> m_IncomingMessages{16};
// Net messages to send on the next flush() call.
boost::lockfree::queue<ENetPacket*> m_OutgoingMessages{16};
boost::lockfree::queue<std::vector<std::uint8_t>*> m_OutgoingMessages{16};
// Last known state. If false, flushing errors are silenced.
bool m_Connected{false};
@ -116,7 +117,7 @@ private:
// Whether this session was ever connected to the server.
bool m_WasConnected{false};
// Wrapper around enet stats - those are atomic as the code is lock-free.
// Wrapper around stats - those are atomic as the code is lock-free.
std::atomic<u32> m_LastReceivedTime{0};
std::atomic<u32> m_MeanRTT{0};
@ -124,9 +125,9 @@ private:
std::atomic<bool> m_LoopRunning{false};
std::atomic<bool> m_ShouldShutdown{false};
std::unique_ptr<ENetHost, DestroyHost> m_Host;
std::unique_ptr<ENetPeer, DestroyPeer> m_Server;
std::unique_ptr<CNetStatsTable> m_Stats;
const std::unique_ptr<Quic> m_Quic;
};
#endif // NETSESSION_H

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -25,32 +25,10 @@
#include "ps/CLogger.h"
#include <cstddef>
#include <numeric>
#include <vector>
bool CNetHost::SendMessage(const CNetMessage* message, ENetPeer* peer, const char* peerName)
{
ENetPacket* packet = CreatePacket(message);
if (!packet)
return false;
LOGMESSAGE("Net: Sending message %s of size %lu to %s", message->ToString().c_str(), (unsigned long)packet->dataLength, peerName);
// Let ENet send the message to peer
if (enet_peer_send(peer, DEFAULT_CHANNEL, packet) < 0)
{
LOGERROR("Net: Failed to send packet to peer");
return false;
}
// Don't call enet_host_flush now - let it queue up all the packets
// and send them during the next frame
//
// TODO: we should flush explicitly at some appropriate point before the next frame
return true;
}
ENetPacket* CNetHost::CreatePacket(const CNetMessage* message)
std::vector<std::uint8_t> CNetHost::CreatePacket(const CNetMessage* message)
{
size_t size = message->GetSerializedLength();
@ -63,12 +41,7 @@ ENetPacket* CNetHost::CreatePacket(const CNetMessage* message)
// Save message to internal buffer
message->Serialize(&buffer[0]);
// Create a reliable packet
ENetPacket* packet = enet_packet_create(&buffer[0], size, ENET_PACKET_FLAG_RELIABLE);
if (!packet)
LOGERROR("Net: Failed to construct packet");
return packet;
return buffer;
}
void CNetHost::Initialize()
@ -81,3 +54,81 @@ void CNetHost::Deinitialize()
{
enet_deinitialize();
}
Stream::Stream(const std::int64_t streamId):
m_Id{streamId}
{}
void Stream::PushData(std::vector<std::uint8_t> data)
{
m_SendBuffer.push_back(std::move(data));
}
void Stream::PushMessage(const CNetMessage* message)
{
m_SendBuffer.push_back(CNetHost::CreatePacket(message));
}
std::optional<std::span<const std::uint8_t>> Stream::PeekData()
{
const std::size_t startOffset{m_SentOffset - m_AckedOffset};
std::size_t offset{0};
for (std::vector<std::uint8_t>& bytes : m_SendBuffer)
{
if (startOffset - offset < bytes.size())
{
const std::size_t temp{startOffset - offset};
return std::span{bytes.data() + temp, bytes.size() - temp};
}
offset += bytes.size();
}
return std::nullopt;
}
void Stream::MarkSent(const std::size_t offset)
{
m_SentOffset += offset;
}
void Stream::MarkAcknowledged(const std::size_t offset)
{
while (!m_SendBuffer.empty())
{
std::vector<uint8_t>& head{m_SendBuffer.front()};
if (m_AckedOffset + head.size() > offset)
break;
m_AckedOffset += head.size();
m_SendBuffer.pop_front();
}
}
std::optional<std::vector<std::uint8_t>> Stream::Receive(const std::span<const std::uint8_t> data)
{
m_ReceiveBuffer.emplace_back(data.begin(), data.end());
const std::size_t bufferSize{std::transform_reduce(m_ReceiveBuffer.begin(), m_ReceiveBuffer.end(),
static_cast<std::size_t>(0), std::plus<>{}, std::mem_fn(&std::vector<std::uint8_t>::size))};
if (bufferSize < 3)
return std::nullopt;
const auto& message = m_ReceiveBuffer.front();
auto bufferIter = message.begin();
std::size_t messageSize;
Deserialize_int_1(bufferIter, std::ignore);
Deserialize_int_2(bufferIter, messageSize);
if (messageSize > bufferSize)
return std::nullopt;
std::vector<std::uint8_t> messageCopy;
while (messageCopy.size() < messageSize)
{
messageCopy.insert(messageCopy.end(), m_ReceiveBuffer.front().begin(),
m_ReceiveBuffer.front().end());
m_ReceiveBuffer.pop_front();
}
return messageCopy;
}

View file

@ -27,7 +27,14 @@
#include "lib/types.h"
#include "ps/CStr.h"
#include <deque>
#include <map>
#include <ngtcp2/ngtcp2.h>
#include <optional>
#include <span>
#include <vector>
#include <gnutls/gnutls.h>
class CNetMessage;
@ -35,6 +42,38 @@ typedef struct _ENetPeer ENetPeer;
typedef struct _ENetPacket ENetPacket;
typedef struct _ENetHost ENetHost;
constexpr std::size_t MAX_UDP_PAYLOAD_SIZE{64 * KiB};
struct CredentialsDeleter
{
void operator()(const gnutls_certificate_credentials_t cred) const
{
gnutls_certificate_free_credentials(cred);
}
};
struct SessionDeleter
{
void operator()(const gnutls_session_t p) const
{
gnutls_deinit(p);
}
};
struct ConnectionDeleter
{
void operator()(ngtcp2_conn* p) const
{
ngtcp2_conn_del(p);
}
};
struct AddressStorage
{
ngtcp2_sockaddr_union address;
ngtcp2_socklen length{sizeof(address)};
};
struct PlayerAssignment
{
/**
@ -106,20 +145,11 @@ class CNetHost
public:
static const int DEFAULT_CHANNEL = 0;
/**
* Transmit a message to the given peer.
* @param message message to send
* @param peer peer to send to
* @param peerName name of peer for debug logs
* @return true on success, false on failure
*/
static bool SendMessage(const CNetMessage* message, ENetPeer* peer, const char* peerName);
/**
* Construct an ENet packet by serialising the given message.
* @return NULL on failure
*/
static ENetPacket* CreatePacket(const CNetMessage* message);
static std::vector<std::uint8_t> CreatePacket(const CNetMessage* message);
/**
* Initialize ENet.
@ -133,4 +163,25 @@ public:
static void Deinitialize();
};
class Stream
{
public:
Stream(const std::int64_t streamId);
void PushData(std::vector<std::uint8_t> data);
void PushMessage(const CNetMessage* message);
std::optional<std::span<const std::uint8_t>> PeekData();
void MarkSent(const std::size_t offset);
void MarkAcknowledged(const std::size_t offset);
std::optional<std::vector<std::uint8_t>> Receive(const std::span<const std::uint8_t> data);
std::int64_t m_Id;
private:
std::deque<std::vector<std::uint8_t>> m_SendBuffer;
std::deque<std::vector<std::uint8_t>> m_ReceiveBuffer;
/* invariant: m_SentOffset >= m_AckedOffset */
std::size_t m_SentOffset{0};
std::size_t m_AckedOffset{0};
};
#endif // NETHOST_H

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -53,10 +53,7 @@ u8* CNetMessage::Serialize(u8* pBuffer) const
const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
{
if (pStart + 3 > pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (smaller than header)");
return NULL;
}
throw std::invalid_argument{"CNetMessage: Corrupt packet (smaller than header)"};
const u8* pBuffer = pStart;
@ -67,10 +64,7 @@ const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
m_Type = (NetMessageType)type;
if (pStart + size != pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (incorrect size)");
return NULL;
}
throw std::invalid_argument{fmt::format("CNetMessage: Corrupt packet (incorrect size) %i %i", size, pEnd - pStart)};
return pBuffer;
}
@ -91,15 +85,14 @@ CStr CNetMessage::ToString() const
return "Unknown Message " + CStr::FromInt(GetType());
}
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
size_t dataSize,
const ScriptInterface& scriptInterface)
CNetMessage* CNetMessageFactory::CreateMessage(const std::span<const std::uint8_t> data,
const ScriptInterface& scriptInterface)
{
CNetMessage* pNewMessage = NULL;
CNetMessage header;
// Figure out message type
header.Deserialize((const u8*)pData, (const u8*)pData + dataSize);
header.Deserialize(std::to_address(data.begin()), std::to_address(data.end()));
switch (header.GetType())
{
@ -230,7 +223,7 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
}
if (pNewMessage)
pNewMessage->Deserialize((const u8*)pData, (const u8*)pData + dataSize);
pNewMessage->Deserialize(std::to_address(data.begin()), std::to_address(data.end()));
return pNewMessage;
}

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -27,6 +27,7 @@
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
#include <span>
class ScriptInterface;
@ -108,12 +109,12 @@ public:
/**
* Factory method which creates a message object based on the given data
*
* @param pData Data buffer
* @param dataSize Size of data buffer
* @param data Data buffer
* @param scriptInterface Script instance to use when constructing scripted messages
* @return The new message created
*/
static CNetMessage* CreateMessage(const void* pData, size_t dataSize, const ScriptInterface& scriptInterface);
static CNetMessage* CreateMessage(const std::span<const std::uint8_t> data,
const ScriptInterface& scriptInterface);
};
/**

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -35,6 +35,7 @@
#include <js/Value.h>
#include <sstream>
#include <string>
#include "ps/CLogger.h"
class ScriptInterface;

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -19,10 +19,14 @@
#include "NetProtocol.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <ngtcp2/ngtcp2.h>
#include <gnutls/crypto.h>
namespace
{
@ -59,3 +63,36 @@ std::optional<HandshakeError> CheckHandshake(const CSrvHandshakeMessage& serverM
return {};
}
uint64_t timestamp()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
void increaseWindow(ngtcp2_conn* conn, const std::uint64_t streamId, const std::uint64_t size)
{
ngtcp2_conn_extend_max_offset(conn, size);
ngtcp2_conn_extend_max_stream_offset(conn, streamId, size);
}
void OnRandomRequest(std::uint8_t* destination, const std::size_t destinationLength, const ngtcp2_rand_ctx*)
{
const int ret{gnutls_rnd(GNUTLS_RND_RANDOM, destination, destinationLength)};
if (ret < 0)
LOGERROR("gnutls_rnd: %s", gnutls_strerror(ret));
}
int OnNewConnectionIdRequest(ngtcp2_conn*, ngtcp2_cid* cid, std::uint8_t* token, const std::size_t cidlen,
void* /*userData*/)
{
if (gnutls_rnd(GNUTLS_RND_RANDOM, cid->data, cidlen))
return NGTCP2_ERR_CALLBACK_FAILURE;
cid->datalen = cidlen;
if (gnutls_rnd (GNUTLS_RND_RANDOM, token, NGTCP2_STATELESS_RESET_TOKENLEN))
return NGTCP2_ERR_CALLBACK_FAILURE;
return 0;
}

View file

@ -27,11 +27,17 @@
#include <type_traits>
#include <vector>
#include <gnutls/gnutls.h>
/**
* Report the peer if we didn't receive a packet after this time (milliseconds).
*/
inline constexpr u32 NETWORK_WARNING_TIMEOUT{2000};
inline constexpr const char* TLS_PRIORITY{
"PERFORMANCE:-VERS-DTLS-ALL:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2:-SHA1:-AES-128-CBC:-AES-256-CBC:"
"-SIGN-RSA-SHA1:-SIGN-ECDSA-SHA1:%DISABLE_TLS13_COMPAT_MODE"};
struct HandshakeError
{
std::string componentType;
@ -67,4 +73,17 @@ Message CreateHandshake() {
std::optional<HandshakeError> CheckHandshake(const CSrvHandshakeMessage& serverMessage, const CCliHandshakeMessage& clientMessage);
uint64_t timestamp();
struct ngtcp2_conn;
void increaseWindow(ngtcp2_conn* conn, const std::uint64_t streamId, const std::uint64_t size);
struct ngtcp2_rand_ctx;
void OnRandomRequest(std::uint8_t* destination, const std::size_t destinationLength,
const ngtcp2_rand_ctx*);
struct ngtcp2_cid;
int OnNewConnectionIdRequest(ngtcp2_conn*, ngtcp2_cid* cid, std::uint8_t *token, const std::size_t cidlen,
void* /*userData*/);
#endif

View file

@ -22,6 +22,7 @@
#include "lib/code_generation.h"
#include "lib/debug.h"
#include "lib/external_libraries/enet.h"
#include "lib/hash.h"
#include "lib/secure_crt.h"
#include "lib/status.h"
#include "lib/types.h"
@ -54,8 +55,17 @@
#include <algorithm>
#include <cstring>
#include <functional>
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
#include <iterator>
#include <memory>
#include <netdb.h>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <numeric>
#include <poll.h>
#include <ranges>
#include <set>
#include <sstream>
#include <string>
@ -97,15 +107,256 @@ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
namespace
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
// static CStr DebugName(CNetServerSession* session)
// {
// if (session == NULL)
// return "[unknown host]";
// if (session->GetGUID().empty())
// return "[unauthed host]";
// return "[" + session->GetGUID().substr(0, 8) + "...]";
// }
struct CidHash
{
std::size_t operator()(const ngtcp2_cid& cid) const
{
return std::accumulate(cid.data, cid.data + cid.datalen, static_cast<std::size_t>(0),
[](std::size_t carry, const std::uint8_t byte)
{
hash_combine(carry, byte);
return carry;
});
}
};
struct CidEqual
{
bool operator()(const ngtcp2_cid& a, const ngtcp2_cid& b) const
{
return ngtcp2_cid_eq(&a, &b);
}
};
std::size_t ReceivePackage(const int fd, std::span<std::uint8_t> data, AddressStorage& remoteAddress)
{
iovec iov{
.iov_base{data.data()},
.iov_len{data.size()}
};
msghdr msg{};
msg.msg_name = &remoteAddress.address.sa;
msg.msg_namelen = remoteAddress.length;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t ret;
do
ret = recvmsg(fd, &msg, MSG_DONTWAIT);
while (ret < 0 && errno == EINTR);
if (ret < 0)
throw std::system_error{errno, std::generic_category(), "recvmsg"};
remoteAddress.length = msg.msg_namelen;
return ret;
}
void GetRandomCid(ngtcp2_cid* cid)
{
std::array<std::uint8_t, NGTCP2_MAX_CIDLEN> buf;
const int ret{gnutls_rnd(GNUTLS_RND_RANDOM, buf.data(), buf.size())};
if (ret < 0)
throw std::runtime_error{fmt::format("gnutls_rnd: {}", gnutls_strerror(ret))};
ngtcp2_cid_init(cid, buf.data(), buf.size());
}
int ResolveAndBind(const char *port, AddressStorage& localAddress)
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
addrinfo* result;
if (getaddrinfo("127.0.0.1", port, &hints, &result))
return -1;
int fd;
addrinfo* rp;
for (rp = result; rp; rp = rp->ai_next)
{
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd == -1)
continue;
if (bind(fd, rp->ai_addr, rp->ai_addrlen))
{
close(fd);
continue;
}
break;
}
freeaddrinfo(result);
if (!rp)
return -1;
memcpy(&localAddress.address, rp->ai_addr, rp->ai_addrlen);
localAddress.length = rp->ai_addrlen;
return fd;
}
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> CreateTlsServerCredentials()
{
gnutls_certificate_credentials_t temp;
if (const int ret{gnutls_certificate_allocate_credentials(&temp)})
{
throw std::runtime_error{fmt::format("gnutls_certificate_allocate_credentials: {}",
gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> cred{temp};
if (const int ret{gnutls_certificate_set_x509_key_file(cred.get(), "certificate.pem",
"privatekey.pem", GNUTLS_X509_FMT_PEM)})
{
throw std::runtime_error{fmt::format("gnutls_certificate_set_x509_key_file: {}",
gnutls_strerror(ret))};
}
return cred;
}
ngtcp2_settings InitSettings()
{
ngtcp2_settings settings;
ngtcp2_settings_default(&settings);
settings.initial_ts = timestamp();
return settings;
}
} // anonymous namespace
class CNetServerWorker::Quic
{
public:
explicit Quic(const std::uint16_t port):
m_SocketFd{ResolveAndBind(std::to_string(port).c_str(), m_LocalAddress)}
{}
~Quic()
{
if (m_SocketFd >= 0)
close(m_SocketFd);
}
AddressStorage m_LocalAddress;
int m_SocketFd;
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> m_Credentials{
CreateTlsServerCredentials()};
ngtcp2_settings m_Settings{InitSettings()};
void HandleIncoming(CNetServerWorker& server)
{
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
while (true)
{
AddressStorage remoteAddress;
std::size_t n_read;
try
{
n_read = ReceivePackage(m_SocketFd, buf, remoteAddress);
}
catch (std::system_error& e)
{
if (e.code().value() == EAGAIN || e.code().value() == EWOULDBLOCK)
return;
throw;
}
ngtcp2_version_cid version;
if (const int ret{ngtcp2_pkt_decode_version_cid(&version, buf.data(), n_read,
NGTCP2_MAX_CIDLEN)})
{
throw std::runtime_error{fmt::format("ngtcp2_pkt_decode_version_cid: {}",
ngtcp2_strerror(ret))};
}
const ngtcp2_addr remote{
.addr{&remoteAddress.address.sa},
.addrlen{remoteAddress.length}
};
/* Find any existing connection by DCID */
ngtcp2_cid tempCid;
ngtcp2_cid_init(&tempCid, version.dcid, version.dcidlen);
const auto connectionIter = std::ranges::find_if(server.m_Sessions,
[&tempCid](const std::vector<ngtcp2_cid> association)
{
return std::ranges::any_of(association, [&tempCid](const ngtcp2_cid& elem)
{
return ngtcp2_cid_eq(&elem, &tempCid);
});
},
[](auto& connection)
{
const std::size_t amount{ngtcp2_conn_get_scid(
connection->m_Connection.m_QuicConnection.get(), nullptr)};
std::vector<ngtcp2_cid> cids(amount);
ngtcp2_conn_get_scid(connection->m_Connection.m_QuicConnection.get(),
cids.data());
return cids;
});
const bool existing{connectionIter != server.m_Sessions.end()};
if (!existing)
{
ngtcp2_pkt_hd header;
if (ngtcp2_accept(&header, buf.data(), n_read))
throw std::invalid_argument{"Failed parsing package header"};
ngtcp2_cid newScid;
GetRandomCid(&newScid);
const ngtcp2_path path{
.local{
.addr{&m_LocalAddress.address.sa},
.addrlen{m_LocalAddress.length}
},
.remote{remote}
};
server.m_Sessions.push_back(std::make_unique<CNetServerSession>(server, m_SocketFd,
m_Settings, m_Credentials.get(), header, newScid, path));
server.SetupSession(server.m_Sessions.back().get());
}
auto& session{existing ? *connectionIter : server.m_Sessions.back()};
try
{
session->m_Connection.Read(remote, {buf.data(), n_read});
}
catch (const std::system_error&)
{
throw;
}
catch (const std::runtime_error&)
{
const auto session = existing ? std::move(*connectionIter) :
std::move(server.m_Sessions.back());
if (existing)
server.m_Sessions.erase(connectionIter);
else
server.m_Sessions.pop_back();
session->Update(static_cast<uint>(NMT_CONNECTION_LOST), nullptr);
}
}
}
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See https://gitea.wildfiregames.com/0ad/0ad/issues/654
@ -120,17 +371,17 @@ CNetServerWorker::CNetServerWorker(const bool continueSavedGame, std::uint16_t p
m_Password{std::move(password)}
{
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// ENetAddress addr;
// addr.host = ENET_HOST_ANY;
// addr.port = port;
// Create ENet server
m_Host.reset(PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT));
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
throw std::runtime_error{"Failed to start server"};
}
// m_Host.reset(PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT));
// if (!m_Host)
// {
// LOGERROR("Net server: enet_host_create failed");
// throw std::runtime_error{"Failed to start server"};
// }
m_Stats = std::make_unique<CNetStatsTable>();
if (CProfileViewer::IsInitialised())
@ -140,7 +391,7 @@ CNetServerWorker::CNetServerWorker(const bool continueSavedGame, std::uint16_t p
// Launch the worker thread
m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this,
std::move(initAttributes));
std::move(initAttributes), port);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
@ -163,10 +414,6 @@ CNetServerWorker::~CNetServerWorker()
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
for (const auto& session : m_Sessions)
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
}
@ -322,21 +569,17 @@ void CNetServerWorker::SetupUPnP(const u16 port)
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
bool CNetServerWorker::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host);
m_Sessions.front()->SendMessage(message);
CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
return true;
}
bool CNetServerWorker::Multicast(const CNetMessage* message,
const std::vector<NetServerSessionState>& targetStates,
const std::optional<std::vector<std::string>>& receivers /* = std::nullopt */)
{
ENSURE(m_Host);
const auto isReceiver = [&](const CNetServerSession& session)
{
if (!PS::contains(targetStates,
@ -361,14 +604,14 @@ bool CNetServerWorker::Multicast(const CNetMessage* message,
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data, const std::string& initAttributes)
void CNetServerWorker::RunThread(CNetServerWorker* data, const std::string& initAttributes, u16 port)
{
debug_SetThreadName("NetServer");
data->Run(initAttributes);
data->Run(initAttributes, port);
}
void CNetServerWorker::Run(const std::string& initAttributes)
void CNetServerWorker::Run(const std::string& initAttributes, u16 port)
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
@ -386,22 +629,27 @@ void CNetServerWorker::Run(const std::string& initAttributes)
m_InitAttributes = gameAttributesVal;
}
Quic quic{port};
while (true)
{
if (!RunStep())
if (!RunStep(quic))
break;
// Update profiler stats
m_Stats->LatchHostState(*m_Host);
m_Stats->LatchHostState(m_Sessions);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
for (const auto& session : m_Sessions)
session->Disconnect(NDR_SERVER_SHUTDOWN);
}
bool CNetServerWorker::RunStep()
bool CNetServerWorker::RunStep(Quic& quic)
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
@ -442,103 +690,40 @@ bool CNetServerWorker::RunStep()
CheckClientConnections();
// Process network events:
pollfd pollFd{
.fd{quic.m_SocketFd},
.events{EPOLLIN | EPOLLOUT}
};
const int ready{poll(&pollFd, 1, 25)};
ENetEvent event;
int status = enet_host_service(m_Host.get(), &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
if (ready < 0)
throw std::runtime_error{fmt::format("epoll_wait: {}", std::strerror(errno))};
if (ready == 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
const std::unique_ptr<CNetServerSession>& session{m_Sessions.emplace_back(
std::make_unique<CNetServerSession>(*this, event.peer))};
SetupSession(session.get());
ENSURE(event.peer->data == NULL);
event.peer->data = session.get();
HandleConnect(session.get());
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
for (auto& session : m_Sessions)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
const auto iter = std::ranges::find(m_Sessions, session,
&std::unique_ptr<CNetServerSession>::get);
const std::unique_ptr<CNetServerSession> _ = std::move(*iter);
m_Sessions.erase(iter);
session->Update((uint)NMT_CONNECTION_LOST, NULL);
event.peer->data = NULL;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
ngtcp2_conn *conn = session->m_Connection.m_QuicConnection.get();
const int ret{ngtcp2_conn_handle_expiry(conn, timestamp())};
if (ret < 0)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
LOGERROR("ngtcp2_conn_handle_expiry: %s", ngtcp2_strerror(ret));
continue;
}
session->m_Connection.Write(quic.m_SocketFd);
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
else
{
if (pollFd.revents & EPOLLIN)
quic.HandleIncoming(*this);
case ENET_EVENT_TYPE_NONE:
break;
if (pollFd.revents & EPOLLOUT)
{
for (auto& session : m_Sessions)
session->m_Connection.Write(quic.m_SocketFd);
}
}
return true;
@ -1644,10 +1829,8 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
void CNetServerWorker::SendHolePunchingMessage(const CStr& /*ipStr*/, u16 /*port*/)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
@ -1665,9 +1848,9 @@ CNetServer::CNetServer(const bool continueSavedGame, std::uint16_t port, const b
// In lobby, we send our public ip and port on request to the players who want to connect.
// Thus we need to know our public IP and use STUN to get it.
std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
if (!m_Worker.m_Host || !StunClient::FindPublicIP(*m_Worker.m_Host, m_PublicIp, m_PublicPort))
throw std::runtime_error{"Failed to resolve public IP-address."};
// std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
// if (!m_Worker.m_Host || !StunClient::FindPublicIP(*m_Worker.m_Host, m_PublicIp, m_PublicPort))
// throw std::runtime_error{"Failed to resolve public IP-address."};
}
bool CNetServer::UseLobbyAuth() const
@ -1688,9 +1871,7 @@ u16 CNetServer::GetPublicPort() const
u16 CNetServer::GetLocalPort() const
{
std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
if (!m_Worker.m_Host)
return 0;
return m_Worker.m_Host->address.port;
return 0; // m_Worker.m_Host->address.port;
}
bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)

View file

@ -119,7 +119,7 @@ public:
/**
* Send a message to the given network peer.
*/
bool SendMessage(ENetPeer* peer, const CNetMessage* message);
bool SendMessage(const CNetMessage* message);
/**
* Disconnects a player from gamesetup or session.
@ -173,10 +173,12 @@ private:
*/
CStrW DeduplicatePlayerName(const CStrW& original);
public:
/**
* Get the script context used for init attributes.
*/
const ScriptInterface& GetScriptInterface();
private:
/**
* Set the turn length to a fixed value.
@ -227,7 +229,9 @@ private:
void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message);
public:
void HandleMessageReceive(CNetMessage* message, CNetServerSession* session);
public:
/**
* Send a network warning if the connection to a client is being lost or has bad latency.
@ -263,7 +267,8 @@ private:
*/
const bool m_LobbyAuth;
std::unique_ptr<ENetHost, DestroyHost> m_Host;
class Quic;
std::vector<std::unique_ptr<CNetServerSession>> m_Sessions;
std::unique_ptr<CNetStatsTable> m_Stats;
@ -330,9 +335,9 @@ private:
std::thread m_UPnPThread;
#endif
static void RunThread(CNetServerWorker* data, const std::string& initAttributes);
void Run(const std::string& initAttributes);
bool RunStep();
static void RunThread(CNetServerWorker* data, const std::string& initAttributes, u16 port);
void Run(const std::string& initAttributes, u16 port);
bool RunStep(Quic& quic);
std::thread m_WorkerThread;
mutable std::mutex m_WorkerMutex;

View file

@ -25,8 +25,312 @@
#include "network/NetServer.h"
#include "ps/CLogger.h"
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_FileTransferer(*this), m_Peer(peer)
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
namespace
{
void SendPacket(const int socketFd, const std::span<const std::uint8_t> data, const ngtcp2_addr remote)
{
iovec iov{
.iov_base{const_cast<std::uint8_t*>(data.data())},
.iov_len{data.size()}
};
msghdr msg{};
msg.msg_name = remote.addr;
msg.msg_namelen = remote.addrlen;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t ret;
do
ret = sendmsg(socketFd, &msg, MSG_DONTWAIT);
while (ret < 0 && errno == EINTR);
if (ret < 0)
throw std::system_error{errno, std::generic_category(), "Error sending message"};
}
ngtcp2_conn* GetConnection(ngtcp2_crypto_conn_ref* connRef)
{
return reinterpret_cast<std::unique_ptr<ngtcp2_conn, ConnectionDeleter>*>(connRef->user_data)->get();
}
int OnReceiveStreamData(ngtcp2_conn* conn, const std::uint32_t /*flags*/, const std::int64_t streamId,
const std::uint64_t /*offset*/, const std::uint8_t* data, const std::size_t datalen, void* userData,
void* /*streamUserData*/)
{
CNetServerSession& session{*static_cast<CNetServerSession*>(userData)};
const auto messageData = session.m_Connection.m_Stream.value().Receive({data, datalen});
if (messageData.has_value())
{
std::unique_ptr<CNetMessage> message{CNetMessageFactory::CreateMessage(messageData.value(),
session.GetServer().GetScriptInterface())};
session.GetServer().HandleMessageReceive(message.get(), &session);
}
increaseWindow(conn, streamId, datalen);
return 0;
}
int OnAcknowledgedStreamData(ngtcp2_conn*, const std::int64_t, const std::uint64_t offset,
const std::uint64_t dataLength, void* userData, void*)
{
Connection& connection{static_cast<CNetServerSession*>(userData)->m_Connection};
Stream& stream{connection.GetStream()};
stream.MarkAcknowledged(offset + dataLength);
return 0;
}
int OnConnect(ngtcp2_conn*, const ngtcp2_encryption_level level, void* userData)
{
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT)
return 0;
Connection& connection{static_cast<CNetServerSession*>(userData)->m_Connection};
try
{
connection.OpenStream();
}
catch (const std::runtime_error&)
{
return NGTCP2_ERR_CALLBACK_FAILURE;
}
const CSrvHandshakeMessage handshake(CreateHandshake<CSrvHandshakeMessage>());
connection.GetStream().PushMessage(&handshake);
return 0;
}
constexpr ngtcp2_callbacks callbacks{
.recv_client_initial{&ngtcp2_crypto_recv_client_initial_cb},
.recv_crypto_data{&ngtcp2_crypto_recv_crypto_data_cb},
.encrypt{&ngtcp2_crypto_encrypt_cb},
.decrypt{&ngtcp2_crypto_decrypt_cb},
.hp_mask{&ngtcp2_crypto_hp_mask_cb},
.recv_stream_data{&OnReceiveStreamData},
.acked_stream_data_offset{&OnAcknowledgedStreamData},
.recv_retry{&ngtcp2_crypto_recv_retry_cb},
.rand{&OnRandomRequest},
.get_new_connection_id{&OnNewConnectionIdRequest},
.update_key{&ngtcp2_crypto_update_key_cb},
.delete_crypto_aead_ctx{&ngtcp2_crypto_delete_crypto_aead_ctx_cb},
.delete_crypto_cipher_ctx{&ngtcp2_crypto_delete_crypto_cipher_ctx_cb},
.get_path_challenge_data{&ngtcp2_crypto_get_path_challenge_data_cb},
.recv_tx_key{&OnConnect}
};
void WriteToStream(const int socketFd, ngtcp2_conn* conn, Stream* stream, const ngtcp2_addr remote)
{
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
ngtcp2_path_storage ps;
ngtcp2_path_storage_zero(&ps);
ngtcp2_pkt_info pi;
const std::uint64_t ts{timestamp()};
std::uint32_t flags{NGTCP2_WRITE_STREAM_FLAG_MORE};
while (true)
{
ngtcp2_vec datav;
std::int64_t stream_id;
if (stream)
{
auto bytesToSend = stream->PeekData();
if (bytesToSend.has_value())
{
datav.base = const_cast<uint8_t*>(bytesToSend->data());
datav.len = bytesToSend->size();
stream_id = stream->m_Id;
}
else
{
/* No stream data to be sent */
datav.base = nullptr;
datav.len = 0;
stream_id = -1;
flags &= ~NGTCP2_WRITE_STREAM_FLAG_MORE;
}
}
else
{
datav.base = NULL;
datav.len = 0;
stream_id = -1;
}
ngtcp2_ssize n_read;
const ngtcp2_ssize n_written{ngtcp2_conn_writev_stream(conn, &ps.path, &pi, buf.data(),
buf.size(), &n_read, flags, stream_id, &datav, 1, ts)};
if (n_written < 0)
{
if (n_written != NGTCP2_ERR_WRITE_MORE)
{
throw std::runtime_error{fmt::format("ngtcp2_conn_writev_stream: {}",
ngtcp2_strerror(static_cast<int>(n_written)))};
}
if (stream && n_read > 0)
stream->MarkSent(n_read);
continue;
}
if (n_written == 0)
break;
if (stream && n_read > 0)
stream->MarkSent(n_read);
try
{
SendPacket(socketFd, {buf.data(), static_cast<std::size_t>(n_written)}, remote);
}
catch (std::system_error& e)
{
if (e.code().value() == EAGAIN || e.code().value() == EWOULDBLOCK)
break;
throw;
}
/* No stream data to be sent */
if (stream && datav.len == 0)
break;
}
ngtcp2_conn_update_pkt_tx_time(conn, timestamp());
}
std::unique_ptr<gnutls_session_int, SessionDeleter> createTlsSession(
const gnutls_certificate_credentials_t cred)
{
gnutls_session_t tempSession;
if (const int ret{gnutls_init(&tempSession, GNUTLS_SERVER | GNUTLS_ENABLE_EARLY_DATA |
GNUTLS_NO_END_OF_EARLY_DATA)})
{
throw std::runtime_error{fmt::format("gnutls_init: {}", gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_session_int, SessionDeleter> session{tempSession};
if (const int ret{gnutls_priority_set_direct(session.get(), TLS_PRIORITY, NULL)})
throw std::runtime_error{fmt::format("gnutls_priority_set_direct: {}", gnutls_strerror(ret))};
if (const int ret{gnutls_credentials_set(session.get(), GNUTLS_CRD_CERTIFICATE, cred)})
throw std::runtime_error{fmt::format("gnutls_credentials_set: {}", gnutls_strerror(ret))};
return session;
}
}
Connection::Connection(CNetServerSession& session, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& newScid, const ngtcp2_path& path):
m_TlsSession{createTlsSession(credentials)},
m_ConnRef{
.get_conn{&GetConnection},
.user_data{&m_QuicConnection}
}
{
ngtcp2_transport_params params;
ngtcp2_transport_params_default(&params);
params.initial_max_stream_data_bidi_local = 128 * KiB;
params.initial_max_data = 1 * MiB;
params.max_udp_payload_size = MAX_UDP_PAYLOAD_SIZE;
ngtcp2_cid_init(&params.original_dcid, header.dcid.data, header.dcid.datalen);
params.original_dcid_present = true;
params.grease_quic_bit = 1;
ngtcp2_conn* tempConn;
if (const int ret = ngtcp2_conn_server_new(&tempConn, &header.scid, &newScid, &path, header.version,
&callbacks, &settings, &params, nullptr, &session))
{
throw std::runtime_error{fmt::format("ngtcp2_conn_server_new: {}",
ngtcp2_strerror (ret))};
}
m_QuicConnection.reset(tempConn);
memcpy(&m_LocalAddress, path.local.addr, path.local.addrlen);
m_LocalAddressLength = path.local.addrlen;
memcpy(&m_RemoteAddress, path.remote.addr, path.remote.addrlen);
m_RemoteAddressLength = path.remote.addrlen;
ngtcp2_crypto_gnutls_configure_server_session(m_TlsSession.get());
ngtcp2_conn_set_tls_native_handle(m_QuicConnection.get(), m_TlsSession.get());
gnutls_session_set_ptr(m_TlsSession.get(), &m_ConnRef);
m_TimerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (m_TimerFd < 0)
throw std::system_error(errno, std::generic_category(), "timerfd_create");
}
void Connection::OpenStream()
{
std::int64_t streamId;
if (ngtcp2_conn_open_bidi_stream(m_QuicConnection.get(), &streamId, nullptr))
throw std::runtime_error{""};
m_Stream.emplace(streamId);
}
Stream& Connection::GetStream()
{
return m_Stream.value();
}
void Connection::Read(const ngtcp2_addr remote, const std::span<std::uint8_t> data)
{
const ngtcp2_path path{
.local{ngtcp2_conn_get_path(m_QuicConnection.get())->local},
.remote{remote}
};
ngtcp2_pkt_info pi{};
if (const int ret{ngtcp2_conn_read_pkt(m_QuicConnection.get(), &path, &pi, data.data(), data.size(),
timestamp())})
{
throw std::runtime_error{"Destroy connection"};
}
}
void Connection::Write(const int socketFd)
{
WriteToStream(socketFd, m_QuicConnection.get(), nullptr,
{&m_RemoteAddress.sa, m_RemoteAddressLength});
if (m_Stream.has_value())
{
WriteToStream(socketFd, m_QuicConnection.get(), &m_Stream.value(),
{&m_RemoteAddress.sa, m_RemoteAddressLength});
}
const ngtcp2_tstamp expiry{ngtcp2_conn_get_expiry(m_QuicConnection.get())};
const ngtcp2_tstamp now{timestamp()};
itimerspec it{};
if (const int ret{timerfd_settime(m_TimerFd, 0, &it, nullptr)})
throw std::system_error{errno, std::generic_category(), "timerfd_settime"};
if (expiry < now)
{
it.it_value.tv_sec = 0;
it.it_value.tv_nsec = 1;
}
else
{
it.it_value.tv_sec = (expiry - now) / NGTCP2_SECONDS;
it.it_value.tv_nsec = ((expiry - now) % NGTCP2_SECONDS) / NGTCP2_NANOSECONDS;
}
if (const int ret{timerfd_settime(m_TimerFd, 0, &it, nullptr)})
throw std::system_error{errno, std::generic_category(), "timerfd_settime"};
return;
}
CNetServerSession::CNetServerSession(CNetServerWorker& server, const int socketFd,
const ngtcp2_settings& settings, gnutls_certificate_credentials_t credentials,
const ngtcp2_pkt_hd& header, const ngtcp2_cid& newScid, const ngtcp2_path& path) :
m_Server(server),
m_Connection{*this, settings, credentials, header, newScid, path},
m_FileTransferer(*this),
m_SocketFd{socketFd}
{
}
@ -58,18 +362,30 @@ void CNetServerSession::Disconnect(NetDisconnectReason reason)
Update((uint)NMT_CONNECTION_LOST, NULL);
enet_peer_disconnect(m_Peer, static_cast<enet_uint32>(reason));
}
const ngtcp2_ccerr quicReason{
.type{NGTCP2_CCERR_TYPE_APPLICATION},
.error_code{reason}
};
void CNetServerSession::DisconnectNow(NetDisconnectReason reason)
{
if (reason == NDR_UNKNOWN)
LOGWARNING("Disconnecting client without communicating the disconnect reason!");
ngtcp2_sockaddr_union local;
ngtcp2_sockaddr_union remote;
ngtcp2_path path{
.local{.addr{&local.sa}},
.remote{.addr{&remote.sa}}
};
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
const ngtcp2_ssize amount{ngtcp2_conn_write_connection_close(m_Connection.m_QuicConnection.get(), &path, nullptr, buffer.data(),
buffer.size(), &quicReason, timestamp())};
if (amount <= 0)
LOGERROR("closing connection %s", ngtcp2_strerror(static_cast<int>(amount)));
enet_peer_disconnect_now(m_Peer, static_cast<enet_uint32>(reason));
SendPacket(m_SocketFd, {buffer.data(), static_cast<std::size_t>(amount)}, path.remote);
}
bool CNetServerSession::SendMessage(const CNetMessage* message)
{
return m_Server.SendMessage(m_Peer, message);
m_Connection.m_Stream.value().PushMessage(message);
return true;
}

View file

@ -23,10 +23,51 @@
#include "network/NetHost.h"
#include "ps/CStr.h"
#include <deque>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <optional>
#include <span>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/timerfd.h>
#include "network/NetProtocol.h"
#include "ps/CLogger.h"
class CNetServerWorker;
typedef struct _ENetPeer ENetPeer;
class CNetServerSession;
class Connection
{
public:
Connection(CNetServerSession& session, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& scid, const ngtcp2_path& path);
void OpenStream();
Stream& GetStream();
void Read(const ngtcp2_addr remote, const std::span<std::uint8_t> data);
void Write(const int socketFd);
std::unique_ptr<gnutls_session_int, SessionDeleter> m_TlsSession;
std::unique_ptr<ngtcp2_conn, ConnectionDeleter> m_QuicConnection;
int m_TimerFd{-1};
public:
ngtcp2_sockaddr_union m_LocalAddress;
ngtcp2_socklen m_LocalAddressLength;
ngtcp2_sockaddr_union m_RemoteAddress;
ngtcp2_socklen m_RemoteAddressLength;
std::optional<Stream> m_Stream;
ngtcp2_crypto_conn_ref m_ConnRef;
};
/**
* The server's end of a network session.
* Represents an abstraction of the state of the client, storing all the per-client data
@ -40,7 +81,9 @@ class CNetServerSession : public CFsm<CNetServerSession, CNetMessage*>
NONCOPYABLE(CNetServerSession);
public:
CNetServerSession(CNetServerWorker& server, ENetPeer* peer);
CNetServerSession(CNetServerWorker& server, const int socketFd, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& newScid, const ngtcp2_path& path);
CNetServerWorker& GetServer() { return m_Server; }
@ -73,13 +116,6 @@ public:
*/
void Disconnect(NetDisconnectReason reason);
/**
* Sends an unreliable disconnection notification to the client.
* The server will not receive any disconnection notification.
* The server will not receive any further messages sent via this session.
*/
void DisconnectNow(NetDisconnectReason reason);
/**
* Send a message to the client.
*/
@ -89,7 +125,10 @@ public:
private:
CNetServerWorker& m_Server;
public:
Connection m_Connection;
private:
CNetFileTransferer m_FileTransferer;
ENetPeer* m_Peer;
@ -98,6 +137,7 @@ private:
CStrW m_UserName;
u32 m_HostID{0};
CStr m_Password;
int m_SocketFd;
};
#endif // NET_SERVER_SESSION_H

View file

@ -19,6 +19,9 @@
#include "NetStats.h"
#include "network/NetServerSession.h"
#include <ngtcp2/ngtcp2.h>
#include <string>
enum
@ -37,13 +40,8 @@ enum
NumberRows
};
CNetStatsTable::CNetStatsTable(const ENetPeer& peer)
: m_Peer(&peer)
{
}
CNetStatsTable::CNetStatsTable()
: m_Peer(NULL)
CNetStatsTable::CNetStatsTable(ngtcp2_conn* conn):
m_Connection{conn}
{
}
@ -54,7 +52,7 @@ CStr CNetStatsTable::GetName()
CStr CNetStatsTable::GetTitle()
{
if (m_Peer)
if (m_Connection)
return "Network client statistics";
else
return "Network host statistics";
@ -70,7 +68,7 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
m_ColumnDescriptions.clear();
m_ColumnDescriptions.push_back(ProfileColumn("Name", 200));
if (m_Peer)
if (m_Connection)
m_ColumnDescriptions.push_back(ProfileColumn("Value", 80));
else
{
@ -95,22 +93,26 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
#define ROW(id, title, member) \
case id: \
if (col == 0) return title; \
if (m_Peer) return CStr::FromUInt(m_Peer->member); \
if (m_Connection) return member; \
return "???"
ngtcp2_conn_info info;
if (col != 0)
ngtcp2_conn_get_conn_info(m_Connection, &info);
switch(row)
{
ROW(Row_InData, "incoming bytes", incomingDataTotal);
ROW(Row_OutData, "outgoing bytes", outgoingDataTotal);
ROW(Row_LastSendTime, "last send time", lastSendTime);
ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
ROW(Row_NextTimeout, "next timeout", nextTimeout);
ROW(Row_PacketsSent, "packets sent", packetsSent);
ROW(Row_PacketsLost, "packets lost", packetsLost);
ROW(Row_LastRTT, "last RTT", lastRoundTripTime);
ROW(Row_RTT, "mean RTT", roundTripTime);
ROW(Row_MTU, "MTU", mtu);
ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit);
ROW(Row_InData, "incoming bytes", {});
ROW(Row_OutData, "outgoing bytes", {});
ROW(Row_LastSendTime, "last send time", {});
ROW(Row_LastRecvTime, "last receive time", {});
ROW(Row_NextTimeout, "next timeout", CStr::FromUInt(ngtcp2_conn_get_expiry(m_Connection)));
ROW(Row_PacketsSent, "packets sent", {});
ROW(Row_PacketsLost, "packets lost", {});
ROW(Row_LastRTT, "last RTT", CStr::FromUInt(info.latest_rtt));
ROW(Row_RTT, "mean RTT", CStr::FromUInt(info.smoothed_rtt));
ROW(Row_MTU, "MTU", CStr::FromUInt(ngtcp2_conn_get_path_max_tx_udp_payload_size(m_Connection)));
ROW(Row_ReliableInTransit, "reliable data in transit", CStr::FromUInt(info.bytes_in_flight));
default:
return "???";
@ -124,29 +126,39 @@ AbstractProfileTable* CNetStatsTable::GetChild(size_t /*row*/)
return 0;
}
void CNetStatsTable::LatchHostState(const ENetHost& host)
void CNetStatsTable::LatchHostState(const std::span<std::unique_ptr<CNetServerSession>> sessions)
{
std::lock_guard<std::mutex> lock(m_Mutex);
#define ROW(id, title, member) \
m_LatchedData[i].push_back(CStr::FromUInt(host.peers[i].member));
m_LatchedData[i].push_back(CStr::FromUInt(info.member));
m_LatchedData.clear();
m_LatchedData.resize(host.peerCount);
m_LatchedData.resize(sessions.size());
for (size_t i = 0; i < host.peerCount; ++i)
for (size_t i = 0; i < sessions.size(); ++i)
{
ROW(Row_InData, "incoming bytes", incomingDataTotal);
ROW(Row_OutData, "outgoing bytes", outgoingDataTotal);
ROW(Row_LastSendTime, "last send time", lastSendTime);
ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
ROW(Row_NextTimeout, "next timeout", nextTimeout);
ROW(Row_PacketsSent, "packets sent", packetsSent);
ROW(Row_PacketsLost, "packets lost", packetsLost);
ROW(Row_LastRTT, "last RTT", lastRoundTripTime);
ROW(Row_RTT, "mean RTT", roundTripTime);
ROW(Row_MTU, "MTU", mtu);
ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit);
ngtcp2_conn_info info;
ngtcp2_conn_get_conn_info(sessions[i]->m_Connection.m_QuicConnection.get(), &info);
// ROW(Row_InData, "incoming bytes", bytes_recv);
m_LatchedData[i].push_back({});
// ROW(Row_OutData, "outgoing bytes", bytes_sent);
m_LatchedData[i].push_back({});
// ROW(Row_LastSendTime, "last send time", lastSendTime);
m_LatchedData[i].push_back({});
// ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
m_LatchedData[i].push_back({});
m_LatchedData[i].push_back(CStr::FromUInt(ngtcp2_conn_get_expiry(
sessions[i]->m_Connection.m_QuicConnection.get())));
// ROW(Row_PacketsSent, "packets sent", pkt_sent);
m_LatchedData[i].push_back({});
// ROW(Row_PacketsLost, "packets lost", pkt_lost);
m_LatchedData[i].push_back({});
ROW(Row_LastRTT, "last RTT", latest_rtt);
ROW(Row_RTT, "mean RTT", smoothed_rtt);
m_LatchedData[i].push_back(CStr::FromUInt(ngtcp2_conn_get_path_max_tx_udp_payload_size(
sessions[i]->m_Connection.m_QuicConnection.get())));
ROW(Row_ReliableInTransit, "reliable data in transit", bytes_in_flight);
}
#undef ROW
}

View file

@ -25,10 +25,13 @@
#include <cstddef>
#include <mutex>
#include <span>
#include <vector>
typedef struct _ENetPeer ENetPeer;
typedef struct _ENetHost ENetHost;
class CNetServerSession;
struct ngtcp2_conn;
/**
* ENet connection statistics profiler table.
@ -43,8 +46,8 @@ class CNetStatsTable : public AbstractProfileTable
{
NONCOPYABLE(CNetStatsTable);
public:
CNetStatsTable();
CNetStatsTable(const ENetPeer& peer);
CNetStatsTable() = default;
CNetStatsTable(ngtcp2_conn* conn);
CStr GetName() override;
CStr GetTitle() override;
@ -53,10 +56,10 @@ public:
CStr GetCellText(size_t row, size_t col) override;
AbstractProfileTable* GetChild(size_t row) override;
void LatchHostState(const ENetHost& host);
void LatchHostState(const std::span<std::unique_ptr<CNetServerSession>> sessions);
private:
const ENetPeer* m_Peer;
ngtcp2_conn* m_Connection{nullptr};
std::vector<ProfileColumn> m_ColumnDescriptions;
std::mutex m_Mutex;

View file

@ -48,7 +48,7 @@ public:
TS_ASSERT_EQUALS(msg.Serialize(buf) - (buf+len), 0);
TS_ASSERT_EQUALS(buf[len], '!');
CNetMessage* msg2 = CNetMessageFactory::CreateMessage(buf, len, script);
CNetMessage* msg2 = CNetMessageFactory::CreateMessage({buf, len}, script);
TS_ASSERT_STR_EQUALS(((CSimulationMessage*)msg2)->ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }");
delete msg2;