blob: d49cdd7ad56d5212894a1ac17295060cc5f745e5 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mdns
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"net"
"strconv"
"strings"
"testing"
"v.io/v23/discovery"
idiscovery "v.io/x/ref/lib/discovery"
"v.io/x/ref/lib/discovery/plugins/testutil"
"v.io/x/ref/test"
)
var (
testPort int
unusedTCPListener *net.TCPListener
)
func init() {
// Test with an unused UDP port to avoid interference from others.
//
// We try to find an available TCP port since we cannot open multicast UDP
// connection with an opened UDP port.
unusedTCPListener, _ = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)})
_, port, _ := net.SplitHostPort(unusedTCPListener.Addr().String())
testPort, _ = strconv.Atoi(port)
}
func newMDNS(host string) (idiscovery.Plugin, error) {
return newWithLoopback(nil, host, testPort, true)
}
func encryptionKeys(key string) []idiscovery.EncryptionKey {
return []idiscovery.EncryptionKey{idiscovery.EncryptionKey(fmt.Sprintf("key:%x", key))}
}
func withAdReady(adinfos ...idiscovery.AdInfo) []idiscovery.AdInfo {
r := make([]idiscovery.AdInfo, len(adinfos))
for i, adinfo := range adinfos {
adinfo.Status = idiscovery.AdReady
adinfo.DirAddrs = nil
r[i] = adinfo
}
return r
}
func randomString(sz int) string {
// base64 encoding encodes 6 bits in 8. Desired length is sz*8 bits,
// so need sz*6/8 bytes of random data to base64 encode.
rnd := make([]byte, sz*6/8)
if _, err := rand.Read(rnd); err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(rnd)
}
func TestBasic(t *testing.T) {
ctx, shutdown := test.TestContext()
defer shutdown()
adinfos := []idiscovery.AdInfo{
{
Ad: discovery.Advertisement{
Id: discovery.AdId{1, 2, 3},
InterfaceName: "v.io/x",
Addresses: []string{
"/@6@wsh@foo.com:1234@@/x",
},
Attributes: discovery.Attributes{
"a": "a1234",
"b": "b1234",
},
Attachments: discovery.Attachments{
"a": []byte{11, 12, 13},
"p": []byte{21, 22, 23},
},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys("123"),
Hash: idiscovery.AdHash{1, 2, 3},
TimestampNs: 1001,
DirAddrs: []string{
"/@6@wsh@foo.com:1234@@/d",
},
},
{
Ad: discovery.Advertisement{
Id: discovery.AdId{4, 5, 6},
InterfaceName: "v.io/x",
Addresses: []string{
"/@6@wsh@bar.com:1234@@/x",
},
Attributes: discovery.Attributes{
"a": "a5678",
"b": "b5678",
},
Attachments: discovery.Attachments{
"a": []byte{31, 32, 33},
"p": []byte{41, 42, 43},
},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys("456"),
Hash: idiscovery.AdHash{4, 5, 6},
TimestampNs: 1002,
DirAddrs: []string{
"/@6@wsh@bar.com:1234@@/d",
},
},
{
Ad: discovery.Advertisement{
Id: discovery.AdId{7, 8, 9},
InterfaceName: "v.io/y",
Addresses: []string{
"/@6@wsh@foo.com:1234@@/y",
"/@6@wsh@bar.com:1234@@/y",
},
Attributes: discovery.Attributes{
"c": "c1234",
"d": "d1234",
},
Attachments: discovery.Attachments{
"c": []byte{51, 52, 53},
"p": []byte{61, 62, 63},
},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys("789"),
Hash: idiscovery.AdHash{7, 8, 9},
TimestampNs: 1003,
DirAddrs: []string{
"/@6@wsh@foo.com:1234@@/d",
"/@6@wsh@bar.com:1234@@/d",
},
},
}
p1, err := newMDNS("m1")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
var stops []func()
for i, _ := range adinfos {
stop, err := testutil.Advertise(ctx, p1, &adinfos[i])
if err != nil {
t.Fatal(err)
}
stops = append(stops, stop)
}
p2, err := newMDNS("m2")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
// Make sure all advertisements are discovered.
if err := testutil.ScanAndMatch(ctx, p2, "v.io/x", withAdReady(adinfos[0], adinfos[1])...); err != nil {
t.Error(err)
}
if err := testutil.ScanAndMatch(ctx, p2, "v.io/y", withAdReady(adinfos[2])...); err != nil {
t.Error(err)
}
if err := testutil.ScanAndMatch(ctx, p2, "", withAdReady(adinfos...)...); err != nil {
t.Error(err)
}
if err := testutil.ScanAndMatch(ctx, p2, "v.io/z"); err != nil {
t.Error(err)
}
// Make sure it is not discovered when advertising is stopped.
stops[0]()
if err := testutil.ScanAndMatch(ctx, p2, "v.io/x", withAdReady(adinfos[1])...); err != nil {
t.Error(err)
}
if err := testutil.ScanAndMatch(ctx, p2, "", withAdReady(adinfos[1], adinfos[2])...); err != nil {
t.Error(err)
}
// Open a new scan channel and consume expected advertisements first.
scanCh, scanStop, err := testutil.Scan(ctx, p2, "v.io/y")
if err != nil {
t.Error(err)
}
defer scanStop()
adinfo := *<-scanCh
if !testutil.MatchFound([]idiscovery.AdInfo{adinfo}, withAdReady(adinfos[2])...) {
t.Errorf("Unexpected scan: %v, but want %v", adinfo, adinfos[2])
}
// Make sure scan returns the lost advertisement when advertising is stopped.
stops[2]()
adinfo = *<-scanCh
if !testutil.MatchLost([]idiscovery.AdInfo{adinfo}, adinfos[2]) {
t.Errorf("Unexpected scan: %v, but want %v as lost", adinfo, adinfos[2])
}
// Stop advertising the remaining one; Shouldn't discover anything.
stops[1]()
if err := testutil.ScanAndMatch(ctx, p2, ""); err != nil {
t.Error(err)
}
}
func TestLargeAdvertisements(t *testing.T) {
ctx, shutdown := test.TestContext()
defer shutdown()
tests := []struct{ adinfo, want idiscovery.AdInfo }{
{
idiscovery.AdInfo{ // Fit at the maximum size.
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": strings.Repeat("v", 564)},
Attachments: discovery.Attachments{"b": bytes.Repeat([]byte{1}, 513)},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys("k"),
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
},
idiscovery.AdInfo{
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": strings.Repeat("v", 564)},
Attachments: discovery.Attachments{"b": bytes.Repeat([]byte{1}, 513)},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys("k"),
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
Status: idiscovery.AdReady,
},
},
{
idiscovery.AdInfo{ // Not all required fields can fit.
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{
"a1": strings.Repeat("v1", 256),
"a2": strings.Repeat("v2", 287),
},
Attachments: discovery.Attachments{"b": []byte{1}},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
},
idiscovery.AdInfo{
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Attributes: discovery.Attributes{},
Attachments: discovery.Attachments{},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
Status: idiscovery.AdNotReady,
},
},
{
idiscovery.AdInfo{ // Not all required fields can fit.
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
// Addresses are compressed, so
// generate one that is random and so
// big that it will be too large even
// after compression.
Addresses: []string{randomString(2048)},
Attributes: discovery.Attributes{"a": strings.Repeat("v", 255)},
Attachments: discovery.Attachments{"b": []byte{1}},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
},
idiscovery.AdInfo{
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Attributes: discovery.Attributes{},
Attachments: discovery.Attachments{},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
Status: idiscovery.AdNotReady,
},
},
{
idiscovery.AdInfo{ // None of optional fields can fit.
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": strings.Repeat("v", 1064)},
Attachments: discovery.Attachments{"b": bytes.Repeat([]byte{1}, 100)},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
},
idiscovery.AdInfo{
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": strings.Repeat("v", 1064)},
Attachments: discovery.Attachments{},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
Status: idiscovery.AdPartiallyReady,
},
},
{
idiscovery.AdInfo{ // Not all optional fields can fit.
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": "v"},
Attachments: discovery.Attachments{
"a1": bytes.Repeat([]byte{1}, 400),
"a2": bytes.Repeat([]byte{2}, 600),
"a3": bytes.Repeat([]byte{3}, 200),
"a4": bytes.Repeat([]byte{4}, 4000),
"a5": bytes.Repeat([]byte{5}, 800),
},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
},
idiscovery.AdInfo{
Ad: discovery.Advertisement{
Id: discovery.AdId{1},
InterfaceName: "v.io/i",
Addresses: []string{"/@6@wsh@foo.com:1234@@/s"},
Attributes: discovery.Attributes{"a": "v"},
Attachments: discovery.Attachments{
"a1": bytes.Repeat([]byte{1}, 400),
"a3": bytes.Repeat([]byte{3}, 200),
},
},
Hash: idiscovery.AdHash{9},
TimestampNs: 1001,
DirAddrs: []string{"/@6@wsh@foo.com:1234@@/d"},
Status: idiscovery.AdPartiallyReady,
},
},
}
p1, err := newMDNS("m1")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
p2, err := newMDNS("m2")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
for i, test := range tests {
stop, err := testutil.Advertise(ctx, p1, &test.adinfo)
if err != nil {
t.Errorf("[%d]: %v", i, err)
continue
}
if err := testutil.ScanAndMatch(ctx, p2, "", test.want); err != nil {
t.Errorf("[%d]: %v", i, err)
}
stop()
}
}
func TestMultipleLargeAdvertisements(t *testing.T) {
ctx, shutdown := test.TestContext()
defer shutdown()
adinfos := []idiscovery.AdInfo{
{
Ad: discovery.Advertisement{
Id: discovery.AdId{1, 2, 3},
InterfaceName: strings.Repeat("i", 260),
Addresses: []string{
strings.Repeat("a1", 70),
strings.Repeat("a2", 70),
},
Attributes: discovery.Attributes{
"a": strings.Repeat("v", 260),
},
Attachments: discovery.Attachments{
"p": bytes.Repeat([]byte{1}, 260),
},
},
},
{
Ad: discovery.Advertisement{
Id: discovery.AdId{4, 5, 6},
InterfaceName: "v.io/y",
Addresses: []string{"a"},
Attributes: discovery.Attributes{},
Attachments: discovery.Attachments{},
},
EncryptionAlgorithm: idiscovery.TestEncryption,
EncryptionKeys: encryptionKeys(strings.Repeat("k", 260)),
DirAddrs: []string{
strings.Repeat("d1", 130),
strings.Repeat("d2", 130),
},
},
}
// Advertise multiple large advertisements, where each advertisement fits in one packet
// but they do not fit all together, and make sure all advertisements are discovered.
p1, err := newMDNS("m1")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
for _, adinfo := range adinfos {
stop, err := testutil.Advertise(ctx, p1, &adinfo)
if err != nil {
t.Fatal(err)
}
defer stop()
}
p2, err := newMDNS("m2")
if err != nil {
t.Fatalf("New() failed: %v", err)
}
if err := testutil.ScanAndMatch(ctx, p2, "", withAdReady(adinfos...)...); err != nil {
t.Error(err)
}
}