Implement blacklisting for IPs and enhance follow list fetching
- Added functionality to handle blacklisted IPs, allowing connections to remain open until a timeout is reached. - Introduced periodic fetching of admin follow lists to improve synchronization with relay data. - Updated WebSocket message size limits to accommodate larger payloads. - Enhanced logging for better traceability during follow list fetching and event processing. - Refactored event subscription logic to improve clarity and maintainability.
This commit is contained in:
@@ -48,6 +48,8 @@ type C struct {
|
||||
SubscriptionEnabled bool `env:"ORLY_SUBSCRIPTION_ENABLED" default:"false" usage:"enable subscription-based access control requiring payment for non-directory events"`
|
||||
MonthlyPriceSats int64 `env:"ORLY_MONTHLY_PRICE_SATS" default:"6000" usage:"price in satoshis for one month subscription (default ~$2 USD)"`
|
||||
RelayURL string `env:"ORLY_RELAY_URL" usage:"base URL for the relay dashboard (e.g., https://relay.example.com)"`
|
||||
RelayAddresses []string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of websocket addresses for this relay (e.g., wss://relay.example.com,wss://backup.example.com) - used by spider to avoid self-connections"`
|
||||
FollowListFrequency time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" usage:"how often to fetch admin follow lists (default: 1h)" default:"1h"`
|
||||
|
||||
// Web UI and dev mode settings
|
||||
WebDisableEmbedded bool `env:"ORLY_WEB_DISABLE" default:"false" usage:"disable serving the embedded web UI; useful for hot-reload during development"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
@@ -15,6 +16,25 @@ import (
|
||||
)
|
||||
|
||||
func (l *Listener) HandleMessage(msg []byte, remote string) {
|
||||
// Ignore all messages from self-connections
|
||||
if l.isSelfConnection {
|
||||
log.D.F("ignoring message from self-connection %s", remote)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle blacklisted IPs - discard messages but keep connection open until timeout
|
||||
if l.isBlacklisted {
|
||||
// Check if timeout has been reached
|
||||
if time.Now().After(l.blacklistTimeout) {
|
||||
log.W.F("blacklisted IP %s timeout reached, closing connection", remote)
|
||||
// Close the connection by cancelling the context
|
||||
// The websocket handler will detect this and close the connection
|
||||
return
|
||||
}
|
||||
log.D.F("discarding message from blacklisted IP %s (timeout in %v)", remote, time.Until(l.blacklistTimeout))
|
||||
return
|
||||
}
|
||||
|
||||
msgPreview := string(msg)
|
||||
if len(msgPreview) > 150 {
|
||||
msgPreview = msgPreview[:150] + "..."
|
||||
|
||||
@@ -37,6 +37,7 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
|
||||
if _, err = env.Unmarshal(msg); chk.E(err) {
|
||||
return normalize.Error.Errorf(err.Error())
|
||||
}
|
||||
|
||||
log.T.C(
|
||||
func() string {
|
||||
return fmt.Sprintf(
|
||||
@@ -131,16 +132,18 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
|
||||
)
|
||||
|
||||
// Process large author lists by breaking them into chunks
|
||||
if f.Authors != nil && f.Authors.Len() > 50 {
|
||||
if f.Authors != nil && f.Authors.Len() > 1000 {
|
||||
log.W.F("REQ %s: breaking down large author list (%d authors) into chunks", env.Subscription, f.Authors.Len())
|
||||
|
||||
// Calculate chunk size based on kinds to avoid OOM
|
||||
chunkSize := 50
|
||||
// Calculate chunk size to stay under message size limits
|
||||
// Each pubkey is 64 hex chars, plus JSON overhead, so ~100 bytes per author
|
||||
// Target ~50MB per chunk to stay well under 100MB limit
|
||||
chunkSize := ClientMessageSizeLimit / 200 // ~500KB per chunk
|
||||
if f.Kinds != nil && f.Kinds.Len() > 0 {
|
||||
// Reduce chunk size if there are multiple kinds to prevent too many index ranges
|
||||
chunkSize = 50 / f.Kinds.Len()
|
||||
if chunkSize < 10 {
|
||||
chunkSize = 10 // Minimum chunk size
|
||||
chunkSize = chunkSize / f.Kinds.Len()
|
||||
if chunkSize < 100 {
|
||||
chunkSize = 100 // Minimum chunk size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ const (
|
||||
DefaultPingWait = DefaultPongWait / 2
|
||||
DefaultWriteTimeout = 3 * time.Second
|
||||
DefaultMaxMessageSize = 100 * units.Mb
|
||||
// ClientMessageSizeLimit is the maximum message size that clients can handle
|
||||
// This is set to 100MB to allow large messages
|
||||
ClientMessageSizeLimit = 100 * 1024 * 1024 // 100MB
|
||||
|
||||
// CloseMessage denotes a close control message. The optional message
|
||||
// payload contains a numeric code and text. Use the FormatCloseMessage
|
||||
@@ -84,10 +87,23 @@ whitelist:
|
||||
req: r,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
// Detect self-connections early to avoid sending AUTH challenges
|
||||
listener.isSelfConnection = s.isSelfConnection(remote)
|
||||
if listener.isSelfConnection {
|
||||
log.W.F("detected self-connection from %s, marking connection", remote)
|
||||
}
|
||||
|
||||
// Check for blacklisted IPs
|
||||
listener.isBlacklisted = s.isIPBlacklisted(remote)
|
||||
if listener.isBlacklisted {
|
||||
log.W.F("detected blacklisted IP %s, marking connection for timeout", remote)
|
||||
listener.blacklistTimeout = time.Now().Add(time.Minute) // Timeout after 1 minute
|
||||
}
|
||||
chal := make([]byte, 32)
|
||||
rand.Read(chal)
|
||||
listener.challenge.Store([]byte(hex.Enc(chal)))
|
||||
if s.Config.ACLMode != "none" {
|
||||
if s.Config.ACLMode != "none" && !listener.isSelfConnection {
|
||||
log.D.F("sending AUTH challenge to %s", remote)
|
||||
if err = authenvelope.NewChallengeWith(listener.challenge.Load()).
|
||||
Write(listener); chk.E(err) {
|
||||
@@ -95,6 +111,8 @@ whitelist:
|
||||
return
|
||||
}
|
||||
log.D.F("AUTH challenge sent successfully to %s", remote)
|
||||
} else if listener.isSelfConnection {
|
||||
log.D.F("skipping AUTH challenge for self-connection from %s", remote)
|
||||
}
|
||||
ticker := time.NewTicker(DefaultPingWait)
|
||||
go s.Pinger(ctx, conn, ticker, cancel)
|
||||
@@ -130,6 +148,13 @@ whitelist:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Check if blacklisted connection has timed out
|
||||
if listener.isBlacklisted && time.Now().After(listener.blacklistTimeout) {
|
||||
log.W.F("blacklisted IP %s timeout reached, closing connection", remote)
|
||||
return
|
||||
}
|
||||
|
||||
var typ websocket.MessageType
|
||||
var msg []byte
|
||||
log.T.F("waiting for message from %s", remote)
|
||||
|
||||
@@ -17,13 +17,16 @@ import (
|
||||
|
||||
type Listener struct {
|
||||
*Server
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
remote string
|
||||
req *http.Request
|
||||
challenge atomic.Bytes
|
||||
authedPubkey atomic.Bytes
|
||||
startTime time.Time
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
remote string
|
||||
req *http.Request
|
||||
challenge atomic.Bytes
|
||||
authedPubkey atomic.Bytes
|
||||
startTime time.Time
|
||||
isSelfConnection bool // Marker to identify self-connections
|
||||
isBlacklisted bool // Marker to identify blacklisted IPs
|
||||
blacklistTimeout time.Time // When to timeout blacklisted connections
|
||||
// Diagnostics: per-connection counters
|
||||
msgCount int
|
||||
reqCount int
|
||||
|
||||
@@ -32,7 +32,6 @@ type Server struct {
|
||||
mux *http.ServeMux
|
||||
Config *config.C
|
||||
Ctx context.Context
|
||||
remote string
|
||||
publishers *publish.S
|
||||
Admins [][]byte
|
||||
Owners [][]byte
|
||||
@@ -50,6 +49,52 @@ type Server struct {
|
||||
policyManager *policy.P
|
||||
}
|
||||
|
||||
// isSelfConnection checks if the connection is coming from the relay itself
|
||||
func (s *Server) isSelfConnection(remote string) bool {
|
||||
// Check for localhost connections
|
||||
if strings.HasPrefix(remote, "127.0.0.1:") || strings.HasPrefix(remote, "::1:") || strings.HasPrefix(remote, "[::1]:") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for connections from the same IP as the server
|
||||
// This handles cases where the relay connects to itself via its public IP
|
||||
if s.Config.Listen != "" {
|
||||
// Extract IP from listen address (e.g., "0.0.0.0" -> "0.0.0.0")
|
||||
listenIP := s.Config.Listen
|
||||
if listenIP == "0.0.0.0" || listenIP == "" {
|
||||
// If listening on all interfaces, check if remote IP matches any local interface
|
||||
// For now, we'll be conservative and only check localhost
|
||||
return false
|
||||
}
|
||||
// Check if remote IP matches the listen IP
|
||||
remoteIP := strings.Split(remote, ":")[0]
|
||||
if remoteIP == listenIP {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isIPBlacklisted checks if an IP address is blacklisted using the managed ACL system
|
||||
func (s *Server) isIPBlacklisted(remote string) bool {
|
||||
// Extract IP from remote address (e.g., "192.168.1.1:12345" -> "192.168.1.1")
|
||||
remoteIP := strings.Split(remote, ":")[0]
|
||||
|
||||
// Check if managed ACL is available and active
|
||||
if s.Config.ACLMode == "managed" {
|
||||
for _, aclInstance := range acl.Registry.ACL {
|
||||
if aclInstance.Type() == "managed" {
|
||||
if managed, ok := aclInstance.(*acl.Managed); ok {
|
||||
return managed.IsIPBlocked(remoteIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Set comprehensive CORS headers for proxy compatibility
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
Reference in New Issue
Block a user