diff --git a/btcjson/v2/btcjson/btcdextcmds.go b/btcjson/v2/btcjson/btcdextcmds.go
index a56d7d6c..a0e194c6 100644
--- a/btcjson/v2/btcjson/btcdextcmds.go
+++ b/btcjson/v2/btcjson/btcdextcmds.go
@@ -7,6 +7,42 @@
package btcjson
+// NodeSubCmd defines the type used in the addnode JSON-RPC command for the
+// sub command field.
+type NodeSubCmd string
+
+const (
+ // NConnect indicates the specified host that should be connected to.
+ NConnect NodeSubCmd = "connect"
+
+ // NRemove indicates the specified peer that should be removed as a
+ // persistent peer.
+ NRemove NodeSubCmd = "remove"
+
+ // NDisconnect indicates the specified peer should be disonnected.
+ NDisconnect NodeSubCmd = "disconnect"
+)
+
+// NodeCmd defines the dropnode JSON-RPC command.
+type NodeCmd struct {
+ SubCmd NodeSubCmd `jsonrpcusage:"\"connect|remove|disconnect\""`
+ Target string
+ ConnectSubCmd *string `jsonrpcusage:"\"perm|temp\""`
+}
+
+// NewNodeCmd returns a new instance which can be used to issue a `node`
+// JSON-RPC command.
+//
+// The parameters which are pointers indicate they are optional. Passing nil
+// for optional parameters will use the default value.
+func NewNodeCmd(subCmd NodeSubCmd, target string, connectSubCmd *string) *NodeCmd {
+ return &NodeCmd{
+ SubCmd: subCmd,
+ Target: target,
+ ConnectSubCmd: connectSubCmd,
+ }
+}
+
// DebugLevelCmd defines the debuglevel JSON-RPC command. This command is not a
// standard Bitcoin command. It is an extension for btcd.
type DebugLevelCmd struct {
@@ -45,6 +81,7 @@ func init() {
flags := UsageFlag(0)
MustRegisterCmd("debuglevel", (*DebugLevelCmd)(nil), flags)
+ MustRegisterCmd("node", (*NodeCmd)(nil), flags)
MustRegisterCmd("getbestblock", (*GetBestBlockCmd)(nil), flags)
MustRegisterCmd("getcurrentnet", (*GetCurrentNetCmd)(nil), flags)
}
diff --git a/btcjson/v2/btcjson/btcdextcmds_test.go b/btcjson/v2/btcjson/btcdextcmds_test.go
index 58ea83cc..9dd003c3 100644
--- a/btcjson/v2/btcjson/btcdextcmds_test.go
+++ b/btcjson/v2/btcjson/btcdextcmds_test.go
@@ -42,6 +42,64 @@ func TestBtcdExtCmds(t *testing.T) {
LevelSpec: "trace",
},
},
+ {
+ name: "node",
+ newCmd: func() (interface{}, error) {
+ return btcjson.NewCmd("node", btcjson.NRemove, "1.1.1.1")
+ },
+ staticCmd: func() interface{} {
+ return btcjson.NewNodeCmd("remove", "1.1.1.1", nil)
+ },
+ marshalled: `{"jsonrpc":"1.0","method":"node","params":["remove","1.1.1.1"],"id":1}`,
+ unmarshalled: &btcjson.NodeCmd{
+ SubCmd: btcjson.NRemove,
+ Target: "1.1.1.1",
+ },
+ },
+ {
+ name: "node",
+ newCmd: func() (interface{}, error) {
+ return btcjson.NewCmd("node", btcjson.NDisconnect, "1.1.1.1")
+ },
+ staticCmd: func() interface{} {
+ return btcjson.NewNodeCmd("disconnect", "1.1.1.1", nil)
+ },
+ marshalled: `{"jsonrpc":"1.0","method":"node","params":["disconnect","1.1.1.1"],"id":1}`,
+ unmarshalled: &btcjson.NodeCmd{
+ SubCmd: btcjson.NDisconnect,
+ Target: "1.1.1.1",
+ },
+ },
+ {
+ name: "node",
+ newCmd: func() (interface{}, error) {
+ return btcjson.NewCmd("node", btcjson.NConnect, "1.1.1.1", "perm")
+ },
+ staticCmd: func() interface{} {
+ return btcjson.NewNodeCmd("connect", "1.1.1.1", btcjson.String("perm"))
+ },
+ marshalled: `{"jsonrpc":"1.0","method":"node","params":["connect","1.1.1.1","perm"],"id":1}`,
+ unmarshalled: &btcjson.NodeCmd{
+ SubCmd: btcjson.NConnect,
+ Target: "1.1.1.1",
+ ConnectSubCmd: btcjson.String("perm"),
+ },
+ },
+ {
+ name: "node",
+ newCmd: func() (interface{}, error) {
+ return btcjson.NewCmd("node", btcjson.NConnect, "1.1.1.1", "temp")
+ },
+ staticCmd: func() interface{} {
+ return btcjson.NewNodeCmd("connect", "1.1.1.1", btcjson.String("temp"))
+ },
+ marshalled: `{"jsonrpc":"1.0","method":"node","params":["connect","1.1.1.1","temp"],"id":1}`,
+ unmarshalled: &btcjson.NodeCmd{
+ SubCmd: btcjson.NConnect,
+ Target: "1.1.1.1",
+ ConnectSubCmd: btcjson.String("temp"),
+ },
+ },
{
name: "getbestblock",
newCmd: func() (interface{}, error) {
diff --git a/docs/json_rpc_api.md b/docs/json_rpc_api.md
index 0a942a11..2456c1d6 100644
--- a/docs/json_rpc_api.md
+++ b/docs/json_rpc_api.md
@@ -186,7 +186,7 @@ the method name for further details such as parameter and return information.
| | |
|---|---|
|Method|addnode|
-|Parameters|1. peer (string, required) - ip address and port of the peer tooperate on
2. command (string, required) - `add` to add a persistent peer, `remove` to remove a persistent peer, or `onetry` to try a single connection to a peer|
+|Parameters|1. peer (string, required) - ip address and port of the peer to operate on
2. command (string, required) - `add` to add a persistent peer, `remove` to remove a persistent peer, or `onetry` to try a single connection to a peer|
|Description|Attempts to add or remove a persistent peer.|
|Returns|Nothing|
[Return to Overview](#MethodOverview)
@@ -553,6 +553,8 @@ The following is an overview of the RPC methods which are implemented by btcd, b
|2|[getbestblock](#getbestblock)|Y|Get block height and hash of best block in the main chain.|None|
|3|[getcurrentnet](#getcurrentnet)|Y|Get bitcoin network btcd is running on.|None|
|4|[searchrawtransactions](#searchrawtransactions)|Y|Query for transactions related to a particular address.|None|
+|5|[node](#node)|N|Attempts to add or remove a peer. |None|
+
**6.2 Method Details**
@@ -609,6 +611,18 @@ The following is an overview of the RPC methods which are implemented by btcd, b
***
+
+
+| | |
+|---|---|
+|Method|node|
+|Parameters|1. command (string, required) - `connect` to add a peer (defaults to temporary), `remove` to remove a persistent peer, or `disconnect` to remove all matching non-persistent peers
2. peer (string, required) - ip address and port, or ID of the peer to operate on
3. connection type (string, optional) - `perm` indicates the peer should be added as a permanent peer, `temp` indicates a connection should only be attempted once. |
+|Description|Attempts to add or remove a peer.|
+|Returns|Nothing|
+[Return to Overview](#MethodOverview)
+
+***
+
### 7. Websocket Extension Methods (Websocket-specific)
diff --git a/rpcserver.go b/rpcserver.go
index 0067b75e..7c9bdd34 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -155,6 +155,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{
"gettxout": handleGetTxOut,
"getwork": handleGetWork,
"help": handleHelp,
+ "node": handleNode,
"ping": handlePing,
"searchrawtransactions": handleSearchRawTransactions,
"sendrawtransaction": handleSendRawTransaction,
@@ -369,17 +370,125 @@ func handleAddNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in
case "onetry":
err = s.server.AddAddr(addr, false)
default:
- err = errors.New("invalid subcommand for addnode")
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: "invalid subcommand for addnode",
+ }
}
if err != nil {
- return nil, internalRPCError(err.Error(), "")
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: err.Error(),
+ }
}
// no data returned unless an error.
return nil, nil
}
+// handleNode handles node commands.
+func handleNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {
+ c := cmd.(*btcjson.NodeCmd)
+
+ var addr string
+ var nodeId uint64
+ var errN, err error
+ switch c.SubCmd {
+ case "disconnect":
+ // If we have a valid uint disconnect by node id. Otherwise,
+ // attempt to disconnect by address, returning an error if a
+ // valid IP address is not supplied.
+ if nodeId, errN = strconv.ParseUint(c.Target, 10, 32); errN == nil {
+ err = s.server.DisconnectNodeById(int32(nodeId))
+ } else {
+ if _, _, errP := net.SplitHostPort(c.Target); errP == nil || net.ParseIP(c.Target) != nil {
+ addr = normalizeAddress(c.Target, activeNetParams.DefaultPort)
+ err = s.server.DisconnectNodeByAddr(addr)
+ } else {
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: "invalid address or node ID",
+ }
+ }
+ }
+ if err != nil && peerExists(s.server.PeerInfo(), addr, int32(nodeId)) {
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCMisc,
+ Message: "can't disconnect a permanent peer, use remove",
+ }
+ }
+ case "remove":
+ // If we have a valid uint disconnect by node id. Otherwise,
+ // attempt to disconnect by address, returning an error if a
+ // valid IP address is not supplied.
+ if nodeId, errN = strconv.ParseUint(c.Target, 10, 32); errN == nil {
+ err = s.server.RemoveNodeById(int32(nodeId))
+ } else {
+ if _, _, errP := net.SplitHostPort(c.Target); errP == nil || net.ParseIP(c.Target) != nil {
+ addr = normalizeAddress(c.Target, activeNetParams.DefaultPort)
+ err = s.server.RemoveNodeByAddr(addr)
+ } else {
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: "invalid address or node ID",
+ }
+ }
+ }
+ if err != nil && peerExists(s.server.PeerInfo(), addr, int32(nodeId)) {
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCMisc,
+ Message: "can't remove a temporary peer, use disconnect",
+ }
+ }
+ case "connect":
+ addr = normalizeAddress(c.Target, activeNetParams.DefaultPort)
+
+ // Default to temporary connections.
+ subCmd := "temp"
+ if c.ConnectSubCmd != nil {
+ subCmd = *c.ConnectSubCmd
+ }
+
+ switch subCmd {
+ case "perm", "temp":
+ err = s.server.ConnectNode(addr, subCmd == "perm")
+ default:
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: "invalid subcommand for node connect",
+ }
+ }
+ default:
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: "invalid subcommand for node",
+ }
+ }
+
+ if err != nil {
+ return nil, &btcjson.RPCError{
+ Code: btcjson.ErrRPCInvalidParameter,
+ Message: err.Error(),
+ }
+ }
+
+ // no data returned unless an error.
+ return nil, nil
+}
+
+// peerExists determines if a certain peer is currently connected given
+// information about all currently connected peers. Peer existence is
+// determined using either a target address or node id.
+func peerExists(peerInfos []*btcjson.GetPeerInfoResult, addr string, nodeId int32) bool {
+ for _, peerInfo := range peerInfos {
+ if peerInfo.ID == nodeId || peerInfo.Addr == addr {
+ return true
+ }
+ }
+ return false
+}
+
// messageToHex serializes a message to the wire protocol encoding using the
// latest protocol version and returns a hex-encoded string of the result.
func messageToHex(msg wire.Message) (string, error) {
diff --git a/rpcserverhelp.go b/rpcserverhelp.go
index 78950e07..920e1849 100644
--- a/rpcserverhelp.go
+++ b/rpcserverhelp.go
@@ -33,6 +33,12 @@ var helpDescsEnUS = map[string]string{
"addnode-addr": "IP address and port of the peer to operate on",
"addnode-subcmd": "'add' to add a persistent peer, 'remove' to remove a persistent peer, or 'onetry' to try a single connection to a peer",
+ // NodeCmd help.
+ "node--synopsis": "Attempts to add or remove a peer.",
+ "node-subcmd": "'disconnect' to remove all matching non-persistent peers, 'remove' to remove a persistent peer, or 'connect' to connect to a peer",
+ "node-target": "Either the IP address and port of the peer to operate on, or a valid peer ID.",
+ "node-connectsubcmd": "'perm' to make the connected peer a permanent one, 'temp' to try a single connect to a peer",
+
// TransactionInput help.
"transactioninput-txid": "The hash of the input transaction",
"transactioninput-vout": "The specific output of the input transaction to redeem",
@@ -521,6 +527,7 @@ var rpcResultTypes = map[string][]interface{}{
"getrawtransaction": []interface{}{(*string)(nil), (*btcjson.TxRawResult)(nil)},
"gettxout": []interface{}{(*btcjson.GetTxOutResult)(nil)},
"getwork": []interface{}{(*btcjson.GetWorkResult)(nil), (*bool)(nil)},
+ "node": nil,
"help": []interface{}{(*string)(nil), (*string)(nil)},
"ping": nil,
"searchrawtransactions": []interface{}{(*string)(nil), (*[]btcjson.TxRawResult)(nil)},
diff --git a/server.go b/server.go
index 160c9c6f..f559ee92 100644
--- a/server.go
+++ b/server.go
@@ -411,7 +411,7 @@ type addNodeMsg struct {
}
type delNodeMsg struct {
- addr string
+ cmp func(*peer) bool
reply chan error
}
@@ -419,6 +419,22 @@ type getAddedNodesMsg struct {
reply chan []*peer
}
+type disconnectNodeMsg struct {
+ cmp func(*peer) bool
+ reply chan error
+}
+
+type connectNodeMsg struct {
+ addr string
+ permanent bool
+ reply chan error
+}
+
+type removeNodeMsg struct {
+ cmp func(*peer) bool
+ reply chan error
+}
+
// handleQuery is the central handler for all queries and commands from other
// goroutines related to peer state.
func (s *server) handleQuery(querymsg interface{}, state *peerState) {
@@ -475,16 +491,20 @@ func (s *server) handleQuery(querymsg interface{}, state *peerState) {
msg.reply <- infos
case addNodeMsg:
+ case connectNodeMsg:
// XXX(oga) duplicate oneshots?
- if msg.permanent {
- for e := state.persistentPeers.Front(); e != nil; e = e.Next() {
- peer := e.Value.(*peer)
- if peer.addr == msg.addr {
+ for e := state.persistentPeers.Front(); e != nil; e = e.Next() {
+ peer := e.Value.(*peer)
+ if peer.addr == msg.addr {
+ if msg.permanent {
msg.reply <- errors.New("peer already connected")
- return
+ } else {
+ msg.reply <- errors.New("peer exists as a permanent peer")
}
+ return
}
}
+
// TODO(oga) if too many, nuke a non-perm peer.
if s.handleAddPeerMsg(state,
newOutboundPeer(s, msg.addr, msg.permanent, 0)) {
@@ -494,21 +514,12 @@ func (s *server) handleQuery(querymsg interface{}, state *peerState) {
}
case delNodeMsg:
- found := false
- for e := state.persistentPeers.Front(); e != nil; e = e.Next() {
- peer := e.Value.(*peer)
- if peer.addr == msg.addr {
- // Keep group counts ok since we remove from
- // the list now.
- state.outboundGroups[addrmgr.GroupKey(peer.na)]--
- // This is ok because we are not continuing
- // to iterate so won't corrupt the loop.
- state.persistentPeers.Remove(e)
- peer.Disconnect()
- found = true
- break
- }
- }
+ case removeNodeMsg:
+ found := disconnectPeer(state.persistentPeers, msg.cmp, func(p *peer) {
+ // Keep group counts ok since we remove from
+ // the list now.
+ state.outboundGroups[addrmgr.GroupKey(p.na)]--
+ })
if found {
msg.reply <- nil
@@ -525,9 +536,63 @@ func (s *server) handleQuery(querymsg interface{}, state *peerState) {
peers = append(peers, peer)
}
msg.reply <- peers
+ case disconnectNodeMsg:
+ // Check inbound peers. We pass a nil callback since we don't
+ // require any additional actions on disconnect for inbound peers.
+ found := disconnectPeer(state.peers, msg.cmp, nil)
+ if found {
+ msg.reply <- nil
+ return
+ }
+
+ // Check outbound peers.
+ found = disconnectPeer(state.outboundPeers, msg.cmp, func(p *peer) {
+ // Keep group counts ok since we remove from
+ // the list now.
+ state.outboundGroups[addrmgr.GroupKey(p.na)]--
+ })
+ if found {
+ // If there are multiple outbound connections to the same
+ // ip:port, continue disconnecting them all until no such
+ // peers are found.
+ for found {
+ found = disconnectPeer(state.outboundPeers, msg.cmp, func(p *peer) {
+ state.outboundGroups[addrmgr.GroupKey(p.na)]--
+ })
+ }
+ msg.reply <- nil
+ return
+ }
+
+ msg.reply <- errors.New("peer not found")
}
}
+// disconnectPeer attempts to drop the connection of a tageted peer in the
+// passed peer list. Targets are identified via usage of the passed
+// `compareFunc`, which should return `true` if the passed peer is the target
+// peer. This function returns true on success and false if the peer is unable
+// to be located. If the peer is found, and the passed callback: `whenFound'
+// isn't nil, we call it with the peer as the argument before it is removed
+// from the peerList, and is disconnected from the server.
+func disconnectPeer(peerList *list.List, compareFunc func(*peer) bool, whenFound func(*peer)) bool {
+ for e := peerList.Front(); e != nil; e = e.Next() {
+ peer := e.Value.(*peer)
+ if compareFunc(peer) {
+ if whenFound != nil {
+ whenFound(peer)
+ }
+
+ // This is ok because we are not continuing
+ // to iterate so won't corrupt the loop.
+ peerList.Remove(e)
+ peer.Disconnect()
+ return true
+ }
+ }
+ return false
+}
+
// listenHandler is the main listener which accepts incoming connections for the
// server. It must be run as a goroutine.
func (s *server) listenHandler(listener net.Listener) {
@@ -831,7 +896,75 @@ func (s *server) AddAddr(addr string, permanent bool) error {
func (s *server) RemoveAddr(addr string) error {
replyChan := make(chan error)
- s.query <- delNodeMsg{addr: addr, reply: replyChan}
+ s.query <- delNodeMsg{
+ cmp: func(p *peer) bool { return p.addr == addr },
+ reply: replyChan,
+ }
+
+ return <-replyChan
+}
+
+// DisconnectNodeByAddr disconnects a peer by target address. Both outbound and
+// inbound nodes will be searched for the target node. An error message will
+// be returned if the peer was not found.
+func (s *server) DisconnectNodeByAddr(addr string) error {
+ replyChan := make(chan error)
+
+ s.query <- disconnectNodeMsg{
+ cmp: func(p *peer) bool { return p.addr == addr },
+ reply: replyChan,
+ }
+
+ return <-replyChan
+}
+
+// DisconnectNodeByID disconnects a peer by target node id. Both outbound and
+// inbound nodes will be searched for the target node. An error message will be
+// returned if the peer was not found.
+func (s *server) DisconnectNodeById(id int32) error {
+ replyChan := make(chan error)
+
+ s.query <- disconnectNodeMsg{
+ cmp: func(p *peer) bool { return p.id == id },
+ reply: replyChan,
+ }
+
+ return <-replyChan
+}
+
+// RemoveNodeByAddr removes a peer from the list of persistent peers if
+// present. An error will be returned if the peer was not found.
+func (s *server) RemoveNodeByAddr(addr string) error {
+ replyChan := make(chan error)
+
+ s.query <- removeNodeMsg{
+ cmp: func(p *peer) bool { return p.addr == addr },
+ reply: replyChan,
+ }
+
+ return <-replyChan
+}
+
+// RemoveNodeById removes a peer by node ID from the list of persistent peers
+// if present. An error will be returned if the peer was not found.
+func (s *server) RemoveNodeById(id int32) error {
+ replyChan := make(chan error)
+
+ s.query <- removeNodeMsg{
+ cmp: func(p *peer) bool { return p.id == id },
+ reply: replyChan,
+ }
+
+ return <-replyChan
+}
+
+// ConnectNode adds `addr' as a new outbound peer. If permanent is true then the
+// peer will be persistent and reconnect if the connection is lost.
+// It is an error to call this with an already existing peer.
+func (s *server) ConnectNode(addr string, permanent bool) error {
+ replyChan := make(chan error)
+
+ s.query <- connectNodeMsg{addr: addr, permanent: permanent, reply: replyChan}
return <-replyChan
}