diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index 95993dac..30024dda 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -48,6 +48,15 @@ func NewAddNodeCmd(addr string, subCmd AddNodeSubCmd) *AddNodeCmd { } } +// ClearBannedCmd defines the clearbanned JSON-RPC command. +type ClearBannedCmd struct{} + +// NewClearBannedCmd returns a new instance which can be used to issue an clearbanned +// JSON-RPC command. +func NewClearBannedCmd() *ClearBannedCmd { + return &ClearBannedCmd{} +} + // TransactionInput represents the inputs to a transaction. Specifically a // transaction hash and output number pair. type TransactionInput struct { @@ -757,6 +766,15 @@ func NewInvalidateBlockCmd(blockHash string) *InvalidateBlockCmd { } } +// ListBannedCmd defines the listbanned JSON-RPC command. +type ListBannedCmd struct{} + +// NewListBannedCmd returns a new instance which can be used to issue a listbanned +// JSON-RPC command. +func NewListBannedCmd() *ListBannedCmd { + return &ListBannedCmd{} +} + // PingCmd defines the ping JSON-RPC command. type PingCmd struct{} @@ -903,6 +921,39 @@ func NewBitcoindSendRawTransactionCmd(hexTx string, maxFeeRate int32) *SendRawTr } } +// SetBanSubCmd defines the type used in the setban JSON-RPC command for the +// sub command field. +type SetBanSubCmd string + +const ( + // SBAdd indicates the specified host should be added as a persistent + // peer. + SBAdd SetBanSubCmd = "add" + + // SBRemove indicates the specified peer should be removed. + SBRemove SetBanSubCmd = "remove" +) + +// SetBanCmd defines the setban JSON-RPC command. +type SetBanCmd struct { + Addr string + SubCmd SetBanSubCmd `jsonrpcusage:"\"add|remove\""` + BanTime *int `jsonrpcdefault:"0"` + Absolute *bool `jsonrpcdefault:"false"` +} + +// NewSetBanCmd returns a new instance which can be used to issue an setban +// JSON-RPC command. +func NewSetBanCmd(addr string, subCmd SetBanSubCmd, banTime *int, + absolute *bool) *SetBanCmd { + return &SetBanCmd{ + Addr: addr, + SubCmd: subCmd, + BanTime: banTime, + Absolute: absolute, + } +} + // SetGenerateCmd defines the setgenerate JSON-RPC command. type SetGenerateCmd struct { Generate bool @@ -1080,6 +1131,9 @@ func init() { MustRegisterCmd("getnetworkhashps", (*GetNetworkHashPSCmd)(nil), flags) MustRegisterCmd("getnodeaddresses", (*GetNodeAddressesCmd)(nil), flags) MustRegisterCmd("getpeerinfo", (*GetPeerInfoCmd)(nil), flags) + MustRegisterCmd("listbanned", (*ListBannedCmd)(nil), flags) + MustRegisterCmd("setban", (*SetBanCmd)(nil), flags) + MustRegisterCmd("clearbanned", (*ClearBannedCmd)(nil), flags) MustRegisterCmd("getrawmempool", (*GetRawMempoolCmd)(nil), flags) MustRegisterCmd("getrawtransaction", (*GetRawTransactionCmd)(nil), flags) MustRegisterCmd("gettxout", (*GetTxOutCmd)(nil), flags) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index c6a5d550..ad71dcd3 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -721,6 +721,15 @@ type InfoChainResult struct { Errors string `json:"errors"` } +// ListBannedResult models the data returned from the listbanned command. +type ListBannedResult struct { + Address string `json:"address"` + BanCreated int64 `json:"ban_created"` + BannedUntil int64 `json:"banned_until"` + BanDuration int64 `json:"ban_duration"` + TimeRemaining int64 `json:"time_remaining"` +} + // TxRawResult models the data from the getrawtransaction command. type TxRawResult struct { Hex string `json:"hex"` diff --git a/rpcadapters.go b/rpcadapters.go index 2a1af72f..d6f809fc 100644 --- a/rpcadapters.go +++ b/rpcadapters.go @@ -6,6 +6,7 @@ package main import ( "sync/atomic" + "time" "github.com/lbryio/lbcd/blockchain" "github.com/lbryio/lbcd/chaincfg/chainhash" @@ -181,6 +182,57 @@ func (cm *rpcConnManager) ConnectedPeers() []rpcserverPeer { return peers } +// BannedPeers returns a map consisting of all banned host with banned period. +// +// This function is safe for concurrent access and is part of the +// rpcserverConnManager interface implementation. +func (cm *rpcConnManager) BannedPeers() map[string]bannedPeriod { + replyChan := make(chan map[string]bannedPeriod) + cm.server.query <- listBannedPeersMsg{reply: replyChan} + return <-replyChan +} + +// SetBan removes the peer associated with the provided address from the +// list of persistent peers. +// +// This function is safe for concurrent access and is part of the +// rpcserverConnManager interface implementation. +func (cm *rpcConnManager) SetBan(addr string, since, until time.Time) error { + replyChan := make(chan error) + cm.server.query <- setBanMsg{ + addr: addr, + since: since, + until: until, + reply: replyChan, + } + return <-replyChan +} + +// RemoveBan removes a host from banned list. +// +// This function is safe for concurrent access and is part of the +// rpcserverConnManager interface implementation. +func (cm *rpcConnManager) RemoveBan(addr string) error { + replyChan := make(chan error) + cm.server.query <- removeBanMsg{ + addr: addr, + reply: replyChan, + } + return <-replyChan +} + +// ClearBanned removes all banned host with banned period. +// +// This function is safe for concurrent access and is part of the +// rpcserverConnManager interface implementation. +func (cm *rpcConnManager) ClearBanned() error { + replyChan := make(chan error) + cm.server.query <- clearBannedMsg{ + reply: replyChan, + } + return <-replyChan +} + // PersistentPeers returns an array consisting of all the added persistent // peers. // diff --git a/rpcclient/net.go b/rpcclient/net.go index 2d268235..385875df 100644 --- a/rpcclient/net.go +++ b/rpcclient/net.go @@ -355,6 +355,75 @@ func (c *Client) GetPeerInfo() ([]btcjson.GetPeerInfoResult, error) { return c.GetPeerInfoAsync().Receive() } +// FutureListBannedResult is a future promise to deliver the result of a +// ListBannedAsync RPC invocation (or an applicable error). +type FutureListBannedResult chan *Response + +// Receive waits for the Response promised by the future and returns data about +// each connected network peer. +func (r FutureListBannedResult) Receive() ([]btcjson.ListBannedResult, error) { + res, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal result as an array of ListBanned result objects. + var bannedPeers []btcjson.ListBannedResult + err = json.Unmarshal(res, &bannedPeers) + if err != nil { + return nil, err + } + + return bannedPeers, nil +} + +// SetBanCommand enumerates the available commands that the SetBanCommand function +// accepts. +type SetBanCommand string + +// Constants used to indicate the command for the SetBanCommand function. +const ( + // SBAdd indicates the specified host should be added as a banned + // peer. + SBAdd SetBanCommand = "add" + + // SBRemove indicates the specified peer should be removed. + SBRemove SetBanCommand = "remove" +) + +// String returns the SetBanCommand in human-readable form. +func (cmd SetBanCommand) String() string { + return string(cmd) +} + +// FutureSetBanResult is a future promise to deliver the result of an +// SetBanAsync RPC invocation (or an applicable error). +type FutureSetBanResult chan *Response + +// Receive waits for the Response promised by the future and returns an error if +// any occurred when performing the specified command. +func (r FutureSetBanResult) Receive() error { + _, err := ReceiveFuture(r) + return err +} + +// SetBanAsync returns an instance of a type that can be used to get the result +// of the RPC at some future time by invoking the Receive function on the +// returned instance. +func (c *Client) SetBanAsync(addr string, command string, banTime *int, + absolute *bool) FutureSetBanResult { + cmd := btcjson.NewSetBanCmd(addr, btcjson.SetBanSubCmd(command), banTime, + absolute) + return c.SendCmd(cmd) +} + +// SetBan attempts to perform the passed command on the passed persistent peer. +// For example, it can be used to add or a remove a banned peer. +func (c *Client) SetBan(addr string, command string, banTime *int, + absolute *bool) error { + return c.SetBanAsync(addr, command, banTime, absolute).Receive() +} + // FutureGetNetTotalsResult is a future promise to deliver the result of a // GetNetTotalsAsync RPC invocation (or an applicable error). type FutureGetNetTotalsResult chan *Response diff --git a/rpcserver.go b/rpcserver.go index 55b0485e..f02e347a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -134,6 +134,7 @@ type commandHandler func(*rpcServer, interface{}, <-chan struct{}) (interface{}, var rpcHandlers map[string]commandHandler var rpcHandlersBeforeInit = map[string]commandHandler{ "addnode": handleAddNode, + "clearbanned": handleClearBanned, "createrawtransaction": handleCreateRawTransaction, "debuglevel": handleDebugLevel, "decoderawtransaction": handleDecodeRawTransaction, @@ -150,10 +151,10 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "getblockcount": handleGetBlockCount, "getblockhash": handleGetBlockHash, "getblockheader": handleGetBlockHeader, - "getchaintips": handleGetChainTips, "getblocktemplate": handleGetBlockTemplate, "getcfilter": handleGetCFilter, "getcfilterheader": handleGetCFilterHeader, + "getchaintips": handleGetChainTips, "getconnectioncount": handleGetConnectionCount, "getcurrentnet": handleGetCurrentNet, "getdifficulty": handleGetDifficulty, @@ -161,8 +162,8 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "gethashespersec": handleGetHashesPerSec, "getheaders": handleGetHeaders, "getinfo": handleGetInfo, - "getmempoolinfo": handleGetMempoolInfo, "getmempoolentry": handleGetMempoolEntry, + "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, "getnettotals": handleGetNetTotals, "getnetworkhashps": handleGetNetworkHashPS, @@ -174,11 +175,13 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "gettxout": handleGetTxOut, "help": handleHelp, "invalidateblock": handleInvalidateBlock, + "listbanned": handleListBanned, "node": handleNode, "ping": handlePing, "reconsiderblock": handleReconsiderBlock, "searchrawtransactions": handleSearchRawTransactions, "sendrawtransaction": handleSendRawTransaction, + "setban": handleSetBan, "setgenerate": handleSetGenerate, "signmessagewithprivkey": handleSignMessageWithPrivKey, "stop": handleStop, @@ -398,6 +401,21 @@ func handleAddNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in return nil, nil } +// handleClearBanned handles clearbanned commands. +func handleClearBanned(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + + err := s.cfg.ConnMgr.ClearBanned() + if err != nil { + 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) @@ -3146,6 +3164,24 @@ func handleHelp(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter return help, nil } +// handleListBanned handles the listbanned command. +func handleListBanned(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + banned := s.cfg.ConnMgr.BannedPeers() + reply := make([]*btcjson.ListBannedResult, 0, len(banned)) + for address, period := range banned { + since, until := period.since, period.until + r := btcjson.ListBannedResult{ + Address: address, + BanCreated: since.Unix(), + BannedUntil: until.Unix(), + BanDuration: int64(until.Sub(since).Seconds()), + TimeRemaining: int64(time.Until(until).Seconds()), + } + reply = append(reply, &r) + } + return reply, nil +} + // handlePing implements the ping command. func handlePing(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { // Ask server to ping \o_ @@ -3749,6 +3785,59 @@ func handleSendRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan st return tx.Hash().String(), nil } +// handleSetBan handles the setban command. +func handleSetBan(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.SetBanCmd) + + addr := net.ParseIP(c.Addr) + if addr == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "invalid addr for setban", + } + } + + since := time.Now() + until := since.Add(time.Second * time.Duration(*c.BanTime)) + if *c.BanTime == 0 { + until = since.Add(defaultBanDuration) + } + if *c.Absolute { + until = time.Unix(int64(*c.BanTime), 0) + } + + var err error + switch c.SubCmd { + case "add": + err = s.cfg.ConnMgr.SetBan(addr.String(), since, until) + addr := addr.String() + peers := s.cfg.ConnMgr.ConnectedPeers() + for _, peer := range peers { + p := peer.ToPeer() + if p.NA().IP.String() == addr { + p.Disconnect() + } + } + case "remove": + err = s.cfg.ConnMgr.RemoveBan(addr.String()) + default: + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "invalid subcommand for setban", + } + } + + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: err.Error(), + } + } + + // no data returned unless an error. + return nil, nil +} + // handleSetGenerate implements the setgenerate command. func handleSetGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.SetGenerateCmd) @@ -4806,6 +4895,18 @@ type rpcserverConnManager interface { // ConnectedPeers returns an array consisting of all connected peers. ConnectedPeers() []rpcserverPeer + // BannedPeers returns an array consisting of all Banned peers. + BannedPeers() map[string]bannedPeriod + + // SetBan add the addr to the ban list. + SetBan(addr string, since time.Time, until time.Time) error + + // RemoveBan remove the subnet from the ban list. + RemoveBan(subnet string) error + + // ClearBanned removes all banned IPs. + ClearBanned() error + // PersistentPeers returns an array consisting of all the persistent // peers. PersistentPeers() []rpcserverPeer diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 2be322ce..b7680783 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -56,6 +56,9 @@ var helpDescsEnUS = map[string]string{ "createrawtransaction-locktime": "Locktime value; a non-zero value will also locktime-activate the inputs", "createrawtransaction--result0": "Hex-encoded bytes of the serialized transaction", + // ClearBannedCmd help. + "clearbanned--synopsis": "Clear all banned IPs.", + // ScriptSig help. "scriptsig-asm": "Disassembly of the script", "scriptsig-hex": "Hex-encoded bytes of the script", @@ -636,6 +639,16 @@ var helpDescsEnUS = map[string]string{ "ping--synopsis": "Queues a ping to be sent to each connected peer.\n" + "Ping times are provided by getpeerinfo via the pingtime and pingwait fields.", + // ListBannedCmd help. + "listbanned--synopsis": "List all banned IPs.", + + // ListBannedResult help. + "listbannedresult-address": "The IP of the banned node.", + "listbannedresult-ban_created": "The UNIX epoch time the ban was created.", + "listbannedresult-banned_until": "The UNIX epoch time the ban expires.", + "listbannedresult-ban_duration": "The duration of the ban, in seconds.", + "listbannedresult-time_remaining": "The time remaining on the ban, in seconds", + // ReconsiderBlockCmd "reconsiderblock--synopsis": "Reconsider a block for validation.", "reconsiderblock-blockhash": "Hash of the block you want to reconsider", @@ -664,6 +677,13 @@ var helpDescsEnUS = map[string]string{ "sendrawtransaction--result0": "The hash of the transaction", "allowhighfeesormaxfeerate-value": "Either the boolean value for the allowhighfees parameter in bitcoind < v0.19.0 or the numerical value for the maxfeerate field in bitcoind v0.19.0 and later", + // SetBanCmd help. + "setban--synopsis": "Add or remove an IP from the banned list. (Currently, subnet is not supported.)", + "setban-addr": "The IP to ban. (Currently, subnet is not supported.)", + "setban-subcmd": "'add' to add an IP to the list, 'remove' to remove an IP from the list", + "setban-bantime": "Time in seconds the IP is banned (0 or empty means using the default time of 24h which can also be overwritten by the -bantime startup argument)", + "setban-absolute": "If set, the bantime must be an absolute timestamp expressed in UNIX epoch time; default to false.", + // SetGenerateCmd help. "setgenerate--synopsis": "Set the server to generate coins (mine) or not.", "setgenerate-generate": "Use true to enable generation, false to disable it", @@ -880,6 +900,7 @@ var helpDescsEnUS = map[string]string{ // pointer to the type (or nil to indicate no return value). var rpcResultTypes = map[string][]interface{}{ "addnode": nil, + "clearbanned": nil, "createrawtransaction": {(*string)(nil)}, "debuglevel": {(*string)(nil), (*string)(nil)}, "decoderawtransaction": {(*btcjson.TxRawDecodeResult)(nil)}, @@ -892,11 +913,11 @@ var rpcResultTypes = map[string][]interface{}{ "getbestblock": {(*btcjson.GetBestBlockResult)(nil)}, "getbestblockhash": {(*string)(nil)}, "getblock": {(*string)(nil), (*btcjson.GetBlockVerboseResult)(nil)}, + "getblockchaininfo": {(*btcjson.GetBlockChainInfoResult)(nil)}, "getblockcount": {(*int64)(nil)}, "getblockhash": {(*string)(nil)}, "getblockheader": {(*string)(nil), (*btcjson.GetBlockHeaderVerboseResult)(nil)}, "getblocktemplate": {(*btcjson.GetBlockTemplateResult)(nil), (*string)(nil), nil}, - "getblockchaininfo": {(*btcjson.GetBlockChainInfoResult)(nil)}, "getcfilter": {(*string)(nil)}, "getcfilterheader": {(*string)(nil)}, "getchaintips": {(*[]btcjson.GetChainTipsResult)(nil)}, @@ -918,13 +939,15 @@ var rpcResultTypes = map[string][]interface{}{ "getrawmempool": {(*[]string)(nil), (*btcjson.GetRawMempoolVerboseResult)(nil)}, "getrawtransaction": {(*string)(nil), (*btcjson.TxRawResult)(nil)}, "gettxout": {(*btcjson.GetTxOutResult)(nil)}, - "node": nil, "help": {(*string)(nil), (*string)(nil)}, "invalidateblock": nil, + "listbanned": {(*[]btcjson.ListBannedResult)(nil)}, + "node": nil, "ping": nil, "reconsiderblock": nil, "searchrawtransactions": {(*string)(nil), (*[]btcjson.SearchRawTransactionsResult)(nil)}, "sendrawtransaction": {(*string)(nil)}, + "setban": nil, "setgenerate": nil, "signmessagewithprivkey": {(*string)(nil)}, "stop": {(*string)(nil)}, diff --git a/server.go b/server.go index db13ed64..e7ccb3f1 100644 --- a/server.go +++ b/server.go @@ -156,13 +156,18 @@ type updatePeerHeightsMsg struct { originPeer *peer.Peer } +type bannedPeriod struct { + since time.Time + until time.Time +} + // peerState maintains state of inbound, persistent, outbound peers as well // as banned peers and outbound groups. type peerState struct { inboundPeers map[int32]*serverPeer outboundPeers map[int32]*serverPeer persistentPeers map[int32]*serverPeer - banned map[string]time.Time + banned map[string]bannedPeriod outboundGroups map[string]int } @@ -1656,10 +1661,10 @@ func (s *server) handleAddPeerMsg(state *peerState, sp *serverPeer) bool { sp.Disconnect() return false } - if banEnd, ok := state.banned[host]; ok { - if time.Now().Before(banEnd) { - srvrLog.Debugf("Peer %s is banned for another %v - disconnecting", - host, time.Until(banEnd)) + if ban, ok := state.banned[host]; ok { + if time.Now().Before(ban.until) { + srvrLog.Infof("Peer %s is banned for another %v - disconnecting", + host, time.Until(ban.until)) sp.Disconnect() return false } @@ -1781,7 +1786,12 @@ func (s *server) handleBanPeerMsg(state *peerState, sp *serverPeer) { direction := directionString(sp.Inbound()) srvrLog.Infof("Banned peer %s (%s) for %v", host, direction, cfg.BanDuration) - state.banned[host] = time.Now().Add(cfg.BanDuration) + + since := time.Now() + state.banned[host] = bannedPeriod{ + since: since, + until: since.Add(cfg.BanDuration), + } } // handleRelayInvMsg deals with relaying inventory to peers that are not already @@ -1876,6 +1886,25 @@ type getPeersMsg struct { reply chan []*serverPeer } +type listBannedPeersMsg struct { + reply chan map[string]bannedPeriod +} + +type setBanMsg struct { + addr string + since time.Time + until time.Time + reply chan error +} + +type removeBanMsg struct { + addr string + reply chan error +} + +type clearBannedMsg struct { + reply chan error +} type getOutboundGroup struct { key string reply chan int @@ -1924,6 +1953,29 @@ func (s *server) handleQuery(state *peerState, querymsg interface{}) { }) msg.reply <- peers + case listBannedPeersMsg: + banned := map[string]bannedPeriod{} + for host, ban := range state.banned { + banned[host] = ban + } + msg.reply <- banned + + case setBanMsg: + ban := bannedPeriod{ + since: msg.since, + until: msg.until, + } + state.banned[msg.addr] = ban + msg.reply <- nil + + case removeBanMsg: + delete(state.banned, msg.addr) + msg.reply <- nil + + case clearBannedMsg: + state.banned = map[string]bannedPeriod{} + msg.reply <- nil + case connectNodeMsg: // TODO: duplicate oneshots? // Limit max number of total peers. @@ -2161,7 +2213,7 @@ func (s *server) peerHandler() { inboundPeers: make(map[int32]*serverPeer), persistentPeers: make(map[int32]*serverPeer), outboundPeers: make(map[int32]*serverPeer), - banned: make(map[string]time.Time), + banned: make(map[string]bannedPeriod), outboundGroups: make(map[string]int), }