github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/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/ethw3/go-ethereuma/crypto" 27 "github.com/ethw3/go-ethereuma/internal/utesting" 28 "github.com/ethw3/go-ethereuma/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 // IP where the first tester is listening, port will be assigned 41 Listen1 string = "127.0.0.1" 42 // 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 // This test 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 // This test 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 // This test 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 // 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 bond(t, te) 260 261 wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")} 262 pingHash := te.send(te.l1, &v4wire.Ping{ 263 Version: 4, 264 From: wrongEndpoint, 265 To: te.remoteEndpoint(), 266 Expiration: futureExpiration(), 267 }) 268 if reply, _, err := te.read(te.l1); err != nil { 269 t.Fatal(err) 270 } else if err := te.checkPong(reply, pingHash); err != nil { 271 t.Fatal(err) 272 } 273 } 274 275 // This test just sends FINDNODE. The remote node should not reply 276 // because the endpoint proof has not completed. 277 func FindnodeWithoutEndpointProof(t *utesting.T) { 278 te := newTestEnv(Remote, Listen1, Listen2) 279 defer te.close() 280 281 req := v4wire.Findnode{Expiration: futureExpiration()} 282 rand.Read(req.Target[:]) 283 te.send(te.l1, &req) 284 285 for { 286 reply, _, _ := te.read(te.l1) 287 if reply == nil { 288 // No response, all good 289 break 290 } 291 if reply.Kind() == v4wire.PingPacket { 292 continue // A ping is ok, just ignore it 293 } 294 t.Fatalf("Expected no reply, got %v %v", reply.Name(), reply) 295 } 296 } 297 298 // BasicFindnode sends a FINDNODE request after performing the endpoint 299 // proof. The remote node should respond. 300 func BasicFindnode(t *utesting.T) { 301 te := newTestEnv(Remote, Listen1, Listen2) 302 defer te.close() 303 bond(t, te) 304 305 findnode := v4wire.Findnode{Expiration: futureExpiration()} 306 rand.Read(findnode.Target[:]) 307 te.send(te.l1, &findnode) 308 309 reply, _, err := te.read(te.l1) 310 if err != nil { 311 t.Fatal("read find nodes", err) 312 } 313 if reply.Kind() != v4wire.NeighborsPacket { 314 t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply) 315 } 316 } 317 318 // This test sends an unsolicited NEIGHBORS packet after the endpoint proof, then sends 319 // FINDNODE to read the remote table. The remote node should not return the node contained 320 // in the unsolicited NEIGHBORS packet. 321 func UnsolicitedNeighbors(t *utesting.T) { 322 te := newTestEnv(Remote, Listen1, Listen2) 323 defer te.close() 324 bond(t, te) 325 326 // Send unsolicited NEIGHBORS response. 327 fakeKey, _ := crypto.GenerateKey() 328 encFakeKey := v4wire.EncodePubkey(&fakeKey.PublicKey) 329 neighbors := v4wire.Neighbors{ 330 Expiration: futureExpiration(), 331 Nodes: []v4wire.Node{{ 332 ID: encFakeKey, 333 IP: net.IP{1, 2, 3, 4}, 334 UDP: 30303, 335 TCP: 30303, 336 }}, 337 } 338 te.send(te.l1, &neighbors) 339 340 // Check if the remote node included the fake node. 341 te.send(te.l1, &v4wire.Findnode{ 342 Expiration: futureExpiration(), 343 Target: encFakeKey, 344 }) 345 346 reply, _, err := te.read(te.l1) 347 if err != nil { 348 t.Fatal("read find nodes", err) 349 } 350 if reply.Kind() != v4wire.NeighborsPacket { 351 t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply) 352 } 353 nodes := reply.(*v4wire.Neighbors).Nodes 354 if contains(nodes, encFakeKey) { 355 t.Fatal("neighbors response contains node from earlier unsolicited neighbors response") 356 } 357 } 358 359 // This test sends FINDNODE with an expiration timestamp in the past. 360 // The remote node should not respond. 361 func FindnodePastExpiration(t *utesting.T) { 362 te := newTestEnv(Remote, Listen1, Listen2) 363 defer te.close() 364 bond(t, te) 365 366 findnode := v4wire.Findnode{Expiration: -futureExpiration()} 367 rand.Read(findnode.Target[:]) 368 te.send(te.l1, &findnode) 369 370 for { 371 reply, _, _ := te.read(te.l1) 372 if reply == nil { 373 return 374 } else if reply.Kind() == v4wire.NeighborsPacket { 375 t.Fatal("Unexpected NEIGHBORS response for expired FINDNODE request") 376 } 377 } 378 } 379 380 // bond performs the endpoint proof with the remote node. 381 func bond(t *utesting.T, te *testenv) { 382 te.send(te.l1, &v4wire.Ping{ 383 Version: 4, 384 From: te.localEndpoint(te.l1), 385 To: te.remoteEndpoint(), 386 Expiration: futureExpiration(), 387 }) 388 389 var gotPing, gotPong bool 390 for !gotPing || !gotPong { 391 req, hash, err := te.read(te.l1) 392 if err != nil { 393 t.Fatal(err) 394 } 395 switch req.(type) { 396 case *v4wire.Ping: 397 te.send(te.l1, &v4wire.Pong{ 398 To: te.remoteEndpoint(), 399 ReplyTok: hash, 400 Expiration: futureExpiration(), 401 }) 402 gotPing = true 403 case *v4wire.Pong: 404 // TODO: maybe verify pong data here 405 gotPong = true 406 } 407 } 408 } 409 410 // This test attempts to perform a traffic amplification attack against a 411 // 'victim' endpoint using FINDNODE. In this attack scenario, the attacker 412 // attempts to complete the endpoint proof non-interactively by sending a PONG 413 // with mismatching reply token from the 'victim' endpoint. The attack works if 414 // the remote node does not verify the PONG reply token field correctly. The 415 // attacker could then perform traffic amplification by sending many FINDNODE 416 // requests to the discovery node, which would reply to the 'victim' address. 417 func FindnodeAmplificationInvalidPongHash(t *utesting.T) { 418 te := newTestEnv(Remote, Listen1, Listen2) 419 defer te.close() 420 421 // Send PING to start endpoint verification. 422 te.send(te.l1, &v4wire.Ping{ 423 Version: 4, 424 From: te.localEndpoint(te.l1), 425 To: te.remoteEndpoint(), 426 Expiration: futureExpiration(), 427 }) 428 429 var gotPing, gotPong bool 430 for !gotPing || !gotPong { 431 req, _, err := te.read(te.l1) 432 if err != nil { 433 t.Fatal(err) 434 } 435 switch req.(type) { 436 case *v4wire.Ping: 437 // Send PONG from this node ID, but with invalid ReplyTok. 438 te.send(te.l1, &v4wire.Pong{ 439 To: te.remoteEndpoint(), 440 ReplyTok: make([]byte, macSize), 441 Expiration: futureExpiration(), 442 }) 443 gotPing = true 444 case *v4wire.Pong: 445 gotPong = true 446 } 447 } 448 449 // Now send FINDNODE. The remote node should not respond because our 450 // PONG did not reference the PING hash. 451 findnode := v4wire.Findnode{Expiration: futureExpiration()} 452 rand.Read(findnode.Target[:]) 453 te.send(te.l1, &findnode) 454 455 // If we receive a NEIGHBORS response, the attack worked and the test fails. 456 reply, _, _ := te.read(te.l1) 457 if reply != nil && reply.Kind() == v4wire.NeighborsPacket { 458 t.Error("Got neighbors") 459 } 460 } 461 462 // This test attempts to perform a traffic amplification attack using FINDNODE. 463 // The attack works if the remote node does not verify the IP address of FINDNODE 464 // against the endpoint verification proof done by PING/PONG. 465 func FindnodeAmplificationWrongIP(t *utesting.T) { 466 te := newTestEnv(Remote, Listen1, Listen2) 467 defer te.close() 468 469 // Do the endpoint proof from the l1 IP. 470 bond(t, te) 471 472 // Now send FINDNODE from the same node ID, but different IP address. 473 // The remote node should not respond. 474 findnode := v4wire.Findnode{Expiration: futureExpiration()} 475 rand.Read(findnode.Target[:]) 476 te.send(te.l2, &findnode) 477 478 // If we receive a NEIGHBORS response, the attack worked and the test fails. 479 reply, _, _ := te.read(te.l2) 480 if reply != nil { 481 t.Error("Got NEIGHORS response for FINDNODE from wrong IP") 482 } 483 } 484 485 var AllTests = []utesting.Test{ 486 {Name: "Ping/Basic", Fn: BasicPing}, 487 {Name: "Ping/WrongTo", Fn: PingWrongTo}, 488 {Name: "Ping/WrongFrom", Fn: PingWrongFrom}, 489 {Name: "Ping/ExtraData", Fn: PingExtraData}, 490 {Name: "Ping/ExtraDataWrongFrom", Fn: PingExtraDataWrongFrom}, 491 {Name: "Ping/PastExpiration", Fn: PingPastExpiration}, 492 {Name: "Ping/WrongPacketType", Fn: WrongPacketType}, 493 {Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom}, 494 {Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof}, 495 {Name: "Findnode/BasicFindnode", Fn: BasicFindnode}, 496 {Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors}, 497 {Name: "Findnode/PastExpiration", Fn: FindnodePastExpiration}, 498 {Name: "Amplification/InvalidPongHash", Fn: FindnodeAmplificationInvalidPongHash}, 499 {Name: "Amplification/WrongIP", Fn: FindnodeAmplificationWrongIP}, 500 }