Skip to content

Commit

Permalink
feat(netsim/censor): implement blackholing censorship (#34)
Browse files Browse the repository at this point in the history
This is another common case of censorship where after an offending
packet (or for an offending endpoint) communication cannot actually
proceed and further packets are not allowed.
  • Loading branch information
bassosimone authored Nov 27, 2024
1 parent 5024a24 commit cc90113
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 0 deletions.
104 changes: 104 additions & 0 deletions netsim/censor/ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package censor

import (
"bytes"
"net/netip"
"sync"
"time"

"github.com/rbmk-project/x/netsim/packet"
)

// Blackholer implements connection blackholing with optional pattern matching
// and connection tracking. Once a connection is blackholed, all packets matching
// its five-tuple will be dropped for the configured duration.
type Blackholer struct {
// target specifies an optional specific endpoint to filter
// if zero, applies to all connections.
target netip.AddrPort

// pattern is an optional byte pattern to match in payload
// if nil, only considers the target (if set).
pattern []byte

// duration specifies how long to maintain blackholing state, if set.
duration time.Duration

// mu protects access to blocked.
mu sync.Mutex

// blocked tracks blackholed connections using five-tuple.
blocked map[fiveTuple]time.Time
}

// fiveTuple is the five-tuple identifying a connection.
type fiveTuple struct {
proto packet.IPProtocol
srcAddr netip.Addr
srcPort uint16
dstAddr netip.Addr
dstPort uint16
}

// NewBlackholer creates a new [*Blackholer] instance.
//
// The duration parameter controls how long connections remain blackholed.
//
// If target is zero, it applies to all connections.
//
// If pattern is nil, it doesn't perform payload matching.
func NewBlackholer(duration time.Duration, target netip.AddrPort, pattern []byte) *Blackholer {
return &Blackholer{
target: target,
pattern: pattern,
duration: duration,
mu: sync.Mutex{},
blocked: make(map[fiveTuple]time.Time),
}
}

// Filter implements [packet.Filter].
func (t *Blackholer) Filter(pkt *packet.Packet) (packet.Target, []*packet.Packet) {
// Check if this connection is already blocked
tuple := fiveTuple{
proto: pkt.IPProtocol,
srcAddr: pkt.SrcAddr,
srcPort: pkt.SrcPort,
dstAddr: pkt.DstAddr,
dstPort: pkt.DstPort,
}
now := time.Now()
t.mu.Lock()
deadline, ok := t.blocked[tuple]
blocked := ok && now.Before(deadline)
if ok && !blocked {
delete(t.blocked, tuple)
}
t.mu.Unlock()
if blocked {
return packet.DROP, nil
}

// Check if we need to filter specific endpoint
if t.target.IsValid() {
if pkt.DstAddr != t.target.Addr() || pkt.DstPort != t.target.Port() {
return packet.ACCEPT, nil
}
}

// If we have a pattern, check payload
if t.pattern != nil {
if len(pkt.Payload) <= 0 || !bytes.Contains(pkt.Payload, t.pattern) {
return packet.ACCEPT, nil
}
}

// Block this connection
t.mu.Lock()
t.blocked[tuple] = now.Add(t.duration)
t.mu.Unlock()

return packet.DROP, nil
}
40 changes: 40 additions & 0 deletions netsim/example_censor_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/netip"
"time"

"github.com/rbmk-project/x/netsim"
"github.com/rbmk-project/x/netsim/censor"
Expand Down Expand Up @@ -53,3 +54,42 @@ func Example_tlsRSTInjection() {
// Output:
// err: Get "https://dns.google/": connection reset by peer
}

// This example shows how to use [netsim] to simulate SNI-based TLS blocking
// using connection blackholing.
func Example_tlsBlackholing() {
// Create a new scenario using the given directory to cache
// the certificates used by the simulated PKI
scenario := netsim.NewScenario("testdata")
defer scenario.Close()

// Create server stack emulating dns.google.
scenario.Attach(scenario.MustNewGoogleDNSStack())

// Configure blackholing on the scenario router targeting
// connections where the SNI matches "dns.google"
scenario.Router().AddFilter(censor.NewBlackholer(
300*time.Second, // residual censorship duration
netip.AddrPort{}, // match any endpoint
[]byte("dns.google"), // match SNI
))

// Create and attach the client stack.
clientStack := scenario.MustNewClientStack()
scenario.Attach(clientStack)

// Create the HTTP client with a short timeout
clientTxp := scenario.NewHTTPTransport(clientStack)
defer clientTxp.CloseIdleConnections()
clientHTTP := &http.Client{
Transport: clientTxp,
Timeout: 200 * time.Millisecond, // short timeout for testing
}

// Attempt the HTTPS request, which should time out
_, err := clientHTTP.Get("https://dns.google/")
fmt.Printf("err: %v\n", err)

// Output:
// err: Get "https://dns.google/": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
}

0 comments on commit cc90113

Please sign in to comment.