diff --git a/cmd/geth/admin.go b/cmd/geth/admin.go index 31f8d4400..a07e694de 100644 --- a/cmd/geth/admin.go +++ b/cmd/geth/admin.go @@ -25,7 +25,7 @@ func (js *jsre) adminBindings() { js.re.Set("admin", struct{}{}) t, _ := js.re.Get("admin") admin := t.Object() - admin.Set("suggestPeer", js.suggestPeer) + admin.Set("trustPeer", js.trustPeer) admin.Set("startRPC", js.startRPC) admin.Set("stopRPC", js.stopRPC) admin.Set("nodeInfo", js.nodeInfo) @@ -243,13 +243,13 @@ func (js *jsre) stopRPC(call otto.FunctionCall) otto.Value { return otto.FalseValue() } -func (js *jsre) suggestPeer(call otto.FunctionCall) otto.Value { +func (js *jsre) trustPeer(call otto.FunctionCall) otto.Value { nodeURL, err := call.Argument(0).ToString() if err != nil { fmt.Println(err) return otto.FalseValue() } - err = js.ethereum.SuggestPeer(nodeURL) + err = js.ethereum.TrustPeer(nodeURL) if err != nil { fmt.Println(err) return otto.FalseValue() diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ef007051c..d9d1c1b15 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -232,7 +232,8 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso utils.IdentityFlag, utils.UnlockedAccountFlag, utils.PasswordFileFlag, - utils.BootnodesFlag, + utils.BootNodesFlag, + utils.TrustedNodesFlag, utils.DataDirFlag, utils.BlockchainVersionFlag, utils.JSpathFlag, diff --git a/cmd/mist/main.go b/cmd/mist/main.go index 1030d6ada..18fb919b4 100644 --- a/cmd/mist/main.go +++ b/cmd/mist/main.go @@ -69,7 +69,7 @@ func init() { assetPathFlag, rpcCorsFlag, - utils.BootnodesFlag, + utils.BootNodesFlag, utils.DataDirFlag, utils.ListenPortFlag, utils.LogFileFlag, diff --git a/cmd/mist/ui_lib.go b/cmd/mist/ui_lib.go index 34ce56e77..e1a3aa254 100644 --- a/cmd/mist/ui_lib.go +++ b/cmd/mist/ui_lib.go @@ -104,8 +104,8 @@ func (ui *UiLib) Connect(button qml.Object) { } func (ui *UiLib) ConnectToPeer(nodeURL string) { - if err := ui.eth.SuggestPeer(nodeURL); err != nil { - guilogger.Infoln("SuggestPeer error: " + err.Error()) + if err := ui.eth.TrustPeer(nodeURL); err != nil { + guilogger.Infoln("TrustPeer error: " + err.Error()) } } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c013510d8..f52bfc21f 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -202,11 +202,16 @@ var ( Usage: "Network listening port", Value: 30303, } - BootnodesFlag = cli.StringFlag{ + BootNodesFlag = cli.StringFlag{ Name: "bootnodes", Usage: "Space-separated enode URLs for p2p discovery bootstrap", Value: "", } + TrustedNodesFlag = cli.StringFlag{ + Name: "trustednodes", + Usage: "List of trusted nodes (either an enode list or path to a json file of enodes)", + Value: "", + } NodeKeyFileFlag = cli.StringFlag{ Name: "nodekey", Usage: "P2P node key file", @@ -292,7 +297,8 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config { NodeKey: GetNodeKey(ctx), Shh: ctx.GlobalBool(WhisperEnabledFlag.Name), Dial: true, - BootNodes: ctx.GlobalString(BootnodesFlag.Name), + BootNodes: ctx.GlobalString(BootNodesFlag.Name), + TrustedNodes: ctx.GlobalString(TrustedNodesFlag.Name), } } diff --git a/eth/backend.go b/eth/backend.go index c5fa328b0..f8d57c985 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -2,7 +2,10 @@ package eth import ( "crypto/ecdsa" + "encoding/json" "fmt" + "io/ioutil" + "os" "path" "strings" "time" @@ -56,10 +59,13 @@ type Config struct { MaxPeers int Port string - // This should be a space-separated list of - // discovery node URLs. + // Space-separated list of discovery node URLs BootNodes string + // Either a space-separated list of discovery node URLs, or a path to a json + // file containing such a list. + TrustedNodes string + // This key is used to identify the node on the network. // If nil, an ephemeral key is used. NodeKey *ecdsa.PrivateKey @@ -96,6 +102,46 @@ func (cfg *Config) parseBootNodes() []*discover.Node { return ns } +// parseTrustedNodes parses a list of discovery node URLs either given literally, +// or loaded from a .json file. +func (cfg *Config) parseTrustedNodes() []*discover.Node { + // Short circuit if no trusted nodes were given + if cfg.TrustedNodes == "" { + return nil + } + // Try to interpret the trusted node config as a .json file + if _, err := os.Stat(cfg.TrustedNodes); err == nil { + // Load the file from disk + blob, err := ioutil.ReadFile(cfg.TrustedNodes) + if err != nil { + glog.V(logger.Error).Infof("Failed to access trusted nodes: %v", err) + return nil + } + // Interpret the json contents + list := []string{} + if err := json.Unmarshal(blob, &list); err != nil { + glog.V(logger.Error).Infof("Failed to load trusted nodes: %v", err) + return nil + } + // Swap out the configuration for the actual nodes + cfg.TrustedNodes = strings.Join(list, " ") + } + // Interpret the list as a discovery node array + var nodes []*discover.Node + for _, url := range strings.Split(cfg.TrustedNodes, " ") { + if url == "" { + continue + } + node, err := discover.ParseNode(url) + if err != nil { + glog.V(logger.Error).Infof("Trusted node URL %s: %v\n", url, err) + continue + } + nodes = append(nodes, node) + } + return nodes +} + func (cfg *Config) nodeKey() (*ecdsa.PrivateKey, error) { // use explicit key from command line args if set if cfg.NodeKey != nil { @@ -247,6 +293,7 @@ func New(config *Config) (*Ethereum, error) { NAT: config.NAT, NoDial: !config.Dial, BootstrapNodes: config.parseBootNodes(), + TrustedNodes: config.parseTrustedNodes(), NodeDatabase: nodeDb, } if len(config.Port) > 0 { @@ -442,12 +489,13 @@ func (s *Ethereum) StartForTest() { s.txPool.Start() } -func (self *Ethereum) SuggestPeer(nodeURL string) error { +// TrustPeer injects a new node into the list of privileged nodes. +func (self *Ethereum) TrustPeer(nodeURL string) error { n, err := discover.ParseNode(nodeURL) if err != nil { return fmt.Errorf("invalid node URL: %v", err) } - self.net.SuggestPeer(n) + self.net.TrustPeer(n) return nil } diff --git a/p2p/server.go b/p2p/server.go index 5c5883ae8..794c36125 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -18,8 +18,9 @@ import ( ) const ( - defaultDialTimeout = 10 * time.Second - refreshPeersInterval = 30 * time.Second + defaultDialTimeout = 10 * time.Second + refreshPeersInterval = 30 * time.Second + trustedPeerCheckInterval = 15 * time.Second // This is the maximum number of inbound connection // that are allowed to linger between 'accepted' and @@ -59,6 +60,10 @@ type Server struct { // with the rest of the network. BootstrapNodes []*discover.Node + // Trusted nodes are used as privileged connections which are always accepted + // and also always maintained. + TrustedNodes []*discover.Node + // NodeDatabase is the path to the database containing the previously seen // live nodes in the network. NodeDatabase string @@ -99,13 +104,15 @@ type Server struct { running bool peers map[discover.NodeID]*Peer + trusts map[discover.NodeID]*discover.Node // Map of currently trusted remote nodes + trustDial chan *discover.Node // Dial request channel reserved for the trusted nodes + ntab *discover.Table listener net.Listener - quit chan struct{} - loopWG sync.WaitGroup // {dial,listen,nat}Loop - peerWG sync.WaitGroup // active peer goroutines - peerConnect chan *discover.Node + quit chan struct{} + loopWG sync.WaitGroup // {dial,listen,nat}Loop + peerWG sync.WaitGroup // active peer goroutines } type setupFunc func(net.Conn, *ecdsa.PrivateKey, *protoHandshake, *discover.Node, bool) (*conn, error) @@ -131,10 +138,9 @@ func (srv *Server) PeerCount() int { return n } -// SuggestPeer creates a connection to the given Node if it -// is not already connected. -func (srv *Server) SuggestPeer(n *discover.Node) { - srv.peerConnect <- n +// TrustPeer inserts a node into the list of privileged nodes. +func (srv *Server) TrustPeer(node *discover.Node) { + srv.trustDial <- node } // Broadcast sends an RLP-encoded message to all connected peers. @@ -195,7 +201,14 @@ func (srv *Server) Start() (err error) { } srv.quit = make(chan struct{}) srv.peers = make(map[discover.NodeID]*Peer) - srv.peerConnect = make(chan *discover.Node) + + // Create the current trust map, and the associated dialing channel + srv.trusts = make(map[discover.NodeID]*discover.Node) + for _, node := range srv.TrustedNodes { + srv.trusts[node.ID] = node + } + srv.trustDial = make(chan *discover.Node) + if srv.setupFunc == nil { srv.setupFunc = setupConn } @@ -229,6 +242,8 @@ func (srv *Server) Start() (err error) { if srv.NoDial && srv.ListenAddr == "" { glog.V(logger.Warn).Infoln("I will be kind-of useless, neither dialing nor listening.") } + // maintain the trusted peers + go srv.trustLoop() srv.running = true return nil @@ -323,6 +338,45 @@ func (srv *Server) listenLoop() { } } +// trustLoop is responsible for periodically checking that trusted connections +// are actually live, and requests dialing if not. +func (srv *Server) trustLoop() { + // Create a ticker for verifying trusted connections + tick := time.Tick(trustedPeerCheckInterval) + + for { + select { + case <-srv.quit: + // Termination requested, simple return + return + + case <-tick: + // Collect all the non-connected trusted nodes + needed := []*discover.Node{} + srv.lock.RLock() + for id, node := range srv.trusts { + if _, ok := srv.peers[id]; !ok { + needed = append(needed, node) + } + } + srv.lock.RUnlock() + + // Try to dial each of them (don't hang if server terminates) + for _, node := range needed { + glog.V(logger.Error).Infof("Dialing trusted peer %v", node) + select { + case srv.trustDial <- node: + // Ok, dialing + + case <-srv.quit: + // Terminating, return + return + } + } + } + } +} + func (srv *Server) dialLoop() { var ( dialed = make(chan *discover.Node) @@ -373,7 +427,7 @@ func (srv *Server) dialLoop() { // below MaxPeers. refresh.Reset(refreshPeersInterval) } - case dest := <-srv.peerConnect: + case dest := <-srv.trustDial: dial(dest) case dests := <-findresults: for _, dest := range dests { @@ -472,16 +526,26 @@ func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { return true, 0 } +// checkPeer verifies whether a peer looks promising and should be allowed/kept +// in the pool, or if it's of no use. func (srv *Server) checkPeer(id discover.NodeID) (bool, DiscReason) { + // First up, figure out if the peer is trusted + _, trusted := srv.trusts[id] + + // Make sure the peer passes all required checks switch { case !srv.running: return false, DiscQuitting - case len(srv.peers) >= srv.MaxPeers: + + case !trusted && len(srv.peers) >= srv.MaxPeers: return false, DiscTooManyPeers + case srv.peers[id] != nil: return false, DiscAlreadyConnected + case id == srv.ntab.Self().ID: return false, DiscSelf + default: return true, 0 } diff --git a/p2p/server_test.go b/p2p/server_test.go index 53cc3c258..e99d37ed0 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -102,7 +102,7 @@ func TestServerDial(t *testing.T) { // tell the server to connect tcpAddr := listener.Addr().(*net.TCPAddr) - srv.SuggestPeer(&discover.Node{IP: tcpAddr.IP, TCPPort: tcpAddr.Port}) + srv.trustDial <-&discover.Node{IP: tcpAddr.IP, TCPPort: tcpAddr.Port} select { case conn := <-accepted: