rswim 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cdad1327bf5ba2e7fd3a2d975b7f0ad507d99952b9c2b2d01269f4f3b813f25
4
- data.tar.gz: 2050c00ddb9eec045103645d269683ad0454d664d92026f6849f80428e1483ff
3
+ metadata.gz: 1acfab8998ff5ebd1919f16fd0e27c2f708224aedb004376ccbe2f5818bb6648
4
+ data.tar.gz: 5c85faa591b08d74d38b2fb091ee56b1e836af3e863005e51cbd55e35df2f255
5
5
  SHA512:
6
- metadata.gz: 17a26dc6da80bc10db656b7127f2e72c7bb77d962eec38fc63830aa5b5d4642c8e3650fb8c4d2b860efac2e1eaa12c590c98f31dc1e4092adec31018c8b558bf
7
- data.tar.gz: 9563514c494f896b54098b80b1636a20323a84d07b5f9b689577e592592edc6ae480449fcf7020ae1ad79b72084a7d087967983f9b2229a712de9d4712e95d8e
6
+ metadata.gz: a7c312893d1e1363675ad17ab97d7ad87dd016ab39ef4debb208016e970fdf9d8f1813fcfbe7f1c39ad8c0933765cf7b94fda82405dd732daa6bd9b3de2bba6e
7
+ data.tar.gz: 858683dc2c970daaefb085e467249a62b37a1e88e6ad556d5a944112bd51e463cda530168ee0a4abda0d35b175a3aceb031e4b4346dea44f1205c900b43647d4
data/.gitignore CHANGED
@@ -15,4 +15,5 @@
15
15
  # rspec failure tracking
16
16
  .rspec_status
17
17
  .byebug_history
18
+ .ruby-version
18
19
  .DS_Store
data/CHANGELOG.md CHANGED
@@ -1,3 +1,4 @@
1
1
  # 1.0.0 Complete implementation for UDP plus simple, human readable serialisation of messages
2
2
  # 2.0.0 Piggyback custom state on the liveness propagation mechanism using `RSwim::Node#append_custom_state`
3
- # 2.1.0 Use non-blocking I/O by means of `Fiber.shedule` with the scheduler provided by the Async gem
3
+ # 2.1.0 Use non-blocking I/O by means of `Fiber.shedule` with the scheduler provided by the Async gem
4
+ # 2.2.0 Encrypted messages between peers. Run UDP Sender in fiber instead of thread to make whole node single threaded.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rswim (2.1.0)
4
+ rswim (2.2.0)
5
5
  async (~> 1.30)
6
6
  slop (~> 4.9)
7
7
  zeitwerk (~> 2.2)
@@ -15,12 +15,12 @@ GEM
15
15
  timers (~> 4.1)
16
16
  byebug (11.1.3)
17
17
  coderay (1.1.3)
18
- console (1.13.1)
18
+ console (1.14.0)
19
19
  fiber-local
20
- diff-lcs (1.4.4)
21
- ffi (1.15.4)
20
+ diff-lcs (1.5.0)
21
+ ffi (1.15.5)
22
22
  fiber-local (1.0.0)
23
- formatador (0.3.0)
23
+ formatador (1.1.0)
24
24
  fuubar (2.5.1)
25
25
  rspec-core (~> 3.0)
26
26
  ruby-progressbar (~> 1.4)
@@ -38,7 +38,7 @@ GEM
38
38
  guard (~> 2.1)
39
39
  guard-compat (~> 1.1)
40
40
  rspec (>= 2.99.0, < 4.0)
41
- listen (3.7.0)
41
+ listen (3.7.1)
42
42
  rb-fsevent (~> 0.10, >= 0.10.3)
43
43
  rb-inotify (~> 0.9, >= 0.9.10)
44
44
  lumberjack (1.2.8)
@@ -52,28 +52,28 @@ GEM
52
52
  coderay (~> 1.1)
53
53
  method_source (~> 1.0)
54
54
  rake (12.3.3)
55
- rb-fsevent (0.11.0)
55
+ rb-fsevent (0.11.1)
56
56
  rb-inotify (0.10.1)
57
57
  ffi (~> 1.0)
58
- rspec (3.10.0)
59
- rspec-core (~> 3.10.0)
60
- rspec-expectations (~> 3.10.0)
61
- rspec-mocks (~> 3.10.0)
62
- rspec-core (3.10.1)
63
- rspec-support (~> 3.10.0)
64
- rspec-expectations (3.10.1)
58
+ rspec (3.11.0)
59
+ rspec-core (~> 3.11.0)
60
+ rspec-expectations (~> 3.11.0)
61
+ rspec-mocks (~> 3.11.0)
62
+ rspec-core (3.11.0)
63
+ rspec-support (~> 3.11.0)
64
+ rspec-expectations (3.11.0)
65
65
  diff-lcs (>= 1.2.0, < 2.0)
66
- rspec-support (~> 3.10.0)
67
- rspec-mocks (3.10.2)
66
+ rspec-support (~> 3.11.0)
67
+ rspec-mocks (3.11.0)
68
68
  diff-lcs (>= 1.2.0, < 2.0)
69
- rspec-support (~> 3.10.0)
70
- rspec-support (3.10.2)
69
+ rspec-support (~> 3.11.0)
70
+ rspec-support (3.11.0)
71
71
  ruby-progressbar (1.11.0)
72
72
  shellany (0.0.1)
73
73
  slop (4.9.1)
74
- thor (1.1.0)
74
+ thor (1.2.1)
75
75
  timers (4.3.3)
76
- zeitwerk (2.4.2)
76
+ zeitwerk (2.5.4)
77
77
 
78
78
  PLATFORMS
79
79
  ruby
data/README.md CHANGED
@@ -4,11 +4,14 @@ RSwim is a Ruby implementation of the SWIM gossip protocol, a mechanism for disc
4
4
 
5
5
  It is an implementation inspired by the original [SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol](https://www.cs.cornell.edu/projects/Quicksilver/public_pdfs/SWIM.pdf) paper by Abhinandan Das, Indranil Gupta, Ashish Motivala.
6
6
 
7
- The implementation is kept intentionally simple and limited to the features described in the paper, except for the addition in version 2.0.0 of the ability to piggyback custom state on the liveness propagation mechanism, see `RSwim::Node#append_custom_state`
7
+ The implementation is kept intentionally simple and includes only the features described in the paper along with a few additions after version 2.0.0:
8
+
9
+ - The ability to piggyback custom state on the liveness propagation mechanism was added in version 2.0.0, see `RSwim::Node#append_custom_state`
10
+ - Encryption of messsages between peers based on a shared secret was introduced in version 2.2.0, see module `RSwim::Serialization::Encrypted`
8
11
 
9
12
  No attempts have been made to address known security issues such as Byzantine attacks.
10
13
 
11
- Currently RSwim runs on UDP with a custom, human readable serialization format.
14
+ Currently RSwim runs on UDP. In the unencrypted mode it uses a custom, human readable serialization format. Peers in unencrypted mode cannot communicate with peers in encrypted mode.
12
15
 
13
16
 
14
17
  ## Installation
@@ -34,6 +37,9 @@ Example:
34
37
  ```ruby
35
38
  require 'rswim'
36
39
 
40
+ RSwim.encrypted = true
41
+ RSwim.shared_secret = 'santa 2000'
42
+
37
43
  port = 4545
38
44
 
39
45
  # known, running nodes to connect with initially.
data/bin/encrypt ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby --jit
2
+
3
+ require 'openssl'
4
+ require 'console'
5
+
6
+ cipher = OpenSSL::Cipher::AES256.new :CBC
7
+ cipher.encrypt
8
+ iv = cipher.random_iv
9
+ cipher.key = key = Digest::SHA256.digest 'SecretPassword'
10
+ cipher_text = cipher.update('This is a secret message') + cipher.final
11
+
12
+ Console.logger.info cipher_text, 'got this after encrypting'
13
+
14
+ begin
15
+ decipher = OpenSSL::Cipher::AES256.new :CBC
16
+ decipher.decrypt
17
+ decipher.iv = iv # previously saved
18
+ decipher.key = Digest::SHA256.digest 'mismatch'
19
+ plain_text = decipher.update(cipher_text) + decipher.final
20
+
21
+ Console.logger.info plain_text, 'got this after decrypting'
22
+ rescue OpenSSL::Cipher::CipherError => e
23
+ Console.logger.error('Error while decrypting')
24
+ Console.logger.failure(decipher, e)
25
+ end
data/bin/run_node CHANGED
@@ -8,6 +8,8 @@ PORT = 4545
8
8
  opts = Slop.parse do |o|
9
9
  o.array '-s', '--seeds', 'a comma separated list of seed nodes'
10
10
  o.bool '-d', '--debug', 'turn on debug logging'
11
+ o.bool '-e', '--encrypted', 'use encrypted mode'
12
+ o.string '--secret', 'shared secret for encrypted mode', default: 'santa 2000'
11
13
  o.on '--help' do
12
14
  puts o
13
15
  exit
@@ -21,6 +23,12 @@ end
21
23
  puts "Ruby version: #{RUBY_VERSION}"
22
24
 
23
25
  RSwim::Logger.level = ::Logger::DEBUG if opts.debug?
26
+
27
+ if opts.encrypted?
28
+ RSwim.encrypted = true
29
+ RSwim.shared_secret = opts[:secret]
30
+ end
31
+
24
32
  seed_hosts = opts[:seeds]
25
33
  abort 'EOF' if seed_hosts.nil?
26
34
 
@@ -0,0 +1,43 @@
1
+ module RSwim
2
+ module Encryption
3
+ class << self
4
+ def encrypt(message)
5
+ message = message.dup.force_encoding('UTF-8')
6
+ salt = cipher.random_iv
7
+ cipher_text = cipher.update(message) + cipher.final
8
+ [cipher_text, salt]
9
+ rescue StandardError => e
10
+ raise Error, "Failed to encrypt: #{e.message}"
11
+ end
12
+
13
+ def decrypt(cipher_text, salt)
14
+ decipher.iv = salt
15
+ message = decipher.update(cipher_text) + decipher.final
16
+ message.force_encoding('UTF-8')
17
+ rescue StandardError => e
18
+ raise Error, "Failed to decrypt: #{e.message}"
19
+ end
20
+
21
+ private
22
+
23
+ def cipher
24
+ @_cipher ||= begin
25
+ cipher = OpenSSL::Cipher::AES256.new :CBC
26
+ cipher.encrypt
27
+ cipher.key = Digest::SHA256.digest(RSwim.shared_secret)
28
+ cipher
29
+ end
30
+ end
31
+
32
+ def decipher
33
+ @_decipher ||= begin
34
+ cipher = OpenSSL::Cipher::AES256.new :CBC
35
+ cipher.decrypt
36
+ cipher.key = Digest::SHA256.digest(RSwim.shared_secret)
37
+ cipher
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -3,36 +3,6 @@
3
3
  module RSwim
4
4
  module Integration
5
5
  module UDP
6
- class Sender
7
- def initialize(port, out_q)
8
- @out_q = out_q
9
- @port = port
10
- @out_s = UDPSocket.new
11
- end
12
-
13
- def run
14
- Async do
15
- loop do
16
- wire_messages = @out_q.pop
17
- wire_messages.each do |(host, wire_message)|
18
- logger.debug "about to send message to #{host} on port #{@port}"
19
- Fiber.schedule do
20
- @out_s.send(wire_message, 0, host, @port)
21
- rescue StandardError => e
22
- logger.debug("Error while sending: #{e}")
23
- end
24
- end
25
- end
26
- end
27
- end
28
-
29
- private
30
-
31
- def logger
32
- @_logger ||= RSwim::Logger.new(self.class, $stderr)
33
- end
34
- end
35
-
36
6
  class IOLoop < RSwim::IOLoop
37
7
  def initialize(agent, serializer, deserializer, directory, sleep_time_seconds, my_host, port)
38
8
  super(agent, serializer, deserializer, directory, sleep_time_seconds)
@@ -46,7 +16,7 @@ module RSwim
46
16
  @in_s = UDPSocket.new
47
17
  @in_s.bind(@my_host, @port)
48
18
  @out_q = Queue.new
49
- Thread.new { Sender.new(@port, @out_q).run }.abort_on_exception = true
19
+ Fiber.schedule { Sender.new(@port, @out_q).run }
50
20
  end
51
21
 
52
22
  def read
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSwim
4
+ module Integration
5
+ module UDP
6
+ class Sender
7
+ def initialize(port, out_q)
8
+ @out_q = out_q
9
+ @port = port
10
+ @out_s = UDPSocket.new
11
+ end
12
+
13
+ def run
14
+ Async do
15
+ loop do
16
+ wire_messages = @out_q.pop
17
+ wire_messages.each do |(host, wire_message)|
18
+ logger.debug "about to send message to #{host} on port #{@port}"
19
+ Fiber.schedule do
20
+ @out_s.send(wire_message, 0, host, @port)
21
+ rescue StandardError => e
22
+ logger.debug("Error while sending: #{e}")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def logger
32
+ @_logger ||= RSwim::Logger.new(self.class, $stderr)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/rswim/io_loop.rb CHANGED
@@ -12,8 +12,8 @@ module RSwim
12
12
  end
13
13
 
14
14
  def run
15
- before_run
16
15
  Async do
16
+ before_run
17
17
  start_producer
18
18
  loop do
19
19
  in_messages = consume_read_buffer
data/lib/rswim/node.rb CHANGED
@@ -7,11 +7,13 @@ module RSwim
7
7
  end
8
8
 
9
9
  def initialize(my_host, seed_hosts, t_ms, r_ms)
10
+ RSwim.validate_config!
10
11
  @my_host = my_host
11
12
  @directory = Directory.new
12
13
  @my_id = @directory.id(@my_host)
13
- @deserializer = Integration::Deserializer.new(@directory, @my_id)
14
- @serializer = Integration::Serializer.new(@directory)
14
+ serialization = RSwim.encrypted ? Serialization::Encrypted : Serialization::Simple
15
+ @deserializer = serialization::Deserializer.new(@directory, @my_id)
16
+ @serializer = serialization::Serializer.new(@directory)
15
17
  @seed_ids = seed_hosts.map { |host| @directory.id(host) }
16
18
  @t_ms = t_ms
17
19
  @r_ms = r_ms
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSwim
4
+ module Serialization::Encrypted
5
+ class Deserializer
6
+ def initialize(directory, my_id)
7
+ @directory = directory
8
+ @my_id = my_id
9
+ end
10
+
11
+ def deserialize(sender_host, wire_message)
12
+ outer = JSON.parse(wire_message)
13
+ cipher_text, salt = outer.values_at('message', 'salt').map { |s| Base64.decode64(s) }
14
+ inner = JSON.parse(Encryption.decrypt(cipher_text, salt), symbolize_names: true)
15
+
16
+ from = @directory.id(sender_host)
17
+ payload = {}
18
+ payload[:target_id] = @directory.id(inner[:target]) unless inner[:target].nil?
19
+ payload[:updates] = inner[:updates].to_a.map do |u|
20
+ UpdateEntry.new(
21
+ @directory.id(u[:host]),
22
+ u[:status].to_sym,
23
+ u[:incarnation_number].to_i,
24
+ u[:custom_state]
25
+ )
26
+ end
27
+ Message.new(@my_id, from, inner[:type].to_sym, payload)
28
+ rescue StandardError => e
29
+ logger.debug("Failed to parse wire message")
30
+ nil
31
+ end
32
+
33
+ protected
34
+
35
+ def logger
36
+ @_logger ||= RSwim::Logger.new(self.class, $stderr)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSwim
4
+ module Serialization::Encrypted
5
+ class Serializer
6
+ def initialize(directory)
7
+ @directory = directory
8
+ end
9
+
10
+ def serialize(message)
11
+ unencrypted = serialize_unencrypted(message)
12
+ cipher_text, salt = Encryption.encrypt(unencrypted).map { |s| Base64.encode64(s) }
13
+ { message: cipher_text, salt: salt }.to_json
14
+ end
15
+
16
+ protected
17
+
18
+ def logger
19
+ @_logger ||= RSwim::Logger.new(self.class, STDERR)
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_unencrypted(message)
25
+ out = {}
26
+ out[:type] = message.type
27
+ out[:target] = @directory.host(message.payload[:target_id]) if message.type == :ping_req
28
+ out[:updates] = message.payload[:updates].to_a.map do |update|
29
+ {
30
+ host: @directory.host(update.member_id),
31
+ status: update.status,
32
+ incarnation_number: update.incarnation_number,
33
+ custom_state: update.custom_state
34
+ }
35
+ end
36
+
37
+ out.to_json
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSwim
4
- module Integration
4
+ module Serialization::Simple
5
5
  class Deserializer
6
6
  def initialize(directory, my_id)
7
7
  @directory = directory
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSwim
4
- module Integration
4
+ module Serialization::Simple
5
5
  class Serializer
6
6
  def initialize(directory)
7
7
  @directory = directory
data/lib/rswim/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSwim
4
- VERSION = '2.1.0'
4
+ VERSION = '2.2.0'
5
5
  end
data/lib/rswim.rb CHANGED
@@ -4,8 +4,14 @@ require 'logger'
4
4
  require 'socket'
5
5
  require 'zeitwerk'
6
6
  require 'async'
7
+ require 'openssl'
8
+ require 'base64'
7
9
 
8
- # frozen_string_literal: true
10
+ cipher = OpenSSL::Cipher::AES256.new :CBC
11
+ cipher.encrypt
12
+ iv = cipher.random_iv
13
+ cipher.key = key = Digest::SHA256.digest 'SecretPassword'
14
+ cipher_text = cipher.update('This is a secret message') + cipher.final
9
15
 
10
16
  class MyInflector < Zeitwerk::Inflector
11
17
  def camelize(basename, _abspath)
@@ -31,5 +37,19 @@ module RSwim
31
37
  # Roundtrip time, millis
32
38
  R_MS = 10_000
33
39
 
40
+ class << self
41
+ attr_accessor :encrypted, :shared_secret
42
+
43
+ def validate_config!
44
+ validate_shared_secret! if @encrypted
45
+ true
46
+ end
47
+
48
+ def validate_shared_secret!
49
+ raise Error, 'Encrypted mode was set, but no shared secret configured' if @shared_secret.nil?
50
+ raise Error, 'Shared secret too short' if @shared_secret.length < 8
51
+ end
52
+ end
53
+
34
54
  class Error < StandardError; end
35
55
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rswim
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Madsen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-10 00:00:00.000000000 Z
11
+ date: 2022-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -147,7 +147,6 @@ extra_rdoc_files: []
147
147
  files:
148
148
  - ".gitignore"
149
149
  - ".rspec"
150
- - ".ruby-version"
151
150
  - ".travis.yml"
152
151
  - CHANGELOG.md
153
152
  - Gemfile
@@ -158,15 +157,16 @@ files:
158
157
  - Rakefile
159
158
  - bin/async_loop
160
159
  - bin/console
160
+ - bin/encrypt
161
161
  - bin/run_node
162
162
  - bin/setup
163
163
  - lib/rswim.rb
164
164
  - lib/rswim/agent.rb
165
165
  - lib/rswim/directory.rb
166
- - lib/rswim/integration/deserializer.rb
167
- - lib/rswim/integration/serializer.rb
166
+ - lib/rswim/encryption.rb
168
167
  - lib/rswim/integration/udp/io_loop.rb
169
168
  - lib/rswim/integration/udp/node.rb
169
+ - lib/rswim/integration/udp/sender.rb
170
170
  - lib/rswim/io_loop.rb
171
171
  - lib/rswim/logger.rb
172
172
  - lib/rswim/member/ack_responder.rb
@@ -191,6 +191,10 @@ files:
191
191
  - lib/rswim/node.rb
192
192
  - lib/rswim/pipe.rb
193
193
  - lib/rswim/protocol_state.rb
194
+ - lib/rswim/serialization/encrypted/deserializer.rb
195
+ - lib/rswim/serialization/encrypted/serializer.rb
196
+ - lib/rswim/serialization/simple/deserializer.rb
197
+ - lib/rswim/serialization/simple/serializer.rb
194
198
  - lib/rswim/status_report.rb
195
199
  - lib/rswim/update_entry.rb
196
200
  - lib/rswim/version.rb
@@ -219,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
219
223
  - !ruby/object:Gem::Version
220
224
  version: '0'
221
225
  requirements: []
222
- rubygems_version: 3.2.22
226
+ rubygems_version: 3.3.3
223
227
  signing_key:
224
228
  specification_version: 4
225
229
  summary: Ruby implementation of the SWIM gossip protocol
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.0.2