1// Copyright 2018 The go-ethereum Authors
2// This file is part of the go-ethereum library.
3//
4// The go-ethereum library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Lesser 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// The go-ethereum library 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 Lesser General Public License for more details.
13//
14// You should have received a copy of the GNU Lesser General Public License
15// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16
17package netutil
18
19import (
20	"time"
21
22	"github.com/ethereum/go-ethereum/common/mclock"
23)
24
25// IPTracker predicts the external endpoint, i.e. IP address and port, of the local host
26// based on statements made by other hosts.
27type IPTracker struct {
28	window          time.Duration
29	contactWindow   time.Duration
30	minStatements   int
31	clock           mclock.Clock
32	statements      map[string]ipStatement
33	contact         map[string]mclock.AbsTime
34	lastStatementGC mclock.AbsTime
35	lastContactGC   mclock.AbsTime
36}
37
38type ipStatement struct {
39	endpoint string
40	time     mclock.AbsTime
41}
42
43// NewIPTracker creates an IP tracker.
44//
45// The window parameters configure the amount of past network events which are kept. The
46// minStatements parameter enforces a minimum number of statements which must be recorded
47// before any prediction is made. Higher values for these parameters decrease 'flapping' of
48// predictions as network conditions change. Window duration values should typically be in
49// the range of minutes.
50func NewIPTracker(window, contactWindow time.Duration, minStatements int) *IPTracker {
51	return &IPTracker{
52		window:        window,
53		contactWindow: contactWindow,
54		statements:    make(map[string]ipStatement),
55		minStatements: minStatements,
56		contact:       make(map[string]mclock.AbsTime),
57		clock:         mclock.System{},
58	}
59}
60
61// PredictFullConeNAT checks whether the local host is behind full cone NAT. It predicts by
62// checking whether any statement has been received from a node we didn't contact before
63// the statement was made.
64func (it *IPTracker) PredictFullConeNAT() bool {
65	now := it.clock.Now()
66	it.gcContact(now)
67	it.gcStatements(now)
68	for host, st := range it.statements {
69		if c, ok := it.contact[host]; !ok || c > st.time {
70			return true
71		}
72	}
73	return false
74}
75
76// PredictEndpoint returns the current prediction of the external endpoint.
77func (it *IPTracker) PredictEndpoint() string {
78	it.gcStatements(it.clock.Now())
79
80	// The current strategy is simple: find the endpoint with most statements.
81	counts := make(map[string]int)
82	maxcount, max := 0, ""
83	for _, s := range it.statements {
84		c := counts[s.endpoint] + 1
85		counts[s.endpoint] = c
86		if c > maxcount && c >= it.minStatements {
87			maxcount, max = c, s.endpoint
88		}
89	}
90	return max
91}
92
93// AddStatement records that a certain host thinks our external endpoint is the one given.
94func (it *IPTracker) AddStatement(host, endpoint string) {
95	now := it.clock.Now()
96	it.statements[host] = ipStatement{endpoint, now}
97	if time.Duration(now-it.lastStatementGC) >= it.window {
98		it.gcStatements(now)
99	}
100}
101
102// AddContact records that a packet containing our endpoint information has been sent to a
103// certain host.
104func (it *IPTracker) AddContact(host string) {
105	now := it.clock.Now()
106	it.contact[host] = now
107	if time.Duration(now-it.lastContactGC) >= it.contactWindow {
108		it.gcContact(now)
109	}
110}
111
112func (it *IPTracker) gcStatements(now mclock.AbsTime) {
113	it.lastStatementGC = now
114	cutoff := now.Add(-it.window)
115	for host, s := range it.statements {
116		if s.time < cutoff {
117			delete(it.statements, host)
118		}
119	}
120}
121
122func (it *IPTracker) gcContact(now mclock.AbsTime) {
123	it.lastContactGC = now
124	cutoff := now.Add(-it.contactWindow)
125	for host, ct := range it.contact {
126		if ct < cutoff {
127			delete(it.contact, host)
128		}
129	}
130}
131