p2p/netutil: add IPTracker

This commit is contained in:
Felix Lange 2018-09-21 11:12:36 +02:00
parent dcae0d348b
commit c831778ef2
2 changed files with 201 additions and 0 deletions

116
p2p/netutil/iptrack.go Normal file
View file

@ -0,0 +1,116 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package netutil
import (
"time"
"github.com/ethereum/go-ethereum/common/mclock"
)
// IPTracker predicts the external endpoint, i.e. IP address and port, of the local host
// based on statements made by other hosts.
type IPTracker struct {
window time.Duration
contactWindow time.Duration
minStatements int
clock mclock.Clock
statements map[string]ipStatement
contact map[string]mclock.AbsTime
// TODO add DistinctNetSet for additional protection
}
type ipStatement struct {
endpoint string
time mclock.AbsTime
}
// NewIPTracker creates an IP tracker.
//
// The window parameters configure the amount of past network events which are kept. The
// minStatements parameter enforces a minimum number of statements which must be recorded
// before any prediction is made. Higher values for these parameters decrease 'flapping' of
// predictions as network conditions change. Window duration values should typically be in
// the range of minutes.
func NewIPTracker(window, contactWindow time.Duration, minStatements int) *IPTracker {
return &IPTracker{
window: window,
contactWindow: contactWindow,
statements: make(map[string]ipStatement),
minStatements: minStatements,
contact: make(map[string]mclock.AbsTime),
clock: mclock.System{},
}
}
// PredictFullConeNAT checks whether the local host is behind full cone NAT.
func (it *IPTracker) PredictFullConeNAT() bool {
now := it.clock.Now()
it.gcContact(now)
it.gcStatements(now)
for host := range it.statements {
if _, ok := it.contact[host]; !ok {
return true
}
}
return false
}
// PredictEndpoint returns the current prediction of the external endpoint.
func (it *IPTracker) PredictEndpoint() string {
it.gcStatements(it.clock.Now())
// The current strategy is simple: find the endpoint with most statements.
counts := make(map[string]int)
maxcount, max := 0, ""
for _, s := range it.statements {
c := counts[s.endpoint] + 1
counts[s.endpoint] = c
if c > maxcount && c >= it.minStatements {
maxcount, max = c, s.endpoint
}
}
return max
}
// AddStatement records that a certain host thinks our external endpoint is the one given.
func (it *IPTracker) AddStatement(host, endpoint string) {
it.statements[host] = ipStatement{endpoint, it.clock.Now()}
}
// AddContact records that a packet containing endpoint information has been sent to a certain host.
func (it *IPTracker) AddContact(host string) {
it.contact[host] = it.clock.Now()
}
func (it *IPTracker) gcStatements(now mclock.AbsTime) {
cutoff := now.Add(-it.window)
for host, s := range it.statements {
if s.time < cutoff {
delete(it.statements, host)
}
}
}
func (it *IPTracker) gcContact(now mclock.AbsTime) {
cutoff := now.Add(-it.contactWindow)
for host, ct := range it.contact {
if ct < cutoff {
delete(it.contact, host)
}
}
}

View file

@ -0,0 +1,85 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package netutil
import (
"testing"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
)
const (
opStatement = iota
opContact
opPredict
)
type iptrackTestEvent struct {
op int
time int // absolute, in milliseconds
ip, from string
}
func TestIPTracker(t *testing.T) {
tests := map[string][]iptrackTestEvent{
"minStatements": {
{opPredict, 0, "", ""},
{opStatement, 0, "127.0.0.1", "127.0.0.2"},
{opPredict, 1000, "", ""},
{opStatement, 1000, "127.0.0.1", "127.0.0.3"},
{opPredict, 1000, "", ""},
{opStatement, 1000, "127.0.0.1", "127.0.0.4"},
{opPredict, 1000, "127.0.0.1", ""},
},
"window": {
{opStatement, 0, "127.0.0.1", "127.0.0.2"},
{opStatement, 2000, "127.0.0.1", "127.0.0.3"},
{opStatement, 3000, "127.0.0.1", "127.0.0.4"},
{opPredict, 10000, "127.0.0.1", ""},
{opPredict, 10001, "", ""}, // first statement expired
{opStatement, 10100, "127.0.0.1", "127.0.0.2"},
{opPredict, 10200, "127.0.0.1", ""},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) { runIPTrackerTest(t, test) })
}
}
func runIPTrackerTest(t *testing.T, evs []iptrackTestEvent) {
var (
clock mclock.Simulated
it = NewIPTracker(10*time.Second, 10*time.Second, 3)
)
it.clock = &clock
for i, ev := range evs {
evtime := time.Duration(ev.time) * time.Millisecond
clock.Run(evtime - time.Duration(clock.Now()))
switch ev.op {
case opStatement:
it.AddStatement(ev.from, ev.ip)
case opContact:
it.AddContact(ev.from)
case opPredict:
if pred := it.PredictEndpoint(); pred != ev.ip {
t.Errorf("op %d: wrong prediction %q, want %q", i, pred, ev.ip)
}
}
}
}