Merge pull request #148 from mrd0ll4r/deniability-middleware

Deniability middleware, some util packages
This commit is contained in:
Jimmy Zelinskie 2016-04-02 18:02:45 -04:00
commit 83ffc0425e
10 changed files with 553 additions and 0 deletions

View file

@ -21,6 +21,7 @@ import (
_ "github.com/chihaya/chihaya/server/store/memory"
_ "github.com/chihaya/chihaya/server/store/middleware/client"
_ "github.com/chihaya/chihaya/server/store/middleware/ip"
_ "github.com/chihaya/chihaya/middleware/deniability"
)
var configPath string

View file

@ -0,0 +1,39 @@
## Deniability Middleware
This package provides the announce middleware `deniability` which inserts ghost peers into announce responses to achieve plausible deniability.
### Functionality
This middleware will choose random announces and modify the list of peers returned.
A random number of randomly generated peers will be inserted at random positions into the list of peers.
As soon as the list of peers exceeds `numWant`, peers will be replaced rather than inserted.
Note that if a response is picked for augmentation, both IPv4 and IPv6 peers will be modified, in case they are not empty.
Also note that the IP address for the generated peeer consists of bytes in the range [1,254].
### Configuration
This middleware provides the following parameters for configuration:
- `modify_response_probability` (float, >0, <= 1) indicates the probability by which a response will be augmented with random peers.
- `max_random_peers` (int, >0) sets an upper boundary (inclusive) for the amount of peers added.
- `prefix` (string, 20 characters at most) sets the prefix for generated peer IDs.
The peer ID will be padded to 20 bytes using a random string of alphanumeric characters.
- `min_port` (int, >0, <=65535) sets a lower boundary for the port for generated peers.
- `max_port` (int, >0, <=65536, > `min_port`) sets an upper boundary for the port for generated peers.
An example config might look like this:
chihaya:
tracker:
announce_middleware:
- name: deniability
config:
modify_response_probability: 0.2
max_random_peers: 5
prefix: -AZ2060-
min_port: 40000
max_port: 60000
For more information about peer IDs and their prefixes, see [this wiki entry](https://wiki.theory.org/BitTorrentSpecification#peer_id).

View file

@ -0,0 +1,46 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package deniability
import (
"gopkg.in/yaml.v2"
"github.com/chihaya/chihaya"
)
// Config represents the configuration for the deniability middleware.
type Config struct {
// ModifyResponseProbability is the probability by which a response will
// be augmented with random peers.
ModifyResponseProbability float32 `yaml:"modify_response_probability"`
// MaxRandomPeers is the amount of peers that will be added at most.
MaxRandomPeers int `yaml:"max_random_peers"`
// Prefix is the prefix to be used for peer IDs.
Prefix string `yaml:"prefix"`
// MinPort is the minimum port (inclusive) for the generated peer.
MinPort int `yaml:"min_port"`
// MaxPort is the maximum port (exclusive) for the generated peer.
MaxPort int `yaml:"max_port"`
}
// newConfig parses the given MiddlewareConfig as a deniability.Config.
func newConfig(mwcfg chihaya.MiddlewareConfig) (*Config, error) {
bytes, err := yaml.Marshal(mwcfg.Config)
if err != nil {
return nil, err
}
var cfg Config
err = yaml.Unmarshal(bytes, &cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

View file

@ -0,0 +1,63 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package deniability
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
"github.com/chihaya/chihaya"
)
type configTestData struct {
modifyProbability string
maxNewPeers string
prefix string
minPort string
maxPort string
err bool
expected Config
}
var (
configTemplate = `
name: foo
config:
modify_response_probability: %s
max_random_peers: %s
prefix: %s
min_port: %s
max_port: %s`
configData = []configTestData{
{"1.0", "5", "abc", "2000", "3000", false, Config{1.0, 5, "abc", 2000, 3000}},
{"a", "a", "12", "a", "a", true, Config{}},
}
)
func TestNewConfig(t *testing.T) {
var mwconfig chihaya.MiddlewareConfig
cfg, err := newConfig(mwconfig)
assert.Nil(t, err)
assert.NotNil(t, cfg)
for _, test := range configData {
config := fmt.Sprintf(configTemplate, test.modifyProbability, test.maxNewPeers, test.prefix, test.minPort, test.maxPort)
err = yaml.Unmarshal([]byte(config), &mwconfig)
assert.Nil(t, err)
cfg, err = newConfig(mwconfig)
if test.err {
assert.NotNil(t, err)
continue
}
assert.Nil(t, err)
assert.Equal(t, test.expected, *cfg)
}
}

View file

@ -0,0 +1,121 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package deniability
import (
"errors"
"math/rand"
"time"
"github.com/chihaya/chihaya"
"github.com/chihaya/chihaya/pkg/random"
"github.com/chihaya/chihaya/tracker"
)
func init() {
tracker.RegisterAnnounceMiddlewareConstructor("deniability", constructor)
}
type deniabilityMiddleware struct {
cfg *Config
r *rand.Rand
}
// constructor provides a middleware constructor that returns a middleware to
// insert peers into the peer lists returned as a response to an announce.
//
// It returns an error if the config provided is either syntactically or
// semantically incorrect.
func constructor(c chihaya.MiddlewareConfig) (tracker.AnnounceMiddleware, error) {
cfg, err := newConfig(c)
if err != nil {
return nil, err
}
if cfg.ModifyResponseProbability <= 0 || cfg.ModifyResponseProbability > 1 {
return nil, errors.New("modify_response_probability must be in [0,1)")
}
if cfg.MaxRandomPeers <= 0 {
return nil, errors.New("max_random_peers must be > 0")
}
if cfg.MinPort <= 0 {
return nil, errors.New("min_port must not be <= 0")
}
if cfg.MaxPort > 65536 {
return nil, errors.New("max_port must not be > 65536")
}
if cfg.MinPort >= cfg.MaxPort {
return nil, errors.New("max_port must not be <= min_port")
}
if len(cfg.Prefix) > 20 {
return nil, errors.New("prefix must not be longer than 20 bytes")
}
mw := deniabilityMiddleware{
cfg: cfg,
r: rand.New(rand.NewSource(time.Now().UnixNano())),
}
return mw.modifyResponse, nil
}
func (mw *deniabilityMiddleware) modifyResponse(next tracker.AnnounceHandler) tracker.AnnounceHandler {
return func(cfg *chihaya.TrackerConfig, req *chihaya.AnnounceRequest, resp *chihaya.AnnounceResponse) error {
err := next(cfg, req, resp)
if err != nil {
return err
}
if mw.cfg.ModifyResponseProbability == 1 || mw.r.Float32() < mw.cfg.ModifyResponseProbability {
numNewPeers := mw.r.Intn(mw.cfg.MaxRandomPeers) + 1
for i := 0; i < numNewPeers; i++ {
if len(resp.IPv6Peers) > 0 {
if len(resp.IPv6Peers) >= int(req.NumWant) {
mw.replacePeer(resp.IPv6Peers, true)
} else {
resp.IPv6Peers = mw.insertPeer(resp.IPv6Peers, true)
}
}
if len(resp.IPv4Peers) > 0 {
if len(resp.IPv4Peers) >= int(req.NumWant) {
mw.replacePeer(resp.IPv4Peers, false)
} else {
resp.IPv4Peers = mw.insertPeer(resp.IPv4Peers, false)
}
}
}
}
return nil
}
}
// replacePeer replaces a peer from a random position within the given slice
// of peers with a randomly generated one.
//
// replacePeer panics if len(peers) == 0.
func (mw *deniabilityMiddleware) replacePeer(peers []chihaya.Peer, v6 bool) {
peers[mw.r.Intn(len(peers))] = random.Peer(mw.r, mw.cfg.Prefix, v6, mw.cfg.MinPort, mw.cfg.MaxPort)
}
// insertPeer inserts a randomly generated peer at a random position into the
// given slice and returns the new slice.
func (mw *deniabilityMiddleware) insertPeer(peers []chihaya.Peer, v6 bool) []chihaya.Peer {
pos := 0
if len(peers) > 0 {
pos = mw.r.Intn(len(peers))
}
peers = append(peers, chihaya.Peer{})
copy(peers[pos+1:], peers[pos:])
peers[pos] = random.Peer(mw.r, mw.cfg.Prefix, v6, mw.cfg.MinPort, mw.cfg.MaxPort)
return peers
}

View file

@ -0,0 +1,110 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package deniability
import (
"fmt"
"math/rand"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/chihaya/chihaya"
)
type constructorTestData struct {
cfg Config
error bool
}
var constructorData = []constructorTestData{
{Config{1.0, 10, "abc", 1024, 1025}, false},
{Config{1.1, 10, "abc", 1024, 1025}, true},
{Config{0, 10, "abc", 1024, 1025}, true},
{Config{1.0, 0, "abc", 1024, 1025}, true},
{Config{1.0, 10, "01234567890123456789_", 1024, 1025}, true},
{Config{1.0, 10, "abc", 0, 1025}, true},
{Config{1.0, 10, "abc", 1024, 0}, true},
{Config{1.0, 10, "abc", 1024, 65537}, true},
}
func TestReplacePeer(t *testing.T) {
cfg := Config{
Prefix: "abc",
MinPort: 1024,
MaxPort: 1025,
}
mw := deniabilityMiddleware{
r: rand.New(rand.NewSource(0)),
cfg: &cfg,
}
peer := chihaya.Peer{
ID: chihaya.PeerID("abcdefghijklmnoprstu"),
Port: 2000,
IP: net.ParseIP("10.150.255.23"),
}
peers := []chihaya.Peer{peer}
mw.replacePeer(peers, false)
assert.Equal(t, 1, len(peers))
assert.Equal(t, "abc", string(peers[0].ID[:3]))
assert.Equal(t, uint16(1024), peers[0].Port)
assert.NotNil(t, peers[0].IP.To4())
mw.replacePeer(peers, true)
assert.Equal(t, 1, len(peers))
assert.Equal(t, "abc", string(peers[0].ID[:3]))
assert.Equal(t, uint16(1024), peers[0].Port)
assert.Nil(t, peers[0].IP.To4())
peers = []chihaya.Peer{peer, peer}
mw.replacePeer(peers, true)
assert.True(t, (peers[0].Port == peer.Port) != (peers[1].Port == peer.Port), "not exactly one peer was replaced")
}
func TestInsertPeer(t *testing.T) {
cfg := Config{
Prefix: "abc",
MinPort: 1024,
MaxPort: 1025,
}
mw := deniabilityMiddleware{
r: rand.New(rand.NewSource(0)),
cfg: &cfg,
}
peer := chihaya.Peer{
ID: chihaya.PeerID("abcdefghijklmnoprstu"),
Port: 2000,
IP: net.ParseIP("10.150.255.23"),
}
var peers []chihaya.Peer
peers = mw.insertPeer(peers, false)
assert.Equal(t, 1, len(peers))
assert.Equal(t, uint16(1024), peers[0].Port)
assert.Equal(t, "abc", string(peers[0].ID[:3]))
assert.NotNil(t, peers[0].IP.To4())
peers = []chihaya.Peer{peer, peer}
peers = mw.insertPeer(peers, true)
assert.Equal(t, 3, len(peers))
}
func TestConstructor(t *testing.T) {
for _, tt := range constructorData {
_, err := constructor(chihaya.MiddlewareConfig{
Config: tt.cfg,
})
if tt.error {
assert.NotNil(t, err, fmt.Sprintf("error expected for %+v", tt.cfg))
} else {
assert.Nil(t, err, fmt.Sprintf("no error expected for %+v", tt.cfg))
}
}
}

74
pkg/random/peer.go Normal file
View file

@ -0,0 +1,74 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package random
import (
"math/rand"
"net"
"github.com/chihaya/chihaya"
)
// Peer generates a random chihaya.Peer.
//
// prefix is the prefix to use for the peer ID. If len(prefix) > 20, it will be
// truncated to 20 characters. If len(prefix) < 20, it will be padded with an
// alphanumeric random string to have 20 characters.
//
// v6 indicates whether an IPv6 address should be generated.
// Regardless of the length of the generated IP address, its bytes will have
// values in [1,254].
//
// minPort and maxPort describe the range for the randomly generated port, where
// minPort <= port < maxPort.
// minPort and maxPort will be checked and altered so that
// 1 <= minPort <= maxPort <= 65536.
// If minPort == maxPort, port will be set to minPort.
func Peer(r *rand.Rand, prefix string, v6 bool, minPort, maxPort int) chihaya.Peer {
var (
port uint16
ip net.IP
)
if minPort <= 0 {
minPort = 1
}
if maxPort > 65536 {
maxPort = 65536
}
if maxPort < minPort {
maxPort = minPort
}
if len(prefix) > 20 {
prefix = prefix[:20]
}
if minPort == maxPort {
port = uint16(minPort)
} else {
port = uint16(r.Int63()%int64(maxPort-minPort)) + uint16(minPort)
}
if v6 {
b := make([]byte, 16)
ip = net.IP(b)
} else {
b := make([]byte, 4)
ip = net.IP(b)
}
for i := range ip {
b := r.Intn(254) + 1
ip[i] = byte(b)
}
prefix = prefix + AlphaNumericString(r, 20-len(prefix))
return chihaya.Peer{
ID: chihaya.PeerID(prefix),
Port: port,
IP: ip,
}
}

43
pkg/random/peer_test.go Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package random
import (
"math/rand"
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPeer(t *testing.T) {
r := rand.New(rand.NewSource(0))
for i := 0; i < 100; i++ {
minPort := 2000
maxPort := 2010
p := Peer(r, "", false, minPort, maxPort)
assert.Equal(t, 20, len(p.ID))
assert.True(t, p.Port >= uint16(minPort) && p.Port < uint16(maxPort))
assert.NotNil(t, p.IP.To4())
}
for i := 0; i < 100; i++ {
minPort := 2000
maxPort := 2010
p := Peer(r, "", true, minPort, maxPort)
assert.Equal(t, 20, len(p.ID))
assert.True(t, p.Port >= uint16(minPort) && p.Port < uint16(maxPort))
assert.True(t, len(p.IP) == net.IPv6len)
}
p := Peer(r, "abcdefghijklmnopqrst", false, 2000, 2000)
assert.Equal(t, "abcdefghijklmnopqrst", string(p.ID))
assert.Equal(t, uint16(2000), p.Port)
p = Peer(r, "abcdefghijklmnopqrstUVWXYZ", true, -10, -5)
assert.Equal(t, "abcdefghijklmnopqrst", string(p.ID))
assert.True(t, p.Port >= uint16(1) && p.Port <= uint16(65535))
}

26
pkg/random/string.go Normal file
View file

@ -0,0 +1,26 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package random
import "math/rand"
// AlphaNumeric is an alphabet with all lower- and uppercase letters and
// numbers.
const AlphaNumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// AlphaNumericString is a shorthand for String(r, l, AlphaNumeric).
func AlphaNumericString(r rand.Source, l int) string {
return String(r, l, AlphaNumeric)
}
// String generates a random string of length l, containing only runes from
// the alphabet using the random source r.
func String(r rand.Source, l int, alphabet string) string {
b := make([]byte, l)
for i := range b {
b[i] = alphabet[r.Int63()%int64(len(alphabet))]
}
return string(b)
}

30
pkg/random/string_test.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2016 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file.
package random
import (
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAlphaNumericString(t *testing.T) {
r := rand.NewSource(0)
s := AlphaNumericString(r, 0)
assert.Equal(t, 0, len(s))
s = AlphaNumericString(r, 10)
assert.Equal(t, 10, len(s))
for i := 0; i < 100; i++ {
s := AlphaNumericString(r, 10)
for _, c := range s {
assert.True(t, strings.Contains(AlphaNumeric, string(c)))
}
}
}