feat: Add blacklist support for public relays

This commit is contained in:
2025-08-05 14:09:01 -04:00
parent df8e66d9a7
commit b7b7dc7353
5 changed files with 147 additions and 49 deletions

View File

@@ -43,6 +43,7 @@ type C struct {
Owners []string `env:"ORLY_OWNERS" usage:"list of users whose follow lists designate whitelisted users who can publish events, and who can read if public readable is false (comma separated)"`
Private bool `env:"ORLY_PRIVATE" usage:"do not spider for user metadata because the relay is private and this would leak relay memberships" default:"false"`
Whitelist []string `env:"ORLY_WHITELIST" usage:"only allow connections from this list of IP addresses"`
Blacklist []string `env:"ORLY_BLACKLIST" usage:"list of pubkeys to block when auth is not required (comma separated)"`
RelaySecret string `env:"ORLY_SECRET_KEY" usage:"secret key for relay cluster replication authentication"`
PeerRelays []string `env:"ORLY_PEER_RELAYS" usage:"list of peer relays URLs that new events are pushed to in format <pubkey>|<url>"`
}

View File

@@ -42,6 +42,15 @@ func (s *Server) AcceptEvent(
remote string,
) (accept bool, notice string, afterSave func()) {
if !s.AuthRequired() {
// Check blacklist for public relay mode
if len(s.blacklistPubkeys) > 0 {
for _, blockedPubkey := range s.blacklistPubkeys {
if bytes.Equal(blockedPubkey, ev.Pubkey) {
notice = "event author is blacklisted"
return
}
}
}
accept = true
return
}
@@ -64,6 +73,7 @@ func (s *Server) AcceptEvent(
for _, u := range s.OwnersMuted() {
if bytes.Equal(u, authedPubkey) {
notice = "event author is banned from this relay"
accept = false
return
}
}

View File

@@ -12,8 +12,8 @@ import (
// mockServerForEvent is a simple mock implementation of the Server struct for testing AcceptEvent
type mockServerForEvent struct {
authRequired bool
ownersFollowed [][]byte
authRequired bool
ownersFollowed [][]byte
followedFollows [][]byte
}
@@ -203,8 +203,8 @@ func TestAcceptEventWithRealServer(t *testing.T) {
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
if notice != "" {
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
if notice != "client isn't authed" {
t.Errorf("AcceptEvent() notice = %v, want 'client isn't authed'", notice)
}
if afterSave != nil {
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
@@ -234,4 +234,81 @@ func TestAcceptEventWithRealServer(t *testing.T) {
if !accept {
t.Error("AcceptEvent() accept = false, want true")
}
// Test with muted user
s.SetOwnersMuted([][]byte{[]byte("test-pubkey")})
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
if notice != "event author is banned from this relay" {
t.Errorf("AcceptEvent() notice = %v, want 'event author is banned from this relay'", notice)
}
}
// TestAcceptEventWithBlacklist tests the blacklist functionality when auth is not required
func TestAcceptEventWithBlacklist(t *testing.T) {
// Create a context and HTTP request for testing
ctx := context.Bg()
req, _ := http.NewRequest("GET", "http://example.com", nil)
// Test pubkey bytes
testPubkey := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20}
blockedPubkey := []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30}
// Test with public relay mode (auth not required) and no blacklist
s := &Server{
C: &config.C{
AuthRequired: false,
},
Lists: new(Lists),
}
// Create event with test pubkey
testEvent := &event.E{}
testEvent.Pubkey = testPubkey
// Should accept when no blacklist
accept, notice, _ := s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
if !accept {
t.Error("AcceptEvent() accept = false, want true")
}
if notice != "" {
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
}
// Add blacklist with different pubkey
s.blacklistPubkeys = [][]byte{blockedPubkey}
// Should still accept when author not in blacklist
accept, notice, _ = s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
if !accept {
t.Error("AcceptEvent() accept = false, want true")
}
if notice != "" {
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
}
// Create event with blocked pubkey
blockedEvent := &event.E{}
blockedEvent.Pubkey = blockedPubkey
// Should reject when author is in blacklist
accept, notice, _ = s.AcceptEvent(ctx, blockedEvent, req, nil, "127.0.0.1")
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
if notice != "event author is blacklisted" {
t.Errorf("AcceptEvent() notice = %v, want 'event author is blacklisted'", notice)
}
// Test with auth required - blacklist should not apply
s.C.AuthRequired = true
accept, notice, _ = s.AcceptEvent(ctx, blockedEvent, req, nil, "127.0.0.1")
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
if notice != "client isn't authed" {
t.Errorf("AcceptEvent() notice = %v, want 'client isn't authed'", notice)
}
}

View File

@@ -8,41 +8,41 @@ import (
func TestLists_OwnersPubkeys(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
pks := l.OwnersPubkeys()
if len(pks) != 0 {
t.Errorf("Expected empty list, got %d items", len(pks))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("pubkey1"),
[]byte("pubkey2"),
[]byte("pubkey3"),
}
l.SetOwnersPubkeys(testPubkeys)
// Verify length
if l.LenOwnersPubkeys() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersPubkeys())
}
// Verify content
pks = l.OwnersPubkeys()
if len(pks) != len(testPubkeys) {
t.Errorf("Expected %d pubkeys, got %d", len(testPubkeys), len(pks))
}
// Verify each pubkey
for i, pk := range pks {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
// Verify that the returned slice is a copy, not a reference
pks[0] = []byte("modified")
newPks := l.OwnersPubkeys()
@@ -54,37 +54,37 @@ func TestLists_OwnersPubkeys(t *testing.T) {
func TestLists_OwnersFollowed(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
followed := l.OwnersFollowed()
if len(followed) != 0 {
t.Errorf("Expected empty list, got %d items", len(followed))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("followed1"),
[]byte("followed2"),
[]byte("followed3"),
}
l.SetOwnersFollowed(testPubkeys)
// Verify length
if l.LenOwnersFollowed() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersFollowed())
}
// Verify content
followed = l.OwnersFollowed()
if len(followed) != len(testPubkeys) {
t.Errorf("Expected %d followed, got %d", len(testPubkeys), len(followed))
}
// Verify each pubkey
for i, pk := range followed {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
@@ -93,37 +93,37 @@ func TestLists_OwnersFollowed(t *testing.T) {
func TestLists_FollowedFollows(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
follows := l.FollowedFollows()
if len(follows) != 0 {
t.Errorf("Expected empty list, got %d items", len(follows))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("follow1"),
[]byte("follow2"),
[]byte("follow3"),
}
l.SetFollowedFollows(testPubkeys)
// Verify length
if l.LenFollowedFollows() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenFollowedFollows())
}
// Verify content
follows = l.FollowedFollows()
if len(follows) != len(testPubkeys) {
t.Errorf("Expected %d follows, got %d", len(testPubkeys), len(follows))
}
// Verify each pubkey
for i, pk := range follows {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
@@ -132,37 +132,37 @@ func TestLists_FollowedFollows(t *testing.T) {
func TestLists_OwnersMuted(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
muted := l.OwnersMuted()
if len(muted) != 0 {
t.Errorf("Expected empty list, got %d items", len(muted))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("muted1"),
[]byte("muted2"),
[]byte("muted3"),
}
l.SetOwnersMuted(testPubkeys)
// Verify length
if l.LenOwnersMuted() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersMuted())
}
// Verify content
muted = l.OwnersMuted()
if len(muted) != len(testPubkeys) {
t.Errorf("Expected %d muted, got %d", len(testPubkeys), len(muted))
}
// Verify each pubkey
for i, pk := range muted {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
@@ -171,10 +171,10 @@ func TestLists_OwnersMuted(t *testing.T) {
func TestLists_ConcurrentAccess(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test concurrent access to the lists
done := make(chan bool)
// Concurrent reads and writes
go func() {
for i := 0; i < 100; i++ {
@@ -183,7 +183,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetOwnersFollowed([][]byte{[]byte("followed1"), []byte("followed2")})
@@ -191,7 +191,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetFollowedFollows([][]byte{[]byte("follow1"), []byte("follow2")})
@@ -199,7 +199,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetOwnersMuted([][]byte{[]byte("muted1"), []byte("muted2")})
@@ -207,11 +207,11 @@ func TestLists_ConcurrentAccess(t *testing.T) {
}
done <- true
}()
// Wait for all goroutines to complete
for i := 0; i < 4; i++ {
<-done
}
// If we got here without deadlocks or panics, the test passes
}
}

View File

@@ -20,6 +20,7 @@ import (
"orly.dev/pkg/protocol/servemux"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/keys"
"orly.dev/pkg/utils/log"
"github.com/rs/cors"
@@ -29,14 +30,15 @@ import (
// encapsulates various components such as context, cancel function, options,
// relay interface, address, HTTP server, and configuration settings.
type Server struct {
Ctx context.T
Cancel context.F
options *options.T
relay relay.I
Addr string
mux *servemux.S
httpServer *http.Server
listeners *publish.S
Ctx context.T
Cancel context.F
options *options.T
relay relay.I
Addr string
mux *servemux.S
httpServer *http.Server
listeners *publish.S
blacklistPubkeys [][]byte
*config.C
*Lists
*Peers
@@ -105,6 +107,14 @@ func NewServer(
Lists: new(Lists),
Peers: new(Peers),
}
// Parse blacklist pubkeys
for _, v := range s.C.Blacklist {
var pk []byte
if pk, err = keys.DecodeNpubOrHex(v); chk.E(err) {
continue
}
s.blacklistPubkeys = append(s.blacklistPubkeys, pk)
}
chk.E(
s.Peers.Init(sp.C.PeerRelays, sp.C.RelaySecret),
)