Commit 1ae4f985 authored by vicotor's avatar vicotor

add ban ip and local addr black list

parent 1a73d9b8
......@@ -38,6 +38,18 @@ You can specify a file containing a list of IP addresses to be blacklisted. IPs
- `IP_BLACKLIST_FILE`: The path to the file containing blacklisted IP addresses or domains.
### Ban List Configuration
The proxy maintains a persistent ban list for IPs that attempt to send transactions from blacklisted addresses.
- `BAN_LIST_FILE`: The path to the file where banned IPs are stored (default: `banlist.txt`).
### Local Address Blacklist Configuration
You can specify a file containing a list of Ethereum addresses to be blacklisted locally. Requests from these addresses will be blocked, and the sender's IP will be banned.
- `LOCAL_BLACKLIST_FILE`: The path to the file containing blacklisted Ethereum addresses.
## Usage
1. Set the required environment variables.
......@@ -66,3 +78,23 @@ Example:
bad-actor.com
*.botnet.net
```
## Ban List File Format
The ban list file contains one IP address per line. This file is automatically updated by the proxy when a blacklisted sender is detected.
Example:
```
10.0.0.5
192.168.1.20
```
## Local Address Blacklist File Format
The local address blacklist file contains one Ethereum address per line.
Example:
```
0x1234567890abcdef1234567890abcdef12345678
0xabcdef1234567890abcdef1234567890abcdef12
```
......@@ -96,6 +96,16 @@ var (
penaltyBurst int
penaltyDuration time.Duration
penaltyTrigger int
// Ban list (rate limit = 0)
banListFile string
banList map[string]struct{}
banListMu sync.RWMutex
// Local Address Blacklist
localBlacklistFile string
localBlacklist map[string]struct{}
localBlacklistMu sync.RWMutex
)
type visitor struct {
......@@ -105,6 +115,7 @@ type visitor struct {
blockedCount int
lastBlockTime time.Time
isStaticBlacklisted bool
isBanned bool
}
func main() {
......@@ -172,6 +183,25 @@ func main() {
log.Printf("IP_BLACKLIST_FILE not set, ip blacklist feature disabled")
}
// Load Ban list file
banListFile = os.Getenv("BAN_LIST_FILE")
if banListFile == "" {
banListFile = "banlist.txt" // default file
}
banList = loadBanList(banListFile)
log.Printf("loaded ban list entries: %d", len(banList))
// Load Local Address Blacklist
localBlacklistFile = os.Getenv("LOCAL_BLACKLIST_FILE")
if localBlacklistFile != "" {
localBlacklist = loadLocalBlacklist(localBlacklistFile)
log.Printf("loaded local address blacklist entries: %d", len(localBlacklist))
startLocalBlacklistWatcher(localBlacklistFile)
} else {
localBlacklist = map[string]struct{}{}
log.Printf("LOCAL_BLACKLIST_FILE not set, local address blacklist disabled")
}
// Start cache janitor for blacklist cache. Interval can be configured via env BLACKLIST_CACHE_CLEANUP_INTERVAL (e.g. "5m").
cleanupInterval := 5 * time.Minute
if s := os.Getenv("BLACKLIST_CACHE_CLEANUP_INTERVAL"); s != "" {
......@@ -237,6 +267,45 @@ func main() {
log.Fatal(http.ListenAndServe(":8545", nil))
}
func isSendTxFromBlacklist(req RPCRequest, ip string) bool {
if req.Method == "eth_sendRawTransaction" && len(req.Params) > 0 {
if rawHex, ok := req.Params[0].(string); ok {
if fromAddr, err := getSenderFromRawTx(rawHex); err == nil {
// Check local blacklist first
if isLocalBlacklisted(fromAddr) {
log.Printf("sender %s is in local blacklist, banning IP %s\n", fromAddr, ip)
return true
} else {
// check in contract blacklist.
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, _ := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(fromAddr)))
if inBlack {
log.Printf("sender %s is in contract blacklist, banning IP %s\n", fromAddr, ip)
return inBlack
}
}
}
}
}
}
return false
}
func isFromInBlacklist(addr string) bool {
if isLocalBlacklisted(addr) {
return true
} else {
// check in contract blacklist.
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, _ := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(addr)))
if inBlack {
return inBlack
}
}
}
return false
}
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// Add CORS support
setCORSHeaders(w, r)
......@@ -294,6 +363,41 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("encode response error: %v", err)
}
} else {
for _, req := range reqs {
if isSendTxFromBlacklist(req, ip) {
addToBanList(ip)
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
Id: req.Id,
Error: map[string]interface{}{
"code": -32000,
"message": "sender is denied",
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(errResp); err != nil {
log.Printf("encode error: %v", err)
}
return
}
if from, ok := extractFromAddress(req); ok {
if isFromInBlacklist(from) {
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
Id: req.Id,
Error: map[string]interface{}{
"code": -32000,
"message": "sender is denied",
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(errResp); err != nil {
log.Printf("encode error: %v", err)
}
return
}
}
}
forwardToBackend(w, body)
}
return
......@@ -304,6 +408,7 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
forwardToBackend(w, body)
return
}
log.Printf("%s request from %s", req.Method, ip)
// Handle eth_getBalance (existing behavior)
if req.Method == "eth_getBalance" && len(req.Params) > 0 {
......@@ -331,26 +436,15 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
}
}
// First, special-case eth_sendRawTransaction: extract sender from raw tx
if req.Method == "eth_sendRawTransaction" && len(req.Params) > 0 {
if rawHex, ok := req.Params[0].(string); ok {
if fromAddr, err := getSenderFromRawTx(rawHex); err == nil {
// only perform blacklist check if eth client and blacklist contract are configured
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, err := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(fromAddr)))
if err != nil {
log.Printf("blacklist check failed for %s: %v", fromAddr, err)
// fail open: forward
forwardToBackend(w, body)
return
}
if inBlack {
// forbid eth_sendRawTransaction if sender is blacklisted, add to ban list and return error.
if isSendTxFromBlacklist(req, ip) {
addToBanList(ip)
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
Id: req.Id,
Error: map[string]interface{}{
"code": -32000,
"message": "sender is invalid",
"message": "sender is blacklisted",
},
}
w.Header().Set("Content-Type", "application/json")
......@@ -359,28 +453,10 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
}
return
}
}
} else {
// If we couldn't decode the sender, just forward (fail open)
log.Printf("failed to decode raw tx sender: %v", err)
forwardToBackend(w, body)
return
}
}
}
// New: check if request includes a `from` address (common for eth_sendTransaction, eth_call, eth_estimateGas)
if from, ok := extractFromAddress(req); ok {
// only perform blacklist check if eth client and blacklist contract are configured
if ethClient != nil && blacklistContract != (common.Address{}) {
inBlack, err := CachedIsInBlacklist(ethClient, blacklistContract, common.HexToAddress(strings.ToLower(from)))
if err != nil {
// on error, log and forward to backend (fail open)
log.Printf("blacklist check failed for %s: %v", from, err)
forwardToBackend(w, body)
return
}
if inBlack {
if isFromInBlacklist(from) {
// return JSON-RPC error response indicating sender is blacklisted
errResp := RPCResponse{
Jsonrpc: req.Jsonrpc,
......@@ -397,7 +473,6 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
return
}
}
}
// Forward other cases directly
forwardToBackend(w, body)
......@@ -791,6 +866,129 @@ func startIPBlacklistWatcher(path string) {
}()
}
// ===== Local Address Blacklist helper functions =====
func loadLocalBlacklist(path string) map[string]struct{} {
result := make(map[string]struct{})
f, err := os.Open(path)
if err != nil {
log.Printf("open local blacklist file '%s' error: %v", path, err)
return result
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
// Assume addresses are hex strings, normalize to lowercase
addr := strings.ToLower(line)
result[addr] = struct{}{}
}
return result
}
func isLocalBlacklisted(addr string) bool {
localBlacklistMu.RLock()
defer localBlacklistMu.RUnlock()
_, ok := localBlacklist[strings.ToLower(addr)]
return ok
}
func startLocalBlacklistWatcher(path string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("create local blacklist watcher error: %v", err)
return
}
dir := filepath.Dir(path)
if err := watcher.Add(dir); err != nil {
log.Printf("add local blacklist watch dir error: %v", err)
watcher.Close()
return
}
go func() {
defer watcher.Close()
for {
select {
case ev, ok := <-watcher.Events:
if !ok {
return
}
if ev.Name == path {
if ev.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) != 0 {
bl := loadLocalBlacklist(path)
localBlacklistMu.Lock()
localBlacklist = bl
localBlacklistMu.Unlock()
log.Printf("local blacklist reloaded (%d entries) due to event: %s", len(bl), ev.Op.String())
}
if ev.Op&fsnotify.Remove != 0 {
localBlacklistMu.Lock()
localBlacklist = map[string]struct{}{}
localBlacklistMu.Unlock()
log.Printf("local blacklist file removed, cleared entries")
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("local blacklist watcher error: %v", err)
}
}
}()
}
// ===== Ban List helper functions =====
func loadBanList(path string) map[string]struct{} {
result := make(map[string]struct{})
f, err := os.Open(path)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("open ban list file '%s' error: %v", path, err)
}
return result
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
result[line] = struct{}{}
}
}
return result
}
func addToBanList(ip string) {
banListMu.Lock()
defer banListMu.Unlock()
if _, exists := banList[ip]; exists {
return
}
banList[ip] = struct{}{}
// Append to file
f, err := os.OpenFile(banListFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Printf("failed to open ban list file for appending: %v", err)
return
}
defer f.Close()
if _, err := f.WriteString(ip + "\n"); err != nil {
log.Printf("failed to write to ban list file: %v", err)
}
}
func isBanned(ip string) bool {
banListMu.RLock()
defer banListMu.RUnlock()
_, ok := banList[ip]
return ok
}
func checkRateLimit(ip string) bool {
visitorsMu.Lock()
defer visitorsMu.Unlock()
......@@ -803,6 +1001,17 @@ func checkRateLimit(ip string) bool {
}
v.lastSeen = time.Now()
// Check if IP is banned (rate limit 0)
if isBanned(ip) {
if !v.isBanned {
v.isBanned = true
v.limiter.SetLimit(0)
v.limiter.SetBurst(0)
log.Printf("IP %s is banned, blocking all requests", ip)
}
return false
}
// Check if IP is in static blacklist
if isIPBlacklisted(ip) {
if !v.isStaticBlacklisted {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment