Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(netsim/censor): implement blackholing censorship #34

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}