Compare commits

...

5 Commits

3 changed files with 177 additions and 130 deletions

View File

@ -1,64 +1,85 @@
mongodb: mongodb:
uri: "mongodb://localhost:27017/" # MongoDB connection URI uri: mongodb://localhost:27017/
database: "grain" # Database name database: grain
server: server:
port: ":8080" # Port for the server to listen on port: :8181
read_timeout: 10 # Read timeout in seconds read_timeout: 10 # in seconds
write_timeout: 10 # Write timeout in seconds write_timeout: 10 # in seconds
idle_timeout: 120 # Idle timeout in seconds idle_timeout: 120 # in seconds
max_connections: 100 # Maximum number of concurrent connections max_connections: 100
max_subscriptions_per_client: 10 # Maximum number of concurrent subscriptions per client max_subscriptions_per_client: 10
rate_limit:
ws_limit: 100 # WebSocket messages per second
ws_burst: 200 # Allowed burst of WebSocket messages
event_limit: 50 # Events per second
event_burst: 100 # Allowed burst of events
req_limit: 50 # HTTP requests per second
req_burst: 100 # Allowed burst of HTTP requests
max_event_size: 51200 # Maximum size of an event in bytes
# Size limits for specific event kinds
kind_size_limits:
- kind: 0
max_size: 10240 # Maximum size in bytes for kind 0 events
- kind: 1
max_size: 25600 # Maximum size in bytes for kind 1 events
# Rate limits for different event categories
category_limits:
ephemeral:
kind: 0
limit: 100 # Events per second
burst: 200 # Allowed burst
parameterized_replaceable:
kind: 0
limit: 5
burst: 10
regular:
kind: 0
limit: 25
burst: 50
replaceable:
kind: 0
limit: 10
burst: 20
# Rate limits for specific event kinds
kind_limits:
- kind: 0
limit: 1 # Events per second
burst: 5 # Allowed burst
- kind: 1
limit: 25
burst: 50
- kind: 3
limit: 25
burst: 50
pubkey_whitelist: pubkey_whitelist:
enabled: false enabled: false
pubkeys: #["3fe0ab6cbdb7ee27148202249e3fb3b89423c6f6cda6ef43ea5057c3d93088e4", pubkeys: [] # List of allowed public keys
#"cac0e43235806da094f0787a5b04e29ad04cb1a3c7ea5cf61edc1c338734082b"] npubs: [] # List of allowed npubs (Nostr public keys in bech32 format)
npubs: #["npub18ls2km9aklhzw9yzqgjfu0anhz2z83hkeknw7sl22ptu8kfs3rjq54am44"]
kind_whitelist: kind_whitelist:
enabled: false enabled: false
kinds: #[0, 1] kinds: [] # List of allowed event kinds
#If pubkey_whitelist not enabled, domain_whitelist will be ignored
domain_whitelist: domain_whitelist:
enabled: false enabled: false
domains: #["happytavern.co", "nostrplebs.com"] domains: [] # List of allowed domains
rate_limit:
ws_limit: 100 # Global rate limit for WebSocket messages (50 messages per second)
ws_burst: 200 # Global burst limit for WebSocket messages (allows a burst of 100 messages)
event_limit: 50 # Global rate limit for events (25 events per second) blacklist: #Removing a pubkey from the Blacklist requires a hard restart; Blacklist overides the Whitelist
event_burst: 100 # Global burst limit for events (allows a burst of 50 events) enabled: true
req_limit: 50 # Added limit for REQ messages permanent_ban_words: [] # Words that trigger a permanent ban
req_burst: 100 # Added burst limit for REQ messages temp_ban_words: # Words that trigger a temporary ban
- crypto
max_event_size: 51200 # Global maximum event size in bytes (50Kb) - web3
kind_size_limits: - airdrop
- kind: 0 max_temp_bans: 3 # Number of temporary bans before a permanent ban
max_size: 10240 # Maximum event size for kind 0 in bytes (10Kb) temp_ban_duration: 3600 # Temporary ban duration in seconds
- kind: 1 permanent_blacklist_pubkeys: # List of permanently banned public keys
max_size: 25600 # Maximum event size for kind 1 in bytes (25Kb) - db0c9b8acd6101adb9b281c5321f98f6eebb33c5719d230ed1870997538a9765
permanent_blacklist_npubs: # List of permanently banned npubs
category_limits: # Rate limits based on event categories - npub1x0r5gflnk2mn6h3c70nvnywpy2j46gzqwg6k7uw6fxswyz0md9qqnhshtn
regular:
limit: 25 # Rate limit for regular events (50 events per second)
burst: 50 # Burst limit for regular events (allows a burst of 100 events)
replaceable:
limit: 10 # Rate limit for replaceable events (10 events per second)
burst: 20 # Burst limit for replaceable events (allows a burst of 20 events)
parameterized_replaceable:
limit: 5 # Rate limit for parameterized replaceable events (5 events per second)
burst: 10 # Burst limit for parameterized replaceable events (allows a burst of 10 events)
ephemeral:
limit: 100 # Rate limit for ephemeral events (100 events per second)
burst: 200 # Burst limit for ephemeral events (allows a burst of 200 events)
kind_limits: # Specific rate limits for different kinds of events
- kind: 0
limit: 1 # Rate limit for events of kind 0 (1 event per second)
burst: 5 # Burst limit for events of kind 0 (allows a burst of 5 events)
- kind: 1
limit: 25 # Rate limit for events of kind 1 (100 events per second)
burst: 50 # Burst limit for events of kind 1 (allows a burst of 200 events)
- kind: 3
limit: 25 # Rate limit for events of kind 3 (25 events per second)
burst: 50 # Burst limit for events of kind 3 (allows a burst of 50 events)

View File

@ -9,6 +9,11 @@ GRAIN is an open-source Nostr relay implementation written in Go. This project a
- **Dynamic Event Handling**: Capable of processing a wide range of events, categorized by type and kind, including support for event deletion as per NIP-09. - **Dynamic Event Handling**: Capable of processing a wide range of events, categorized by type and kind, including support for event deletion as per NIP-09.
- **Configurable and Extensible**: Easily customizable through configuration files, with plans for future GUI-based configuration management to streamline server adjustments. - **Configurable and Extensible**: Easily customizable through configuration files, with plans for future GUI-based configuration management to streamline server adjustments.
- **Efficient Rate Limiting**: Implements sophisticated rate limiting strategies to manage WebSocket messages, - events, and requests, ensuring fair resource allocation and protection against abuse. - **Efficient Rate Limiting**: Implements sophisticated rate limiting strategies to manage WebSocket messages, - events, and requests, ensuring fair resource allocation and protection against abuse.
- **Extensive Blacklist and Whitelist Functions**:
- Implements a robust blacklisting system with support for temporary and permanent bans based on content, pubkeys, or npubs.
- Features word-based content filtering for automatic temporary or permanent bans.
- Includes a configurable system for escalating temporary bans to permanent bans after a set number of violations.
- Offers whitelist capabilities for pubkeys, npubs, event kinds, and domains, allowing fine-grained control over permitted content and users.
- **Flexible Event Size Management**: Configurable size limits for events, with optional constraints based on - event kind, to maintain performance and prevent oversized data handling. - **Flexible Event Size Management**: Configurable size limits for events, with optional constraints based on - event kind, to maintain performance and prevent oversized data handling.
- **MongoDB Integration 🍃**: Utilizes MongoDB for high-performance storage and management of events, ensuring data integrity and efficient query capabilities. - **MongoDB Integration 🍃**: Utilizes MongoDB for high-performance storage and management of events, ensuring data integrity and efficient query capabilities.
- **Scalable Architecture**: Built with Go, leveraging its concurrency model to provide high throughput and scalability, suitable for handling large volumes of data and connections. - **Scalable Architecture**: Built with Go, leveraging its concurrency model to provide high throughput and scalability, suitable for handling large volumes of data and connections.
@ -20,6 +25,7 @@ GRAIN is an open-source Nostr relay implementation written in Go. This project a
### MongoDB Server 🍃 ### MongoDB Server 🍃
_I plan to add multiple other database types in the future (postgress, sqlite)_
GRAIN 🌾 leverages MongoDB for efficient storage and management of events. MongoDB, known for its high performance and scalability, is an ideal choice for handling large volumes of real-time data. GRAIN 🌾 uses MongoDB collections to store events categorized by kind and ensures quick retrieval and manipulation of these events through its robust querying capabilities. GRAIN 🌾 leverages MongoDB for efficient storage and management of events. MongoDB, known for its high performance and scalability, is an ideal choice for handling large volumes of real-time data. GRAIN 🌾 uses MongoDB collections to store events categorized by kind and ensures quick retrieval and manipulation of these events through its robust querying capabilities.
You can get the free Community Server edition of MongoDB from the official MongoDB website: You can get the free Community Server edition of MongoDB from the official MongoDB website:
@ -30,7 +36,7 @@ MongoDB provides extensive documentation and support to help you get started wit
Grain will automatically create the configurations and relay metadata files necessary if they do not already exist when you first run the program. Grain will automatically create the configurations and relay metadata files necessary if they do not already exist when you first run the program.
They are created in the root directory of Grain. You can change configurations and relay_metadata here. The relay must be restarted for new configurations to take effect. They are created in the root directory of Grain. You can change configurations and relay_metadata here and the server will automatically restart and use the new configurations. The relay must be restarted manually for new blacklist configurations to take effect.
## Development ## Development

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"grain/config" "grain/config"
cfg "grain/config/types" cfg "grain/config/types"
"log"
"os" "os"
"strings" "strings"
"sync" "sync"
@ -12,24 +13,6 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// Structure to manage temporary bans with timestamps
type tempBanEntry struct {
count int // Number of temporary bans
unbanTime time.Time // Time when the pubkey should be unbanned
}
var (
tempBannedPubkeys = make(map[string]*tempBanEntry)
mu sync.Mutex
)
func ClearTemporaryBans() {
mu.Lock()
defer mu.Unlock()
tempBannedPubkeys = make(map[string]*tempBanEntry)
}
// CheckBlacklist checks if a pubkey is in the blacklist based on event content // CheckBlacklist checks if a pubkey is in the blacklist based on event content
func CheckBlacklist(pubkey, eventContent string) (bool, string) { func CheckBlacklist(pubkey, eventContent string) (bool, string) {
cfg := config.GetConfig().Blacklist cfg := config.GetConfig().Blacklist
@ -38,13 +21,17 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
return false, "" return false, ""
} }
log.Printf("Checking blacklist for pubkey: %s", pubkey)
// Check for permanent blacklist by pubkey or npub // Check for permanent blacklist by pubkey or npub
if isPubKeyPermanentlyBlacklisted(pubkey) { if isPubKeyPermanentlyBlacklisted(pubkey) {
log.Printf("Pubkey %s is permanently blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey) return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey)
} }
// Check for temporary ban // Check for temporary ban
if isPubKeyTemporarilyBlacklisted(pubkey) { if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey) return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
} }
@ -55,7 +42,7 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
if err != nil { if err != nil {
return true, fmt.Sprintf("pubkey %s is permanently banned and failed to save: %v", pubkey, err) return true, fmt.Sprintf("pubkey %s is permanently banned and failed to save: %v", pubkey, err)
} }
return true, fmt.Sprintf("pubkey %s is permanently banned for containing forbidden words", pubkey) return true, "blocked: pubkey is permanently banned"
} }
} }
@ -66,7 +53,7 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
if err != nil { if err != nil {
return true, fmt.Sprintf("pubkey %s is temporarily banned and failed to save: %v", pubkey, err) return true, fmt.Sprintf("pubkey %s is temporarily banned and failed to save: %v", pubkey, err)
} }
return true, fmt.Sprintf("pubkey %s is temporarily banned for containing forbidden words", pubkey) return true, "blocked: pubkey is temporarily banned"
} }
} }
@ -81,18 +68,36 @@ func isPubKeyTemporarilyBlacklisted(pubkey string) bool {
entry, exists := tempBannedPubkeys[pubkey] entry, exists := tempBannedPubkeys[pubkey]
if !exists { if !exists {
log.Printf("Pubkey %s not found in temporary blacklist", pubkey)
return false return false
} }
// If the ban has expired, remove it from the temporary ban list now := time.Now()
if time.Now().After(entry.unbanTime) { if now.After(entry.unbanTime) {
delete(tempBannedPubkeys, pubkey) log.Printf("Temporary ban for pubkey %s has expired. Count: %d", pubkey, entry.count)
return false return false
} }
log.Printf("Pubkey %s is currently temporarily blacklisted. Count: %d, Unban time: %s", pubkey, entry.count, entry.unbanTime)
return true return true
} }
func ClearTemporaryBans() {
mu.Lock()
defer mu.Unlock()
tempBannedPubkeys = make(map[string]*tempBanEntry)
}
var (
tempBannedPubkeys = make(map[string]*tempBanEntry)
mu sync.Mutex
)
type tempBanEntry struct {
count int
unbanTime time.Time
}
// Adds a pubkey to the temporary blacklist // Adds a pubkey to the temporary blacklist
func AddToTemporaryBlacklist(pubkey string) error { func AddToTemporaryBlacklist(pubkey string) error {
mu.Lock() mu.Lock()
@ -100,24 +105,41 @@ func AddToTemporaryBlacklist(pubkey string) error {
cfg := config.GetConfig().Blacklist cfg := config.GetConfig().Blacklist
// Check if the pubkey is already temporarily banned
entry, exists := tempBannedPubkeys[pubkey] entry, exists := tempBannedPubkeys[pubkey]
if !exists { if !exists {
log.Printf("Creating new temporary ban entry for pubkey %s", pubkey)
entry = &tempBanEntry{ entry = &tempBanEntry{
count: 1, count: 0,
unbanTime: time.Now().Add(time.Duration(cfg.TempBanDuration) * time.Second), unbanTime: time.Now(),
} }
tempBannedPubkeys[pubkey] = entry tempBannedPubkeys[pubkey] = entry
} else {
log.Printf("Updating existing temporary ban entry for pubkey %s. Current count: %d", pubkey, entry.count)
if time.Now().After(entry.unbanTime) {
log.Printf("Previous ban for pubkey %s has expired. Keeping count at %d", pubkey, entry.count)
}
} }
// Increment the temporary ban count and set the unban time // Increment the count
entry.count++ entry.count++
entry.unbanTime = time.Now().Add(time.Duration(cfg.TempBanDuration) * time.Second) entry.unbanTime = time.Now().Add(time.Duration(cfg.TempBanDuration) * time.Second)
// If the count exceeds max_temp_bans, move to permanent blacklist log.Printf("Pubkey %s temporary ban count updated to: %d, MaxTempBans: %d, New unban time: %s", pubkey, entry.count, cfg.MaxTempBans, entry.unbanTime)
if entry.count >= cfg.MaxTempBans {
if entry.count > cfg.MaxTempBans {
log.Printf("Attempting to move pubkey %s to permanent blacklist", pubkey)
delete(tempBannedPubkeys, pubkey) delete(tempBannedPubkeys, pubkey)
return AddToPermanentBlacklist(pubkey)
// Release the lock before calling AddToPermanentBlacklist
mu.Unlock()
err := AddToPermanentBlacklist(pubkey)
mu.Lock() // Re-acquire the lock
if err != nil {
log.Printf("Error adding pubkey %s to permanent blacklist: %v", pubkey, err)
return err
}
log.Printf("Successfully added pubkey %s to permanent blacklist", pubkey)
} }
return nil return nil
@ -154,9 +176,7 @@ func isPubKeyPermanentlyBlacklisted(pubKey string) bool {
} }
func AddToPermanentBlacklist(pubkey string) error { func AddToPermanentBlacklist(pubkey string) error {
mu.Lock() // Remove the mutex lock from here
defer mu.Unlock()
cfg := config.GetConfig().Blacklist cfg := config.GetConfig().Blacklist
// Check if already blacklisted // Check if already blacklisted