Compare commits

..

No commits in common. "57ca374de438cd04382854a68bd80df1ff423047" and "7d288cec1e0781e6daa407aea9c38ab8f763cb0a" have entirely different histories.

3 changed files with 130 additions and 177 deletions

View File

@ -1,85 +1,64 @@
mongodb: mongodb:
uri: mongodb://localhost:27017/ uri: "mongodb://localhost:27017/" # MongoDB connection URI
database: grain database: "grain" # Database name
server: server:
port: :8181 port: ":8080" # Port for the server to listen on
read_timeout: 10 # in seconds read_timeout: 10 # Read timeout in seconds
write_timeout: 10 # in seconds write_timeout: 10 # Write timeout in seconds
idle_timeout: 120 # in seconds idle_timeout: 120 # Idle timeout in seconds
max_connections: 100 max_connections: 100 # Maximum number of concurrent connections
max_subscriptions_per_client: 10 max_subscriptions_per_client: 10 # Maximum number of concurrent subscriptions per client
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: [] # List of allowed public keys pubkeys: #["3fe0ab6cbdb7ee27148202249e3fb3b89423c6f6cda6ef43ea5057c3d93088e4",
npubs: [] # List of allowed npubs (Nostr public keys in bech32 format) #"cac0e43235806da094f0787a5b04e29ad04cb1a3c7ea5cf61edc1c338734082b"]
npubs: #["npub18ls2km9aklhzw9yzqgjfu0anhz2z83hkeknw7sl22ptu8kfs3rjq54am44"]
kind_whitelist: kind_whitelist:
enabled: false enabled: false
kinds: [] # List of allowed event kinds kinds: #[0, 1]
#If pubkey_whitelist not enabled, domain_whitelist will be ignored
domain_whitelist: domain_whitelist:
enabled: false enabled: false
domains: [] # List of allowed domains domains: #["happytavern.co", "nostrplebs.com"]
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)
blacklist: #Removing a pubkey from the Blacklist requires a hard restart; Blacklist overides the Whitelist event_limit: 50 # Global rate limit for events (25 events per second)
enabled: true event_burst: 100 # Global burst limit for events (allows a burst of 50 events)
permanent_ban_words: [] # Words that trigger a permanent ban req_limit: 50 # Added limit for REQ messages
temp_ban_words: # Words that trigger a temporary ban req_burst: 100 # Added burst limit for REQ messages
- crypto
- web3 max_event_size: 51200 # Global maximum event size in bytes (50Kb)
- airdrop kind_size_limits:
max_temp_bans: 3 # Number of temporary bans before a permanent ban - kind: 0
temp_ban_duration: 3600 # Temporary ban duration in seconds max_size: 10240 # Maximum event size for kind 0 in bytes (10Kb)
permanent_blacklist_pubkeys: # List of permanently banned public keys - kind: 1
- db0c9b8acd6101adb9b281c5321f98f6eebb33c5719d230ed1870997538a9765 max_size: 25600 # Maximum event size for kind 1 in bytes (25Kb)
permanent_blacklist_npubs: # List of permanently banned npubs
- npub1x0r5gflnk2mn6h3c70nvnywpy2j46gzqwg6k7uw6fxswyz0md9qqnhshtn category_limits: # Rate limits based on event categories
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,11 +9,6 @@ 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.
@ -25,7 +20,6 @@ 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:
@ -36,7 +30,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 and the server will automatically restart and use the new configurations. The relay must be restarted manually for new blacklist configurations to take effect. 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.
## Development ## Development

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"grain/config" "grain/config"
cfg "grain/config/types" cfg "grain/config/types"
"log"
"os" "os"
"strings" "strings"
"sync" "sync"
@ -13,27 +12,41 @@ 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
if !cfg.Enabled { if !cfg.Enabled {
return false, "" return false, ""
} }
log.Printf("Checking blacklist for pubkey: %s", pubkey) // Check for permanent blacklist by pubkey or npub
if isPubKeyPermanentlyBlacklisted(pubkey) {
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey)
}
// Check for permanent blacklist by pubkey or npub // Check for temporary ban
if isPubKeyPermanentlyBlacklisted(pubkey) { if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is permanently blacklisted", pubkey) return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey) }
}
// Check for temporary ban
if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
}
// Check for permanent ban based on wordlist // Check for permanent ban based on wordlist
for _, word := range cfg.PermanentBanWords { for _, word := range cfg.PermanentBanWords {
@ -42,7 +55,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, "blocked: pubkey is permanently banned" return true, fmt.Sprintf("pubkey %s is permanently banned for containing forbidden words", pubkey)
} }
} }
@ -53,7 +66,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, "blocked: pubkey is temporarily banned" return true, fmt.Sprintf("pubkey %s is temporarily banned for containing forbidden words", pubkey)
} }
} }
@ -63,86 +76,51 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
// Checks if a pubkey is temporarily blacklisted // Checks if a pubkey is temporarily blacklisted
func isPubKeyTemporarilyBlacklisted(pubkey string) bool { func isPubKeyTemporarilyBlacklisted(pubkey string) bool {
mu.Lock()
defer mu.Unlock()
entry, exists := tempBannedPubkeys[pubkey]
if !exists {
log.Printf("Pubkey %s not found in temporary blacklist", pubkey)
return false
}
now := time.Now()
if now.After(entry.unbanTime) {
log.Printf("Temporary ban for pubkey %s has expired. Count: %d", pubkey, entry.count)
return false
}
log.Printf("Pubkey %s is currently temporarily blacklisted. Count: %d, Unban time: %s", pubkey, entry.count, entry.unbanTime)
return true
}
func ClearTemporaryBans() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
tempBannedPubkeys = make(map[string]*tempBanEntry)
}
var ( entry, exists := tempBannedPubkeys[pubkey]
tempBannedPubkeys = make(map[string]*tempBanEntry) if !exists {
mu sync.Mutex return false
) }
type tempBanEntry struct { // If the ban has expired, remove it from the temporary ban list
count int if time.Now().After(entry.unbanTime) {
unbanTime time.Time delete(tempBannedPubkeys, pubkey)
return false
}
return true
} }
// 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()
defer mu.Unlock() defer mu.Unlock()
cfg := config.GetConfig().Blacklist cfg := config.GetConfig().Blacklist
entry, exists := tempBannedPubkeys[pubkey] // Check if the pubkey is already temporarily banned
if !exists { entry, exists := tempBannedPubkeys[pubkey]
log.Printf("Creating new temporary ban entry for pubkey %s", pubkey) if !exists {
entry = &tempBanEntry{ entry = &tempBanEntry{
count: 0, count: 1,
unbanTime: time.Now(), unbanTime: time.Now().Add(time.Duration(cfg.TempBanDuration) * time.Second),
} }
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 count // Increment the temporary ban count and set the unban time
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)
log.Printf("Pubkey %s temporary ban count updated to: %d, MaxTempBans: %d, New unban time: %s", pubkey, entry.count, cfg.MaxTempBans, entry.unbanTime) // If the count exceeds max_temp_bans, move to permanent blacklist
if entry.count >= cfg.MaxTempBans {
delete(tempBannedPubkeys, pubkey)
return AddToPermanentBlacklist(pubkey)
}
if entry.count > cfg.MaxTempBans { return nil
log.Printf("Attempting to move pubkey %s to permanent blacklist", pubkey)
delete(tempBannedPubkeys, 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
} }
// Checks if a pubkey is permanently blacklisted (only using config.yml) // Checks if a pubkey is permanently blacklisted (only using config.yml)
@ -176,19 +154,21 @@ func isPubKeyPermanentlyBlacklisted(pubKey string) bool {
} }
func AddToPermanentBlacklist(pubkey string) error { func AddToPermanentBlacklist(pubkey string) error {
// Remove the mutex lock from here mu.Lock()
cfg := config.GetConfig().Blacklist defer mu.Unlock()
// Check if already blacklisted cfg := config.GetConfig().Blacklist
if isPubKeyPermanentlyBlacklisted(pubkey) {
return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey)
}
// Add pubkey to the blacklist // Check if already blacklisted
cfg.PermanentBlacklistPubkeys = append(cfg.PermanentBlacklistPubkeys, pubkey) if isPubKeyPermanentlyBlacklisted(pubkey) {
return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey)
}
// Persist changes to config.yml // Add pubkey to the blacklist
return saveBlacklistConfig(cfg) cfg.PermanentBlacklistPubkeys = append(cfg.PermanentBlacklistPubkeys, pubkey)
// Persist changes to config.yml
return saveBlacklistConfig(cfg)
} }
func saveBlacklistConfig(blacklistConfig cfg.BlacklistConfig) error { func saveBlacklistConfig(blacklistConfig cfg.BlacklistConfig) error {