Jelajahi Sumber

Refactor node ID handling, use check digits (fixes #269)

New node ID:s contain four Luhn check digits and are grouped
differently. Code uses NodeID type instead of string, so it's formatted
homogenously everywhere.
Jakob Borg 11 tahun lalu
induk
melakukan
8f3effed32

File diff ditekan karena terlalu besar
+ 0 - 0
auto/gui.files.go


+ 20 - 15
cid/cid.go

@@ -5,27 +5,32 @@
 // Package cid provides a manager for mappings between node ID:s and connection ID:s.
 // Package cid provides a manager for mappings between node ID:s and connection ID:s.
 package cid
 package cid
 
 
-import "sync"
+import (
+	"sync"
+
+	"github.com/calmh/syncthing/protocol"
+)
 
 
 type Map struct {
 type Map struct {
 	sync.Mutex
 	sync.Mutex
-	toCid  map[string]uint
-	toName []string
+	toCid  map[protocol.NodeID]uint
+	toName []protocol.NodeID
 }
 }
 
 
 var (
 var (
-	LocalName      = "<local>"
-	LocalID   uint = 0
+	LocalNodeID      = protocol.NodeID{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+	LocalID     uint = 0
+	emptyNodeID protocol.NodeID
 )
 )
 
 
 func NewMap() *Map {
 func NewMap() *Map {
 	return &Map{
 	return &Map{
-		toCid:  map[string]uint{"<local>": 0},
-		toName: []string{"<local>"},
+		toCid:  map[protocol.NodeID]uint{LocalNodeID: LocalID},
+		toName: []protocol.NodeID{LocalNodeID},
 	}
 	}
 }
 }
 
 
-func (m *Map) Get(name string) uint {
+func (m *Map) Get(name protocol.NodeID) uint {
 	m.Lock()
 	m.Lock()
 	defer m.Unlock()
 	defer m.Unlock()
 
 
@@ -36,7 +41,7 @@ func (m *Map) Get(name string) uint {
 
 
 	// Find a free slot to get a new ID
 	// Find a free slot to get a new ID
 	for i, n := range m.toName {
 	for i, n := range m.toName {
-		if n == "" {
+		if n == emptyNodeID {
 			m.toName[i] = name
 			m.toName[i] = name
 			m.toCid[name] = uint(i)
 			m.toCid[name] = uint(i)
 			return uint(i)
 			return uint(i)
@@ -50,19 +55,19 @@ func (m *Map) Get(name string) uint {
 	return cid
 	return cid
 }
 }
 
 
-func (m *Map) Name(cid uint) string {
+func (m *Map) Name(cid uint) protocol.NodeID {
 	m.Lock()
 	m.Lock()
 	defer m.Unlock()
 	defer m.Unlock()
 
 
 	return m.toName[cid]
 	return m.toName[cid]
 }
 }
 
 
-func (m *Map) Names() []string {
+func (m *Map) Names() []protocol.NodeID {
 	m.Lock()
 	m.Lock()
 
 
-	var names []string
+	var names []protocol.NodeID
 	for _, name := range m.toName {
 	for _, name := range m.toName {
-		if name != "" {
+		if name != emptyNodeID {
 			names = append(names, name)
 			names = append(names, name)
 		}
 		}
 	}
 	}
@@ -71,11 +76,11 @@ func (m *Map) Names() []string {
 	return names
 	return names
 }
 }
 
 
-func (m *Map) Clear(name string) {
+func (m *Map) Clear(name protocol.NodeID) {
 	m.Lock()
 	m.Lock()
 	cid, ok := m.toCid[name]
 	cid, ok := m.toCid[name]
 	if ok {
 	if ok {
-		m.toName[cid] = ""
+		m.toName[cid] = emptyNodeID
 		delete(m.toCid, name)
 		delete(m.toCid, name)
 	}
 	}
 	m.Unlock()
 	m.Unlock()

+ 14 - 7
cid/cid_test.go

@@ -4,28 +4,35 @@
 
 
 package cid
 package cid
 
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/calmh/syncthing/protocol"
+)
 
 
 func TestGet(t *testing.T) {
 func TestGet(t *testing.T) {
 	m := NewMap()
 	m := NewMap()
 
 
-	if i := m.Get("foo"); i != 1 {
+	fooID := protocol.NewNodeID([]byte("foo"))
+	barID := protocol.NewNodeID([]byte("bar"))
+
+	if i := m.Get(fooID); i != 1 {
 		t.Errorf("Unexpected id %d != 1", i)
 		t.Errorf("Unexpected id %d != 1", i)
 	}
 	}
-	if i := m.Get("bar"); i != 2 {
+	if i := m.Get(barID); i != 2 {
 		t.Errorf("Unexpected id %d != 2", i)
 		t.Errorf("Unexpected id %d != 2", i)
 	}
 	}
-	if i := m.Get("foo"); i != 1 {
+	if i := m.Get(fooID); i != 1 {
 		t.Errorf("Unexpected id %d != 1", i)
 		t.Errorf("Unexpected id %d != 1", i)
 	}
 	}
-	if i := m.Get("bar"); i != 2 {
+	if i := m.Get(barID); i != 2 {
 		t.Errorf("Unexpected id %d != 2", i)
 		t.Errorf("Unexpected id %d != 2", i)
 	}
 	}
 
 
 	if LocalID != 0 {
 	if LocalID != 0 {
 		t.Error("LocalID should be 0")
 		t.Error("LocalID should be 0")
 	}
 	}
-	if i := m.Get(LocalName); i != LocalID {
-		t.Errorf("Unexpected id %d != %c", i, LocalID)
+	if i := m.Get(LocalNodeID); i != LocalID {
+		t.Errorf("Unexpected id %d != %d", i, LocalID)
 	}
 	}
 }
 }

+ 8 - 8
cmd/stcli/main.go

@@ -46,12 +46,12 @@ func connect(target string) {
 		log.Fatal(err)
 		log.Fatal(err)
 	}
 	}
 
 
-	myID := string(certID(cert.Certificate[0]))
+	myID := protocol.NewNodeID(cert.Certificate[0])
 
 
 	tlsCfg := &tls.Config{
 	tlsCfg := &tls.Config{
 		Certificates:           []tls.Certificate{cert},
 		Certificates:           []tls.Certificate{cert},
 		NextProtos:             []string{"bep/1.0"},
 		NextProtos:             []string{"bep/1.0"},
-		ServerName:             myID,
+		ServerName:             myID.String(),
 		ClientAuth:             tls.RequestClientCert,
 		ClientAuth:             tls.RequestClientCert,
 		SessionTicketsDisabled: true,
 		SessionTicketsDisabled: true,
 		InsecureSkipVerify:     true,
 		InsecureSkipVerify:     true,
@@ -63,7 +63,7 @@ func connect(target string) {
 		log.Fatal(err)
 		log.Fatal(err)
 	}
 	}
 
 
-	remoteID := certID(conn.ConnectionState().PeerCertificates[0].Raw)
+	remoteID := protocol.NewNodeID(conn.ConnectionState().PeerCertificates[0].Raw)
 
 
 	pc = protocol.NewConnection(remoteID, conn, conn, Model{})
 	pc = protocol.NewConnection(remoteID, conn, conn, Model{})
 
 
@@ -82,7 +82,7 @@ func prtIndex(files []protocol.FileInfo) {
 	}
 	}
 }
 }
 
 
-func (m Model) Index(nodeID string, repo string, files []protocol.FileInfo) {
+func (m Model) Index(nodeID protocol.NodeID, repo string, files []protocol.FileInfo) {
 	log.Printf("Received index for repo %q", repo)
 	log.Printf("Received index for repo %q", repo)
 	if cmd == "idx" {
 	if cmd == "idx" {
 		prtIndex(files)
 		prtIndex(files)
@@ -121,7 +121,7 @@ func getFile(f protocol.FileInfo) {
 	fd.Close()
 	fd.Close()
 }
 }
 
 
-func (m Model) IndexUpdate(nodeID string, repo string, files []protocol.FileInfo) {
+func (m Model) IndexUpdate(nodeID protocol.NodeID, repo string, files []protocol.FileInfo) {
 	log.Printf("Received index update for repo %q", repo)
 	log.Printf("Received index update for repo %q", repo)
 	if cmd == "idx" {
 	if cmd == "idx" {
 		prtIndex(files)
 		prtIndex(files)
@@ -131,16 +131,16 @@ func (m Model) IndexUpdate(nodeID string, repo string, files []protocol.FileInfo
 	}
 	}
 }
 }
 
 
-func (m Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessage) {
+func (m Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterConfigMessage) {
 	log.Println("Received cluster config")
 	log.Println("Received cluster config")
 	log.Printf("%#v", config)
 	log.Printf("%#v", config)
 }
 }
 
 
-func (m Model) Request(nodeID, repo string, name string, offset int64, size int) ([]byte, error) {
+func (m Model) Request(nodeID protocol.NodeID, repo string, name string, offset int64, size int) ([]byte, error) {
 	log.Println("Received request")
 	log.Println("Received request")
 	return nil, io.EOF
 	return nil, io.EOF
 }
 }
 
 
-func (m Model) Close(nodeID string, err error) {
+func (m Model) Close(nodeID protocol.NodeID, err error) {
 	log.Println("Received close")
 	log.Println("Received close")
 }
 }

+ 3 - 11
cmd/stcli/tls.go

@@ -5,20 +5,12 @@
 package main
 package main
 
 
 import (
 import (
-	"crypto/sha256"
 	"crypto/tls"
 	"crypto/tls"
-	"encoding/base32"
 	"path/filepath"
 	"path/filepath"
-	"strings"
 )
 )
 
 
 func loadCert(dir string) (tls.Certificate, error) {
 func loadCert(dir string) (tls.Certificate, error) {
-	return tls.LoadX509KeyPair(filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem"))
-}
-
-func certID(bs []byte) string {
-	hf := sha256.New()
-	hf.Write(bs)
-	id := hf.Sum(nil)
-	return strings.Trim(base32.StdEncoding.EncodeToString(id), "=")
+	cf := filepath.Join(dir, "cert.pem")
+	kf := filepath.Join(dir, "key.pem")
+	return tls.LoadX509KeyPair(cf, kf)
 }
 }

+ 1 - 1
cmd/syncthing/gui.go

@@ -327,7 +327,7 @@ func restGetSystem(w http.ResponseWriter) {
 	runtime.ReadMemStats(&m)
 	runtime.ReadMemStats(&m)
 
 
 	res := make(map[string]interface{})
 	res := make(map[string]interface{})
-	res["myID"] = myID
+	res["myID"] = myID.String()
 	res["goroutines"] = runtime.NumGoroutine()
 	res["goroutines"] = runtime.NumGoroutine()
 	res["alloc"] = m.Alloc
 	res["alloc"] = m.Alloc
 	res["sys"] = m.Sys
 	res["sys"] = m.Sys

+ 6 - 6
cmd/syncthing/main.go

@@ -61,7 +61,7 @@ func init() {
 
 
 var (
 var (
 	cfg        config.Configuration
 	cfg        config.Configuration
-	myID       string
+	myID       protocol.NodeID
 	confDir    string
 	confDir    string
 	logFlags   int = log.Ltime
 	logFlags   int = log.Ltime
 	rateBucket *ratelimit.Bucket
 	rateBucket *ratelimit.Bucket
@@ -181,8 +181,8 @@ func main() {
 		l.FatalErr(err)
 		l.FatalErr(err)
 	}
 	}
 
 
-	myID = certID(cert.Certificate[0])
-	l.SetPrefix(fmt.Sprintf("[%s] ", myID[:5]))
+	myID = protocol.NewNodeID(cert.Certificate[0])
+	l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
 
 
 	l.Infoln(LongVersion)
 	l.Infoln(LongVersion)
 	l.Infoln("My ID:", myID)
 	l.Infoln("My ID:", myID)
@@ -263,7 +263,7 @@ func main() {
 	tlsCfg := &tls.Config{
 	tlsCfg := &tls.Config{
 		Certificates:           []tls.Certificate{cert},
 		Certificates:           []tls.Certificate{cert},
 		NextProtos:             []string{"bep/1.0"},
 		NextProtos:             []string{"bep/1.0"},
-		ServerName:             myID,
+		ServerName:             myID.String(),
 		ClientAuth:             tls.RequestClientCert,
 		ClientAuth:             tls.RequestClientCert,
 		SessionTicketsDisabled: true,
 		SessionTicketsDisabled: true,
 		InsecureSkipVerify:     true,
 		InsecureSkipVerify:     true,
@@ -567,7 +567,7 @@ func saveConfig() {
 	saveConfigCh <- struct{}{}
 	saveConfigCh <- struct{}{}
 }
 }
 
 
-func listenConnect(myID string, m *model.Model, tlsCfg *tls.Config) {
+func listenConnect(myID protocol.NodeID, m *model.Model, tlsCfg *tls.Config) {
 	var conns = make(chan *tls.Conn)
 	var conns = make(chan *tls.Conn)
 
 
 	// Listen
 	// Listen
@@ -673,7 +673,7 @@ next:
 			conn.Close()
 			conn.Close()
 			continue
 			continue
 		}
 		}
-		remoteID := certID(certs[0].Raw)
+		remoteID := protocol.NewNodeID(certs[0].Raw)
 
 
 		if remoteID == myID {
 		if remoteID == myID {
 			l.Infof("Connected to myself (%s) - should not happen", remoteID)
 			l.Infof("Connected to myself (%s) - should not happen", remoteID)

+ 3 - 10
cmd/syncthing/tls.go

@@ -11,14 +11,12 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"crypto/x509/pkix"
-	"encoding/base32"
 	"encoding/binary"
 	"encoding/binary"
 	"encoding/pem"
 	"encoding/pem"
 	"math/big"
 	"math/big"
 	mr "math/rand"
 	mr "math/rand"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
-	"strings"
 	"time"
 	"time"
 )
 )
 
 
@@ -28,14 +26,9 @@ const (
 )
 )
 
 
 func loadCert(dir string, prefix string) (tls.Certificate, error) {
 func loadCert(dir string, prefix string) (tls.Certificate, error) {
-	return tls.LoadX509KeyPair(filepath.Join(dir, prefix+"cert.pem"), filepath.Join(dir, prefix+"key.pem"))
-}
-
-func certID(bs []byte) string {
-	hf := sha256.New()
-	hf.Write(bs)
-	id := hf.Sum(nil)
-	return strings.Trim(base32.StdEncoding.EncodeToString(id), "=")
+	cf := filepath.Join(dir, prefix+"cert.pem")
+	kf := filepath.Join(dir, prefix+"key.pem")
+	return tls.LoadX509KeyPair(cf, kf)
 }
 }
 
 
 func certSeed(bs []byte) int64 {
 func certSeed(bs []byte) int64 {

+ 1 - 1
cmd/syncthing/usage_report.go

@@ -23,7 +23,7 @@ var stopUsageReportingCh = make(chan struct{})
 
 
 func reportData(m *model.Model) map[string]interface{} {
 func reportData(m *model.Model) map[string]interface{} {
 	res := make(map[string]interface{})
 	res := make(map[string]interface{})
-	res["uniqueID"] = strings.ToLower(certID([]byte(myID)))[:6]
+	res["uniqueID"] = strings.ToLower(myID.String()[:6])
 	res["version"] = Version
 	res["version"] = Version
 	res["longVersion"] = LongVersion
 	res["longVersion"] = LongVersion
 	res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
 	res["platform"] = runtime.GOOS + "-" + runtime.GOARCH

+ 15 - 30
config/config.go

@@ -14,10 +14,10 @@ import (
 	"regexp"
 	"regexp"
 	"sort"
 	"sort"
 	"strconv"
 	"strconv"
-	"strings"
 
 
 	"code.google.com/p/go.crypto/bcrypt"
 	"code.google.com/p/go.crypto/bcrypt"
 	"github.com/calmh/syncthing/logger"
 	"github.com/calmh/syncthing/logger"
+	"github.com/calmh/syncthing/protocol"
 	"github.com/calmh/syncthing/scanner"
 	"github.com/calmh/syncthing/scanner"
 )
 )
 
 
@@ -69,7 +69,7 @@ type RepositoryConfiguration struct {
 	Versioning        VersioningConfiguration `xml:"versioning"`
 	Versioning        VersioningConfiguration `xml:"versioning"`
 	SyncOrderPatterns []SyncOrderPattern      `xml:"syncorder>pattern"`
 	SyncOrderPatterns []SyncOrderPattern      `xml:"syncorder>pattern"`
 
 
-	nodeIDs []string
+	nodeIDs []protocol.NodeID
 }
 }
 
 
 type VersioningConfiguration struct {
 type VersioningConfiguration struct {
@@ -113,7 +113,7 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl
 	return nil
 	return nil
 }
 }
 
 
-func (r *RepositoryConfiguration) NodeIDs() []string {
+func (r *RepositoryConfiguration) NodeIDs() []protocol.NodeID {
 	if r.nodeIDs == nil {
 	if r.nodeIDs == nil {
 		for _, n := range r.Nodes {
 		for _, n := range r.Nodes {
 			r.nodeIDs = append(r.nodeIDs, n.NodeID)
 			r.nodeIDs = append(r.nodeIDs, n.NodeID)
@@ -138,9 +138,9 @@ func (r RepositoryConfiguration) FileRanker() func(scanner.File) int {
 }
 }
 
 
 type NodeConfiguration struct {
 type NodeConfiguration struct {
-	NodeID    string   `xml:"id,attr"`
-	Name      string   `xml:"name,attr,omitempty"`
-	Addresses []string `xml:"address,omitempty"`
+	NodeID    protocol.NodeID `xml:"id,attr"`
+	Name      string          `xml:"name,attr,omitempty"`
+	Addresses []string        `xml:"address,omitempty"`
 }
 }
 
 
 type OptionsConfiguration struct {
 type OptionsConfiguration struct {
@@ -174,8 +174,8 @@ type GUIConfiguration struct {
 	APIKey   string `xml:"apikey,omitempty"`
 	APIKey   string `xml:"apikey,omitempty"`
 }
 }
 
 
-func (cfg *Configuration) NodeMap() map[string]NodeConfiguration {
-	m := make(map[string]NodeConfiguration, len(cfg.Nodes))
+func (cfg *Configuration) NodeMap() map[protocol.NodeID]NodeConfiguration {
+	m := make(map[protocol.NodeID]NodeConfiguration, len(cfg.Nodes))
 	for _, n := range cfg.Nodes {
 	for _, n := range cfg.Nodes {
 		m[n.NodeID] = n
 		m[n.NodeID] = n
 	}
 	}
@@ -276,7 +276,7 @@ func uniqueStrings(ss []string) []string {
 	return us
 	return us
 }
 }
 
 
-func Load(rd io.Reader, myID string) (Configuration, error) {
+func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
 	var cfg Configuration
 	var cfg Configuration
 
 
 	setDefaults(&cfg)
 	setDefaults(&cfg)
@@ -297,15 +297,6 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
 		cfg.Repositories = []RepositoryConfiguration{}
 		cfg.Repositories = []RepositoryConfiguration{}
 	}
 	}
 
 
-	// Sanitize node IDs
-	for i := range cfg.Nodes {
-		node := &cfg.Nodes[i]
-		// Strip spaces and dashes
-		node.NodeID = strings.Replace(node.NodeID, "-", "", -1)
-		node.NodeID = strings.Replace(node.NodeID, " ", "", -1)
-		node.NodeID = strings.ToUpper(node.NodeID)
-	}
-
 	// Check for missing, bad or duplicate repository ID:s
 	// Check for missing, bad or duplicate repository ID:s
 	var seenRepos = map[string]*RepositoryConfiguration{}
 	var seenRepos = map[string]*RepositoryConfiguration{}
 	var uniqueCounter int
 	var uniqueCounter int
@@ -321,13 +312,6 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
 			repo.ID = "default"
 			repo.ID = "default"
 		}
 		}
 
 
-		for i := range repo.Nodes {
-			node := &repo.Nodes[i]
-			// Strip spaces and dashes
-			node.NodeID = strings.Replace(node.NodeID, "-", "", -1)
-			node.NodeID = strings.Replace(node.NodeID, " ", "", -1)
-		}
-
 		if seen, ok := seenRepos[repo.ID]; ok {
 		if seen, ok := seenRepos[repo.ID]; ok {
 			l.Warnf("Multiple repositories with ID %q; disabling", repo.ID)
 			l.Warnf("Multiple repositories with ID %q; disabling", repo.ID)
 
 
@@ -390,8 +374,9 @@ func convertV1V2(cfg *Configuration) {
 	for i, repo := range cfg.Repositories {
 	for i, repo := range cfg.Repositories {
 		cfg.Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly
 		cfg.Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly
 		for j, node := range repo.Nodes {
 		for j, node := range repo.Nodes {
-			if _, ok := nodes[node.NodeID]; !ok {
-				nodes[node.NodeID] = node
+			id := node.NodeID.String()
+			if _, ok := nodes[id]; !ok {
+				nodes[id] = node
 			}
 			}
 			cfg.Repositories[i].Nodes[j] = NodeConfiguration{NodeID: node.NodeID}
 			cfg.Repositories[i].Nodes[j] = NodeConfiguration{NodeID: node.NodeID}
 		}
 		}
@@ -416,7 +401,7 @@ func convertV1V2(cfg *Configuration) {
 type NodeConfigurationList []NodeConfiguration
 type NodeConfigurationList []NodeConfiguration
 
 
 func (l NodeConfigurationList) Less(a, b int) bool {
 func (l NodeConfigurationList) Less(a, b int) bool {
-	return l[a].NodeID < l[b].NodeID
+	return l[a].NodeID.Compare(l[b].NodeID) == -1
 }
 }
 func (l NodeConfigurationList) Swap(a, b int) {
 func (l NodeConfigurationList) Swap(a, b int) {
 	l[a], l[b] = l[b], l[a]
 	l[a], l[b] = l[b], l[a]
@@ -425,10 +410,10 @@ func (l NodeConfigurationList) Len() int {
 	return len(l)
 	return len(l)
 }
 }
 
 
-func ensureNodePresent(nodes []NodeConfiguration, myID string) []NodeConfiguration {
+func ensureNodePresent(nodes []NodeConfiguration, myID protocol.NodeID) []NodeConfiguration {
 	var myIDExists bool
 	var myIDExists bool
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.NodeID == myID {
+		if node.NodeID.Equals(myID) {
 			myIDExists = true
 			myIDExists = true
 			break
 			break
 		}
 		}

+ 41 - 95
config/config_test.go

@@ -12,9 +12,19 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/calmh/syncthing/files"
 	"github.com/calmh/syncthing/files"
+	"github.com/calmh/syncthing/protocol"
 	"github.com/calmh/syncthing/scanner"
 	"github.com/calmh/syncthing/scanner"
 )
 )
 
 
+var node1, node2, node3, node4 protocol.NodeID
+
+func init() {
+	node1, _ = protocol.NodeIDFromString("AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ")
+	node2, _ = protocol.NodeIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
+	node3, _ = protocol.NodeIDFromString("LGFPDIT-7SKNNJL-VJZA4FC-7QNCRKA-CE753K7-2BW5QDK-2FOZ7FR-FEP57QJ")
+	node4, _ = protocol.NodeIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
+}
+
 func TestDefaultValues(t *testing.T) {
 func TestDefaultValues(t *testing.T) {
 	expected := OptionsConfiguration{
 	expected := OptionsConfiguration{
 		ListenAddress:      []string{"0.0.0.0:22000"},
 		ListenAddress:      []string{"0.0.0.0:22000"},
@@ -31,7 +41,7 @@ func TestDefaultValues(t *testing.T) {
 		UPnPEnabled:        true,
 		UPnPEnabled:        true,
 	}
 	}
 
 
-	cfg, err := Load(bytes.NewReader(nil), "nodeID")
+	cfg, err := Load(bytes.NewReader(nil), node1)
 	if err != io.EOF {
 	if err != io.EOF {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
@@ -45,10 +55,10 @@ func TestNodeConfig(t *testing.T) {
 	v1data := []byte(`
 	v1data := []byte(`
 <configuration version="1">
 <configuration version="1">
     <repository id="test" directory="~/Sync">
     <repository id="test" directory="~/Sync">
-        <node id="NODE1" name="node one">
+        <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
             <address>a</address>
             <address>a</address>
         </node>
         </node>
-        <node id="NODE2" name="node two">
+        <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
             <address>b</address>
             <address>b</address>
         </node>
         </node>
     </repository>
     </repository>
@@ -61,20 +71,20 @@ func TestNodeConfig(t *testing.T) {
 	v2data := []byte(`
 	v2data := []byte(`
 <configuration version="2">
 <configuration version="2">
     <repository id="test" directory="~/Sync" ro="true">
     <repository id="test" directory="~/Sync" ro="true">
-        <node id="NODE1"/>
-        <node id="NODE2"/>
+        <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
+        <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
     </repository>
     </repository>
-    <node id="NODE1" name="node one">
+    <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
         <address>a</address>
         <address>a</address>
     </node>
     </node>
-    <node id="NODE2" name="node two">
+    <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
         <address>b</address>
         <address>b</address>
     </node>
     </node>
 </configuration>
 </configuration>
 `)
 `)
 
 
 	for i, data := range [][]byte{v1data, v2data} {
 	for i, data := range [][]byte{v1data, v2data} {
-		cfg, err := Load(bytes.NewReader(data), "NODE1")
+		cfg, err := Load(bytes.NewReader(data), node1)
 		if err != nil {
 		if err != nil {
 			t.Error(err)
 			t.Error(err)
 		}
 		}
@@ -83,23 +93,23 @@ func TestNodeConfig(t *testing.T) {
 			{
 			{
 				ID:        "test",
 				ID:        "test",
 				Directory: "~/Sync",
 				Directory: "~/Sync",
-				Nodes:     []NodeConfiguration{{NodeID: "NODE1"}, {NodeID: "NODE2"}},
+				Nodes:     []NodeConfiguration{{NodeID: node1}, {NodeID: node4}},
 				ReadOnly:  true,
 				ReadOnly:  true,
 			},
 			},
 		}
 		}
 		expectedNodes := []NodeConfiguration{
 		expectedNodes := []NodeConfiguration{
 			{
 			{
-				NodeID:    "NODE1",
+				NodeID:    node1,
 				Name:      "node one",
 				Name:      "node one",
 				Addresses: []string{"a"},
 				Addresses: []string{"a"},
 			},
 			},
 			{
 			{
-				NodeID:    "NODE2",
+				NodeID:    node4,
 				Name:      "node two",
 				Name:      "node two",
 				Addresses: []string{"b"},
 				Addresses: []string{"b"},
 			},
 			},
 		}
 		}
-		expectedNodeIDs := []string{"NODE1", "NODE2"}
+		expectedNodeIDs := []protocol.NodeID{node1, node4}
 
 
 		if cfg.Version != 2 {
 		if cfg.Version != 2 {
 			t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
 			t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
@@ -118,18 +128,13 @@ func TestNodeConfig(t *testing.T) {
 
 
 func TestNoListenAddress(t *testing.T) {
 func TestNoListenAddress(t *testing.T) {
 	data := []byte(`<configuration version="1">
 	data := []byte(`<configuration version="1">
-    <repository directory="~/Sync">
-        <node id="..." name="...">
-            <address>dynamic</address>
-        </node>
-    </repository>
     <options>
     <options>
         <listenAddress></listenAddress>
         <listenAddress></listenAddress>
     </options>
     </options>
 </configuration>
 </configuration>
 `)
 `)
 
 
-	cfg, err := Load(bytes.NewReader(data), "nodeID")
+	cfg, err := Load(bytes.NewReader(data), node1)
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
@@ -142,11 +147,6 @@ func TestNoListenAddress(t *testing.T) {
 
 
 func TestOverriddenValues(t *testing.T) {
 func TestOverriddenValues(t *testing.T) {
 	data := []byte(`<configuration version="2">
 	data := []byte(`<configuration version="2">
-    <repository directory="~/Sync">
-        <node id="..." name="...">
-            <address>dynamic</address>
-        </node>
-    </repository>
     <options>
     <options>
        <listenAddress>:23000</listenAddress>
        <listenAddress>:23000</listenAddress>
         <allowDelete>false</allowDelete>
         <allowDelete>false</allowDelete>
@@ -180,7 +180,7 @@ func TestOverriddenValues(t *testing.T) {
 		UPnPEnabled:        false,
 		UPnPEnabled:        false,
 	}
 	}
 
 
-	cfg, err := Load(bytes.NewReader(data), "nodeID")
+	cfg, err := Load(bytes.NewReader(data), node1)
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
@@ -193,13 +193,13 @@ func TestOverriddenValues(t *testing.T) {
 func TestNodeAddresses(t *testing.T) {
 func TestNodeAddresses(t *testing.T) {
 	data := []byte(`
 	data := []byte(`
 <configuration version="2">
 <configuration version="2">
-    <node id="n1">
-        <address>dynamic</address>
-    </node>
-    <node id="n2">
+    <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
         <address></address>
         <address></address>
     </node>
     </node>
-    <node id="n3">
+    <node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
+    </node>
+    <node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
+        <address>dynamic</address>
     </node>
     </node>
 </configuration>
 </configuration>
 `)
 `)
@@ -207,25 +207,25 @@ func TestNodeAddresses(t *testing.T) {
 	name, _ := os.Hostname()
 	name, _ := os.Hostname()
 	expected := []NodeConfiguration{
 	expected := []NodeConfiguration{
 		{
 		{
-			NodeID:    "N1",
+			NodeID:    node1,
 			Addresses: []string{"dynamic"},
 			Addresses: []string{"dynamic"},
 		},
 		},
 		{
 		{
-			NodeID:    "N2",
+			NodeID:    node2,
 			Addresses: []string{"dynamic"},
 			Addresses: []string{"dynamic"},
 		},
 		},
 		{
 		{
-			NodeID:    "N3",
+			NodeID:    node3,
 			Addresses: []string{"dynamic"},
 			Addresses: []string{"dynamic"},
 		},
 		},
 		{
 		{
-			NodeID:    "N4",
+			NodeID:    node4,
 			Name:      name, // Set when auto created
 			Name:      name, // Set when auto created
 			Addresses: []string{"dynamic"},
 			Addresses: []string{"dynamic"},
 		},
 		},
 	}
 	}
 
 
-	cfg, err := Load(bytes.NewReader(data), "N4")
+	cfg, err := Load(bytes.NewReader(data), node4)
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
@@ -235,86 +235,32 @@ func TestNodeAddresses(t *testing.T) {
 	}
 	}
 }
 }
 
 
-func TestStripNodeIs(t *testing.T) {
-	data := []byte(`
-<configuration version="2">
-    <node id="AAAA-BBBB-CCCC">
-        <address>dynamic</address>
-    </node>
-    <node id="AAAA BBBB DDDD">
-        <address></address>
-    </node>
-    <node id="AAAABBBBEEEE">
-        <address></address>
-    </node>
-    <repository directory="~/Sync">
-        <node id="AAA ABBB-BCC CC" name=""></node>
-        <node id="AA-AAB BBBD-DDD" name=""></node>
-        <node id="AAA AB-BBB EEE-E" name=""></node>
-    </repository>
-</configuration>
-`)
-
-	expected := []NodeConfiguration{
-		{
-			NodeID:    "AAAABBBBCCCC",
-			Addresses: []string{"dynamic"},
-		},
-		{
-			NodeID:    "AAAABBBBDDDD",
-			Addresses: []string{"dynamic"},
-		},
-		{
-			NodeID:    "AAAABBBBEEEE",
-			Addresses: []string{"dynamic"},
-		},
-	}
-
-	cfg, err := Load(bytes.NewReader(data), "n4")
-	if err != nil {
-		t.Error(err)
-	}
-
-	for i := range expected {
-		if !reflect.DeepEqual(cfg.Nodes[i], expected[i]) {
-			t.Errorf("Nodes[%d] differ;\n  E: %#v\n  A: %#v", i, expected[i], cfg.Nodes[i])
-		}
-		if cfg.Repositories[0].Nodes[i].NodeID != expected[i].NodeID {
-			t.Errorf("Repo nodes[%d] differ;\n  E: %#v\n  A: %#v", i, expected[i].NodeID, cfg.Repositories[0].Nodes[i].NodeID)
-		}
-	}
-}
-
 func TestSyncOrders(t *testing.T) {
 func TestSyncOrders(t *testing.T) {
 	data := []byte(`
 	data := []byte(`
 <configuration version="2">
 <configuration version="2">
-    <node id="AAAA-BBBB-CCCC">
-        <address>dynamic</address>
-    </node>
     <repository directory="~/Sync">
     <repository directory="~/Sync">
         <syncorder>
         <syncorder>
             <pattern pattern="\.jpg$" priority="1" />
             <pattern pattern="\.jpg$" priority="1" />
         </syncorder>
         </syncorder>
-        <node id="AAAA-BBBB-CCCC" name=""></node>
     </repository>
     </repository>
 </configuration>
 </configuration>
 `)
 `)
 
 
 	expected := []SyncOrderPattern{
 	expected := []SyncOrderPattern{
 		{
 		{
-			Pattern: "\\.jpg$",
-			Priority:  1,
+			Pattern:  "\\.jpg$",
+			Priority: 1,
 		},
 		},
 	}
 	}
 
 
-	cfg, err := Load(bytes.NewReader(data), "n4")
+	cfg, err := Load(bytes.NewReader(data), node1)
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 
 
 	for i := range expected {
 	for i := range expected {
 		if !reflect.DeepEqual(cfg.Repositories[0].SyncOrderPatterns[i], expected[i]) {
 		if !reflect.DeepEqual(cfg.Repositories[0].SyncOrderPatterns[i], expected[i]) {
-			t.Errorf("Nodes[%d] differ;\n  E: %#v\n  A: %#v", i, expected[i], cfg.Repositories[0].SyncOrderPatterns[i])
+			t.Errorf("Patterns[%d] differ;\n  E: %#v\n  A: %#v", i, expected[i], cfg.Repositories[0].SyncOrderPatterns[i])
 		}
 		}
 	}
 	}
 }
 }
@@ -361,9 +307,9 @@ func TestFileSorter(t *testing.T) {
 	if !reflect.DeepEqual(f, expected) {
 	if !reflect.DeepEqual(f, expected) {
 		t.Errorf(
 		t.Errorf(
 			"\n\nexpected:\n" +
 			"\n\nexpected:\n" +
-			formatFiles(expected) + "\n" +
-			"got:\n" +
-			formatFiles(f) + "\n\n",
+				formatFiles(expected) + "\n" +
+				"got:\n" +
+				formatFiles(f) + "\n\n",
 		)
 		)
 	}
 	}
 }
 }

+ 20 - 3
discover/cmd/discosrv/main.go

@@ -17,6 +17,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/calmh/syncthing/discover"
 	"github.com/calmh/syncthing/discover"
+	"github.com/calmh/syncthing/protocol"
 	"github.com/golang/groupcache/lru"
 	"github.com/golang/groupcache/lru"
 	"github.com/juju/ratelimit"
 	"github.com/juju/ratelimit"
 )
 )
@@ -32,7 +33,7 @@ type address struct {
 }
 }
 
 
 var (
 var (
-	nodes      = make(map[string]node)
+	nodes      = make(map[protocol.NodeID]node)
 	lock       sync.Mutex
 	lock       sync.Mutex
 	queries    = 0
 	queries    = 0
 	announces  = 0
 	announces  = 0
@@ -182,8 +183,16 @@ func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
 		updated:   time.Now(),
 		updated:   time.Now(),
 	}
 	}
 
 
+	var id protocol.NodeID
+	if len(pkt.This.ID) == 32 {
+		// Raw node ID
+		copy(id[:], pkt.This.ID)
+	} else {
+		id.UnmarshalText(pkt.This.ID)
+	}
+
 	lock.Lock()
 	lock.Lock()
-	nodes[pkt.This.ID] = node
+	nodes[id] = node
 	lock.Unlock()
 	lock.Unlock()
 }
 }
 
 
@@ -199,8 +208,16 @@ func handleQueryV2(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
 		log.Printf("<- %v %#v", addr, pkt)
 		log.Printf("<- %v %#v", addr, pkt)
 	}
 	}
 
 
+	var id protocol.NodeID
+	if len(pkt.NodeID) == 32 {
+		// Raw node ID
+		copy(id[:], pkt.NodeID)
+	} else {
+		id.UnmarshalText(pkt.NodeID)
+	}
+
 	lock.Lock()
 	lock.Lock()
-	node, ok := nodes[pkt.NodeID]
+	node, ok := nodes[id]
 	queries++
 	queries++
 	lock.Unlock()
 	lock.Unlock()
 
 

+ 24 - 18
discover/discover.go

@@ -5,6 +5,7 @@
 package discover
 package discover
 
 
 import (
 import (
+	"bytes"
 	"encoding/hex"
 	"encoding/hex"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -14,15 +15,16 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/calmh/syncthing/beacon"
 	"github.com/calmh/syncthing/beacon"
+	"github.com/calmh/syncthing/protocol"
 )
 )
 
 
 type Discoverer struct {
 type Discoverer struct {
-	myID             string
+	myID             protocol.NodeID
 	listenAddrs      []string
 	listenAddrs      []string
 	localBcastIntv   time.Duration
 	localBcastIntv   time.Duration
 	globalBcastIntv  time.Duration
 	globalBcastIntv  time.Duration
 	beacon           *beacon.Beacon
 	beacon           *beacon.Beacon
-	registry         map[string][]string
+	registry         map[protocol.NodeID][]string
 	registryLock     sync.RWMutex
 	registryLock     sync.RWMutex
 	extServer        string
 	extServer        string
 	extPort          uint16
 	extPort          uint16
@@ -41,7 +43,7 @@ var (
 // When we hit this many errors in succession, we stop.
 // When we hit this many errors in succession, we stop.
 const maxErrors = 30
 const maxErrors = 30
 
 
-func NewDiscoverer(id string, addresses []string, localPort int) (*Discoverer, error) {
+func NewDiscoverer(id protocol.NodeID, addresses []string, localPort int) (*Discoverer, error) {
 	b, err := beacon.New(localPort)
 	b, err := beacon.New(localPort)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -52,7 +54,7 @@ func NewDiscoverer(id string, addresses []string, localPort int) (*Discoverer, e
 		localBcastIntv:  30 * time.Second,
 		localBcastIntv:  30 * time.Second,
 		globalBcastIntv: 1800 * time.Second,
 		globalBcastIntv: 1800 * time.Second,
 		beacon:          b,
 		beacon:          b,
-		registry:        make(map[string][]string),
+		registry:        make(map[protocol.NodeID][]string),
 	}
 	}
 
 
 	go disc.recvAnnouncements()
 	go disc.recvAnnouncements()
@@ -78,7 +80,7 @@ func (d *Discoverer) ExtAnnounceOK() bool {
 	return d.extAnnounceOK
 	return d.extAnnounceOK
 }
 }
 
 
-func (d *Discoverer) Lookup(node string) []string {
+func (d *Discoverer) Lookup(node protocol.NodeID) []string {
 	d.registryLock.Lock()
 	d.registryLock.Lock()
 	addr, ok := d.registry[node]
 	addr, ok := d.registry[node]
 	d.registryLock.Unlock()
 	d.registryLock.Unlock()
@@ -94,15 +96,17 @@ func (d *Discoverer) Lookup(node string) []string {
 
 
 func (d *Discoverer) Hint(node string, addrs []string) {
 func (d *Discoverer) Hint(node string, addrs []string) {
 	resAddrs := resolveAddrs(addrs)
 	resAddrs := resolveAddrs(addrs)
+	var id protocol.NodeID
+	id.UnmarshalText([]byte(node))
 	d.registerNode(nil, Node{
 	d.registerNode(nil, Node{
-		ID:        node,
 		Addresses: resAddrs,
 		Addresses: resAddrs,
+		ID:        id[:],
 	})
 	})
 }
 }
 
 
-func (d *Discoverer) All() map[string][]string {
+func (d *Discoverer) All() map[protocol.NodeID][]string {
 	d.registryLock.RLock()
 	d.registryLock.RLock()
-	nodes := make(map[string][]string, len(d.registry))
+	nodes := make(map[protocol.NodeID][]string, len(d.registry))
 	for node, addrs := range d.registry {
 	for node, addrs := range d.registry {
 		addrsCopy := make([]string, len(addrs))
 		addrsCopy := make([]string, len(addrs))
 		copy(addrsCopy, addrs)
 		copy(addrsCopy, addrs)
@@ -132,7 +136,7 @@ func (d *Discoverer) announcementPkt() []byte {
 	}
 	}
 	var pkt = AnnounceV2{
 	var pkt = AnnounceV2{
 		Magic: AnnouncementMagicV2,
 		Magic: AnnouncementMagicV2,
-		This:  Node{d.myID, addrs},
+		This:  Node{d.myID[:], addrs},
 	}
 	}
 	return pkt.MarshalXDR()
 	return pkt.MarshalXDR()
 }
 }
@@ -142,7 +146,7 @@ func (d *Discoverer) sendLocalAnnouncements() {
 
 
 	var pkt = AnnounceV2{
 	var pkt = AnnounceV2{
 		Magic: AnnouncementMagicV2,
 		Magic: AnnouncementMagicV2,
-		This:  Node{d.myID, addrs},
+		This:  Node{d.myID[:], addrs},
 	}
 	}
 
 
 	for {
 	for {
@@ -153,7 +157,7 @@ func (d *Discoverer) sendLocalAnnouncements() {
 				break
 				break
 			}
 			}
 
 
-			anode := Node{node, resolveAddrs(addrs)}
+			anode := Node{node[:], resolveAddrs(addrs)}
 			pkt.Extra = append(pkt.Extra, anode)
 			pkt.Extra = append(pkt.Extra, anode)
 		}
 		}
 		d.registryLock.RUnlock()
 		d.registryLock.RUnlock()
@@ -184,7 +188,7 @@ func (d *Discoverer) sendExternalAnnouncements() {
 	if d.extPort != 0 {
 	if d.extPort != 0 {
 		var pkt = AnnounceV2{
 		var pkt = AnnounceV2{
 			Magic: AnnouncementMagicV2,
 			Magic: AnnouncementMagicV2,
-			This:  Node{d.myID, []Address{{Port: d.extPort}}},
+			This:  Node{d.myID[:], []Address{{Port: d.extPort}}},
 		}
 		}
 		buf = pkt.MarshalXDR()
 		buf = pkt.MarshalXDR()
 	} else {
 	} else {
@@ -246,11 +250,11 @@ func (d *Discoverer) recvAnnouncements() {
 		}
 		}
 
 
 		var newNode bool
 		var newNode bool
-		if pkt.This.ID != d.myID {
+		if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
 			n := d.registerNode(addr, pkt.This)
 			n := d.registerNode(addr, pkt.This)
 			newNode = newNode || n
 			newNode = newNode || n
 			for _, node := range pkt.Extra {
 			for _, node := range pkt.Extra {
-				if node.ID != d.myID {
+				if bytes.Compare(node.ID, d.myID[:]) != 0 {
 					n := d.registerNode(nil, node)
 					n := d.registerNode(nil, node)
 					newNode = newNode || n
 					newNode = newNode || n
 				}
 				}
@@ -287,14 +291,16 @@ func (d *Discoverer) registerNode(addr net.Addr, node Node) bool {
 	if debug {
 	if debug {
 		l.Debugf("discover: register: %s -> %#v", node.ID, addrs)
 		l.Debugf("discover: register: %s -> %#v", node.ID, addrs)
 	}
 	}
+	var id protocol.NodeID
+	copy(id[:], node.ID)
 	d.registryLock.Lock()
 	d.registryLock.Lock()
-	_, seen := d.registry[node.ID]
-	d.registry[node.ID] = addrs
+	_, seen := d.registry[id]
+	d.registry[id] = addrs
 	d.registryLock.Unlock()
 	d.registryLock.Unlock()
 	return !seen
 	return !seen
 }
 }
 
 
-func (d *Discoverer) externalLookup(node string) []string {
+func (d *Discoverer) externalLookup(node protocol.NodeID) []string {
 	extIP, err := net.ResolveUDPAddr("udp", d.extServer)
 	extIP, err := net.ResolveUDPAddr("udp", d.extServer)
 	if err != nil {
 	if err != nil {
 		if debug {
 		if debug {
@@ -320,7 +326,7 @@ func (d *Discoverer) externalLookup(node string) []string {
 		return nil
 		return nil
 	}
 	}
 
 
-	buf := QueryV2{QueryMagicV2, node}.MarshalXDR()
+	buf := QueryV2{QueryMagicV2, node[:]}.MarshalXDR()
 	_, err = conn.Write(buf)
 	_, err = conn.Write(buf)
 	if err != nil {
 	if err != nil {
 		if debug {
 		if debug {

+ 2 - 2
discover/packets.go

@@ -11,7 +11,7 @@ const (
 
 
 type QueryV2 struct {
 type QueryV2 struct {
 	Magic  uint32
 	Magic  uint32
-	NodeID string // max:64
+	NodeID []byte // max:32
 }
 }
 
 
 type AnnounceV2 struct {
 type AnnounceV2 struct {
@@ -21,7 +21,7 @@ type AnnounceV2 struct {
 }
 }
 
 
 type Node struct {
 type Node struct {
-	ID        string    // max:64
+	ID        []byte    // max:32
 	Addresses []Address // max:16
 	Addresses []Address // max:16
 }
 }
 
 

+ 6 - 10
discover/packets_xdr.go

@@ -1,7 +1,3 @@
-// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
-// Use of this source code is governed by an MIT-style license that can be
-// found in the LICENSE file.
-
 package discover
 package discover
 
 
 import (
 import (
@@ -25,10 +21,10 @@ func (o QueryV2) MarshalXDR() []byte {
 
 
 func (o QueryV2) encodeXDR(xw *xdr.Writer) (int, error) {
 func (o QueryV2) encodeXDR(xw *xdr.Writer) (int, error) {
 	xw.WriteUint32(o.Magic)
 	xw.WriteUint32(o.Magic)
-	if len(o.NodeID) > 64 {
+	if len(o.NodeID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
 	}
-	xw.WriteString(o.NodeID)
+	xw.WriteBytes(o.NodeID)
 	return xw.Tot(), xw.Error()
 	return xw.Tot(), xw.Error()
 }
 }
 
 
@@ -45,7 +41,7 @@ func (o *QueryV2) UnmarshalXDR(bs []byte) error {
 
 
 func (o *QueryV2) decodeXDR(xr *xdr.Reader) error {
 func (o *QueryV2) decodeXDR(xr *xdr.Reader) error {
 	o.Magic = xr.ReadUint32()
 	o.Magic = xr.ReadUint32()
-	o.NodeID = xr.ReadStringMax(64)
+	o.NodeID = xr.ReadBytesMax(32)
 	return xr.Error()
 	return xr.Error()
 }
 }
 
 
@@ -112,10 +108,10 @@ func (o Node) MarshalXDR() []byte {
 }
 }
 
 
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
-	if len(o.ID) > 64 {
+	if len(o.ID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
 	}
-	xw.WriteString(o.ID)
+	xw.WriteBytes(o.ID)
 	if len(o.Addresses) > 16 {
 	if len(o.Addresses) > 16 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
 	}
@@ -138,7 +134,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
 }
 }
 
 
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
-	o.ID = xr.ReadStringMax(64)
+	o.ID = xr.ReadBytesMax(32)
 	_AddressesSize := int(xr.ReadUint32())
 	_AddressesSize := int(xr.ReadUint32())
 	if _AddressesSize > 16 {
 	if _AddressesSize > 16 {
 		return xdr.ErrElementSizeExceeded
 		return xdr.ErrElementSizeExceeded

+ 4 - 15
gui/app.js

@@ -410,7 +410,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
 
 
         $('#editNode').modal('hide');
         $('#editNode').modal('hide');
         nodeCfg = $scope.currentNode;
         nodeCfg = $scope.currentNode;
-        nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
+        nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').toLowerCase().trim();
         nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
         nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
 
 
         done = false;
         done = false;
@@ -711,7 +711,7 @@ function randomString(len, bits)
         newStr = Math.random().toString(bits).slice(2);
         newStr = Math.random().toString(bits).slice(2);
         outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
         outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
     }
     }
-    return outStr.toUpperCase();
+    return outStr.toLowerCase();
 }
 }
 
 
 syncthing.filter('natural', function () {
 syncthing.filter('natural', function () {
@@ -777,17 +777,6 @@ syncthing.filter('alwaysNumber', function () {
     };
     };
 });
 });
 
 
-syncthing.filter('chunkID', function () {
-    return function (input) {
-        if (input === undefined)
-            return "";
-        var parts = input.match(/.{1,6}/g);
-        if (!parts)
-            return "";
-        return parts.join('-');
-    };
-});
-
 syncthing.filter('shortPath', function () {
 syncthing.filter('shortPath', function () {
     return function (input) {
     return function (input) {
         if (input === undefined)
         if (input === undefined)
@@ -860,8 +849,8 @@ syncthing.directive('validNodeid', function() {
                     // we shouldn't validate
                     // we shouldn't validate
                     ctrl.$setValidity('validNodeid', true);
                     ctrl.$setValidity('validNodeid', true);
                 } else {
                 } else {
-                    var cleaned = viewValue.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
-                    if (cleaned.match(/^[A-Z2-7]{52}$/)) {
+                    var cleaned = viewValue.replace(/ /g, '').replace(/-/g, '').toLowerCase().trim();
+                    if (cleaned.match(/^[a-z2-7]{52}$/)) {
                         ctrl.$setValidity('validNodeid', true);
                         ctrl.$setValidity('validNodeid', true);
                     } else {
                     } else {
                         ctrl.$setValidity('validNodeid', false);
                         ctrl.$setValidity('validNodeid', false);

+ 3 - 3
gui/index.html

@@ -418,8 +418,8 @@ found in the LICENSE file.
           </h4>
           </h4>
         </div>
         </div>
         <div class="modal-body">
         <div class="modal-body">
-          <div class="well well-sm text-monospace text-center">{{myID | chunkID}}</div>
-          <img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
+          <div class="well well-sm text-monospace text-center">{{myID}}</div>
+          <img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID}}"/>
         </div>
         </div>
         <div class="modal-footer">
         <div class="modal-footer">
           <button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;Close</button>
           <button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;Close</button>
@@ -442,7 +442,7 @@ found in the LICENSE file.
             <div class="form-group" ng-class="{'has-error': nodeEditor.nodeID.$invalid && nodeEditor.nodeID.$dirty}">
             <div class="form-group" ng-class="{'has-error': nodeEditor.nodeID.$invalid && nodeEditor.nodeID.$dirty}">
               <label for="nodeID">Node ID</label>
               <label for="nodeID">Node ID</label>
               <input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required valid-nodeid></input>
               <input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required valid-nodeid></input>
-              <div ng-if="editingExisting" class="well well-sm text-monospace">{{currentNode.NodeID | chunkID}}</div>
+              <div ng-if="editingExisting" class="well well-sm text-monospace">{{currentNode.NodeID}}</div>
               <p class="help-block">
               <p class="help-block">
                 <span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).
                 <span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).
                   <span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>
                   <span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>

+ 6 - 2
integration/h2/config.xml

@@ -1,19 +1,22 @@
 <configuration version="2">
 <configuration version="2">
     <repository id="default" directory="s2" ro="false" ignorePerms="false">
     <repository id="default" directory="s2" ro="false" ignorePerms="false">
-        <node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
         <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
         <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
+        <node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
         <versioning></versioning>
         <versioning></versioning>
+        <syncorder></syncorder>
     </repository>
     </repository>
     <repository id="s12" directory="s12-2" ro="false" ignorePerms="false">
     <repository id="s12" directory="s12-2" ro="false" ignorePerms="false">
         <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
         <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
         <versioning></versioning>
         <versioning></versioning>
+        <syncorder></syncorder>
     </repository>
     </repository>
     <repository id="s23" directory="s23-2" ro="false" ignorePerms="false">
     <repository id="s23" directory="s23-2" ro="false" ignorePerms="false">
-        <node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
         <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
+        <node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
         <versioning></versioning>
         <versioning></versioning>
+        <syncorder></syncorder>
     </repository>
     </repository>
     <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
     <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
         <address>127.0.0.1:22001</address>
         <address>127.0.0.1:22001</address>
@@ -41,5 +44,6 @@
         <maxChangeKbps>10000</maxChangeKbps>
         <maxChangeKbps>10000</maxChangeKbps>
         <startBrowser>false</startBrowser>
         <startBrowser>false</startBrowser>
         <upnpEnabled>true</upnpEnabled>
         <upnpEnabled>true</upnpEnabled>
+        <urAccepted>-1</urAccepted>
     </options>
     </options>
 </configuration>
 </configuration>

+ 3 - 3
integration/test.sh

@@ -6,9 +6,9 @@
 
 
 iterations=${1:-5}
 iterations=${1:-5}
 
 
-id1=I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA
-id2=JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ
-id3=373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA
+id1=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU
+id2=JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU
+id3=373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU
 
 
 go build genfiles.go
 go build genfiles.go
 go build md5r.go
 go build md5r.go

+ 37 - 0
luhn/luhn.go

@@ -0,0 +1,37 @@
+package luhn
+
+import "strings"
+
+type Alphabet string
+
+var (
+	Base32        Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567="
+	Base32Trimmed Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+)
+
+func (a Alphabet) Generate(s string) rune {
+	factor := 1
+	sum := 0
+	n := len(a)
+
+	for i := range s {
+		codepoint := strings.IndexByte(string(a), s[i])
+		addend := factor * codepoint
+		if factor == 2 {
+			factor = 1
+		} else {
+			factor = 2
+		}
+		addend = (addend / n) + (addend % n)
+		sum += addend
+	}
+	remainder := sum % n
+	checkCodepoint := (n - remainder) % n
+	return rune(a[checkCodepoint])
+}
+
+func (a Alphabet) Validate(s string) bool {
+	t := s[:len(s)-1]
+	c := a.Generate(t)
+	return rune(s[len(s)-1]) == c
+}

+ 25 - 0
luhn/luhn_test.go

@@ -0,0 +1,25 @@
+package luhn_test
+
+import (
+	"testing"
+
+	"github.com/calmh/syncthing/luhn"
+)
+
+func TestGenerate(t *testing.T) {
+	a := luhn.Alphabet("abcdef")
+	c := a.Generate("abcdef")
+	if c != 'e' {
+		t.Errorf("Incorrect check digit %c != e", c)
+	}
+}
+
+func TestValidate(t *testing.T) {
+	a := luhn.Alphabet("abcdef")
+	if !a.Validate("abcdefe") {
+		t.Errorf("Incorrect validation response for abcdefe")
+	}
+	if a.Validate("abcdefd") {
+		t.Errorf("Incorrect validation response for abcdefd")
+	}
+}

+ 23 - 23
model/model.go

@@ -49,8 +49,8 @@ type Model struct {
 
 
 	repoCfgs   map[string]config.RepositoryConfiguration // repo -> cfg
 	repoCfgs   map[string]config.RepositoryConfiguration // repo -> cfg
 	repoFiles  map[string]*files.Set                     // repo -> files
 	repoFiles  map[string]*files.Set                     // repo -> files
-	repoNodes  map[string][]string                       // repo -> nodeIDs
-	nodeRepos  map[string][]string                       // nodeID -> repos
+	repoNodes  map[string][]protocol.NodeID              // repo -> nodeIDs
+	nodeRepos  map[protocol.NodeID][]string              // nodeID -> repos
 	suppressor map[string]*suppressor                    // repo -> suppressor
 	suppressor map[string]*suppressor                    // repo -> suppressor
 	rmut       sync.RWMutex                              // protects the above
 	rmut       sync.RWMutex                              // protects the above
 
 
@@ -59,9 +59,9 @@ type Model struct {
 
 
 	cm *cid.Map
 	cm *cid.Map
 
 
-	protoConn map[string]protocol.Connection
-	rawConn   map[string]io.Closer
-	nodeVer   map[string]string
+	protoConn map[protocol.NodeID]protocol.Connection
+	rawConn   map[protocol.NodeID]io.Closer
+	nodeVer   map[protocol.NodeID]string
 	pmut      sync.RWMutex // protects protoConn and rawConn
 	pmut      sync.RWMutex // protects protoConn and rawConn
 
 
 	sup suppressor
 	sup suppressor
@@ -86,14 +86,14 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
 		clientVersion: clientVersion,
 		clientVersion: clientVersion,
 		repoCfgs:      make(map[string]config.RepositoryConfiguration),
 		repoCfgs:      make(map[string]config.RepositoryConfiguration),
 		repoFiles:     make(map[string]*files.Set),
 		repoFiles:     make(map[string]*files.Set),
-		repoNodes:     make(map[string][]string),
-		nodeRepos:     make(map[string][]string),
+		repoNodes:     make(map[string][]protocol.NodeID),
+		nodeRepos:     make(map[protocol.NodeID][]string),
 		repoState:     make(map[string]repoState),
 		repoState:     make(map[string]repoState),
 		suppressor:    make(map[string]*suppressor),
 		suppressor:    make(map[string]*suppressor),
 		cm:            cid.NewMap(),
 		cm:            cid.NewMap(),
-		protoConn:     make(map[string]protocol.Connection),
-		rawConn:       make(map[string]io.Closer),
-		nodeVer:       make(map[string]string),
+		protoConn:     make(map[protocol.NodeID]protocol.Connection),
+		rawConn:       make(map[protocol.NodeID]io.Closer),
+		nodeVer:       make(map[protocol.NodeID]string),
 		sup:           suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
 		sup:           suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
 	}
 	}
 
 
@@ -182,7 +182,7 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
 			ci.Completion = int(100 * have / tot)
 			ci.Completion = int(100 * have / tot)
 		}
 		}
 
 
-		res[node] = ci
+		res[node.String()] = ci
 	}
 	}
 
 
 	m.rmut.RUnlock()
 	m.rmut.RUnlock()
@@ -261,7 +261,7 @@ func (m *Model) NeedFilesRepo(repo string) []scanner.File {
 
 
 // Index is called when a new node is connected and we receive their full index.
 // Index is called when a new node is connected and we receive their full index.
 // Implements the protocol.Model interface.
 // Implements the protocol.Model interface.
-func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
+func (m *Model) Index(nodeID protocol.NodeID, repo string, fs []protocol.FileInfo) {
 	if debug {
 	if debug {
 		l.Debugf("IDX(in): %s %q: %d files", nodeID, repo, len(fs))
 		l.Debugf("IDX(in): %s %q: %d files", nodeID, repo, len(fs))
 	}
 	}
@@ -297,7 +297,7 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
 
 
 // IndexUpdate is called for incremental updates to connected nodes' indexes.
 // IndexUpdate is called for incremental updates to connected nodes' indexes.
 // Implements the protocol.Model interface.
 // Implements the protocol.Model interface.
-func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo) {
+func (m *Model) IndexUpdate(nodeID protocol.NodeID, repo string, fs []protocol.FileInfo) {
 	if debug {
 	if debug {
 		l.Debugf("IDXUP(in): %s / %q: %d files", nodeID, repo, len(fs))
 		l.Debugf("IDXUP(in): %s / %q: %d files", nodeID, repo, len(fs))
 	}
 	}
@@ -331,7 +331,7 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
 	m.rmut.RUnlock()
 	m.rmut.RUnlock()
 }
 }
 
 
-func (m *Model) repoSharedWith(repo, nodeID string) bool {
+func (m *Model) repoSharedWith(repo string, nodeID protocol.NodeID) bool {
 	m.rmut.RLock()
 	m.rmut.RLock()
 	defer m.rmut.RUnlock()
 	defer m.rmut.RUnlock()
 	for _, nrepo := range m.nodeRepos[nodeID] {
 	for _, nrepo := range m.nodeRepos[nodeID] {
@@ -342,7 +342,7 @@ func (m *Model) repoSharedWith(repo, nodeID string) bool {
 	return false
 	return false
 }
 }
 
 
-func (m *Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessage) {
+func (m *Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterConfigMessage) {
 	compErr := compareClusterConfig(m.clusterConfig(nodeID), config)
 	compErr := compareClusterConfig(m.clusterConfig(nodeID), config)
 	if debug {
 	if debug {
 		l.Debugf("ClusterConfig: %s: %#v", nodeID, config)
 		l.Debugf("ClusterConfig: %s: %#v", nodeID, config)
@@ -365,7 +365,7 @@ func (m *Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessag
 
 
 // Close removes the peer from the model and closes the underlying connection if possible.
 // Close removes the peer from the model and closes the underlying connection if possible.
 // Implements the protocol.Model interface.
 // Implements the protocol.Model interface.
-func (m *Model) Close(node string, err error) {
+func (m *Model) Close(node protocol.NodeID, err error) {
 	if debug {
 	if debug {
 		l.Debugf("%s: %v", node, err)
 		l.Debugf("%s: %v", node, err)
 	}
 	}
@@ -397,7 +397,7 @@ func (m *Model) Close(node string, err error) {
 
 
 // Request returns the specified data segment by reading it from local disk.
 // Request returns the specified data segment by reading it from local disk.
 // Implements the protocol.Model interface.
 // Implements the protocol.Model interface.
-func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]byte, error) {
+func (m *Model) Request(nodeID protocol.NodeID, repo, name string, offset int64, size int) ([]byte, error) {
 	// Verify that the requested file exists in the local model.
 	// Verify that the requested file exists in the local model.
 	m.rmut.RLock()
 	m.rmut.RLock()
 	r, ok := m.repoFiles[repo]
 	r, ok := m.repoFiles[repo]
@@ -423,7 +423,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
 		return nil, ErrNoSuchFile
 		return nil, ErrNoSuchFile
 	}
 	}
 
 
-	if debug && nodeID != "<local>" {
+	if debug && nodeID != cid.LocalNodeID {
 		l.Debugf("REQ(in): %s: %q / %q o=%d s=%d", nodeID, repo, name, offset, size)
 		l.Debugf("REQ(in): %s: %q / %q o=%d s=%d", nodeID, repo, name, offset, size)
 	}
 	}
 	m.rmut.RLock()
 	m.rmut.RLock()
@@ -489,7 +489,7 @@ func (cf cFiler) CurrentFile(file string) scanner.File {
 }
 }
 
 
 // ConnectedTo returns true if we are connected to the named node.
 // ConnectedTo returns true if we are connected to the named node.
-func (m *Model) ConnectedTo(nodeID string) bool {
+func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
 	m.pmut.RLock()
 	m.pmut.RLock()
 	_, ok := m.protoConn[nodeID]
 	_, ok := m.protoConn[nodeID]
 	m.pmut.RUnlock()
 	m.pmut.RUnlock()
@@ -560,7 +560,7 @@ func (m *Model) updateLocal(repo string, f scanner.File) {
 	m.rmut.RUnlock()
 	m.rmut.RUnlock()
 }
 }
 
 
-func (m *Model) requestGlobal(nodeID, repo, name string, offset int64, size int, hash []byte) ([]byte, error) {
+func (m *Model) requestGlobal(nodeID protocol.NodeID, repo, name string, offset int64, size int, hash []byte) ([]byte, error) {
 	m.pmut.RLock()
 	m.pmut.RLock()
 	nc, ok := m.protoConn[nodeID]
 	nc, ok := m.protoConn[nodeID]
 	m.pmut.RUnlock()
 	m.pmut.RUnlock()
@@ -639,7 +639,7 @@ func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
 	m.repoFiles[cfg.ID] = files.NewSet()
 	m.repoFiles[cfg.ID] = files.NewSet()
 	m.suppressor[cfg.ID] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
 	m.suppressor[cfg.ID] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
 
 
-	m.repoNodes[cfg.ID] = make([]string, len(cfg.Nodes))
+	m.repoNodes[cfg.ID] = make([]protocol.NodeID, len(cfg.Nodes))
 	for i, node := range cfg.Nodes {
 	for i, node := range cfg.Nodes {
 		m.repoNodes[cfg.ID][i] = node.NodeID
 		m.repoNodes[cfg.ID][i] = node.NodeID
 		m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], cfg.ID)
 		m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], cfg.ID)
@@ -805,7 +805,7 @@ func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {
 }
 }
 
 
 // clusterConfig returns a ClusterConfigMessage that is correct for the given peer node
 // clusterConfig returns a ClusterConfigMessage that is correct for the given peer node
-func (m *Model) clusterConfig(node string) protocol.ClusterConfigMessage {
+func (m *Model) clusterConfig(node protocol.NodeID) protocol.ClusterConfigMessage {
 	cm := protocol.ClusterConfigMessage{
 	cm := protocol.ClusterConfigMessage{
 		ClientName:    m.clientName,
 		ClientName:    m.clientName,
 		ClientVersion: m.clientVersion,
 		ClientVersion: m.clientVersion,
@@ -819,7 +819,7 @@ func (m *Model) clusterConfig(node string) protocol.ClusterConfigMessage {
 		for _, node := range m.repoNodes[repo] {
 		for _, node := range m.repoNodes[repo] {
 			// TODO: Set read only bit when relevant
 			// TODO: Set read only bit when relevant
 			cr.Nodes = append(cr.Nodes, protocol.Node{
 			cr.Nodes = append(cr.Nodes, protocol.Node{
-				ID:    node,
+				ID:    node[:],
 				Flags: protocol.FlagShareTrusted,
 				Flags: protocol.FlagShareTrusted,
 			})
 			})
 		}
 		}

+ 29 - 22
model/model_test.go

@@ -17,6 +17,13 @@ import (
 	"github.com/calmh/syncthing/scanner"
 	"github.com/calmh/syncthing/scanner"
 )
 )
 
 
+var node1, node2 protocol.NodeID
+
+func init() {
+	node1, _ = protocol.NodeIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
+	node2, _ = protocol.NodeIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
+}
+
 var testDataExpected = map[string]scanner.File{
 var testDataExpected = map[string]scanner.File{
 	"foo": scanner.File{
 	"foo": scanner.File{
 		Name:     "foo",
 		Name:     "foo",
@@ -56,7 +63,7 @@ func TestRequest(t *testing.T) {
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.ScanRepo("default")
 	m.ScanRepo("default")
 
 
-	bs, err := m.Request("some node", "default", "foo", 0, 6)
+	bs, err := m.Request(node1, "default", "foo", 0, 6)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -64,7 +71,7 @@ func TestRequest(t *testing.T) {
 		t.Errorf("Incorrect data from request: %q", string(bs))
 		t.Errorf("Incorrect data from request: %q", string(bs))
 	}
 	}
 
 
-	bs, err = m.Request("some node", "default", "../walk.go", 0, 6)
+	bs, err = m.Request(node1, "default", "../walk.go", 0, 6)
 	if err == nil {
 	if err == nil {
 		t.Error("Unexpected nil error on insecure file read")
 		t.Error("Unexpected nil error on insecure file read")
 	}
 	}
@@ -95,7 +102,7 @@ func BenchmarkIndex10000(b *testing.B) {
 
 
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		m.Index("42", "default", files)
+		m.Index(node1, "default", files)
 	}
 	}
 }
 }
 
 
@@ -107,7 +114,7 @@ func BenchmarkIndex00100(b *testing.B) {
 
 
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		m.Index("42", "default", files)
+		m.Index(node1, "default", files)
 	}
 	}
 }
 }
 
 
@@ -116,11 +123,11 @@ func BenchmarkIndexUpdate10000f10000(b *testing.B) {
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.ScanRepo("default")
 	m.ScanRepo("default")
 	files := genFiles(10000)
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		m.IndexUpdate("42", "default", files)
+		m.IndexUpdate(node1, "default", files)
 	}
 	}
 }
 }
 
 
@@ -129,12 +136,12 @@ func BenchmarkIndexUpdate10000f00100(b *testing.B) {
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.ScanRepo("default")
 	m.ScanRepo("default")
 	files := genFiles(10000)
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 
 	ufiles := genFiles(100)
 	ufiles := genFiles(100)
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		m.IndexUpdate("42", "default", ufiles)
+		m.IndexUpdate(node1, "default", ufiles)
 	}
 	}
 }
 }
 
 
@@ -143,17 +150,17 @@ func BenchmarkIndexUpdate10000f00001(b *testing.B) {
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	m.ScanRepo("default")
 	m.ScanRepo("default")
 	files := genFiles(10000)
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 
 	ufiles := genFiles(1)
 	ufiles := genFiles(1)
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		m.IndexUpdate("42", "default", ufiles)
+		m.IndexUpdate(node1, "default", ufiles)
 	}
 	}
 }
 }
 
 
 type FakeConnection struct {
 type FakeConnection struct {
-	id          string
+	id          protocol.NodeID
 	requestData []byte
 	requestData []byte
 }
 }
 
 
@@ -161,8 +168,8 @@ func (FakeConnection) Close() error {
 	return nil
 	return nil
 }
 }
 
 
-func (f FakeConnection) ID() string {
-	return string(f.id)
+func (f FakeConnection) ID() protocol.NodeID {
+	return f.id
 }
 }
 
 
 func (f FakeConnection) Option(string) string {
 func (f FakeConnection) Option(string) string {
@@ -202,15 +209,15 @@ func BenchmarkRequest(b *testing.B) {
 	}
 	}
 
 
 	fc := FakeConnection{
 	fc := FakeConnection{
-		id:          "42",
+		id:          node1,
 		requestData: []byte("some data to return"),
 		requestData: []byte("some data to return"),
 	}
 	}
 	m.AddConnection(fc, fc)
 	m.AddConnection(fc, fc)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		data, err := m.requestGlobal("42", "default", files[i%n].Name, 0, 32, nil)
+		data, err := m.requestGlobal(node1, "default", files[i%n].Name, 0, 32, nil)
 		if err != nil {
 		if err != nil {
 			b.Error(err)
 			b.Error(err)
 		}
 		}
@@ -222,26 +229,26 @@ func BenchmarkRequest(b *testing.B) {
 
 
 func TestActivityMap(t *testing.T) {
 func TestActivityMap(t *testing.T) {
 	cm := cid.NewMap()
 	cm := cid.NewMap()
-	fooID := cm.Get("foo")
+	fooID := cm.Get(node1)
 	if fooID == 0 {
 	if fooID == 0 {
 		t.Fatal("ID cannot be zero")
 		t.Fatal("ID cannot be zero")
 	}
 	}
-	barID := cm.Get("bar")
+	barID := cm.Get(node2)
 	if barID == 0 {
 	if barID == 0 {
 		t.Fatal("ID cannot be zero")
 		t.Fatal("ID cannot be zero")
 	}
 	}
 
 
 	m := make(activityMap)
 	m := make(activityMap)
-	if node := m.leastBusyNode(1<<fooID, cm); node != "foo" {
+	if node := m.leastBusyNode(1<<fooID, cm); node != node1 {
 		t.Errorf("Incorrect least busy node %q", node)
 		t.Errorf("Incorrect least busy node %q", node)
 	}
 	}
-	if node := m.leastBusyNode(1<<barID, cm); node != "bar" {
+	if node := m.leastBusyNode(1<<barID, cm); node != node2 {
 		t.Errorf("Incorrect least busy node %q", node)
 		t.Errorf("Incorrect least busy node %q", node)
 	}
 	}
-	if node := m.leastBusyNode(1<<fooID|1<<barID, cm); node != "foo" {
+	if node := m.leastBusyNode(1<<fooID|1<<barID, cm); node != node1 {
 		t.Errorf("Incorrect least busy node %q", node)
 		t.Errorf("Incorrect least busy node %q", node)
 	}
 	}
-	if node := m.leastBusyNode(1<<fooID|1<<barID, cm); node != "bar" {
+	if node := m.leastBusyNode(1<<fooID|1<<barID, cm); node != node2 {
 		t.Errorf("Incorrect least busy node %q", node)
 		t.Errorf("Incorrect least busy node %q", node)
 	}
 	}
 }
 }

+ 6 - 6
model/puller.go

@@ -21,7 +21,7 @@ import (
 )
 )
 
 
 type requestResult struct {
 type requestResult struct {
-	node     string
+	node     protocol.NodeID
 	file     scanner.File
 	file     scanner.File
 	filepath string // full filepath name
 	filepath string // full filepath name
 	offset   int64
 	offset   int64
@@ -39,11 +39,11 @@ type openFile struct {
 	done         bool  // we have sent all requests for this file
 	done         bool  // we have sent all requests for this file
 }
 }
 
 
-type activityMap map[string]int
+type activityMap map[protocol.NodeID]int
 
 
-func (m activityMap) leastBusyNode(availability uint64, cm *cid.Map) string {
+func (m activityMap) leastBusyNode(availability uint64, cm *cid.Map) protocol.NodeID {
 	var low int = 2<<30 - 1
 	var low int = 2<<30 - 1
-	var selected string
+	var selected protocol.NodeID
 	for _, node := range cm.Names() {
 	for _, node := range cm.Names() {
 		id := cm.Get(node)
 		id := cm.Get(node)
 		if id == cid.LocalID {
 		if id == cid.LocalID {
@@ -61,7 +61,7 @@ func (m activityMap) leastBusyNode(availability uint64, cm *cid.Map) string {
 	return selected
 	return selected
 }
 }
 
 
-func (m activityMap) decrease(node string) {
+func (m activityMap) decrease(node protocol.NodeID) {
 	m[node]--
 	m[node]--
 }
 }
 
 
@@ -540,7 +540,7 @@ func (p *puller) handleRequestBlock(b bqBlock) bool {
 	of.outstanding++
 	of.outstanding++
 	p.openFiles[f.Name] = of
 	p.openFiles[f.Name] = of
 
 
-	go func(node string, b bqBlock) {
+	go func(node protocol.NodeID, b bqBlock) {
 		if debug {
 		if debug {
 			l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repoCfg.ID, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
 			l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repoCfg.ID, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
 		}
 		}

+ 7 - 5
model/util.go

@@ -58,12 +58,14 @@ func fileInfoFromFile(f scanner.File) protocol.FileInfo {
 	return pf
 	return pf
 }
 }
 
 
-func cmMap(cm protocol.ClusterConfigMessage) map[string]map[string]uint32 {
-	m := make(map[string]map[string]uint32)
+func cmMap(cm protocol.ClusterConfigMessage) map[string]map[protocol.NodeID]uint32 {
+	m := make(map[string]map[protocol.NodeID]uint32)
 	for _, repo := range cm.Repositories {
 	for _, repo := range cm.Repositories {
-		m[repo.ID] = make(map[string]uint32)
+		m[repo.ID] = make(map[protocol.NodeID]uint32)
 		for _, node := range repo.Nodes {
 		for _, node := range repo.Nodes {
-			m[repo.ID][node.ID] = node.Flags
+			var id protocol.NodeID
+			copy(id[:], node.ID)
+			m[repo.ID][id] = node.Flags
 		}
 		}
 	}
 	}
 	return m
 	return m
@@ -72,7 +74,7 @@ func cmMap(cm protocol.ClusterConfigMessage) map[string]map[string]uint32 {
 type ClusterConfigMismatch error
 type ClusterConfigMismatch error
 
 
 // compareClusterConfig returns nil for two equivalent configurations,
 // compareClusterConfig returns nil for two equivalent configurations,
-// otherwise a decriptive error
+// otherwise a descriptive error
 func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
 func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
 	lm := cmMap(local)
 	lm := cmMap(local)
 	rm := cmMap(remote)
 	rm := cmMap(remote)

+ 0 - 137
model/util_test.go

@@ -1,137 +0,0 @@
-// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
-// Use of this source code is governed by an MIT-style license that can be
-// found in the LICENSE file.
-
-package model
-
-import (
-	"testing"
-
-	"github.com/calmh/syncthing/protocol"
-)
-
-var testcases = []struct {
-	local, remote protocol.ClusterConfigMessage
-	err           string
-}{
-	{
-		local:  protocol.ClusterConfigMessage{},
-		remote: protocol.ClusterConfigMessage{},
-		err:    "",
-	},
-	{
-		local:  protocol.ClusterConfigMessage{ClientName: "a", ClientVersion: "b"},
-		remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
-		err:    "",
-	},
-	{
-		local: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{ID: "foo"},
-				{ID: "bar"},
-			},
-		},
-		remote: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{ID: "foo"},
-				{ID: "bar"},
-			},
-		},
-		err: "",
-	},
-	{
-		local: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{
-					ID: "foo",
-					Nodes: []protocol.Node{
-						{ID: "a"},
-					},
-				},
-				{ID: "bar"},
-			},
-		},
-		remote: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{ID: "foo"},
-				{ID: "bar"},
-			},
-		},
-		err: "",
-	},
-
-	{
-		local: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{
-					ID: "foo",
-					Nodes: []protocol.Node{
-						{ID: "a"},
-					},
-				},
-				{ID: "bar"},
-			},
-		},
-		remote: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{
-					ID: "foo",
-					Nodes: []protocol.Node{
-						{ID: "a"},
-						{ID: "b"},
-					},
-				},
-				{ID: "bar"},
-			},
-		},
-		err: "",
-	},
-
-	{
-		local: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{
-					ID: "foo",
-					Nodes: []protocol.Node{
-						{
-							ID:    "a",
-							Flags: protocol.FlagShareReadOnly,
-						},
-					},
-				},
-				{ID: "bar"},
-			},
-		},
-		remote: protocol.ClusterConfigMessage{
-			Repositories: []protocol.Repository{
-				{
-					ID: "foo",
-					Nodes: []protocol.Node{
-						{
-							ID:    "a",
-							Flags: protocol.FlagShareTrusted,
-						},
-					},
-				},
-				{ID: "bar"},
-			},
-		},
-		err: `remote has different sharing flags for node "a" in repository "foo"`,
-	},
-}
-
-func TestCompareClusterConfig(t *testing.T) {
-	for i, tc := range testcases {
-		err := compareClusterConfig(tc.local, tc.remote)
-		switch {
-		case tc.err == "" && err != nil:
-			t.Errorf("#%d: unexpected error: %v", i, err)
-
-		case tc.err != "" && err == nil:
-			t.Errorf("#%d: unexpected nil error", i)
-
-		case tc.err != "" && err != nil && tc.err != err.Error():
-			t.Errorf("#%d: incorrect error: %q != %q", i, err, tc.err)
-		}
-	}
-}

+ 5 - 5
protocol/common_test.go

@@ -24,13 +24,13 @@ func newTestModel() *TestModel {
 	}
 	}
 }
 }
 
 
-func (t *TestModel) Index(nodeID string, repo string, files []FileInfo) {
+func (t *TestModel) Index(nodeID NodeID, repo string, files []FileInfo) {
 }
 }
 
 
-func (t *TestModel) IndexUpdate(nodeID string, repo string, files []FileInfo) {
+func (t *TestModel) IndexUpdate(nodeID NodeID, repo string, files []FileInfo) {
 }
 }
 
 
-func (t *TestModel) Request(nodeID, repo, name string, offset int64, size int) ([]byte, error) {
+func (t *TestModel) Request(nodeID NodeID, repo, name string, offset int64, size int) ([]byte, error) {
 	t.repo = repo
 	t.repo = repo
 	t.name = name
 	t.name = name
 	t.offset = offset
 	t.offset = offset
@@ -38,11 +38,11 @@ func (t *TestModel) Request(nodeID, repo, name string, offset int64, size int) (
 	return t.data, nil
 	return t.data, nil
 }
 }
 
 
-func (t *TestModel) Close(nodeID string, err error) {
+func (t *TestModel) Close(nodeID NodeID, err error) {
 	close(t.closedCh)
 	close(t.closedCh)
 }
 }
 
 
-func (t *TestModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
+func (t *TestModel) ClusterConfig(nodeID NodeID, config ClusterConfigMessage) {
 }
 }
 
 
 func (t *TestModel) isClosed() bool {
 func (t *TestModel) isClosed() bool {

+ 1 - 1
protocol/message_types.go

@@ -42,7 +42,7 @@ type Repository struct {
 }
 }
 
 
 type Node struct {
 type Node struct {
-	ID         string // max:64
+	ID         []byte // max:32
 	Flags      uint32
 	Flags      uint32
 	MaxVersion uint64
 	MaxVersion uint64
 }
 }

+ 3 - 7
protocol/message_xdr.go

@@ -1,7 +1,3 @@
-// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
-// Use of this source code is governed by an MIT-style license that can be
-// found in the LICENSE file.
-
 package protocol
 package protocol
 
 
 import (
 import (
@@ -337,10 +333,10 @@ func (o Node) MarshalXDR() []byte {
 }
 }
 
 
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
-	if len(o.ID) > 64 {
+	if len(o.ID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
 	}
-	xw.WriteString(o.ID)
+	xw.WriteBytes(o.ID)
 	xw.WriteUint32(o.Flags)
 	xw.WriteUint32(o.Flags)
 	xw.WriteUint64(o.MaxVersion)
 	xw.WriteUint64(o.MaxVersion)
 	return xw.Tot(), xw.Error()
 	return xw.Tot(), xw.Error()
@@ -358,7 +354,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
 }
 }
 
 
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
-	o.ID = xr.ReadStringMax(64)
+	o.ID = xr.ReadBytesMax(32)
 	o.Flags = xr.ReadUint32()
 	o.Flags = xr.ReadUint32()
 	o.MaxVersion = xr.ReadUint64()
 	o.MaxVersion = xr.ReadUint64()
 	return xr.Error()
 	return xr.Error()

+ 5 - 5
protocol/nativemodel_darwin.go

@@ -14,29 +14,29 @@ type nativeModel struct {
 	next Model
 	next Model
 }
 }
 
 
-func (m nativeModel) Index(nodeID string, repo string, files []FileInfo) {
+func (m nativeModel) Index(nodeID NodeID, repo string, files []FileInfo) {
 	for i := range files {
 	for i := range files {
 		files[i].Name = norm.NFD.String(files[i].Name)
 		files[i].Name = norm.NFD.String(files[i].Name)
 	}
 	}
 	m.next.Index(nodeID, repo, files)
 	m.next.Index(nodeID, repo, files)
 }
 }
 
 
-func (m nativeModel) IndexUpdate(nodeID string, repo string, files []FileInfo) {
+func (m nativeModel) IndexUpdate(nodeID NodeID, repo string, files []FileInfo) {
 	for i := range files {
 	for i := range files {
 		files[i].Name = norm.NFD.String(files[i].Name)
 		files[i].Name = norm.NFD.String(files[i].Name)
 	}
 	}
 	m.next.IndexUpdate(nodeID, repo, files)
 	m.next.IndexUpdate(nodeID, repo, files)
 }
 }
 
 
-func (m nativeModel) Request(nodeID, repo string, name string, offset int64, size int) ([]byte, error) {
+func (m nativeModel) Request(nodeID NodeID, repo string, name string, offset int64, size int) ([]byte, error) {
 	name = norm.NFD.String(name)
 	name = norm.NFD.String(name)
 	return m.next.Request(nodeID, repo, name, offset, size)
 	return m.next.Request(nodeID, repo, name, offset, size)
 }
 }
 
 
-func (m nativeModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
+func (m nativeModel) ClusterConfig(nodeID NodeID, config ClusterConfigMessage) {
 	m.next.ClusterConfig(nodeID, config)
 	m.next.ClusterConfig(nodeID, config)
 }
 }
 
 
-func (m nativeModel) Close(nodeID string, err error) {
+func (m nativeModel) Close(nodeID NodeID, err error) {
 	m.next.Close(nodeID, err)
 	m.next.Close(nodeID, err)
 }
 }

+ 136 - 0
protocol/nodeid.go

@@ -0,0 +1,136 @@
+package protocol
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/base32"
+	"errors"
+	"fmt"
+	"log"
+	"regexp"
+	"strings"
+
+	"github.com/calmh/syncthing/luhn"
+)
+
+type NodeID [32]byte
+
+// NewNodeID generates a new node ID from the raw bytes of a certificate
+func NewNodeID(rawCert []byte) NodeID {
+	var n NodeID
+	hf := sha256.New()
+	hf.Write(rawCert)
+	hf.Sum(n[:0])
+	return n
+}
+
+func NodeIDFromString(s string) (NodeID, error) {
+	var n NodeID
+	err := n.UnmarshalText([]byte(s))
+	return n, err
+}
+
+// String returns the canonical string representation of the node ID
+func (n NodeID) String() string {
+	id := base32.StdEncoding.EncodeToString(n[:])
+	id = strings.Trim(id, "=")
+	id = luhnify(id)
+	id = chunkify(id)
+	return id
+}
+
+func (n NodeID) GoString() string {
+	return n.String()
+}
+
+func (n NodeID) Compare(other NodeID) int {
+	return bytes.Compare(n[:], other[:])
+}
+
+func (n NodeID) Equals(other NodeID) bool {
+	return bytes.Compare(n[:], other[:]) == 0
+}
+
+func (n *NodeID) MarshalText() ([]byte, error) {
+	return []byte(n.String()), nil
+}
+
+func (n *NodeID) UnmarshalText(bs []byte) error {
+	id := string(bs)
+	id = strings.Trim(id, "=")
+	id = strings.ToUpper(id)
+	id = untypeoify(id)
+	id = unchunkify(id)
+
+	var err error
+	switch len(id) {
+	case 56:
+		// New style, with check digits
+		id, err = unluhnify(id)
+		if err != nil {
+			return err
+		}
+		fallthrough
+	case 52:
+		// Old style, no check digits
+		dec, err := base32.StdEncoding.DecodeString(id + "====")
+		if err != nil {
+			return err
+		}
+		copy(n[:], dec)
+		return nil
+	default:
+		return errors.New("node ID invalid: incorrect length")
+	}
+}
+
+func luhnify(s string) string {
+	if len(s) != 52 {
+		panic("unsupported string length")
+	}
+
+	res := make([]string, 0, 4)
+	for i := 0; i < 4; i++ {
+		p := s[i*13 : (i+1)*13]
+		l := luhn.Base32Trimmed.Generate(p)
+		res = append(res, fmt.Sprintf("%s%c", p, l))
+	}
+	return res[0] + res[1] + res[2] + res[3]
+}
+
+func unluhnify(s string) (string, error) {
+	if len(s) != 56 {
+		return "", fmt.Errorf("unsupported string length %d", len(s))
+	}
+
+	res := make([]string, 0, 4)
+	for i := 0; i < 4; i++ {
+		p := s[i*14 : (i+1)*14-1]
+		l := luhn.Base32Trimmed.Generate(p)
+		if g := fmt.Sprintf("%s%c", p, l); g != s[i*14:(i+1)*14] {
+			log.Printf("%q; %q", g, s[i*14:(i+1)*14])
+			return "", errors.New("check digit incorrect")
+		}
+		res = append(res, p)
+	}
+	return res[0] + res[1] + res[2] + res[3], nil
+}
+
+func chunkify(s string) string {
+	s = regexp.MustCompile("(.{7})").ReplaceAllString(s, "$1-")
+	s = strings.Trim(s, "-")
+	return s
+}
+
+func unchunkify(s string) string {
+	s = strings.Replace(s, "-", "", -1)
+	s = strings.Replace(s, " ", "", -1)
+	return s
+}
+
+func untypeoify(s string) string {
+	s = strings.Replace(s, "0", "O", -1)
+	s = strings.Replace(s, "1", "I", -1)
+	s = strings.Replace(s, "8", "B", -1)
+	return s
+}

+ 74 - 0
protocol/nodeid_test.go

@@ -0,0 +1,74 @@
+package protocol
+
+import "testing"
+
+var formatted = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"
+var formatCases = []string{
+	"P56IOI-7MZJNU-2IQGDR-EYDM2M-GTMGL3-BXNPQ6-W5BTBB-Z4TJXZ-WICQ",
+	"P56IOI-7MZJNU2Y-IQGDR-EYDM2M-GTI-MGL3-BXNPQ6-W5BM-TBB-Z4TJXZ-WICQ2",
+	"P56IOI7 MZJNU2I QGDREYD M2MGTMGL 3BXNPQ6W 5BTB BZ4T JXZWICQ",
+	"P56IOI7 MZJNU2Y IQGDREY DM2MGTI MGL3BXN PQ6W5BM TBBZ4TJ XZWICQ2",
+	"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ",
+	"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicq",
+	"P56IOI7MZJNU2YIQGDREYDM2MGTIMGL3BXNPQ6W5BMTBBZ4TJXZWICQ2",
+	"P561017MZJNU2YIQGDREYDM2MGTIMGL3BXNPQ6W5BMT88Z4TJXZWICQ2",
+	"p56ioi7mzjnu2yiqgdreydm2mgtimgl3bxnpq6w5bmtbbz4tjxzwicq2",
+	"p561017mzjnu2yiqgdreydm2mgtimgl3bxnpq6w5bmt88z4tjxzwicq2",
+}
+
+func TestFormatNodeID(t *testing.T) {
+	for i, tc := range formatCases {
+		var id NodeID
+		err := id.UnmarshalText([]byte(tc))
+		if err != nil {
+			t.Errorf("#%d UnmarshalText(%q); %v", i, tc, err)
+		} else if f := id.String(); f != formatted {
+			t.Errorf("#%d FormatNodeID(%q)\n\t%q !=\n\t%q", i, tc, f, formatted)
+		}
+	}
+}
+
+var validateCases = []struct {
+	s  string
+	ok bool
+}{
+	{"", false},
+	{"P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2", true},
+	{"P56IOI7-MZJNU2-IQGDREY-DM2MGT-MGL3BXN-PQ6W5B-TBBZ4TJ-XZWICQ", true},
+	{"P56IOI7 MZJNU2I QGDREYD M2MGTMGL 3BXNPQ6W 5BTB BZ4T JXZWICQ", true},
+	{"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ", true},
+	{"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQCCCC", false},
+	{"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicq", true},
+	{"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicqCCCC", false},
+}
+
+func TestValidateNodeID(t *testing.T) {
+	for _, tc := range validateCases {
+		var id NodeID
+		err := id.UnmarshalText([]byte(tc.s))
+		if (err == nil && !tc.ok) || (err != nil && tc.ok) {
+			t.Errorf("ValidateNodeID(%q); %v != %v", tc.s, err, tc.ok)
+		}
+	}
+}
+
+func TestMarshallingNodeID(t *testing.T) {
+	n0 := NodeID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 10, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
+	n1 := NodeID{}
+	n2 := NodeID{}
+
+	bs, _ := n0.MarshalText()
+	n1.UnmarshalText(bs)
+	bs, _ = n1.MarshalText()
+	n2.UnmarshalText(bs)
+
+	if n2.String() != n0.String() {
+		t.Errorf("String marshalling error; %q != %q", n2.String(), n0.String())
+	}
+	if !n2.Equals(n0) {
+		t.Error("Equals error")
+	}
+	if n2.Compare(n0) != 0 {
+		t.Error("Compare error")
+	}
+}

+ 10 - 10
protocol/protocol.go

@@ -48,19 +48,19 @@ var (
 
 
 type Model interface {
 type Model interface {
 	// An index was received from the peer node
 	// An index was received from the peer node
-	Index(nodeID string, repo string, files []FileInfo)
+	Index(nodeID NodeID, repo string, files []FileInfo)
 	// An index update was received from the peer node
 	// An index update was received from the peer node
-	IndexUpdate(nodeID string, repo string, files []FileInfo)
+	IndexUpdate(nodeID NodeID, repo string, files []FileInfo)
 	// A request was made by the peer node
 	// A request was made by the peer node
-	Request(nodeID string, repo string, name string, offset int64, size int) ([]byte, error)
+	Request(nodeID NodeID, repo string, name string, offset int64, size int) ([]byte, error)
 	// A cluster configuration message was received
 	// A cluster configuration message was received
-	ClusterConfig(nodeID string, config ClusterConfigMessage)
+	ClusterConfig(nodeID NodeID, config ClusterConfigMessage)
 	// The peer node closed the connection
 	// The peer node closed the connection
-	Close(nodeID string, err error)
+	Close(nodeID NodeID, err error)
 }
 }
 
 
 type Connection interface {
 type Connection interface {
-	ID() string
+	ID() NodeID
 	Index(repo string, files []FileInfo)
 	Index(repo string, files []FileInfo)
 	Request(repo string, name string, offset int64, size int) ([]byte, error)
 	Request(repo string, name string, offset int64, size int) ([]byte, error)
 	ClusterConfig(config ClusterConfigMessage)
 	ClusterConfig(config ClusterConfigMessage)
@@ -68,7 +68,7 @@ type Connection interface {
 }
 }
 
 
 type rawConnection struct {
 type rawConnection struct {
-	id       string
+	id       NodeID
 	receiver Model
 	receiver Model
 
 
 	reader io.ReadCloser
 	reader io.ReadCloser
@@ -102,7 +102,7 @@ const (
 	pingIdleTime = 60 * time.Second
 	pingIdleTime = 60 * time.Second
 )
 )
 
 
-func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
+func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model) Connection {
 	cr := &countingReader{Reader: reader}
 	cr := &countingReader{Reader: reader}
 	cw := &countingWriter{Writer: writer}
 	cw := &countingWriter{Writer: writer}
 
 
@@ -139,7 +139,7 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
 	return wireFormatConnection{&c}
 	return wireFormatConnection{&c}
 }
 }
 
 
-func (c *rawConnection) ID() string {
+func (c *rawConnection) ID() NodeID {
 	return c.id
 	return c.id
 }
 }
 
 
@@ -295,7 +295,7 @@ func (c *rawConnection) readerLoop() (err error) {
 
 
 type incomingIndex struct {
 type incomingIndex struct {
 	update bool
 	update bool
-	id     string
+	id     NodeID
 	repo   string
 	repo   string
 	files  []FileInfo
 	files  []FileInfo
 }
 }

+ 17 - 12
protocol/protocol_test.go

@@ -11,6 +11,11 @@ import (
 	"testing/quick"
 	"testing/quick"
 )
 )
 
 
+var (
+	c0ID = NewNodeID([]byte{1})
+	c1ID = NewNodeID([]byte{2})
+)
+
 func TestHeaderFunctions(t *testing.T) {
 func TestHeaderFunctions(t *testing.T) {
 	f := func(ver, id, typ int) bool {
 	f := func(ver, id, typ int) bool {
 		ver = int(uint(ver) % 16)
 		ver = int(uint(ver) % 16)
@@ -54,8 +59,8 @@ func TestPing(t *testing.T) {
 	ar, aw := io.Pipe()
 	ar, aw := io.Pipe()
 	br, bw := io.Pipe()
 	br, bw := io.Pipe()
 
 
-	c0 := NewConnection("c0", ar, bw, nil).(wireFormatConnection).next.(*rawConnection)
-	c1 := NewConnection("c1", br, aw, nil).(wireFormatConnection).next.(*rawConnection)
+	c0 := NewConnection(c0ID, ar, bw, nil).(wireFormatConnection).next.(*rawConnection)
+	c1 := NewConnection(c1ID, br, aw, nil).(wireFormatConnection).next.(*rawConnection)
 
 
 	if ok := c0.ping(); !ok {
 	if ok := c0.ping(); !ok {
 		t.Error("c0 ping failed")
 		t.Error("c0 ping failed")
@@ -78,8 +83,8 @@ func TestPingErr(t *testing.T) {
 			eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
 			eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
 			ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
 			ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
 
 
-			c0 := NewConnection("c0", ar, ebw, m0).(wireFormatConnection).next.(*rawConnection)
-			NewConnection("c1", br, eaw, m1)
+			c0 := NewConnection(c0ID, ar, ebw, m0).(wireFormatConnection).next.(*rawConnection)
+			NewConnection(c1ID, br, eaw, m1)
 
 
 			res := c0.ping()
 			res := c0.ping()
 			if (i < 4 || j < 4) && res {
 			if (i < 4 || j < 4) && res {
@@ -106,8 +111,8 @@ func TestPingErr(t *testing.T) {
 // 			eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
 // 			eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
 // 			ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
 // 			ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
 
 
-// 			NewConnection("c0", ar, ebw, m0, nil)
-// 			c1 := NewConnection("c1", br, eaw, m1, nil).(wireFormatConnection).next.(*rawConnection)
+// 			NewConnection(c0ID, ar, ebw, m0, nil)
+// 			c1 := NewConnection(c1ID, br, eaw, m1, nil).(wireFormatConnection).next.(*rawConnection)
 
 
 // 			d, err := c1.Request("default", "tn", 1234, 5678)
 // 			d, err := c1.Request("default", "tn", 1234, 5678)
 // 			if err == e || err == ErrClosed {
 // 			if err == e || err == ErrClosed {
@@ -154,8 +159,8 @@ func TestVersionErr(t *testing.T) {
 	ar, aw := io.Pipe()
 	ar, aw := io.Pipe()
 	br, bw := io.Pipe()
 	br, bw := io.Pipe()
 
 
-	c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
-	NewConnection("c1", br, aw, m1)
+	c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
+	NewConnection(c1ID, br, aw, m1)
 
 
 	c0.xw.WriteUint32(encodeHeader(header{
 	c0.xw.WriteUint32(encodeHeader(header{
 		version: 2,
 		version: 2,
@@ -176,8 +181,8 @@ func TestTypeErr(t *testing.T) {
 	ar, aw := io.Pipe()
 	ar, aw := io.Pipe()
 	br, bw := io.Pipe()
 	br, bw := io.Pipe()
 
 
-	c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
-	NewConnection("c1", br, aw, m1)
+	c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
+	NewConnection(c1ID, br, aw, m1)
 
 
 	c0.xw.WriteUint32(encodeHeader(header{
 	c0.xw.WriteUint32(encodeHeader(header{
 		version: 0,
 		version: 0,
@@ -198,8 +203,8 @@ func TestClose(t *testing.T) {
 	ar, aw := io.Pipe()
 	ar, aw := io.Pipe()
 	br, bw := io.Pipe()
 	br, bw := io.Pipe()
 
 
-	c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
-	NewConnection("c1", br, aw, m1)
+	c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
+	NewConnection(c1ID, br, aw, m1)
 
 
 	c0.close(nil)
 	c0.close(nil)
 
 

+ 3 - 3
protocol/wireformat.go

@@ -14,11 +14,11 @@ type wireFormatConnection struct {
 	next Connection
 	next Connection
 }
 }
 
 
-func (c wireFormatConnection) ID() string {
+func (c wireFormatConnection) ID() NodeID {
 	return c.next.ID()
 	return c.next.ID()
 }
 }
 
 
-func (c wireFormatConnection) Index(node string, fs []FileInfo) {
+func (c wireFormatConnection) Index(repo string, fs []FileInfo) {
 	var myFs = make([]FileInfo, len(fs))
 	var myFs = make([]FileInfo, len(fs))
 	copy(myFs, fs)
 	copy(myFs, fs)
 
 
@@ -26,7 +26,7 @@ func (c wireFormatConnection) Index(node string, fs []FileInfo) {
 		myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
 		myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
 	}
 	}
 
 
-	c.next.Index(node, myFs)
+	c.next.Index(repo, myFs)
 }
 }
 
 
 func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {
 func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini