discovery: support large attributes in mdns

  Each txt record in mDNS is limited to 255 bytes.
  This CL support large attributes than this limit by splitting
  the attribute into multiple txt records.
  But the total txt records size is still limited to 1300 bytes
  by the underlying mdns package.

  Also add an optional InstanceName attribute to service.

MultiPart: 2/2
Change-Id: I9d67c006f73d989aadea6022e73ad8acc93dd693
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)
+	}
+}