snell 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ # Ruby API 使用的不可变资源预算:时间以秒计,容量以字节计;
5
+ # 只有 pool_max_idle 允许为零,表示不保留空闲连接。
6
+ class Limits
7
+ DEFAULTS = {
8
+ max_connections: 2048, handshake_concurrency: 64,
9
+ handshake_rate: 100, handshake_burst: 200, udp_associations: 256,
10
+ buffer_bytes: 131_072, dial_timeout: 10, handshake_timeout: 10,
11
+ idle_timeout: 300, pool_max_idle: 10, pool_idle_timeout: 180,
12
+ drain_timeout: 2, drain_max_bytes: 524_288, shutdown_grace: 30
13
+ }.freeze
14
+ INTEGERS = %i[max_connections handshake_concurrency handshake_burst udp_associations
15
+ buffer_bytes pool_max_idle drain_max_bytes].freeze
16
+
17
+ def initialize(values = {})
18
+ raise ArgumentError, "limits must be a Hash" unless values.is_a?(Hash)
19
+ raise ArgumentError, "Invalid limit name" unless values.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
20
+
21
+ values = values.to_h.transform_keys(&:to_sym)
22
+ unknown = values.keys - DEFAULTS.keys
23
+ raise ArgumentError, "Unknown limits: #{unknown.join(', ')}" unless unknown.empty?
24
+
25
+ @values = DEFAULTS.merge(values).freeze
26
+ @values.each do |name, value|
27
+ valid = value.is_a?(Numeric) && value.real? && value.finite? && value.positive?
28
+ valid &&= value.is_a?(Integer) if INTEGERS.include?(name)
29
+ valid ||= name == :pool_max_idle && value.is_a?(Integer) && value.zero?
30
+ raise ArgumentError, "Invalid limit #{name}" unless valid
31
+ end
32
+ freeze
33
+ end
34
+
35
+ def [](name)
36
+ @values.fetch(name)
37
+ end
38
+
39
+ def to_h
40
+ @values.dup
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Snell
6
+ # 旧版协议可选的外层混淆;HTTP/TLS 形态不提供证书认证,
7
+ # 也不建立安全 TLS 会话。
8
+ module Obfuscation
9
+ module_function
10
+
11
+ def wrap(transport, role:, mode: nil, kind: nil, host: nil, uri: "/")
12
+ raise ArgumentError, "conflicting obfuscation modes" if mode && kind && mode != kind
13
+
14
+ Protocol.validate_role!(role)
15
+ selected = mode || kind || :none
16
+ case selected
17
+ when :none then transport
18
+ when :http then HTTP.new(transport, role: role, host: host || "bing.com", uri: uri)
19
+ when :tls then TLS.new(transport, role: role, host: host || "cloudfront.net")
20
+ else raise ArgumentError, "unsupported obfuscation mode"
21
+ end
22
+ end
23
+
24
+ # 沿用底层传输的期限和所有权;每个方向在整个物理连接中
25
+ # 只发送或读取一次混淆握手。
26
+ class Wrapper
27
+ def initialize(transport, role:, host:)
28
+ unless host.is_a?(String) && !host.empty? && host.bytesize <= 255 && !host.match?(/[\r\n\0]/)
29
+ raise ArgumentError, "obfuscation host must contain 1..255 bytes without line breaks or NUL"
30
+ end
31
+
32
+ @transport = transport
33
+ @role = role
34
+ @host = host.b.freeze
35
+ @read_started = false
36
+ @write_started = false
37
+ @read_buffer = +"".b
38
+ end
39
+
40
+ def with_timeout(seconds, &)
41
+ @transport.with_timeout(seconds, &)
42
+ end
43
+
44
+ def close
45
+ @transport.close
46
+ end
47
+
48
+ def close_write
49
+ @transport.close_write
50
+ end
51
+
52
+ def closed?
53
+ @transport.closed?
54
+ end
55
+
56
+ def inspect
57
+ "#<#{self.class} role=#{@role}>"
58
+ end
59
+
60
+ private
61
+
62
+ def continuation(length)
63
+ return +"".b if length.zero?
64
+
65
+ @transport.read_exactly(length)
66
+ rescue EOFError
67
+ raise ProtocolError, "truncated obfuscation frame", cause: nil
68
+ end
69
+ end
70
+
71
+ # 有界的 HTTP 形态头部后直接传输 Snell 字节,不实现 WebSocket。
72
+ class HTTP < Wrapper
73
+ MAX_HEADER = 16 * 1024
74
+
75
+ def initialize(transport, role:, host:, uri:)
76
+ super(transport, role: role, host: host)
77
+ unless uri.is_a?(String) && uri.start_with?("/") && uri.bytesize <= 4096 && !uri.match?(/[\x00-\x20\x7f]/)
78
+ raise ArgumentError, "HTTP obfuscation URI must be an absolute path without whitespace or controls"
79
+ end
80
+
81
+ @uri = uri.b.freeze
82
+ end
83
+
84
+ def write_all(bytes)
85
+ raise ArgumentError, "payload must be a String" unless bytes.is_a?(String)
86
+ return 0 if bytes.empty?
87
+
88
+ wire = @write_started ? bytes : header(bytes.bytesize) + bytes.b
89
+ @write_started = true
90
+ @transport.write_all(wire)
91
+ bytes.bytesize
92
+ end
93
+
94
+ def read_exactly(length)
95
+ raise ArgumentError, "length must be nonnegative" unless length.is_a?(Integer) && length >= 0
96
+ return +"".b if length.zero?
97
+
98
+ read_header unless @read_started
99
+ @transport.read_exactly(length)
100
+ end
101
+
102
+ private
103
+
104
+ def header(length)
105
+ if @role == :client
106
+ "GET #{@uri} HTTP/1.1\r\nHost: #{@host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" \
107
+ "Content-Length: #{length}\r\nSec-WebSocket-Key: #{[SecureRandom.random_bytes(16)].pack('m0')}\r\n\r\n".b
108
+ else
109
+ "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" \
110
+ "Sec-WebSocket-Accept: #{[SecureRandom.random_bytes(20)].pack('m0')}\r\n\r\n".b
111
+ end
112
+ end
113
+
114
+ def read_header
115
+ bytes = @transport.read_exactly(1)
116
+ until bytes.end_with?("\r\n\r\n")
117
+ raise ProtocolError, "HTTP obfuscation header exceeds limit" if bytes.bytesize >= MAX_HEADER
118
+
119
+ bytes << continuation(1)
120
+ end
121
+ @read_started = true
122
+ end
123
+ end
124
+
125
+ # 使用 TLS 形态记录传输字节,不提供 TLS 安全保证。
126
+ class TLS < Wrapper
127
+ MAX_RECORD = 16_384
128
+ MAX_EMPTY_RECORDS = 64
129
+ CIPHERS = [0xc02f, 0xc030, 0xcca8, 0xcca9, 0xc02b, 0xc02c, 0xc013, 0xc014,
130
+ 0xc009, 0xc00a, 0x009c, 0x009d, 0x002f, 0x0035, 0x003c, 0x003d,
131
+ 0x0067, 0x006b, 0x0033, 0x0039, 0x009e, 0x009f, 0xc027, 0xc028,
132
+ 0xc023, 0xc024, 0x00a2, 0x00a3].pack("n*").freeze
133
+
134
+ def write_all(bytes)
135
+ raise ArgumentError, "payload must be a String" unless bytes.is_a?(String)
136
+ return 0 if bytes.empty?
137
+
138
+ wire = +"".b
139
+ offset = 0
140
+ unless @write_started
141
+ length = [bytes.bytesize, @role == :client ? 1024 : MAX_RECORD].min
142
+ first = bytes.byteslice(0, length).b
143
+ wire << (@role == :client ? client_hello(first) : server_hello(first))
144
+ offset = length
145
+ @write_started = true
146
+ end
147
+ while offset < bytes.bytesize
148
+ length = [bytes.bytesize - offset, MAX_RECORD].min
149
+ wire << [23, 0x0303, length].pack("Cnn") << bytes.byteslice(offset, length).b
150
+ offset += length
151
+ end
152
+ @transport.write_all(wire)
153
+ bytes.bytesize
154
+ end
155
+
156
+ def read_exactly(length)
157
+ raise ArgumentError, "length must be nonnegative" unless length.is_a?(Integer) && length >= 0
158
+ return +"".b if length.zero?
159
+
160
+ empty_records = 0
161
+ while @read_buffer.bytesize < length
162
+ begin
163
+ part = @read_started ? read_frame : read_handshake
164
+ rescue EOFError
165
+ raise if @read_buffer.empty?
166
+
167
+ raise ProtocolError, "truncated obfuscation stream", cause: nil
168
+ end
169
+ if part.empty?
170
+ empty_records += 1
171
+ raise ProtocolError, "too many empty obfuscation frames" if empty_records > MAX_EMPTY_RECORDS
172
+ end
173
+ @read_buffer << part
174
+ end
175
+ result = @read_buffer.byteslice(0, length)
176
+ @read_buffer = @read_buffer.byteslice(length, @read_buffer.bytesize - length)
177
+ result
178
+ end
179
+
180
+ private
181
+
182
+ def client_hello(payload)
183
+ record_length = @host.bytesize + payload.bytesize + 212
184
+ hello = [22, 0x0301, record_length].pack("Cnn")
185
+ hello << "\x01".b << uint24(record_length - 4) << "\x03\x03".b
186
+ hello << SecureRandom.random_bytes(32) << "\x20".b << SecureRandom.random_bytes(32)
187
+ hello << [CIPHERS.bytesize].pack("n") << CIPHERS << "\x01\x00".b
188
+ hello << [@host.bytesize + payload.bytesize + 79, 35, payload.bytesize].pack("n*") << payload
189
+ hello << [0, @host.bytesize + 5, @host.bytesize + 3, 0, @host.bytesize].pack("nnnCn") << @host
190
+ hello << [10, 8, 6, 29, 23, 24].pack("n*")
191
+ hello << [13, 14, 12, 0x0403, 0x0503, 0x0603, 0x0401, 0x0501, 0x0601].pack("n*")
192
+ hello << ["000b0002010000170000ff0100010000150011"].pack("H*") << ("\0".b * 17)
193
+ hello
194
+ end
195
+
196
+ def server_hello(payload)
197
+ hello = [22, 0x0303, 70].pack("Cnn") + "\x02".b + uint24(66) + "\x03\x03".b
198
+ hello << SecureRandom.random_bytes(32) << "\x1a".b << SecureRandom.random_bytes(26)
199
+ hello << [0xc02f, 0, 0].pack("nCn")
200
+ hello << [20, 0x0303, 1, 1].pack("CnnC")
201
+ hello << [22, 0x0303, 16].pack("Cnn") << SecureRandom.random_bytes(16)
202
+ hello << [23, 0x0303, payload.bytesize].pack("Cnn") << payload
203
+ end
204
+
205
+ def uint24(number)
206
+ [number].pack("N").byteslice(1, 3)
207
+ end
208
+
209
+ def read_handshake
210
+ prefix_length = @role == :server ? 140 : 105
211
+ @transport.read_exactly(1)
212
+ continuation(prefix_length - 1)
213
+ length = continuation(2).unpack1("n")
214
+ limit = @role == :server ? 1024 : MAX_RECORD
215
+ raise ProtocolError, "oversized obfuscation handshake payload" if length > limit
216
+
217
+ payload = continuation(length)
218
+ if @role == :server
219
+ name_header = continuation(9)
220
+ name_length = name_header.byteslice(7, 2).unpack1("n")
221
+ raise ProtocolError, "oversized obfuscation hostname" if name_length > 255
222
+
223
+ continuation(name_length + 66)
224
+ end
225
+ @read_started = true
226
+ payload
227
+ end
228
+
229
+ def read_frame
230
+ first = @transport.read_exactly(1)
231
+ header = first + continuation(4)
232
+ type, version, length = header.unpack("Cnn")
233
+ unless type == 23 && version == 0x0303 && length <= MAX_RECORD
234
+ raise ProtocolError, "invalid obfuscation record header"
235
+ end
236
+
237
+ continuation(length)
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ # 请求、回复和 Snell UDP 载荷的纯编解码逻辑,不执行网络 I/O。
5
+ # 解码器区分非法协议数据与编码器调用参数错误。
6
+ module Protocol
7
+ COMMANDS = [0, 1, 5, 6].freeze
8
+ REPLIES = [0, 1, 2].freeze
9
+
10
+ # remaining 是提前发送的 TCP 数据,由 Server 解释与转发。
11
+ # 请求诊断不输出用户密钥或流量内容。
12
+ Request = Data.define(:command, :user_key, :destination, :remaining) do
13
+ def inspect
14
+ "#<#{self.class} command=#{command} destination=#{destination.inspect} remaining_bytes=#{remaining.bytesize}>"
15
+ end
16
+ end
17
+ Reply = Data.define(:kind, :code, :message, :payload)
18
+
19
+ module_function
20
+
21
+ def encode_request(command:, user_key: "", destination: nil)
22
+ raise ArgumentError, "unsupported command" unless COMMANDS.include?(command)
23
+
24
+ validate_bytes!(user_key, "user key", maximum: 255)
25
+ bytes = [1, command, user_key.bytesize].pack("C*") + user_key.b
26
+ if [1, 5].include?(command)
27
+ validate_address!(destination)
28
+ bytes << [destination.host.bytesize].pack("C") << destination.host << [destination.port].pack("n")
29
+ elsif destination
30
+ raise ArgumentError, "command does not accept a destination"
31
+ end
32
+ bytes
33
+ end
34
+
35
+ def decode_request(bytes)
36
+ cursor = Cursor.new(bytes)
37
+ raise ProtocolError, "unsupported request version" unless cursor.byte == 1
38
+
39
+ command = cursor.byte
40
+ raise ProtocolError, "unsupported request command" unless COMMANDS.include?(command)
41
+
42
+ user_key = cursor.take(cursor.byte).freeze
43
+ destination = [1, 5].include?(command) ? cursor.domain_address : nil
44
+ Request.new(command: command, user_key: user_key, destination: destination, remaining: cursor.rest.freeze)
45
+ end
46
+
47
+ def encode_reply(kind:, code: nil, message: nil, payload: "")
48
+ raise ArgumentError, "unsupported reply kind" unless REPLIES.include?(kind)
49
+
50
+ validate_bytes!(payload, "payload")
51
+ bytes = [kind].pack("C")
52
+ if kind == 2
53
+ raise ArgumentError, "error code must be a byte" unless code.is_a?(Integer) && code.between?(0, 255)
54
+
55
+ validate_bytes!(message, "error message", maximum: 255)
56
+ bytes << [code, message.bytesize].pack("CC") << message.b
57
+ elsif code || message
58
+ raise ArgumentError, "only error replies accept a code or message"
59
+ end
60
+ bytes << payload.b
61
+ end
62
+
63
+ def decode_reply(bytes)
64
+ cursor = Cursor.new(bytes)
65
+ kind = cursor.byte
66
+ raise ProtocolError, "unsupported reply kind" unless REPLIES.include?(kind)
67
+
68
+ code = kind == 2 ? cursor.byte : nil
69
+ message = kind == 2 ? cursor.take(cursor.byte).freeze : nil
70
+ Reply.new(kind: kind, code: code, message: message, payload: cursor.rest.freeze)
71
+ end
72
+
73
+ # encode_packet 的 role 表示发送方,decode_packet 的 role 表示接收方。
74
+ def encode_packet(payload, address, role:)
75
+ validate_role!(role)
76
+ validate_bytes!(payload, "payload")
77
+ validate_address!(address)
78
+ ip = address.ip
79
+ bytes = if role == :server
80
+ raise ArgumentError, "UDP source must be an IP address" unless ip
81
+
82
+ [ip.ipv4? ? 4 : 6].pack("C") + ip.hton
83
+ elsif ip
84
+ [1, 0, ip.ipv4? ? 4 : 6].pack("C*") + ip.hton
85
+ else
86
+ [1, address.host.bytesize].pack("CC") + address.host
87
+ end
88
+ bytes << [address.port].pack("n") << payload.b
89
+ end
90
+
91
+ def decode_packet(bytes, role:)
92
+ validate_role!(role)
93
+ cursor = Cursor.new(bytes)
94
+ if role == :server
95
+ raise ProtocolError, "unsupported UDP command" unless cursor.byte == 1
96
+
97
+ length = cursor.byte
98
+ address = length.zero? ? cursor.ip_address : cursor.domain_address(length)
99
+ else
100
+ address = cursor.ip_address
101
+ end
102
+ Datagram.new(payload: cursor.rest, address: address)
103
+ end
104
+
105
+ def validate_role!(role)
106
+ raise ArgumentError, "role must be :client or :server" unless %i[client server].include?(role)
107
+ end
108
+
109
+ def validate_address!(address)
110
+ raise ArgumentError, "destination must be a Snell::Address" unless address.is_a?(Address)
111
+ end
112
+
113
+ def validate_bytes!(bytes, name, maximum: nil)
114
+ raise ArgumentError, "#{name} must be a String" unless bytes.is_a?(String)
115
+ raise ArgumentError, "#{name} exceeds #{maximum} bytes" if maximum && bytes.bytesize > maximum
116
+ end
117
+
118
+ # 游标推进前先校验边界,截断输入不会产生不完整字段。
119
+ # 地址校验错误在此转换为协议异常。
120
+ class Cursor
121
+ def initialize(bytes)
122
+ raise ArgumentError, "wire data must be a String" unless bytes.is_a?(String)
123
+
124
+ @bytes = bytes.b
125
+ @offset = 0
126
+ end
127
+
128
+ def take(length)
129
+ raise ProtocolError, "truncated protocol message" if length > @bytes.bytesize - @offset
130
+
131
+ value = @bytes.byteslice(@offset, length)
132
+ @offset += length
133
+ value
134
+ end
135
+
136
+ def byte
137
+ take(1).getbyte(0)
138
+ end
139
+
140
+ def rest
141
+ take(@bytes.bytesize - @offset)
142
+ end
143
+
144
+ def domain_address(length = byte)
145
+ make_address(take(length), take(2).unpack1("n"))
146
+ end
147
+
148
+ def ip_address
149
+ family = byte
150
+ length = { 4 => 4, 6 => 16 }[family]
151
+ raise ProtocolError, "unsupported IP address family" unless length
152
+
153
+ make_address(IPAddr.new_ntoh(take(length)).to_s, take(2).unpack1("n"))
154
+ end
155
+
156
+ private
157
+
158
+ def make_address(host, port)
159
+ Address.new(host: host, port: port)
160
+ rescue ArgumentError
161
+ raise ProtocolError, "invalid protocol address", cause: nil
162
+ end
163
+ end
164
+ private_constant :Cursor
165
+ end
166
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "v6/profile"
4
+
5
+ module Snell
6
+ # 一个物理连接的入站方向;逻辑 EOF 与顺序复用不重置
7
+ # 密码器或序列状态,上层必须串行化同方向的读取。
8
+ # 任何未完成或被拒绝的记录都会永久停用该读取器。
9
+ class RecordReader
10
+ attr_reader :salt
11
+ # 完整首条记录认证成功后才公开盐值;raw 模式没有认证盐值。
12
+ alias authenticated_salt salt
13
+
14
+ def initialize(transport, psk:, version:, role:, mode: :default)
15
+ validate_profile!(psk, version, mode, role)
16
+ @transport = transport
17
+ @psk = psk.b.freeze
18
+ @version = version
19
+ @mode = mode
20
+ @role = role
21
+ @cipher = nil
22
+ @salt = nil
23
+ @failed = false
24
+ @sequence = 0
25
+ @profile = V6::Profile.new(psk) if version == 6 && mode == :default
26
+ end
27
+
28
+ # 空载荷表示逻辑 EOF;记录开始前的物理 EOF 使用 EOFError。
29
+ # 记录开始后的截断使用 ProtocolError,不视为正常结束。
30
+ def read_record
31
+ completed = false
32
+ raise ProtocolError, "record reader is no longer usable" if @failed
33
+
34
+ first = @transport.read_exactly(1)
35
+ payload = read_started_record(first)
36
+ completed = true
37
+ payload
38
+ ensure
39
+ @failed = true unless completed
40
+ end
41
+
42
+ def inspect
43
+ "#<#{self.class} version=#{@version} mode=#{@mode} role=#{@role}>"
44
+ end
45
+
46
+ private
47
+
48
+ def read_started_record(first)
49
+ return read_shaped_record(first) if @profile
50
+
51
+ return decode_record(first + continuation(6), encrypted: false) if @mode == :raw
52
+
53
+ if @cipher
54
+ header = first + continuation(22)
55
+ else
56
+ candidate_salt = first + continuation(15)
57
+ @cipher = Crypto::Cipher.new(Crypto.derive_key(@psk, candidate_salt))
58
+ header = continuation(23)
59
+ end
60
+ payload = decode_record(@cipher.decrypt(header), encrypted: true)
61
+ @salt = candidate_salt.freeze if candidate_salt
62
+ payload
63
+ end
64
+
65
+ def read_shaped_record(first)
66
+ if @cipher
67
+ prefix = first + continuation(@profile.prefix_length(@sequence) - 1)
68
+ else
69
+ block = first + continuation(15 + @profile.salt_prefix_length)
70
+ candidate_salt = @profile.extract_salt(block)
71
+ @cipher = Crypto::Cipher.new(Crypto.derive_key(@psk, candidate_salt))
72
+ prefix = continuation(@profile.prefix_length(@sequence))
73
+ end
74
+ header = @cipher.decrypt(continuation(23), aad: prefix)
75
+ type, _reserved, padding_length, payload_length = header.unpack("Cnnn")
76
+ raise ProtocolError, "unsupported record type" unless type == 4
77
+
78
+ padding = continuation(padding_length)
79
+ payload = +"".b
80
+ if payload_length.positive?
81
+ payload = continuation(payload_length + Crypto::TAG_BYTES)
82
+ @profile.mix!(padding, payload, @sequence)
83
+ payload = @cipher.decrypt(payload, aad: padding)
84
+ end
85
+ @salt = candidate_salt.freeze if candidate_salt
86
+ @sequence = (@sequence + 1) & 0xffff_ffff
87
+ payload
88
+ end
89
+
90
+ def decode_record(header, encrypted:)
91
+ type, reserved, padding_length, payload_length = header.unpack("Cnnn")
92
+ raise ProtocolError, "unsupported record type" unless type == 4
93
+ if @version == 6 && (reserved.nonzero? || padding_length.nonzero?)
94
+ raise ProtocolError, "invalid version 6 record header"
95
+ end
96
+
97
+ # 旧版混合规则先从 padding 恢复被交换的密文字节,再验证标签。
98
+ # 该格式中未参与混合的 padding 不在认证范围内。
99
+ padding = continuation(padding_length)
100
+ return +"".b if payload_length.zero?
101
+
102
+ payload = continuation(payload_length + (encrypted ? Crypto::TAG_BYTES : 0))
103
+ return payload unless encrypted
104
+
105
+ [padding.bytesize, payload.bytesize].min.times do |index|
106
+ next if index.odd?
107
+
108
+ payload.setbyte(index, padding.getbyte(index))
109
+ end
110
+ @cipher.decrypt(payload)
111
+ end
112
+
113
+ def continuation(length)
114
+ return +"".b if length.zero?
115
+
116
+ @transport.read_exactly(length)
117
+ rescue EOFError
118
+ raise ProtocolError, "truncated record", cause: nil
119
+ end
120
+
121
+ def validate_profile!(psk, version, mode, role)
122
+ raise ArgumentError, "psk must be a nonempty String" unless psk.is_a?(String) && !psk.empty?
123
+ raise ArgumentError, "unsupported Snell version" unless [4, 5, 6].include?(version)
124
+ raise ArgumentError, "unsupported record mode" unless %i[default unshaped raw].include?(mode)
125
+ raise ArgumentError, "record mode requires version 6" if version != 6 && mode != :default
126
+ if version == 6 && !psk.bytesize.between?(12, 255)
127
+ raise ArgumentError, "version 6 psk must contain 12 to 255 bytes"
128
+ end
129
+
130
+ Protocol.validate_role!(role)
131
+ end
132
+ end
133
+ end