summaryrefslogtreecommitdiffstats
path: root/discover
diff options
context:
space:
mode:
authorJakob Borg <jakob@nym.se>2013-12-22 16:29:23 -0500
committerJakob Borg <jakob@nym.se>2013-12-22 16:29:23 -0500
commite48222ada0f3d67539eec0ab37fed9eebd8d751d (patch)
tree716e7cfc7bc1773f4b18be4f56fd29f22c185a48 /discover
parent8e65d3669159c92e42e462d1f9e46b9ad0913a55 (diff)
Send external announcements
Diffstat (limited to 'discover')
-rw-r--r--discover/discover.go244
-rw-r--r--discover/discover_test.go111
2 files changed, 318 insertions, 37 deletions
diff --git a/discover/discover.go b/discover/discover.go
index 8501831986..c8407d72c4 100644
--- a/discover/discover.go
+++ b/discover/discover.go
@@ -4,31 +4,75 @@ served by something more standardized, such as mDNS / DNS-SD. In practice, this
was much easier and quicker to get up and running.
The mode of operation is to periodically (currently once every 30 seconds)
-transmit a broadcast UDP packet to the well known port number 21025. The packet
-has the following format:
+broadcast an Announcement packet to UDP port 21025. The packet has the
+following format:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Magic Number |
+ | Magic Number (0x20121025) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Port Number | Length of NodeID |
+ | Port Number | Reserved |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Length of NodeID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ NodeID (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+This is the XDR encoding of:
+
+struct Announcement {
+ unsigned int Magic;
+ unsigned short Port;
+ string NodeID<>;
+}
+
+(Hence NodeID is padded to a multiple of 32 bits)
+
The sending node's address is not encoded -- it is taken to be the source
address of the announcement. Every time such a packet is received, a local
table that maps NodeID to Address is updated. When the local node wants to
connect to another node with the address specification 'dynamic', this table is
consulted.
+
+For external discovery, an identical packet is sent every 30 minutes to the
+external discovery server. The server keeps information for up to 60 minutes.
+To query the server, and UDP packet with the format below is sent.
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Magic Number (0x19760309) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Length of NodeID |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ / /
+ \ NodeID (variable length) \
+ / /
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+This is the XDR encoding of:
+
+struct Announcement {
+ unsigned int Magic;
+ string NodeID<>;
+}
+
+(Hence NodeID is padded to a multiple of 32 bits)
+
+It is answered with an announcement packet for the queried node ID if the
+information is available. There is no answer for queries about unknown nodes. A
+reasonable timeout is recommended instead. (This, combined with server side
+rate limits for packets per source IP and queries per node ID, prevents the
+server from being used as an amplifier in a DDoS attack.)
*/
package discover
import (
"encoding/binary"
+ "errors"
"fmt"
"log"
"net"
@@ -36,14 +80,34 @@ import (
"time"
)
+const (
+ AnnouncementPort = 21025
+ AnnouncementMagic = 0x20121025
+ QueryMagic = 0x19760309
+)
+
+var (
+ errBadMagic = errors.New("bad magic")
+ errFormat = errors.New("incorrect packet format")
+)
+
type Discoverer struct {
- MyID string
- ListenPort int
- BroadcastIntv time.Duration
+ MyID string
+ ListenPort int
+ BroadcastIntv time.Duration
+ ExtListenPort int
+ ExtBroadcastIntv time.Duration
conn *net.UDPConn
registry map[string]string
registryLock sync.RWMutex
+ extServer string
+}
+
+type packet struct {
+ magic uint32 // AnnouncementMagic or QueryMagic
+ port uint16 // unset if magic == QueryMagic
+ id string
}
// We tolerate a certain amount of errors because we might be running in
@@ -51,50 +115,71 @@ type Discoverer struct {
// When we hit this many errors in succession, we stop.
const maxErrors = 30
-func NewDiscoverer(id string, port int) (*Discoverer, error) {
- local4 := &net.UDPAddr{IP: net.IP{0, 0, 0, 0}, Port: 21025}
+func NewDiscoverer(id string, port int, extPort int, extServer string) (*Discoverer, error) {
+ local4 := &net.UDPAddr{IP: net.IP{0, 0, 0, 0}, Port: AnnouncementPort}
conn, err := net.ListenUDP("udp4", local4)
if err != nil {
return nil, err
}
disc := &Discoverer{
- MyID: id,
- ListenPort: port,
- BroadcastIntv: 30 * time.Second,
- conn: conn,
- registry: make(map[string]string),
+ MyID: id,
+ ListenPort: port,
+ BroadcastIntv: 30 * time.Second,
+ ExtListenPort: extPort,
+ ExtBroadcastIntv: 1800 * time.Second,
+
+ conn: conn,
+ registry: make(map[string]string),
+ extServer: extServer,
}
- go disc.sendAnnouncements()
go disc.recvAnnouncements()
+ if disc.ListenPort > 0 {
+ disc.sendAnnouncements()
+ }
+ if len(disc.extServer) > 0 && disc.ExtListenPort > 0 {
+ disc.sendExtAnnouncements()
+ }
+
return disc, nil
}
func (d *Discoverer) sendAnnouncements() {
- remote4 := &net.UDPAddr{IP: net.IP{255, 255, 255, 255}, Port: 21025}
+ remote4 := &net.UDPAddr{IP: net.IP{255, 255, 255, 255}, Port: AnnouncementPort}
- idbs := []byte(d.MyID)
- buf := make([]byte, 4+4+4+len(idbs))
+ buf := encodePacket(packet{AnnouncementMagic, uint16(d.ListenPort), d.MyID})
+ go d.writeAnnouncements(buf, remote4, d.BroadcastIntv)
+}
- binary.BigEndian.PutUint32(buf, uint32(0x121025))
- binary.BigEndian.PutUint16(buf[4:], uint16(d.ListenPort))
- binary.BigEndian.PutUint16(buf[6:], uint16(len(idbs)))
- copy(buf[8:], idbs)
+func (d *Discoverer) sendExtAnnouncements() {
+ extIPs, err := net.LookupIP(d.extServer)
+ if err != nil {
+ log.Printf("discover/external: %v; no external announcements", err)
+ return
+ }
+
+ buf := encodePacket(packet{AnnouncementMagic, uint16(d.ExtListenPort), d.MyID})
+ for _, extIP := range extIPs {
+ remote4 := &net.UDPAddr{IP: extIP, Port: AnnouncementPort}
+ go d.writeAnnouncements(buf, remote4, d.ExtBroadcastIntv)
+ }
+}
+func (d *Discoverer) writeAnnouncements(buf []byte, remote *net.UDPAddr, intv time.Duration) {
var errCounter = 0
var err error
for errCounter < maxErrors {
- _, _, err = d.conn.WriteMsgUDP(buf, nil, remote4)
+ _, _, err = d.conn.WriteMsgUDP(buf, nil, remote)
if err != nil {
errCounter++
} else {
errCounter = 0
}
- time.Sleep(d.BroadcastIntv)
+ time.Sleep(intv)
}
- log.Println("discover/write: stopping due to too many errors:", err)
+ log.Println("discover/write: %v: stopping due to too many errors:", remote, err)
}
func (d *Discoverer) recvAnnouncements() {
@@ -102,26 +187,27 @@ func (d *Discoverer) recvAnnouncements() {
var errCounter = 0
var err error
for errCounter < maxErrors {
- _, addr, err := d.conn.ReadFromUDP(buf)
+ n, addr, err := d.conn.ReadFromUDP(buf)
if err != nil {
+ errCounter++
time.Sleep(time.Second)
continue
}
- errCounter = 0
- magic := binary.BigEndian.Uint32(buf)
- if magic != 0x121025 {
+
+ pkt, err := decodePacket(buf[:n])
+ if err != nil || pkt.magic != AnnouncementMagic {
+ errCounter++
+ time.Sleep(time.Second)
continue
}
- port := binary.BigEndian.Uint16(buf[4:])
- l := binary.BigEndian.Uint16(buf[6:])
- idbs := buf[8 : l+8]
- id := string(idbs)
- if id != d.MyID {
- nodeAddr := fmt.Sprintf("%s:%d", addr.IP.String(), port)
+ errCounter = 0
+
+ if pkt.id != d.MyID {
+ nodeAddr := fmt.Sprintf("%s:%d", addr.IP.String(), pkt.port)
d.registryLock.Lock()
- if d.registry[id] != nodeAddr {
- d.registry[id] = nodeAddr
+ if d.registry[pkt.id] != nodeAddr {
+ d.registry[pkt.id] = nodeAddr
}
d.registryLock.Unlock()
}
@@ -135,3 +221,87 @@ func (d *Discoverer) Lookup(node string) (string, bool) {
addr, ok := d.registry[node]
return addr, ok
}
+
+func encodePacket(pkt packet) []byte {
+ var idbs = []byte(pkt.id)
+ var l = len(idbs) + pad(len(idbs)) + 4 + 4
+ if pkt.magic == AnnouncementMagic {
+ l += 4
+ }
+
+ var buf = make([]byte, l)
+ var offset = 0
+
+ binary.BigEndian.PutUint32(buf[offset:], pkt.magic)
+ offset += 4
+
+ if pkt.magic == AnnouncementMagic {
+ binary.BigEndian.PutUint16(buf[offset:], uint16(pkt.port))
+ offset += 4
+ }
+
+ binary.BigEndian.PutUint32(buf[offset:], uint32(len(idbs)))
+ offset += 4
+ copy(buf[offset:], idbs)
+
+ return buf
+}
+
+func decodePacket(buf []byte) (*packet, error) {
+ var p packet
+ var offset int
+
+ if len(buf) < 4 {
+ // short packet
+ return nil, errFormat
+ }
+ p.magic = binary.BigEndian.Uint32(buf[offset:])
+ offset += 4
+
+ if p.magic != AnnouncementMagic && p.magic != QueryMagic {
+ return nil, errBadMagic
+ }
+
+ if p.magic == AnnouncementMagic {
+ if len(buf) < offset+4 {
+ // short packet
+ return nil, errFormat
+ }
+ p.port = binary.BigEndian.Uint16(buf[offset:])
+ offset += 2
+ reserved := binary.BigEndian.Uint16(buf[offset:])
+ if reserved != 0 {
+ return nil, errFormat
+ }
+ offset += 2
+ }
+
+ if len(buf) < offset+4 {
+ // short packet
+ return nil, errFormat
+ }
+ l := binary.BigEndian.Uint32(buf[offset:])
+ offset += 4
+
+ if len(buf) < offset+int(l)+pad(int(l)) {
+ // short packet
+ return nil, errFormat
+ }
+ idbs := buf[offset : offset+int(l)]
+ p.id = string(idbs)
+ offset += int(l) + pad(int(l))
+ if len(buf[offset:]) > 0 {
+ // extra data
+ return nil, errFormat
+ }
+
+ return &p, nil
+}
+
+func pad(l int) int {
+ d := l % 4
+ if d == 0 {
+ return 0
+ }
+ return 4 - d
+}
diff --git a/discover/discover_test.go b/discover/discover_test.go
new file mode 100644
index 0000000000..2caf17bc08
--- /dev/null
+++ b/discover/discover_test.go
@@ -0,0 +1,111 @@
+package discover
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+)
+
+var testdata = []struct {
+ data []byte
+ packet *packet
+ err error
+}{
+ {
+ []byte{0x20, 0x12, 0x10, 0x25,
+ 0x12, 0x34, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x05,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x00},
+ &packet{
+ magic: 0x20121025,
+ port: 0x1234,
+ id: "hello",
+ },
+ nil,
+ },
+ {
+ []byte{0x20, 0x12, 0x10, 0x25,
+ 0x34, 0x56, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x08,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x21, 0x21},
+ &packet{
+ magic: 0x20121025,
+ port: 0x3456,
+ id: "hello!!!",
+ },
+ nil,
+ },
+ {
+ []byte{0x19, 0x76, 0x03, 0x09,
+ 0x00, 0x00, 0x00, 0x06,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x00, 0x00},
+ &packet{
+ magic: 0x19760309,
+ id: "hello!",
+ },
+ nil,
+ },
+ {
+ []byte{0x20, 0x12, 0x10, 0x25,
+ 0x12, 0x34, 0x12, 0x34, // reserved bits not set to zero
+ 0x00, 0x00, 0x00, 0x06,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x00, 0x00},
+ nil,
+ errFormat,
+ },
+ {
+ []byte{0x20, 0x12, 0x10, 0x25,
+ 0x12, 0x34, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x06,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21}, // missing padding
+ nil,
+ errFormat,
+ },
+ {
+ []byte{0x19, 0x77, 0x03, 0x09, // incorrect magic
+ 0x00, 0x00, 0x00, 0x06,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x00, 0x00},
+ nil,
+ errBadMagic,
+ },
+ {
+ []byte{0x19, 0x76, 0x03, 0x09,
+ 0x6c, 0x6c, 0x6c, 0x6c, // length exceeds packet size
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x00, 0x00},
+ nil,
+ errFormat,
+ },
+ {
+ []byte{0x19, 0x76, 0x03, 0x09,
+ 0x00, 0x00, 0x00, 0x06,
+ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x00, 0x00,
+ 0x23}, // extra data at the end
+ nil,
+ errFormat,
+ },
+}
+
+func TestDecodePacket(t *testing.T) {
+ for i, test := range testdata {
+ p, err := decodePacket(test.data)
+ if err != test.err {
+ t.Errorf("%d: unexpected error %v", i, err)
+ } else {
+ if !reflect.DeepEqual(p, test.packet) {
+ t.Errorf("%d: incorrect packet\n%v\n%v", i, test.packet, p)
+ }
+ }
+ }
+}
+
+func TestEncodePacket(t *testing.T) {
+ for i, test := range testdata {
+ if test.err != nil {
+ continue
+ }
+ buf := encodePacket(*test.packet)
+ if bytes.Compare(buf, test.data) != 0 {
+ t.Errorf("%d: incorrect encoded packet\n% x\n% 0x", i, test.data, buf)
+ }
+ }
+}