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