diff --git a/cmd/chihaya/main.go b/cmd/chihaya/main.go index 3d59721..592e446 100644 --- a/cmd/chihaya/main.go +++ b/cmd/chihaya/main.go @@ -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 diff --git a/middleware/deniability/README.md b/middleware/deniability/README.md new file mode 100644 index 0000000..fd50614 --- /dev/null +++ b/middleware/deniability/README.md @@ -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). \ No newline at end of file diff --git a/middleware/deniability/config.go b/middleware/deniability/config.go new file mode 100644 index 0000000..0423493 --- /dev/null +++ b/middleware/deniability/config.go @@ -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 +} diff --git a/middleware/deniability/config_test.go b/middleware/deniability/config_test.go new file mode 100644 index 0000000..b271ba4 --- /dev/null +++ b/middleware/deniability/config_test.go @@ -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) + } +} diff --git a/middleware/deniability/deniability.go b/middleware/deniability/deniability.go new file mode 100644 index 0000000..0e27347 --- /dev/null +++ b/middleware/deniability/deniability.go @@ -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 +} diff --git a/middleware/deniability/deniability_test.go b/middleware/deniability/deniability_test.go new file mode 100644 index 0000000..dbef933 --- /dev/null +++ b/middleware/deniability/deniability_test.go @@ -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)) + } + } +} diff --git a/pkg/random/peer.go b/pkg/random/peer.go new file mode 100644 index 0000000..c950221 --- /dev/null +++ b/pkg/random/peer.go @@ -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, + } +} diff --git a/pkg/random/peer_test.go b/pkg/random/peer_test.go new file mode 100644 index 0000000..f3c883c --- /dev/null +++ b/pkg/random/peer_test.go @@ -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)) +} diff --git a/pkg/random/string.go b/pkg/random/string.go new file mode 100644 index 0000000..bdfa9f2 --- /dev/null +++ b/pkg/random/string.go @@ -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) +} diff --git a/pkg/random/string_test.go b/pkg/random/string_test.go new file mode 100644 index 0000000..e2bd0b2 --- /dev/null +++ b/pkg/random/string_test.go @@ -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))) + } + } +}