ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MP/MP/Transport.pm
(Generate patch)

Comparing AnyEvent-MP/MP/Transport.pm (file contents):
Revision 1.5 by root, Sun Aug 2 14:44:37 2009 UTC vs.
Revision 1.28 by root, Sun Aug 9 16:08:16 2009 UTC

1=head1 NAME 1=head1 NAME
2 2
3AnyEvent::MP::Transport - actual transport protocol 3AnyEvent::MP::Transport - actual transport protocol handler
4 4
5=head1 SYNOPSIS 5=head1 SYNOPSIS
6 6
7 use AnyEvent::MP::Transport; 7 use AnyEvent::MP::Transport;
8 8
9=head1 DESCRIPTION 9=head1 DESCRIPTION
10 10
11This is the superclass for MP transports, most of which is considered an 11This implements the actual transport protocol for MP (it represents a
12implementation detail. 12single link), most of which is considered an implementation detail.
13 13
14Future versions might document the actual protocol. 14See the "PROTOCOL" section below if you want to write another client for
15this protocol.
15 16
16=head1 FUNCTIONS/METHODS 17=head1 FUNCTIONS/METHODS
17 18
18=over 4 19=over 4
19 20
21 22
22package AnyEvent::MP::Transport; 23package AnyEvent::MP::Transport;
23 24
24use common::sense; 25use common::sense;
25 26
26use Scalar::Util; 27use Scalar::Util ();
28use List::Util ();
27use MIME::Base64 (); 29use MIME::Base64 ();
28use Storable (); 30use Storable ();
29use JSON::XS (); 31use JSON::XS ();
30 32
33use Digest::MD6 ();
34use Digest::HMAC_MD6 ();
35
31use AE (); 36use AE ();
32use AnyEvent::Socket (); 37use AnyEvent::Socket ();
33use AnyEvent::Handle (); 38use AnyEvent::Handle 4.92 ();
34
35use AnyEvent::MP::Util ();
36 39
37use base Exporter::; 40use base Exporter::;
38 41
39our $VERSION = '0.0';
40our $PROTOCOL_VERSION = 0; 42our $PROTOCOL_VERSION = 0;
41 43
42=item $listener = mp_listener $host, $port, <constructor-args>, $cb->($transport) 44=item $listener = mp_listener $host, $port, <constructor-args>, $cb->($transport)
43 45
44Creates a listener on the given host/port using 46Creates a listener on the given host/port using
45C<AnyEvent::Socket::tcp_server>. 47C<AnyEvent::Socket::tcp_server>.
46 48
47See C<new>, below, for constructor arguments. 49See C<new>, below, for constructor arguments.
48 50
49Defaults for peerhost, peerport, fh and tls are provided. 51Defaults for peerhost, peerport and fh are provided.
50 52
51=cut 53=cut
52 54
53sub mp_server($$@) { 55sub mp_server($$@) {
54 my $cb = pop; 56 my $cb = pop;
59 61
60 $cb->(new AnyEvent::MP::Transport 62 $cb->(new AnyEvent::MP::Transport
61 fh => $fh, 63 fh => $fh,
62 peerhost => $host, 64 peerhost => $host,
63 peerport => $port, 65 peerport => $port,
64 tls => "accept",
65 @args, 66 @args,
66 ); 67 );
67 } 68 }
68} 69}
69 70
83 $cb->(new AnyEvent::MP::Transport 84 $cb->(new AnyEvent::MP::Transport
84 fh => $fh, 85 fh => $fh,
85 peername => $host, 86 peername => $host,
86 peerhost => $nhost, 87 peerhost => $nhost,
87 peerport => $nport, 88 peerport => $nport,
88 tls => "accept",
89 @args, 89 @args,
90 ); 90 );
91 } 91 }
92} 92}
93 93
106 on_eof => sub { clean-close-callback }, 106 on_eof => sub { clean-close-callback },
107 on_connect => sub { successful-connect-callback }, 107 on_connect => sub { successful-connect-callback },
108 greeting => { key => value }, 108 greeting => { key => value },
109 109
110 # tls support 110 # tls support
111 tls => "accept|connect",
112 tls_ctx => AnyEvent::TLS, 111 tls_ctx => AnyEvent::TLS,
113 peername => $peername, # for verification 112 peername => $peername, # for verification
114 ; 113 ;
115 114
116=cut 115=cut
117 116
118our @FRAMING_WANT = qw(json storable);#d##TODO# 117sub LATENCY() { 3 } # assumed max. network latency
118
119our @FRAMINGS = qw(json storable); # the framing types we accept and send, in order of preference
120our @AUTH_SND = qw(hmac_md6_64_256); # auth types we send
121our @AUTH_RCV = (@AUTH_SND, qw(cleartext)); # auth types we accept
122
123#AnyEvent::Handle::register_write_type mp_record => sub {
124#};
119 125
120sub new { 126sub new {
121 my ($class, %arg) = @_; 127 my ($class, %arg) = @_;
122 128
123 my $self = bless \%arg, $class; 129 my $self = bless \%arg, $class;
125 $self->{queue} = []; 131 $self->{queue} = [];
126 132
127 { 133 {
128 Scalar::Util::weaken (my $self = $self); 134 Scalar::Util::weaken (my $self = $self);
129 135
130 if (exists $arg{connect}) {
131 $arg{tls} ||= "connect";
132 $arg{tls_ctx} ||= { sslv2 => 0, sslv3 => 0, tlsv1 => 1, verify => 1 };
133 }
134
135 $arg{secret} = AnyEvent::MP::Base::default_secret () 136 $arg{secret} = AnyEvent::MP::Base::default_secret ()
136 unless exists $arg{secret}; 137 unless exists $arg{secret};
137 138
139 $arg{timeout} = 30
140 unless exists $arg{timeout};
141
142 $arg{timeout} = 1 + LATENCY
143 if $arg{timeout} < 1 + LATENCY;
144
145 my $secret = $arg{secret};
146
147 if ($secret =~ /-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/s) {
148 # assume TLS mode
149 $arg{tls_ctx} = {
150 sslv2 => 0,
151 sslv3 => 0,
152 tlsv1 => 1,
153 verify => 1,
154 cert => $secret,
155 ca_cert => $secret,
156 verify_require_client_cert => 1,
157 };
158 }
159
138 $self->{hdl} = new AnyEvent::Handle 160 $self->{hdl} = new AnyEvent::Handle
139 fh => delete $arg{fh}, 161 fh => delete $arg{fh},
140 rbuf_max => 64 * 1024,
141 autocork => 1, 162 autocork => 1,
142 no_delay => 1, 163 no_delay => 1,
143 on_error => sub { 164 on_error => sub {
144 $self->error ($_[2]); 165 $self->error ($_[2]);
145 }, 166 },
167 rtimeout => $AnyEvent::MP::Base::CONNECT_TIMEOUT,
146 peername => delete $arg{peername}, 168 peername => delete $arg{peername},
147 ; 169 ;
148 170
149 my $secret = $arg{secret};
150 my $greeting_kv = $self->{greeting} ||= {}; 171 my $greeting_kv = $self->{greeting} ||= {};
172
173 $self->{local_node} = $AnyEvent::MP::Base::NODE;
174
151 $greeting_kv->{"tls1.0"} ||= $arg{tls} 175 $greeting_kv->{"tls"} = "1.0" if $arg{tls_ctx};
152 if exists $arg{tls} && $arg{tls_ctx};
153 $greeting_kv->{provider} = "AE-$VERSION"; 176 $greeting_kv->{provider} = "AE-$AnyEvent::MP::Base::VERSION";
177 $greeting_kv->{peeraddr} = AnyEvent::Socket::format_hostport $self->{peerhost}, $self->{peerport};
178 $greeting_kv->{timeout} = $arg{timeout};
154 179
155 # send greeting 180 # send greeting
156 my $lgreeting = "aemp;$PROTOCOL_VERSION;$PROTOCOL_VERSION" # version, min 181 my $lgreeting1 = "aemp;$PROTOCOL_VERSION"
157 . ";$AnyEvent::MP::Base::UNIQ" 182 . ";$self->{local_node}"
158 . ";$AnyEvent::MP::Base::NODE" 183 . ";" . (join ",", @AUTH_RCV)
159 . ";" . (MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), "") 184 . ";" . (join ",", @FRAMINGS)
160 . ";hmac_md6_64_256" # hardcoded atm.
161 . ";json" # hardcoded atm.
162 . ";$self->{peerhost};$self->{peerport}"
163 . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv); 185 . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv);
164 186
187 my $lgreeting2 = MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), "";
188
165 $self->{hdl}->push_write ("$lgreeting\012"); 189 $self->{hdl}->push_write ("$lgreeting1\012$lgreeting2\012");
166 190
167 # expect greeting 191 # expect greeting
192 $self->{hdl}->rbuf_max (4 * 1024);
168 $self->{hdl}->push_read (line => sub { 193 $self->{hdl}->push_read (line => sub {
169 my $rgreeting = $_[1]; 194 my $rgreeting1 = $_[1];
170 195
171 my ($aemp, $version, $version_min, $uniq, $rnode, undef, $auth, $framing, $peerport, $peerhost, @kv) = split /;/, $rgreeting; 196 my ($aemp, $version, $rnode, $auths, $framings, @kv) = split /;/, $rgreeting1;
172 197
173 if ($aemp ne "aemp") { 198 if ($aemp ne "aemp") {
174 return $self->error ("unparsable greeting"); 199 return $self->error ("unparsable greeting");
175 } elsif ($version_min > $PROTOCOL_VERSION) { 200 } elsif ($version != $PROTOCOL_VERSION) {
176 return $self->error ("version mismatch (we: $PROTOCOL_VERSION, they: $version_min .. $version)"); 201 return $self->error ("version mismatch (we: $PROTOCOL_VERSION, they: $version)");
177 } elsif ($auth ne "hmac_md6_64_256") {
178 return $self->error ("unsupported auth method ($auth)");
179 } elsif ($framing ne "json") {
180 return $self->error ("unsupported framing method ($auth)");
181 } 202 }
182 203
183 $self->{remote_uniq} = $uniq; 204 my $s_auth;
205 for my $auth_ (split /,/, $auths) {
206 if (grep $auth_ eq $_, @AUTH_SND) {
207 $s_auth = $auth_;
208 last;
209 }
210 }
211
212 defined $s_auth
213 or return $self->error ("$auths: no common auth type supported");
214
215 die unless $s_auth eq "hmac_md6_64_256"; # hardcoded atm.
216
217 my $s_framing;
218 for my $framing_ (split /,/, $framings) {
219 if (grep $framing_ eq $_, @FRAMINGS) {
220 $s_framing = $framing_;
221 last;
222 }
223 }
224
225 defined $s_framing
226 or return $self->error ("$framings: no common framing method supported");
227
184 $self->{remote_node} = $rnode; 228 $self->{remote_node} = $rnode;
185 229
186 $self->{remote_greeting} = { 230 $self->{remote_greeting} = {
187 map /^([^=]+)(?:=(.*))?/ ? ($1 => $2) : (), 231 map /^([^=]+)(?:=(.*))?/ ? ($1 => $2) : (),
188 @kv 232 @kv
189 }; 233 };
190 234
191 if (exists $self->{tls} and $self->{tls_ctx} and exists $self->{remote_greeting}{"tls1.0"}) { 235 # read nonce
236 $self->{hdl}->push_read (line => sub {
237 my $rgreeting2 = $_[1];
238
239 "$lgreeting1\012$lgreeting2" ne "$rgreeting1\012$rgreeting2" # echo attack?
240 or return $self->error ("authentication error, echo attack?");
241
242 my $key = Digest::MD6::md6 $secret;
243 my $lauth;
244
192 if ($self->{tls} ne $self->{remote_greeting}{"tls1.0"}) { 245 if ($self->{tls_ctx} and 1 == int $self->{remote_greeting}{tls}) {
193 return $self->error ("TLS server/client mismatch"); 246 $self->{tls} = $lgreeting2 lt $rgreeting2 ? "connect" : "accept";
247 $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx});
248 $s_auth = "tls";
249 $lauth = "";
250 } else {
251 # we currently only support hmac_md6_64_256
252 $lauth = Digest::HMAC_MD6::hmac_md6_hex $key, "$lgreeting1\012$lgreeting2\012$rgreeting1\012$rgreeting2\012", 64, 256;
194 } 253 }
195 $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx});
196 }
197 254
198 # auth
199 require Digest::MD6;
200 require Digest::HMAC_MD6;
201
202 my $key = Digest::MD6::md6_hex ($secret);
203 my $lauth = Digest::HMAC_MD6::hmac_md6_base64 ($key, "$lgreeting\012$rgreeting", 64, 256);
204 my $rauth = Digest::HMAC_MD6::hmac_md6_base64 ($key, "$rgreeting\012$lgreeting", 64, 256);
205
206 $lauth ne $rauth # echo attack?
207 or return $self->error ("authentication error");
208
209 $self->{hdl}->push_write ("$auth;$lauth;$framing\012"); 255 $self->{hdl}->push_write ("$s_auth;$lauth;$s_framing\012");
210 256
211 $self->{hdl}->rbuf_max (64); # enough for 44 reply bytes or so 257 # read the authentication response
212 $self->{hdl}->push_read (line => sub { 258 $self->{hdl}->push_read (line => sub {
213 my ($hdl, $rline) = @_; 259 my ($hdl, $rline) = @_;
214 260
215 my ($auth_method, $rauth2, $r_framing) = split /;/, $rline; 261 my ($auth_method, $rauth2, $r_framing) = split /;/, $rline;
216 262
263 my $rauth =
264 $auth_method eq "hmac_md6_64_256" ? Digest::HMAC_MD6::hmac_md6_hex $key, "$rgreeting1\012$rgreeting2\012$lgreeting1\012$lgreeting2\012", 64, 256
265 : $auth_method eq "cleartext" ? unpack "H*", $secret
266 : $auth_method eq "tls" ? ($self->{tls} ? "" : "\012\012") # \012\012 never matches
267 : return $self->error ("$auth_method: fatal, selected unsupported auth method");
268
217 if ($rauth2 ne $rauth) { 269 if ($rauth2 ne $rauth) {
218 return $self->error ("authentication failure/shared secret mismatch"); 270 return $self->error ("authentication failure/shared secret mismatch");
219 } 271 }
220 272
221 $self->{s_framing} = "json";#d# 273 $self->{s_framing} = $s_framing;
222 274
223 $hdl->rbuf_max (undef); 275 $hdl->rbuf_max (undef);
224 my $queue = delete $self->{queue}; # we are connected 276 my $queue = delete $self->{queue}; # we are connected
225 277
278 $self->{hdl}->rtimeout ($self->{remote_greeting}{timeout});
279 $self->{hdl}->wtimeout ($arg{timeout} - LATENCY);
280 $self->{hdl}->on_wtimeout (sub { $self->send (["", "devnull"]) });
281
226 $self->connected; 282 $self->connected;
227 283
228 $hdl->push_write ($self->{s_framing} => $_) 284 # send queued messages
285 $self->send ($_)
229 for @$queue; 286 for @$queue;
230 287
288 # receive handling
289 my $src_node = $self->{node};
290
231 my $rmsg; $rmsg = sub { 291 my $rmsg; $rmsg = sub {
232 $_[0]->push_read ($r_framing => $rmsg); 292 $_[0]->push_read ($r_framing => $rmsg);
233 293
294 local $AnyEvent::MP::Base::SRCNODE = $src_node;
234 AnyEvent::MP::Base::_inject ($_[1]); 295 AnyEvent::MP::Base::_inject (@{ $_[1] });
235 }; 296 };
236 $hdl->push_read ($r_framing => $rmsg); 297 $hdl->push_read ($r_framing => $rmsg);
298 });
237 }); 299 });
238 }); 300 });
239 } 301 }
240 302
241 $self 303 $self
243 305
244sub error { 306sub error {
245 my ($self, $msg) = @_; 307 my ($self, $msg) = @_;
246 308
247 if ($self->{node} && $self->{node}{transport} == $self) { 309 if ($self->{node} && $self->{node}{transport} == $self) {
310 #TODO: store error, but do not instantly fail
311 $self->{node}->fail (transport_error => $self->{node}{noderef}, $msg);
248 $self->{node}->clr_transport; 312 $self->{node}->clr_transport;
249 } 313 }
250# $self->{on_error}($self, $msg); 314 $AnyEvent::MP::Base::WARN->("$self->{peerhost}:$self->{peerport}: $msg");
251 $self->destroy; 315 $self->destroy;
252} 316}
253 317
254sub connected { 318sub connected {
255 my ($self) = @_; 319 my ($self) = @_;
320
321 if (ref $AnyEvent::MP::Base::SLAVE) {
322 # first connect with a master node
323 my $via = $self->{remote_node};
324 $via =~ s/,/!/g;
325 $AnyEvent::MP::Base::NODE .= "\@$via";
326 $AnyEvent::MP::Base::NODE{$AnyEvent::MP::Base::NODE} = $AnyEvent::MP::Base::NODE{""};
327 $AnyEvent::MP::Base::SLAVE->();
328 }
329
330 if ($self->{local_node} ne $AnyEvent::MP::Base::NODE) {
331 # node changed its name since first greeting
332 $self->send (["", iam => $AnyEvent::MP::Base::NODE]);
333 }
256 334
257 my $node = AnyEvent::MP::Base::add_node ($self->{remote_node}); 335 my $node = AnyEvent::MP::Base::add_node ($self->{remote_node});
258 Scalar::Util::weaken ($self->{node} = $node); 336 Scalar::Util::weaken ($self->{node} = $node);
259 $node->set_transport ($self); 337 $node->set_transport ($self);
260} 338}
276 $self->destroy; 354 $self->destroy;
277} 355}
278 356
279=back 357=back
280 358
359=head1 PROTOCOL
360
361The protocol is relatively simple, and consists of three phases which are
362symmetrical for both sides: greeting (followed by optionally switching to
363TLS mode), authentication and packet exchange.
364
365the protocol is designed to allow both full-text and binary streams.
366
367The greeting consists of two text lines that are ended by either an ASCII
368CR LF pair, or a single ASCII LF (recommended).
369
370=head2 GREETING
371
372All the lines until after authentication must not exceed 4kb in length,
373including delimiter. Afterwards there is no limit on the packet size that
374can be received.
375
376=head3 First Greeting Line
377
378Example:
379
380 aemp;0;fec.4a7720fc;127.0.0.1:1235,[::1]:1235;hmac_md6_64_256;json,storable;provider=AE-0.0
381
382The first line contains strings separated (not ended) by C<;>
383characters. The first even ixtrings are fixed by the protocol, the
384remaining strings are C<KEY=VALUE> pairs. None of them may contain C<;>
385characters themselves.
386
387The fixed strings are:
388
389=over 4
390
391=item protocol identification
392
393The constant C<aemp> to identify the protocol.
394
395=item protocol version
396
397The protocol version supported by this end, currently C<0>. If the
398versions don't match then no communication is possible. Minor extensions
399are supposed to be handled through additional key-value pairs.
400
401=item the node endpoint descriptors
402
403for public nodes, this is a comma-separated list of protocol endpoints,
404i.e., the noderef. For slave nodes, this is a unique identifier of the
405form C<slave/nonce>.
406
407=item the acceptable authentication methods
408
409A comma-separated list of authentication methods supported by the
410node. Note that AnyEvent::MP supports a C<hex_secret> authentication
411method that accepts a cleartext password (hex-encoded), but will not use
412this auth method itself.
413
414The receiving side should choose the first auth method it supports.
415
416=item the acceptable framing formats
417
418A comma-separated list of packet encoding/framign formats understood. The
419receiving side should choose the first framing format it supports for
420sending packets (which might be different from the format it has to accept).
421
422=back
423
424The remaining arguments are C<KEY=VALUE> pairs. The following key-value
425pairs are known at this time:
426
427=over 4
428
429=item provider=<module-version>
430
431The software provider for this implementation. For AnyEvent::MP, this is
432C<AE-0.0> or whatever version it currently is at.
433
434=item peeraddr=<host>:<port>
435
436The peer address (socket address of the other side) as seen locally, in the same format
437as noderef endpoints.
438
439=item tls=<major>.<minor>
440
441Indicates that the other side supports TLS (version should be 1.0) and
442wishes to do a TLS handshake.
443
444=item timeout=<seconds>
445
446The amount of time after which this node should be detected as dead unless
447some data has been received. The node is responsible to send traffic
448reasonably more often than this interval (such as every timeout minus five
449seconds).
450
451=back
452
453=head3 Second Greeting Line
454
455After this greeting line there will be a second line containing a
456cryptographic nonce, i.e. random data of high quality. To keep the
457protocol text-only, these are usually 32 base64-encoded octets, but
458it could be anything that doesn't contain any ASCII CR or ASCII LF
459characters.
460
461I<< The two nonces B<must> be different, and an aemp implementation
462B<must> check and fail when they are identical >>.
463
464Example of a nonce line:
465
466 p/I122ql7kJR8lumW3lXlXCeBnyDAvz8NQo3x5IFowE4
467
468=head2 TLS handshake
469
470I<< If, after the handshake, both sides indicate interest in TLS, then the
471connection B<must> use TLS, or fail. >>
472
473Both sides compare their nonces, and the side who sent the lower nonce
474value ("string" comparison on the raw octet values) becomes the client,
475and the one with the higher nonce the server.
476
477=head2 AUTHENTICATION PHASE
478
479After the greeting is received (and the optional TLS handshake),
480the authentication phase begins, which consists of sending a single
481C<;>-separated line with three fixed strings and any number of
482C<KEY=VALUE> pairs.
483
484The three fixed strings are:
485
486=over 4
487
488=item the authentication method chosen
489
490This must be one of the methods offered by the other side in the greeting.
491
492The currently supported authentication methods are:
493
494=over 4
495
496=item cleartext
497
498This is simply the shared secret, lowercase-hex-encoded. This method is of
499course very insecure, unless TLS is used, which is why this module will
500accept, but not generate, cleartext auth replies.
501
502=item hmac_md6_64_256
503
504This method uses an MD6 HMAC with 64 bit blocksize and 256 bit hash. First, the shared secret
505is hashed with MD6:
506
507 key = MD6 (secret)
508
509This secret is then used to generate the "local auth reply", by taking
510the two local greeting lines and the two remote greeting lines (without
511line endings), appending \012 to all of them, concatenating them and
512calculating the MD6 HMAC with the key.
513
514 lauth = HMAC_MD6 key, "lgreeting1\012lgreeting2\012rgreeting1\012rgreeting2\012"
515
516This authentication token is then lowercase-hex-encoded and sent to the
517other side.
518
519Then the remote auth reply is generated using the same method, but local
520and remote greeting lines swapped:
521
522 rauth = HMAC_MD6 key, "rgreeting1\012rgreeting2\012lgreeting1\012lgreeting2\012"
523
524This is the token that is expected from the other side.
525
526=item tls
527
528This type is only valid iff TLS was enabled and the TLS handshake
529was successful. It has no authentication data, as the server/client
530certificate was successfully verified.
531
532Implementations supporting TLS I<must> accept this authentication type.
533
534=back
535
536=item the authentication data
537
538The authentication data itself, usually base64 or hex-encoded data, see
539above.
540
541=item the framing protocol chosen
542
543This must be one of the framing protocols offered by the other side in the
544greeting. Each side must accept the choice of the other side.
545
546=back
547
548Example of an authentication reply:
549
550 hmac_md6_64_256;363d5175df38bd9eaddd3f6ca18aa1c0c4aa22f0da245ac638d048398c26b8d3;json
551
552=head2 DATA PHASE
553
554After this, packets get exchanged using the chosen framing protocol. It is
555quite possible that both sides use a different framing protocol.
556
557=head2 FULL EXAMPLE
558
559This is an actual protocol dump of a handshake, followed by a single data
560packet. The greater than/less than lines indicate the direction of the
561transfer only.
562
563 > aemp;0;nndKd+gn;10.0.0.1:4040;hmac_md6_64_256,cleartext;json,storable;provider=AE-0.0;peeraddr=127.0.0.1:1235
564 > sRG8bbc4TDbkpvH8FTP4HBs87OhepH6VuApoZqXXskuG
565 < aemp;0;nmpKd+gh;127.0.0.1:1235,[::1]:1235;hmac_md6_64_256,cleartext;json,storable;provider=AE-0.0;peeraddr=127.0.0.1:58760
566 < dCEUcL/LJVSTJcx8byEsOzrwhzJYOq+L3YcopA5T6EAo
567 > hmac_md6_64_256;9513d4b258975accfcb2ab7532b83690e9c119a502c612203332a591c7237788;json
568 < hmac_md6_64_256;0298d6ba2240faabb2b2e881cf86b97d70a113ca74a87dc006f9f1e9d3010f90;json
569 > ["","lookup","pinger","10.0.0.1:4040#nndKd+gn.a","resolved"]
570
281=head1 SEE ALSO 571=head1 SEE ALSO
282 572
283L<AnyEvent>. 573L<AnyEvent>.
284 574
285=head1 AUTHOR 575=head1 AUTHOR

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines