github.com/tirogen/go-ethereum@v1.10.12-0.20221226051715-250cfede41b6/cmd/devp2p/internal/v4test/discv4tests.go (about) 1 // Copyright 2020 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package v4test 18 19 import ( 20 "bytes" 21 "crypto/rand" 22 "fmt" 23 "net" 24 "time" 25 26 "github.com/tirogen/go-ethereum/crypto" 27 "github.com/tirogen/go-ethereum/internal/utesting" 28 "github.com/tirogen/go-ethereum/p2p/discover/v4wire" 29 ) 30 31 const ( 32 expiration = 20 * time.Second 33 wrongPacket = 66 34 macSize = 256 / 8 35 ) 36 37 var ( 38 // Remote node under test 39 Remote string 40 // Listen1 is the IP where the first tester is listening, port will be assigned 41 Listen1 string = "127.0.0.1" 42 // Listen2 is the IP where the second tester is listening, port will be assigned 43 // Before running the test, you may have to `sudo ifconfig lo0 add 127.0.0.2` (on MacOS at least) 44 Listen2 string = "127.0.0.2" 45 ) 46 47 type pingWithJunk struct { 48 Version uint 49 From, To v4wire.Endpoint 50 Expiration uint64 51 JunkData1 uint 52 JunkData2 []byte 53 } 54 55 func (req *pingWithJunk) Name() string { return "PING/v4" } 56 func (req *pingWithJunk) Kind() byte { return v4wire.PingPacket } 57 58 type pingWrongType struct { 59 Version uint 60 From, To v4wire.Endpoint 61 Expiration uint64 62 } 63 64 func (req *pingWrongType) Name() string { return "WRONG/v4" } 65 func (req *pingWrongType) Kind() byte { return wrongPacket } 66 67 func futureExpiration() uint64 { 68 return uint64(time.Now().Add(expiration).Unix()) 69 } 70 71 // BasicPing just sends a PING packet and expects a response. 72 func BasicPing(t *utesting.T) { 73 te := newTestEnv(Remote, Listen1, Listen2) 74 defer te.close() 75 76 pingHash := te.send(te.l1, &v4wire.Ping{ 77 Version: 4, 78 From: te.localEndpoint(te.l1), 79 To: te.remoteEndpoint(), 80 Expiration: futureExpiration(), 81 }) 82 if err := te.checkPingPong(pingHash); err != nil { 83 t.Fatal(err) 84 } 85 } 86 87 // checkPingPong verifies that the remote side sends both a PONG with the 88 // correct hash, and a PING. 89 // The two packets do not have to be in any particular order. 90 func (te *testenv) checkPingPong(pingHash []byte) error { 91 var ( 92 pings int 93 pongs int 94 ) 95 for i := 0; i < 2; i++ { 96 reply, _, err := te.read(te.l1) 97 if err != nil { 98 return err 99 } 100 switch reply.Kind() { 101 case v4wire.PongPacket: 102 if err := te.checkPong(reply, pingHash); err != nil { 103 return err 104 } 105 pongs++ 106 case v4wire.PingPacket: 107 pings++ 108 default: 109 return fmt.Errorf("expected PING or PONG, got %v %v", reply.Name(), reply) 110 } 111 } 112 if pongs == 1 && pings == 1 { 113 return nil 114 } 115 return fmt.Errorf("expected 1 PING (got %d) and 1 PONG (got %d)", pings, pongs) 116 } 117 118 // checkPong verifies that reply is a valid PONG matching the given ping hash, 119 // and a PING. The two packets do not have to be in any particular order. 120 func (te *testenv) checkPong(reply v4wire.Packet, pingHash []byte) error { 121 if reply == nil { 122 return fmt.Errorf("expected PONG reply, got nil") 123 } 124 if reply.Kind() != v4wire.PongPacket { 125 return fmt.Errorf("expected PONG reply, got %v %v", reply.Name(), reply) 126 } 127 pong := reply.(*v4wire.Pong) 128 if !bytes.Equal(pong.ReplyTok, pingHash) { 129 return fmt.Errorf("PONG reply token mismatch: got %x, want %x", pong.ReplyTok, pingHash) 130 } 131 if want := te.localEndpoint(te.l1); !want.IP.Equal(pong.To.IP) || want.UDP != pong.To.UDP { 132 return fmt.Errorf("PONG 'to' endpoint mismatch: got %+v, want %+v", pong.To, want) 133 } 134 if v4wire.Expired(pong.Expiration) { 135 return fmt.Errorf("PONG is expired (%v)", pong.Expiration) 136 } 137 return nil 138 } 139 140 // PingWrongTo sends a PING packet with wrong 'to' field and expects a PONG response. 141 func PingWrongTo(t *utesting.T) { 142 te := newTestEnv(Remote, Listen1, Listen2) 143 defer te.close() 144 145 wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")} 146 pingHash := te.send(te.l1, &v4wire.Ping{ 147 Version: 4, 148 From: te.localEndpoint(te.l1), 149 To: wrongEndpoint, 150 Expiration: futureExpiration(), 151 }) 152 if err := te.checkPingPong(pingHash); err != nil { 153 t.Fatal(err) 154 } 155 } 156 157 // PingWrongFrom sends a PING packet with wrong 'from' field and expects a PONG response. 158 func PingWrongFrom(t *utesting.T) { 159 te := newTestEnv(Remote, Listen1, Listen2) 160 defer te.close() 161 162 wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")} 163 pingHash := te.send(te.l1, &v4wire.Ping{ 164 Version: 4, 165 From: wrongEndpoint, 166 To: te.remoteEndpoint(), 167 Expiration: futureExpiration(), 168 }) 169 170 if err := te.checkPingPong(pingHash); err != nil { 171 t.Fatal(err) 172 } 173 } 174 175 // PingExtraData This test sends a PING packet with additional data at the end and expects a PONG 176 // response. The remote node should respond because EIP-8 mandates ignoring additional 177 // trailing data. 178 func PingExtraData(t *utesting.T) { 179 te := newTestEnv(Remote, Listen1, Listen2) 180 defer te.close() 181 182 pingHash := te.send(te.l1, &pingWithJunk{ 183 Version: 4, 184 From: te.localEndpoint(te.l1), 185 To: te.remoteEndpoint(), 186 Expiration: futureExpiration(), 187 JunkData1: 42, 188 JunkData2: []byte{9, 8, 7, 6, 5, 4, 3, 2, 1}, 189 }) 190 191 if err := te.checkPingPong(pingHash); err != nil { 192 t.Fatal(err) 193 } 194 } 195 196 // This test sends a PING packet with additional data and wrong 'from' field 197 // and expects a PONG response. 198 func PingExtraDataWrongFrom(t *utesting.T) { 199 te := newTestEnv(Remote, Listen1, Listen2) 200 defer te.close() 201 202 wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")} 203 req := pingWithJunk{ 204 Version: 4, 205 From: wrongEndpoint, 206 To: te.remoteEndpoint(), 207 Expiration: futureExpiration(), 208 JunkData1: 42, 209 JunkData2: []byte{9, 8, 7, 6, 5, 4, 3, 2, 1}, 210 } 211 pingHash := te.send(te.l1, &req) 212 if err := te.checkPingPong(pingHash); err != nil { 213 t.Fatal(err) 214 } 215 } 216 217 // This test sends a PING packet with an expiration in the past. 218 // The remote node should not respond. 219 func PingPastExpiration(t *utesting.T) { 220 te := newTestEnv(Remote, Listen1, Listen2) 221 defer te.close() 222 223 te.send(te.l1, &v4wire.Ping{ 224 Version: 4, 225 From: te.localEndpoint(te.l1), 226 To: te.remoteEndpoint(), 227 Expiration: -futureExpiration(), 228 }) 229 230 reply, _, _ := te.read(te.l1) 231 if reply != nil { 232 t.Fatalf("Expected no reply, got %v %v", reply.Name(), reply) 233 } 234 } 235 236 // This test sends an invalid packet. The remote node should not respond. 237 func WrongPacketType(t *utesting.T) { 238 te := newTestEnv(Remote, Listen1, Listen2) 239 defer te.close() 240 241 te.send(te.l1, &pingWrongType{ 242 Version: 4, 243 From: te.localEndpoint(te.l1), 244 To: te.remoteEndpoint(), 245 Expiration: futureExpiration(), 246 }) 247 248 reply, _, _ := te.read(te.l1) 249 if reply != nil { 250 t.Fatalf("Expected no reply, got %v %v", reply.Name(), reply) 251 } 252 } 253 254 // This test verifies that the default behaviour of ignoring 'from' fields is unaffected by 255 // the bonding process. After bonding, it pings the target with a different from endpoint. 256 func BondThenPingWithWrongFrom(t *utesting.T) { 257 te := newTestEnv(Remote, Listen1, Listen2) 258 defer te.close() 259 260 bond(t, te) 261 262 wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")} 263 pingHash := te.send(te.l1, &v4wire.Ping{ 264 Version: 4, 265 From: wrongEndpoint, 266 To: te.remoteEndpoint(), 267 Expiration: futureExpiration(), 268 }) 269 270 waitForPong: 271 for { 272 reply, _, err := te.read(te.l1) 273 if err != nil { 274 t.Fatal(err) 275 } 276 switch reply.Kind() { 277 case v4wire.PongPacket: 278 if err := te.checkPong(reply, pingHash); err != nil { 279 t.Fatal(err) 280 } 281 break waitForPong 282 case v4wire.FindnodePacket: 283 // FINDNODE from the node is acceptable here since the endpoint 284 // verification was performed earlier. 285 default: 286 t.Fatalf("Expected PONG, got %v %v", reply.Name(), reply) 287 } 288 } 289 } 290 291 // This test just sends FINDNODE. The remote node should not reply 292 // because the endpoint proof has not completed. 293 func FindnodeWithoutEndpointProof(t *utesting.T) { 294 te := newTestEnv(Remote, Listen1, Listen2) 295 defer te.close() 296 297 req := v4wire.Findnode{Expiration: futureExpiration()} 298 rand.Read(req.Target[:]) 299 te.send(te.l1, &req) 300 301 for { 302 reply, _, _ := te.read(te.l1) 303 if reply == nil { 304 // No response, all good 305 break 306 } 307 if reply.Kind() == v4wire.PingPacket { 308 continue // A ping is ok, just ignore it 309 } 310 t.Fatalf("Expected no reply, got %v %v", reply.Name(), reply) 311 } 312 } 313 314 // BasicFindnode sends a FINDNODE request after performing the endpoint 315 // proof. The remote node should respond. 316 func BasicFindnode(t *utesting.T) { 317 te := newTestEnv(Remote, Listen1, Listen2) 318 defer te.close() 319 bond(t, te) 320 321 findnode := v4wire.Findnode{Expiration: futureExpiration()} 322 rand.Read(findnode.Target[:]) 323 te.send(te.l1, &findnode) 324 325 reply, _, err := te.read(te.l1) 326 if err != nil { 327 t.Fatal("read find nodes", err) 328 } 329 if reply.Kind() != v4wire.NeighborsPacket { 330 t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply) 331 } 332 } 333 334 // This test sends an unsolicited NEIGHBORS packet after the endpoint proof, then sends 335 // FINDNODE to read the remote table. The remote node should not return the node contained 336 // in the unsolicited NEIGHBORS packet. 337 func UnsolicitedNeighbors(t *utesting.T) { 338 te := newTestEnv(Remote, Listen1, Listen2) 339 defer te.close() 340 bond(t, te) 341 342 // Send unsolicited NEIGHBORS response. 343 fakeKey, _ := crypto.GenerateKey() 344 encFakeKey := v4wire.EncodePubkey(&fakeKey.PublicKey) 345 neighbors := v4wire.Neighbors{ 346 Expiration: futureExpiration(), 347 Nodes: []v4wire.Node{{ 348 ID: encFakeKey, 349 IP: net.IP{1, 2, 3, 4}, 350 UDP: 30303, 351 TCP: 30303, 352 }}, 353 } 354 te.send(te.l1, &neighbors) 355 356 // Check if the remote node included the fake node. 357 te.send(te.l1, &v4wire.Findnode{ 358 Expiration: futureExpiration(), 359 Target: encFakeKey, 360 }) 361 362 reply, _, err := te.read(te.l1) 363 if err != nil { 364 t.Fatal("read find nodes", err) 365 } 366 if reply.Kind() != v4wire.NeighborsPacket { 367 t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply) 368 } 369 nodes := reply.(*v4wire.Neighbors).Nodes 370 if contains(nodes, encFakeKey) { 371 t.Fatal("neighbors response contains node from earlier unsolicited neighbors response") 372 } 373 } 374 375 // This test sends FINDNODE with an expiration timestamp in the past. 376 // The remote node should not respond. 377 func FindnodePastExpiration(t *utesting.T) { 378 te := newTestEnv(Remote, Listen1, Listen2) 379 defer te.close() 380 bond(t, te) 381 382 findnode := v4wire.Findnode{Expiration: -futureExpiration()} 383 rand.Read(findnode.Target[:]) 384 te.send(te.l1, &findnode) 385 386 for { 387 reply, _, _ := te.read(te.l1) 388 if reply == nil { 389 return 390 } else if reply.Kind() == v4wire.NeighborsPacket { 391 t.Fatal("Unexpected NEIGHBORS response for expired FINDNODE request") 392 } 393 } 394 } 395 396 // bond performs the endpoint proof with the remote node. 397 func bond(t *utesting.T, te *testenv) { 398 te.send(te.l1, &v4wire.Ping{ 399 Version: 4, 400 From: te.localEndpoint(te.l1), 401 To: te.remoteEndpoint(), 402 Expiration: futureExpiration(), 403 }) 404 405 var gotPing, gotPong bool 406 for !gotPing || !gotPong { 407 req, hash, err := te.read(te.l1) 408 if err != nil { 409 t.Fatal(err) 410 } 411 switch req.(type) { 412 case *v4wire.Ping: 413 te.send(te.l1, &v4wire.Pong{ 414 To: te.remoteEndpoint(), 415 ReplyTok: hash, 416 Expiration: futureExpiration(), 417 }) 418 gotPing = true 419 case *v4wire.Pong: 420 // TODO: maybe verify pong data here 421 gotPong = true 422 } 423 } 424 } 425 426 // This test attempts to perform a traffic amplification attack against a 427 // 'victim' endpoint using FINDNODE. In this attack scenario, the attacker 428 // attempts to complete the endpoint proof non-interactively by sending a PONG 429 // with mismatching reply token from the 'victim' endpoint. The attack works if 430 // the remote node does not verify the PONG reply token field correctly. The 431 // attacker could then perform traffic amplification by sending many FINDNODE 432 // requests to the discovery node, which would reply to the 'victim' address. 433 func FindnodeAmplificationInvalidPongHash(t *utesting.T) { 434 te := newTestEnv(Remote, Listen1, Listen2) 435 defer te.close() 436 437 // Send PING to start endpoint verification. 438 te.send(te.l1, &v4wire.Ping{ 439 Version: 4, 440 From: te.localEndpoint(te.l1), 441 To: te.remoteEndpoint(), 442 Expiration: futureExpiration(), 443 }) 444 445 var gotPing, gotPong bool 446 for !gotPing || !gotPong { 447 req, _, err := te.read(te.l1) 448 if err != nil { 449 t.Fatal(err) 450 } 451 switch req.(type) { 452 case *v4wire.Ping: 453 // Send PONG from this node ID, but with invalid ReplyTok. 454 te.send(te.l1, &v4wire.Pong{ 455 To: te.remoteEndpoint(), 456 ReplyTok: make([]byte, macSize), 457 Expiration: futureExpiration(), 458 }) 459 gotPing = true 460 case *v4wire.Pong: 461 gotPong = true 462 } 463 } 464 465 // Now send FINDNODE. The remote node should not respond because our 466 // PONG did not reference the PING hash. 467 findnode := v4wire.Findnode{Expiration: futureExpiration()} 468 rand.Read(findnode.Target[:]) 469 te.send(te.l1, &findnode) 470 471 // If we receive a NEIGHBORS response, the attack worked and the test fails. 472 reply, _, _ := te.read(te.l1) 473 if reply != nil && reply.Kind() == v4wire.NeighborsPacket { 474 t.Error("Got neighbors") 475 } 476 } 477 478 // This test attempts to perform a traffic amplification attack using FINDNODE. 479 // The attack works if the remote node does not verify the IP address of FINDNODE 480 // against the endpoint verification proof done by PING/PONG. 481 func FindnodeAmplificationWrongIP(t *utesting.T) { 482 te := newTestEnv(Remote, Listen1, Listen2) 483 defer te.close() 484 485 // Do the endpoint proof from the l1 IP. 486 bond(t, te) 487 488 // Now send FINDNODE from the same node ID, but different IP address. 489 // The remote node should not respond. 490 findnode := v4wire.Findnode{Expiration: futureExpiration()} 491 rand.Read(findnode.Target[:]) 492 te.send(te.l2, &findnode) 493 494 // If we receive a NEIGHBORS response, the attack worked and the test fails. 495 reply, _, _ := te.read(te.l2) 496 if reply != nil { 497 t.Error("Got NEIGHORS response for FINDNODE from wrong IP") 498 } 499 } 500 501 var AllTests = []utesting.Test{ 502 {Name: "Ping/Basic", Fn: BasicPing}, 503 {Name: "Ping/WrongTo", Fn: PingWrongTo}, 504 {Name: "Ping/WrongFrom", Fn: PingWrongFrom}, 505 {Name: "Ping/ExtraData", Fn: PingExtraData}, 506 {Name: "Ping/ExtraDataWrongFrom", Fn: PingExtraDataWrongFrom}, 507 {Name: "Ping/PastExpiration", Fn: PingPastExpiration}, 508 {Name: "Ping/WrongPacketType", Fn: WrongPacketType}, 509 {Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom}, 510 {Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof}, 511 {Name: "Findnode/BasicFindnode", Fn: BasicFindnode}, 512 {Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors}, 513 {Name: "Findnode/PastExpiration", Fn: FindnodePastExpiration}, 514 {Name: "Amplification/InvalidPongHash", Fn: FindnodeAmplificationInvalidPongHash}, 515 {Name: "Amplification/WrongIP", Fn: FindnodeAmplificationWrongIP}, 516 }