Merge "discovery: support large attributes in mdns"
diff --git a/lib/discovery/advertise.go b/lib/discovery/advertise.go
index 3930f63..5abdbe4 100644
--- a/lib/discovery/advertise.go
+++ b/lib/discovery/advertise.go
@@ -25,15 +25,13 @@
if len(service.InterfaceName) == 0 {
return verror.New(errNoInterfaceName, ctx)
}
- if !IsAttributePackable(service.Attrs) {
- return verror.New(errNotPackableAttributes, ctx)
- }
if len(service.Addrs) == 0 {
return verror.New(errNoAddresses, ctx)
}
- if !IsAddressPackable(service.Addrs) {
- return verror.New(errNotPackableAddresses, ctx)
+ if err := validateAttributes(service.Attrs); err != nil {
+ return err
}
+
if len(service.InstanceUuid) == 0 {
service.InstanceUuid = NewInstanceUUID()
}
diff --git a/lib/discovery/discovery.go b/lib/discovery/discovery.go
index 4f2210b..e260074 100644
--- a/lib/discovery/discovery.go
+++ b/lib/discovery/discovery.go
@@ -36,7 +36,7 @@
Lost bool
}
-type EncryptionAlgorithm byte
+type EncryptionAlgorithm int
type EncryptionKey []byte
const (
diff --git a/lib/discovery/encoding.go b/lib/discovery/encoding.go
index 0f27651..d58b315 100644
--- a/lib/discovery/encoding.go
+++ b/lib/discovery/encoding.go
@@ -6,91 +6,102 @@
import (
"bytes"
- "fmt"
+ "encoding/binary"
+ "errors"
+ "io"
"strings"
"v.io/v23/discovery"
)
-// TODO(jhahn): Figure out how to overcome the size limit.
-
-// isAttributePackage returns false if the provided attributes cannot be serialized safely.
-func IsAttributePackable(attrs discovery.Attributes) bool {
- for k, v := range attrs {
- if strings.HasPrefix(k, "_") || strings.Contains(k, "=") {
- return false
+// validateAttributes returns an error if the attributes are not suitable for advertising.
+func validateAttributes(attrs discovery.Attributes) error {
+ for k, _ := range attrs {
+ if len(k) == 0 {
+ return errors.New("empty key")
}
- if len(k)+len(v) > 254 {
- return false
+ if strings.HasPrefix(k, "_") {
+ return errors.New("key starts with '_'")
+ }
+ for _, c := range k {
+ if c < 0x20 || c > 0x7e {
+ return errors.New("key is not printable US-ASCII")
+ }
+ if c == '=' {
+ return errors.New("key includes '='")
+ }
}
}
- return true
+ return nil
}
-// IsAddressPackable returns false if any address is larger than 250 bytes.
-//
-// go-mdns-sd package limits the size of each txt record to 255 bytes. We use
-// 5 bytes for tag, so we limit the address to 250 bytes.
-func IsAddressPackable(addrs []string) bool {
- for _, a := range addrs {
- if len(a) > 250 {
- return false
- }
- }
- return true
-}
-
-// PackAddresses packs addresses into a byte slice. If any address exceeds
-// 255 bytes, it will panic.
+// PackAddresses packs addresses into a byte slice.
func PackAddresses(addrs []string) []byte {
- var b bytes.Buffer
+ var buf bytes.Buffer
for _, a := range addrs {
- n := len(a)
- if n > 255 {
- panic(fmt.Sprintf("too large address %d: %s", n, a))
- }
- b.WriteByte(byte(n))
- b.WriteString(a)
+ writeInt(&buf, len(a))
+ buf.WriteString(a)
}
- return b.Bytes()
+ return buf.Bytes()
}
// UnpackAddresses unpacks addresses from a byte slice.
-func UnpackAddresses(data []byte) []string {
- addrs := []string{}
- for off := 0; off < len(data); {
- n := int(data[off])
- off++
- addrs = append(addrs, string(data[off:off+n]))
- off += n
- }
- return addrs
-}
-
-// PackEncryptionKeys packs keys into a byte slice.
-func PackEncryptionKeys(algo EncryptionAlgorithm, keys []EncryptionKey) []byte {
- var b bytes.Buffer
- b.WriteByte(byte(algo))
- for _, k := range keys {
- n := len(k)
- if n > 255 {
- panic(fmt.Sprintf("too large key %d", n))
+func UnpackAddresses(data []byte) ([]string, error) {
+ var addrs []string
+ for r := bytes.NewBuffer(data); r.Len() > 0; {
+ n, err := readInt(r)
+ if err != nil {
+ return nil, err
}
- b.WriteByte(byte(n))
- b.Write(k)
+ b := r.Next(n)
+ if len(b) != n {
+ return nil, errors.New("invalid addresses")
+ }
+ addrs = append(addrs, string(b))
}
- return b.Bytes()
+ return addrs, nil
}
-// UnpackEncryptionKeys unpacks keys from a byte slice.
-func UnpackEncryptionKeys(data []byte) (EncryptionAlgorithm, []EncryptionKey) {
- algo := EncryptionAlgorithm(data[0])
- keys := []EncryptionKey{}
- for off := 1; off < len(data); {
- n := int(data[off])
- off++
- keys = append(keys, EncryptionKey(data[off:off+n]))
- off += n
+// PackEncryptionKeys packs encryption algorithm and keys into a byte slice.
+func PackEncryptionKeys(algo EncryptionAlgorithm, keys []EncryptionKey) []byte {
+ var buf bytes.Buffer
+ writeInt(&buf, int(algo))
+ for _, k := range keys {
+ writeInt(&buf, len(k))
+ buf.Write(k)
}
- return algo, keys
+ return buf.Bytes()
+}
+
+// UnpackEncryptionKeys unpacks encryption algorithm and keys from a byte slice.
+func UnpackEncryptionKeys(data []byte) (EncryptionAlgorithm, []EncryptionKey, error) {
+ buf := bytes.NewBuffer(data)
+ algo, err := readInt(buf)
+ if err != nil {
+ return NoEncryption, nil, err
+ }
+ var keys []EncryptionKey
+ for buf.Len() > 0 {
+ n, err := readInt(buf)
+ if err != nil {
+ return NoEncryption, nil, err
+ }
+ v := buf.Next(n)
+ if len(v) != n {
+ return NoEncryption, nil, errors.New("invalid encryption keys")
+ }
+ keys = append(keys, EncryptionKey(v))
+ }
+ return EncryptionAlgorithm(algo), keys, nil
+}
+
+func writeInt(w io.Writer, x int) {
+ var b [binary.MaxVarintLen64]byte
+ n := binary.PutUvarint(b[:], uint64(x))
+ w.Write(b[0:n])
+}
+
+func readInt(r io.ByteReader) (int, error) {
+ x, err := binary.ReadUvarint(r)
+ return int(x), err
}
diff --git a/lib/discovery/encoding_test.go b/lib/discovery/encoding_test.go
index ca74b57..488bc77 100644
--- a/lib/discovery/encoding_test.go
+++ b/lib/discovery/encoding_test.go
@@ -6,41 +6,31 @@
import (
"reflect"
- "strings"
"testing"
"v.io/v23/discovery"
)
-func TestAttributePackable(t *testing.T) {
- tests := []struct {
- addrs discovery.Attributes
- want bool
- }{
- {discovery.Attributes{"k": "v"}, true},
- {discovery.Attributes{"_k": "v"}, false},
- {discovery.Attributes{"k=": "v"}, false},
- {discovery.Attributes{strings.Repeat("k", 100): strings.Repeat("v", 154)}, true},
- {discovery.Attributes{strings.Repeat("k", 100): strings.Repeat("v", 155)}, false},
+func TestValidateAttributes(t *testing.T) {
+ valids := []discovery.Attributes{
+ discovery.Attributes{"key": "v"},
+ discovery.Attributes{"k_e.y": "v"},
+ discovery.Attributes{"k!": "v"},
}
- for i, test := range tests {
- if got := IsAttributePackable(test.addrs); got != test.want {
- t.Errorf("[%d]: packable %v, but want %v", i, got, test.want)
+ for i, attrs := range valids {
+ if err := validateAttributes(attrs); err != nil {
+ t.Errorf("[%d]: valid attributes got error: %v", i, err)
}
}
-}
-func TestAddressPackable(t *testing.T) {
- tests := []struct {
- addrs []string
- want bool
- }{
- {[]string{strings.Repeat("a", 250)}, true},
- {[]string{strings.Repeat("a", 10), strings.Repeat("a", 251)}, false},
+ invalids := []discovery.Attributes{
+ discovery.Attributes{"_key": "v"},
+ discovery.Attributes{"k=ey": "v"},
+ discovery.Attributes{"key\n": "v"},
}
- for i, test := range tests {
- if got := IsAddressPackable(test.addrs); got != test.want {
- t.Errorf("[%d]: packable %v, but want %v", i, got, test.want)
+ for i, attrs := range invalids {
+ if err := validateAttributes(attrs); err == nil {
+ t.Errorf("[%d]: invalid attributes didn't get error", i)
}
}
}
@@ -49,12 +39,16 @@
tests := [][]string{
[]string{"a12345"},
[]string{"a1234", "b5678", "c9012"},
- []string{},
+ nil,
}
for _, test := range tests {
pack := PackAddresses(test)
- unpack := UnpackAddresses(pack)
+ unpack, err := UnpackAddresses(pack)
+ if err != nil {
+ t.Errorf("unpacked error: %v", err)
+ continue
+ }
if !reflect.DeepEqual(test, unpack) {
t.Errorf("unpacked to %v, but want %v", unpack, test)
}
@@ -68,12 +62,16 @@
}{
{TestEncryption, []EncryptionKey{EncryptionKey("0123456789")}},
{IbeEncryption, []EncryptionKey{EncryptionKey("012345"), EncryptionKey("123456"), EncryptionKey("234567")}},
- {NoEncryption, []EncryptionKey{}},
+ {NoEncryption, nil},
}
for _, test := range tests {
pack := PackEncryptionKeys(test.algo, test.keys)
- algo, keys := UnpackEncryptionKeys(pack)
+ algo, keys, err := UnpackEncryptionKeys(pack)
+ if err != nil {
+ t.Errorf("unpacked error: %v", err)
+ continue
+ }
if algo != test.algo || !reflect.DeepEqual(keys, test.keys) {
t.Errorf("unpacked to (%d, %v), but want (%d, %v)", algo, keys, test.algo, test.keys)
}
diff --git a/lib/discovery/plugins/ble/advertisement.go b/lib/discovery/plugins/ble/advertisement.go
index b64ade1..7a8a3f9 100644
--- a/lib/discovery/plugins/ble/advertisement.go
+++ b/lib/discovery/plugins/ble/advertisement.go
@@ -25,6 +25,7 @@
// This uuids are v5 uuid generated out of band. These constants need
// to be accessible in all the languages that have a ble implementation
instanceUUID = "12db9a9c-1c7c-5560-bc6b-73a115c93413" // NewAttributeUUID("_instanceuuid")
+ instanceNameUUID = "ffbdcff3-e56f-58f0-8c1a-e416c39aac0d" // NewAttributeUUID("_instancename")
interfaceNameUUID = "b2cadfd4-d003-576c-acad-58b8e3a9cbc8" // NewAttributeUUID("_interfacename")
addrsUUID = "ad2566b7-59d8-50ae-8885-222f43f65fdc" // NewAttributeUUID("_addrs")
encryptionUUID = "6286d80a-adaa-519a-8a06-281a4645a607" // NewAttributeUUID("_encryption")
@@ -34,8 +35,15 @@
attrs := map[string][]byte{
instanceUUID: adv.InstanceUuid,
interfaceNameUUID: []byte(adv.InterfaceName),
- addrsUUID: discovery.PackAddresses(adv.Addrs),
- encryptionUUID: discovery.PackEncryptionKeys(adv.EncryptionAlgorithm, adv.EncryptionKeys),
+ }
+ if len(adv.InstanceName) > 0 {
+ attrs[instanceNameUUID] = []byte(adv.InstanceName)
+ }
+ if len(adv.Addrs) > 0 {
+ attrs[addrsUUID] = discovery.PackAddresses(adv.Addrs)
+ }
+ if adv.EncryptionAlgorithm != discovery.NoEncryption {
+ attrs[encryptionUUID] = discovery.PackEncryptionKeys(adv.EncryptionAlgorithm, adv.EncryptionKeys)
}
for k, v := range adv.Attrs {
@@ -58,16 +66,23 @@
ServiceUuid: a.serviceUUID,
}
+ var err error
for k, v := range a.attrs {
switch k {
case instanceUUID:
adv.InstanceUuid = v
+ case instanceNameUUID:
+ adv.InstanceName = string(v)
case interfaceNameUUID:
adv.InterfaceName = string(v)
case addrsUUID:
- adv.Addrs = discovery.UnpackAddresses(v)
+ if adv.Addrs, err = discovery.UnpackAddresses(v); err != nil {
+ return nil, err
+ }
case encryptionUUID:
- adv.EncryptionAlgorithm, adv.EncryptionKeys = discovery.UnpackEncryptionKeys(v)
+ if adv.EncryptionAlgorithm, adv.EncryptionKeys, err = discovery.UnpackEncryptionKeys(v); err != nil {
+ return nil, err
+ }
default:
parts := strings.SplitN(string(v), "=", 2)
if len(parts) != 2 {
diff --git a/lib/discovery/plugins/ble/advertisement_test.go b/lib/discovery/plugins/ble/advertisement_test.go
index 3fe95db..7525be1 100644
--- a/lib/discovery/plugins/ble/advertisement_test.go
+++ b/lib/discovery/plugins/ble/advertisement_test.go
@@ -19,6 +19,7 @@
v23Adv := discovery.Advertisement{
Service: vdiscovery.Service{
InstanceUuid: []byte(discovery.NewInstanceUUID()),
+ InstanceName: "service",
Attrs: vdiscovery.Attributes{
"key1": "value1",
"key2": "value2",
@@ -27,7 +28,7 @@
},
ServiceUuid: uuid.NewUUID(),
EncryptionAlgorithm: discovery.TestEncryption,
- EncryptionKeys: []discovery.EncryptionKey{discovery.EncryptionKey("k1"), discovery.EncryptionKey("k2")},
+ EncryptionKeys: []discovery.EncryptionKey{discovery.EncryptionKey("k")},
}
adv := newAdvertisment(v23Adv)
diff --git a/lib/discovery/plugins/mdns/encoding.go b/lib/discovery/plugins/mdns/encoding.go
new file mode 100644
index 0000000..2610dae
--- /dev/null
+++ b/lib/discovery/plugins/mdns/encoding.go
@@ -0,0 +1,105 @@
+// 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 (
+ "encoding/base32"
+ "errors"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+const (
+ // Limit the maximum large txt records to the maximum total txt records size.
+ maxLargeTxtRecordLen = maxTotalTxtRecordsLen
+)
+
+var (
+ // The key of encoded large txt records is "_x<i><j>", where 'i' and 'j' will
+ // be one digit numbers since we limit the large txt record to 1300 bytes.
+ reLargeTxtRecord = regexp.MustCompile("^" + attrLargeTxtPrefix + "[0-9][0-9]=")
+
+ errInvalidLargeTxtRecord = errors.New("invalid large txt record")
+)
+
+// encodeInstanceUuid encodes the given instance uuid to a valid host name by using
+// "Extended Hex Alphabet" defined in RFC 4648. This removes any padding characters.
+func encodeInstanceUuid(instanceUuid []byte) string {
+ return strings.TrimRight(base32.HexEncoding.EncodeToString(instanceUuid), "=")
+}
+
+// decodeInstanceUuid decodes the given host name to an instance uuid.
+func decodeInstanceUuid(hostname string) ([]byte, error) {
+ // Add padding characters if needed.
+ if p := len(hostname) % 8; p > 0 {
+ hostname += strings.Repeat("=", 8-p)
+ }
+ return base32.HexEncoding.DecodeString(hostname)
+}
+
+// maybeSplitLargeTXT slices txt records larger than 255 bytes into multiple txt records.
+func maybeSplitLargeTXT(txt []string) ([]string, error) {
+ splitted := make([]string, 0, len(txt))
+ xno := 0
+ for _, v := range txt {
+ switch n := len(v); {
+ case n > maxLargeTxtRecordLen:
+ return nil, errMaxTxtRecordLenExceeded
+ case n > maxTxtRecordLen:
+ var buf [maxTxtRecordLen]byte
+ copy(buf[:], attrLargeTxtPrefix)
+ for i, off := 0, 0; off < n; i++ {
+ buf[2] = byte(xno + '0')
+ buf[3] = byte(i + '0')
+ buf[4] = '='
+ c := copy(buf[5:], v[off:])
+ splitted = append(splitted, string(buf[:5+c]))
+ off += c
+ }
+ xno++
+ default:
+ splitted = append(splitted, v)
+ }
+ }
+ return splitted, nil
+}
+
+// maybeJoinLargeTXT joins the splitted large txt records.
+func maybeJoinLargeTXT(txt []string) ([]string, error) {
+ joined, splitted := make([]string, 0, len(txt)), make([]string, 0)
+ for _, v := range txt {
+ switch {
+ case strings.HasPrefix(v, attrLargeTxtPrefix):
+ if !reLargeTxtRecord.MatchString(v) {
+ return nil, errInvalidLargeTxtRecord
+ }
+ splitted = append(splitted, v)
+ default:
+ joined = append(joined, v)
+ }
+ }
+ if len(splitted) == 0 {
+ return joined, nil
+ }
+
+ sort.Strings(splitted)
+
+ var buf [maxLargeTxtRecordLen]byte
+ xno, off := 0, 0
+ for _, v := range splitted {
+ i := int(v[2] - '0')
+ if i > xno {
+ // A new large txt record started.
+ joined = append(joined, string(buf[:off]))
+ xno++
+ off = 0
+ }
+ c := copy(buf[off:], v[5:])
+ off += c
+ }
+ joined = append(joined, string(buf[:off]))
+ return joined, nil
+}
diff --git a/lib/discovery/plugins/mdns/encoding_test.go b/lib/discovery/plugins/mdns/encoding_test.go
new file mode 100644
index 0000000..cbfa213
--- /dev/null
+++ b/lib/discovery/plugins/mdns/encoding_test.go
@@ -0,0 +1,87 @@
+// 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 (
+ "crypto/rand"
+ "encoding/base64"
+ "reflect"
+ "sort"
+ "testing"
+)
+
+func TestEncodeInstanceUuid(t *testing.T) {
+ tests := [][]byte{
+ randInstanceUuid(1),
+ randInstanceUuid(10),
+ randInstanceUuid(16),
+ randInstanceUuid(32),
+ }
+
+ for i, test := range tests {
+ encoded := encodeInstanceUuid(test)
+ instanceUuid, err := decodeInstanceUuid(encoded)
+ if err != nil {
+ t.Errorf("[%d]: decodeInstanceUuid failed: %v", i, err)
+ continue
+ }
+ if !reflect.DeepEqual(instanceUuid, test) {
+ t.Errorf("[%d]: decoded to %v, but want %v", i, instanceUuid, test)
+ }
+ }
+}
+
+func randInstanceUuid(n int) []byte {
+ b := make([]byte, n)
+ _, err := rand.Read(b)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+func TestSplitLargeTxt(t *testing.T) {
+ tests := [][]string{
+ []string{randTxt(maxTxtRecordLen / 2)},
+ []string{randTxt(maxTxtRecordLen / 2), randTxt(maxTxtRecordLen / 3)},
+ []string{randTxt(maxTxtRecordLen * 2)},
+ []string{randTxt(maxTxtRecordLen * 2), randTxt(maxTxtRecordLen * 3)},
+ []string{randTxt(maxTxtRecordLen / 2), randTxt(maxTxtRecordLen * 3), randTxt(maxTxtRecordLen * 2), randTxt(maxTxtRecordLen / 3)},
+ }
+
+ for i, test := range tests {
+ splitted, err := maybeSplitLargeTXT(test)
+ if err != nil {
+ t.Errorf("[%d]: encodeLargeTxt failed: %v", i, err)
+ continue
+ }
+ for _, v := range splitted {
+ if len(v) > maxTxtRecordLen {
+ t.Errorf("[%d]: too large encoded txt %d - %v", i, len(v), v)
+ }
+ }
+
+ txt, err := maybeJoinLargeTXT(splitted)
+ if err != nil {
+ t.Errorf("[%d]: decodeLargeTxt failed: %v", i, err)
+ continue
+ }
+
+ sort.Strings(txt)
+ sort.Strings(test)
+ if !reflect.DeepEqual(txt, test) {
+ t.Errorf("[%d]: decoded to %#v, but want %#v", i, txt, test)
+ }
+ }
+}
+
+func randTxt(n int) string {
+ b := make([]byte, int((n*3+3)/4))
+ _, err := rand.Read(b)
+ if err != nil {
+ panic(err)
+ }
+ return base64.RawStdEncoding.EncodeToString(b)[:n]
+}
diff --git a/lib/discovery/plugins/mdns/mdns.go b/lib/discovery/plugins/mdns/mdns.go
index b036716..5056621 100644
--- a/lib/discovery/plugins/mdns/mdns.go
+++ b/lib/discovery/plugins/mdns/mdns.go
@@ -15,9 +15,9 @@
package mdns
import (
- "encoding/hex"
+ "bytes"
+ "errors"
"fmt"
- "strconv"
"strings"
"sync"
"time"
@@ -35,14 +35,25 @@
v23ServiceName = "v23"
serviceNameSuffix = "._sub._" + v23ServiceName
- // The attribute names should not exceed 4 bytes due to the txt record
- // size limit.
- attrServiceUuid = "_srv"
- attrInterface = "_itf"
- attrAddr = "_adr"
- // TODO(jhahn): Remove attrEncryptionAlgorithm.
- attrEncryptionAlgorithm = "_xxx"
- attrEncryptionKeys = "_key"
+ // Use short attribute names due to the txt record size limit.
+ attrName = "_n"
+ attrInterface = "_i"
+ attrAddrs = "_a"
+ attrEncryption = "_e"
+
+ // The prefix for attribute names for encoded large txt records.
+ attrLargeTxtPrefix = "_x"
+
+ // RFC 6763 limits each DNS txt record to 255 bytes and recommends to not have
+ // the cumulative size be larger than 1300 bytes.
+ //
+ // TODO(jhahn): Figure out how to overcome this limit.
+ maxTxtRecordLen = 255
+ maxTotalTxtRecordsLen = 1300
+)
+
+var (
+ errMaxTxtRecordLenExceeded = errors.New("max txt record size exceeded")
)
type plugin struct {
@@ -64,8 +75,8 @@
serviceName := ad.ServiceUuid.String() + serviceNameSuffix
// We use the instance uuid as the host name so that we can get the instance uuid
// from the lost service instance, which has no txt records at all.
- hostName := hex.EncodeToString(ad.InstanceUuid)
- txt, err := createTXTRecords(&ad)
+ hostName := encodeInstanceUuid(ad.InstanceUuid)
+ txt, err := createTxtRecords(&ad)
if err != nil {
return err
}
@@ -134,7 +145,7 @@
case <-ctx.Done():
return
}
- ad, err := decodeAdvertisement(service)
+ ad, err := createAdvertisement(service)
if err != nil {
ctx.Error(err)
continue
@@ -149,65 +160,89 @@
return nil
}
-func createTXTRecords(ad *ldiscovery.Advertisement) ([]string, error) {
- // Prepare a TXT record with attributes and addresses to announce.
- //
- // TODO(jhahn): Currently, the packet size is limited to 2000 bytes in
- // go-mdns-sd package. Think about how to handle a large number of TXT
- // records.
- txt := make([]string, 0, len(ad.Attrs)+4)
- txt = append(txt, fmt.Sprintf("%s=%s", attrServiceUuid, ad.ServiceUuid))
- txt = append(txt, fmt.Sprintf("%s=%s", attrInterface, ad.InterfaceName))
+func createTxtRecords(ad *ldiscovery.Advertisement) ([]string, error) {
+ // Prepare a txt record with attributes and addresses to announce.
+ txt := appendTxtRecord(nil, attrInterface, ad.InterfaceName)
+ if len(ad.InstanceName) > 0 {
+ txt = appendTxtRecord(txt, attrName, ad.InstanceName)
+ }
+ if len(ad.Addrs) > 0 {
+ addrs := ldiscovery.PackAddresses(ad.Addrs)
+ txt = appendTxtRecord(txt, attrAddrs, string(addrs))
+ }
+ if ad.EncryptionAlgorithm != ldiscovery.NoEncryption {
+ enc := ldiscovery.PackEncryptionKeys(ad.EncryptionAlgorithm, ad.EncryptionKeys)
+ txt = appendTxtRecord(txt, attrEncryption, string(enc))
+ }
for k, v := range ad.Attrs {
- txt = append(txt, fmt.Sprintf("%s=%s", k, v))
+ txt = appendTxtRecord(txt, k, v)
}
- for _, a := range ad.Addrs {
- txt = append(txt, fmt.Sprintf("%s=%s", attrAddr, a))
+ txt, err := maybeSplitLargeTXT(txt)
+ if err != nil {
+ return nil, err
}
- txt = append(txt, fmt.Sprintf("%s=%d", attrEncryptionAlgorithm, ad.EncryptionAlgorithm))
- for _, k := range ad.EncryptionKeys {
- txt = append(txt, fmt.Sprintf("%s=%s", attrEncryptionKeys, k))
+ n := 0
+ for _, v := range txt {
+ n += len(v)
+ if n > maxTotalTxtRecordsLen {
+ return nil, errMaxTxtRecordLenExceeded
+ }
}
return txt, nil
}
-func decodeAdvertisement(service mdns.ServiceInstance) (ldiscovery.Advertisement, error) {
+func appendTxtRecord(txt []string, k, v string) []string {
+ var buf bytes.Buffer
+ buf.WriteString(k)
+ buf.WriteByte('=')
+ buf.WriteString(v)
+ kv := buf.String()
+ txt = append(txt, kv)
+ return txt
+}
+
+func createAdvertisement(service mdns.ServiceInstance) (ldiscovery.Advertisement, error) {
// Note that service.Name starts with a host name, which is the instance uuid.
p := strings.SplitN(service.Name, ".", 2)
if len(p) < 1 {
- return ldiscovery.Advertisement{}, fmt.Errorf("invalid host name: %s", service.Name)
+ return ldiscovery.Advertisement{}, fmt.Errorf("invalid service name: %s", service.Name)
}
- instanceUuid, err := hex.DecodeString(p[0])
+ instanceUuid, err := decodeInstanceUuid(p[0])
if err != nil {
return ldiscovery.Advertisement{}, fmt.Errorf("invalid host name: %v", err)
}
- ad := ldiscovery.Advertisement{
- Service: discovery.Service{
- InstanceUuid: instanceUuid,
- Attrs: make(discovery.Attributes),
- },
- Lost: len(service.SrvRRs) == 0 && len(service.TxtRRs) == 0,
+ ad := ldiscovery.Advertisement{Service: discovery.Service{InstanceUuid: instanceUuid}}
+ if len(service.SrvRRs) == 0 && len(service.TxtRRs) == 0 {
+ ad.Lost = true
+ return ad, nil
}
+ ad.Attrs = make(discovery.Attributes)
for _, rr := range service.TxtRRs {
- for _, txt := range rr.Txt {
- kv := strings.SplitN(txt, "=", 2)
- if len(kv) != 2 {
+ txt, err := maybeJoinLargeTXT(rr.Txt)
+ if err != nil {
+ return ldiscovery.Advertisement{}, err
+ }
+
+ for _, kv := range txt {
+ p := strings.SplitN(kv, "=", 2)
+ if len(p) != 2 {
return ldiscovery.Advertisement{}, fmt.Errorf("invalid txt record: %s", txt)
}
- switch k, v := kv[0], kv[1]; k {
- case attrServiceUuid:
- ad.ServiceUuid = uuid.Parse(v)
+ switch k, v := p[0], p[1]; k {
+ case attrName:
+ ad.InstanceName = v
case attrInterface:
ad.InterfaceName = v
- case attrAddr:
- ad.Addrs = append(ad.Addrs, v)
- case attrEncryptionAlgorithm:
- a, _ := strconv.Atoi(v)
- ad.EncryptionAlgorithm = ldiscovery.EncryptionAlgorithm(a)
- case attrEncryptionKeys:
- ad.EncryptionKeys = append(ad.EncryptionKeys, ldiscovery.EncryptionKey(v))
+ case attrAddrs:
+ if ad.Addrs, err = ldiscovery.UnpackAddresses([]byte(v)); err != nil {
+ return ldiscovery.Advertisement{}, err
+ }
+ case attrEncryption:
+ if ad.EncryptionAlgorithm, ad.EncryptionKeys, err = ldiscovery.UnpackEncryptionKeys([]byte(v)); err != nil {
+ return ldiscovery.Advertisement{}, err
+ }
default:
ad.Attrs[k] = v
}
diff --git a/lib/discovery/plugins/mdns/mdns_test.go b/lib/discovery/plugins/mdns/mdns_test.go
index 5b6a7c3..27956dc 100644
--- a/lib/discovery/plugins/mdns/mdns_test.go
+++ b/lib/discovery/plugins/mdns/mdns_test.go
@@ -8,6 +8,7 @@
"fmt"
"reflect"
"runtime"
+ "strings"
"testing"
"time"
@@ -128,6 +129,7 @@
services := []discovery.Service{
{
InstanceUuid: ldiscovery.NewInstanceUUID(),
+ InstanceName: "service1",
InterfaceName: "v.io/x",
Attrs: discovery.Attributes{
"a": "a1234",
@@ -139,6 +141,7 @@
},
{
InstanceUuid: ldiscovery.NewInstanceUUID(),
+ InstanceName: "service2",
InterfaceName: "v.io/x",
Attrs: discovery.Attributes{
"a": "a5678",
@@ -150,6 +153,7 @@
},
{
InstanceUuid: ldiscovery.NewInstanceUUID(),
+ InstanceName: "service3",
InterfaceName: "v.io/y",
Attrs: discovery.Attributes{
"c": "c1234",
@@ -229,3 +233,40 @@
t.Error(err)
}
}
+
+func TestLargeTxt(t *testing.T) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+
+ service := discovery.Service{
+ InstanceUuid: ldiscovery.NewInstanceUUID(),
+ InstanceName: "service2",
+ InterfaceName: strings.Repeat("i", 280),
+ Attrs: discovery.Attributes{
+ "k": strings.Repeat("v", 280),
+ },
+ Addrs: []string{
+ strings.Repeat("a1", 100),
+ strings.Repeat("a2", 100),
+ },
+ }
+
+ p1, err := newWithLoopback("m1", true)
+ if err != nil {
+ t.Fatalf("New() failed: %v", err)
+ }
+ stop, err := advertise(ctx, p1, service)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer stop()
+
+ p2, err := newWithLoopback("m2", true)
+ if err != nil {
+ t.Fatalf("New() failed: %v", err)
+ }
+
+ if err := scanAndMatch(ctx, p2, "", service); err != nil {
+ t.Error(err)
+ }
+}