Explorar el Código

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 hace 11 años
padre
commit
8f3effed32

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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
 
-import "sync"
+import (
+	"sync"
+
+	"github.com/calmh/syncthing/protocol"
+)
 
 type Map struct {
 	sync.Mutex
-	toCid  map[string]uint
-	toName []string
+	toCid  map[protocol.NodeID]uint
+	toName []protocol.NodeID
 }
 
 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 {
 	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()
 	defer m.Unlock()
 
@@ -36,7 +41,7 @@ func (m *Map) Get(name string) uint {
 
 	// Find a free slot to get a new ID
 	for i, n := range m.toName {
-		if n == "" {
+		if n == emptyNodeID {
 			m.toName[i] = name
 			m.toCid[name] = uint(i)
 			return uint(i)
@@ -50,19 +55,19 @@ func (m *Map) Get(name string) uint {
 	return cid
 }
 
-func (m *Map) Name(cid uint) string {
+func (m *Map) Name(cid uint) protocol.NodeID {
 	m.Lock()
 	defer m.Unlock()
 
 	return m.toName[cid]
 }
 
-func (m *Map) Names() []string {
+func (m *Map) Names() []protocol.NodeID {
 	m.Lock()
 
-	var names []string
+	var names []protocol.NodeID
 	for _, name := range m.toName {
-		if name != "" {
+		if name != emptyNodeID {
 			names = append(names, name)
 		}
 	}
@@ -71,11 +76,11 @@ func (m *Map) Names() []string {
 	return names
 }
 
-func (m *Map) Clear(name string) {
+func (m *Map) Clear(name protocol.NodeID) {
 	m.Lock()
 	cid, ok := m.toCid[name]
 	if ok {
-		m.toName[cid] = ""
+		m.toName[cid] = emptyNodeID
 		delete(m.toCid, name)
 	}
 	m.Unlock()

+ 14 - 7
cid/cid_test.go

@@ -4,28 +4,35 @@
 
 package cid
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/calmh/syncthing/protocol"
+)
 
 func TestGet(t *testing.T) {
 	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)
 	}
-	if i := m.Get("bar"); i != 2 {
+	if i := m.Get(barID); i != 2 {
 		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)
 	}
-	if i := m.Get("bar"); i != 2 {
+	if i := m.Get(barID); i != 2 {
 		t.Errorf("Unexpected id %d != 2", i)
 	}
 
 	if LocalID != 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)
 	}
 
-	myID := string(certID(cert.Certificate[0]))
+	myID := protocol.NewNodeID(cert.Certificate[0])
 
 	tlsCfg := &tls.Config{
 		Certificates:           []tls.Certificate{cert},
 		NextProtos:             []string{"bep/1.0"},
-		ServerName:             myID,
+		ServerName:             myID.String(),
 		ClientAuth:             tls.RequestClientCert,
 		SessionTicketsDisabled: true,
 		InsecureSkipVerify:     true,
@@ -63,7 +63,7 @@ func connect(target string) {
 		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{})
 
@@ -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)
 	if cmd == "idx" {
 		prtIndex(files)
@@ -121,7 +121,7 @@ func getFile(f protocol.FileInfo) {
 	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)
 	if cmd == "idx" {
 		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.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")
 	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")
 }

+ 3 - 11
cmd/stcli/tls.go

@@ -5,20 +5,12 @@
 package main
 
 import (
-	"crypto/sha256"
 	"crypto/tls"
-	"encoding/base32"
 	"path/filepath"
-	"strings"
 )
 
 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)
 
 	res := make(map[string]interface{})
-	res["myID"] = myID
+	res["myID"] = myID.String()
 	res["goroutines"] = runtime.NumGoroutine()
 	res["alloc"] = m.Alloc
 	res["sys"] = m.Sys

+ 6 - 6
cmd/syncthing/main.go

@@ -61,7 +61,7 @@ func init() {
 
 var (
 	cfg        config.Configuration
-	myID       string
+	myID       protocol.NodeID
 	confDir    string
 	logFlags   int = log.Ltime
 	rateBucket *ratelimit.Bucket
@@ -181,8 +181,8 @@ func main() {
 		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("My ID:", myID)
@@ -263,7 +263,7 @@ func main() {
 	tlsCfg := &tls.Config{
 		Certificates:           []tls.Certificate{cert},
 		NextProtos:             []string{"bep/1.0"},
-		ServerName:             myID,
+		ServerName:             myID.String(),
 		ClientAuth:             tls.RequestClientCert,
 		SessionTicketsDisabled: true,
 		InsecureSkipVerify:     true,
@@ -567,7 +567,7 @@ func saveConfig() {
 	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)
 
 	// Listen
@@ -673,7 +673,7 @@ next:
 			conn.Close()
 			continue
 		}
-		remoteID := certID(certs[0].Raw)
+		remoteID := protocol.NewNodeID(certs[0].Raw)
 
 		if remoteID == myID {
 			l.Infof("Connected to myself (%s) - should not happen", remoteID)

+ 3 - 10
cmd/syncthing/tls.go

@@ -11,14 +11,12 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/base32"
 	"encoding/binary"
 	"encoding/pem"
 	"math/big"
 	mr "math/rand"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 )
 
@@ -28,14 +26,9 @@ const (
 )
 
 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 {

+ 1 - 1
cmd/syncthing/usage_report.go

@@ -23,7 +23,7 @@ var stopUsageReportingCh = make(chan struct{})
 
 func reportData(m *model.Model) 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["longVersion"] = LongVersion
 	res["platform"] = runtime.GOOS + "-" + runtime.GOARCH

+ 15 - 30
config/config.go

@@ -14,10 +14,10 @@ import (
 	"regexp"
 	"sort"
 	"strconv"
-	"strings"
 
 	"code.google.com/p/go.crypto/bcrypt"
 	"github.com/calmh/syncthing/logger"
+	"github.com/calmh/syncthing/protocol"
 	"github.com/calmh/syncthing/scanner"
 )
 
@@ -69,7 +69,7 @@ type RepositoryConfiguration struct {
 	Versioning        VersioningConfiguration `xml:"versioning"`
 	SyncOrderPatterns []SyncOrderPattern      `xml:"syncorder>pattern"`
 
-	nodeIDs []string
+	nodeIDs []protocol.NodeID
 }
 
 type VersioningConfiguration struct {
@@ -113,7 +113,7 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl
 	return nil
 }
 
-func (r *RepositoryConfiguration) NodeIDs() []string {
+func (r *RepositoryConfiguration) NodeIDs() []protocol.NodeID {
 	if r.nodeIDs == nil {
 		for _, n := range r.Nodes {
 			r.nodeIDs = append(r.nodeIDs, n.NodeID)
@@ -138,9 +138,9 @@ func (r RepositoryConfiguration) FileRanker() func(scanner.File) int {
 }
 
 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 {
@@ -174,8 +174,8 @@ type GUIConfiguration struct {
 	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 {
 		m[n.NodeID] = n
 	}
@@ -276,7 +276,7 @@ func uniqueStrings(ss []string) []string {
 	return us
 }
 
-func Load(rd io.Reader, myID string) (Configuration, error) {
+func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
 	var cfg Configuration
 
 	setDefaults(&cfg)
@@ -297,15 +297,6 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
 		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
 	var seenRepos = map[string]*RepositoryConfiguration{}
 	var uniqueCounter int
@@ -321,13 +312,6 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
 			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 {
 			l.Warnf("Multiple repositories with ID %q; disabling", repo.ID)
 
@@ -390,8 +374,9 @@ func convertV1V2(cfg *Configuration) {
 	for i, repo := range cfg.Repositories {
 		cfg.Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly
 		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}
 		}
@@ -416,7 +401,7 @@ func convertV1V2(cfg *Configuration) {
 type NodeConfigurationList []NodeConfiguration
 
 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) {
 	l[a], l[b] = l[b], l[a]
@@ -425,10 +410,10 @@ func (l NodeConfigurationList) Len() int {
 	return len(l)
 }
 
-func ensureNodePresent(nodes []NodeConfiguration, myID string) []NodeConfiguration {
+func ensureNodePresent(nodes []NodeConfiguration, myID protocol.NodeID) []NodeConfiguration {
 	var myIDExists bool
 	for _, node := range nodes {
-		if node.NodeID == myID {
+		if node.NodeID.Equals(myID) {
 			myIDExists = true
 			break
 		}

+ 41 - 95
config/config_test.go

@@ -12,9 +12,19 @@ import (
 	"testing"
 
 	"github.com/calmh/syncthing/files"
+	"github.com/calmh/syncthing/protocol"
 	"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) {
 	expected := OptionsConfiguration{
 		ListenAddress:      []string{"0.0.0.0:22000"},
@@ -31,7 +41,7 @@ func TestDefaultValues(t *testing.T) {
 		UPnPEnabled:        true,
 	}
 
-	cfg, err := Load(bytes.NewReader(nil), "nodeID")
+	cfg, err := Load(bytes.NewReader(nil), node1)
 	if err != io.EOF {
 		t.Error(err)
 	}
@@ -45,10 +55,10 @@ func TestNodeConfig(t *testing.T) {
 	v1data := []byte(`
 <configuration version="1">
     <repository id="test" directory="~/Sync">
-        <node id="NODE1" name="node one">
+        <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
             <address>a</address>
         </node>
-        <node id="NODE2" name="node two">
+        <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
             <address>b</address>
         </node>
     </repository>
@@ -61,20 +71,20 @@ func TestNodeConfig(t *testing.T) {
 	v2data := []byte(`
 <configuration version="2">
     <repository id="test" directory="~/Sync" ro="true">
-        <node id="NODE1"/>
-        <node id="NODE2"/>
+        <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
+        <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
     </repository>
-    <node id="NODE1" name="node one">
+    <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
         <address>a</address>
     </node>
-    <node id="NODE2" name="node two">
+    <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
         <address>b</address>
     </node>
 </configuration>
 `)
 
 	for i, data := range [][]byte{v1data, v2data} {
-		cfg, err := Load(bytes.NewReader(data), "NODE1")
+		cfg, err := Load(bytes.NewReader(data), node1)
 		if err != nil {
 			t.Error(err)
 		}
@@ -83,23 +93,23 @@ func TestNodeConfig(t *testing.T) {
 			{
 				ID:        "test",
 				Directory: "~/Sync",
-				Nodes:     []NodeConfiguration{{NodeID: "NODE1"}, {NodeID: "NODE2"}},
+				Nodes:     []NodeConfiguration{{NodeID: node1}, {NodeID: node4}},
 				ReadOnly:  true,
 			},
 		}
 		expectedNodes := []NodeConfiguration{
 			{
-				NodeID:    "NODE1",
+				NodeID:    node1,
 				Name:      "node one",
 				Addresses: []string{"a"},
 			},
 			{
-				NodeID:    "NODE2",
+				NodeID:    node4,
 				Name:      "node two",
 				Addresses: []string{"b"},
 			},
 		}
-		expectedNodeIDs := []string{"NODE1", "NODE2"}
+		expectedNodeIDs := []protocol.NodeID{node1, node4}
 
 		if cfg.Version != 2 {
 			t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
@@ -118,18 +128,13 @@ func TestNodeConfig(t *testing.T) {
 
 func TestNoListenAddress(t *testing.T) {
 	data := []byte(`<configuration version="1">
-    <repository directory="~/Sync">
-        <node id="..." name="...">
-            <address>dynamic</address>
-        </node>
-    </repository>
     <options>
         <listenAddress></listenAddress>
     </options>
 </configuration>
 `)
 
-	cfg, err := Load(bytes.NewReader(data), "nodeID")
+	cfg, err := Load(bytes.NewReader(data), node1)
 	if err != nil {
 		t.Error(err)
 	}
@@ -142,11 +147,6 @@ func TestNoListenAddress(t *testing.T) {
 
 func TestOverriddenValues(t *testing.T) {
 	data := []byte(`<configuration version="2">
-    <repository directory="~/Sync">
-        <node id="..." name="...">
-            <address>dynamic</address>
-        </node>
-    </repository>
     <options>
        <listenAddress>:23000</listenAddress>
         <allowDelete>false</allowDelete>
@@ -180,7 +180,7 @@ func TestOverriddenValues(t *testing.T) {
 		UPnPEnabled:        false,
 	}
 
-	cfg, err := Load(bytes.NewReader(data), "nodeID")
+	cfg, err := Load(bytes.NewReader(data), node1)
 	if err != nil {
 		t.Error(err)
 	}
@@ -193,13 +193,13 @@ func TestOverriddenValues(t *testing.T) {
 func TestNodeAddresses(t *testing.T) {
 	data := []byte(`
 <configuration version="2">
-    <node id="n1">
-        <address>dynamic</address>
-    </node>
-    <node id="n2">
+    <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
         <address></address>
     </node>
-    <node id="n3">
+    <node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
+    </node>
+    <node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
+        <address>dynamic</address>
     </node>
 </configuration>
 `)
@@ -207,25 +207,25 @@ func TestNodeAddresses(t *testing.T) {
 	name, _ := os.Hostname()
 	expected := []NodeConfiguration{
 		{
-			NodeID:    "N1",
+			NodeID:    node1,
 			Addresses: []string{"dynamic"},
 		},
 		{
-			NodeID:    "N2",
+			NodeID:    node2,
 			Addresses: []string{"dynamic"},
 		},
 		{
-			NodeID:    "N3",
+			NodeID:    node3,
 			Addresses: []string{"dynamic"},
 		},
 		{
-			NodeID:    "N4",
+			NodeID:    node4,
 			Name:      name, // Set when auto created
 			Addresses: []string{"dynamic"},
 		},
 	}
 
-	cfg, err := Load(bytes.NewReader(data), "N4")
+	cfg, err := Load(bytes.NewReader(data), node4)
 	if err != nil {
 		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) {
 	data := []byte(`
 <configuration version="2">
-    <node id="AAAA-BBBB-CCCC">
-        <address>dynamic</address>
-    </node>
     <repository directory="~/Sync">
         <syncorder>
             <pattern pattern="\.jpg$" priority="1" />
         </syncorder>
-        <node id="AAAA-BBBB-CCCC" name=""></node>
     </repository>
 </configuration>
 `)
 
 	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 {
 		t.Error(err)
 	}
 
 	for i := range expected {
 		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) {
 		t.Errorf(
 			"\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"
 
 	"github.com/calmh/syncthing/discover"
+	"github.com/calmh/syncthing/protocol"
 	"github.com/golang/groupcache/lru"
 	"github.com/juju/ratelimit"
 )
@@ -32,7 +33,7 @@ type address struct {
 }
 
 var (
-	nodes      = make(map[string]node)
+	nodes      = make(map[protocol.NodeID]node)
 	lock       sync.Mutex
 	queries    = 0
 	announces  = 0
@@ -182,8 +183,16 @@ func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
 		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()
-	nodes[pkt.This.ID] = node
+	nodes[id] = node
 	lock.Unlock()
 }
 
@@ -199,8 +208,16 @@ func handleQueryV2(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
 		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()
-	node, ok := nodes[pkt.NodeID]
+	node, ok := nodes[id]
 	queries++
 	lock.Unlock()
 

+ 24 - 18
discover/discover.go

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

+ 2 - 2
discover/packets.go

@@ -11,7 +11,7 @@ const (
 
 type QueryV2 struct {
 	Magic  uint32
-	NodeID string // max:64
+	NodeID []byte // max:32
 }
 
 type AnnounceV2 struct {
@@ -21,7 +21,7 @@ type AnnounceV2 struct {
 }
 
 type Node struct {
-	ID        string    // max:64
+	ID        []byte    // max:32
 	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
 
 import (
@@ -25,10 +21,10 @@ func (o QueryV2) MarshalXDR() []byte {
 
 func (o QueryV2) encodeXDR(xw *xdr.Writer) (int, error) {
 	xw.WriteUint32(o.Magic)
-	if len(o.NodeID) > 64 {
+	if len(o.NodeID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
-	xw.WriteString(o.NodeID)
+	xw.WriteBytes(o.NodeID)
 	return xw.Tot(), xw.Error()
 }
 
@@ -45,7 +41,7 @@ func (o *QueryV2) UnmarshalXDR(bs []byte) error {
 
 func (o *QueryV2) decodeXDR(xr *xdr.Reader) error {
 	o.Magic = xr.ReadUint32()
-	o.NodeID = xr.ReadStringMax(64)
+	o.NodeID = xr.ReadBytesMax(32)
 	return xr.Error()
 }
 
@@ -112,10 +108,10 @@ func (o Node) MarshalXDR() []byte {
 }
 
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
-	if len(o.ID) > 64 {
+	if len(o.ID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
-	xw.WriteString(o.ID)
+	xw.WriteBytes(o.ID)
 	if len(o.Addresses) > 16 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
@@ -138,7 +134,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
 }
 
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
-	o.ID = xr.ReadStringMax(64)
+	o.ID = xr.ReadBytesMax(32)
 	_AddressesSize := int(xr.ReadUint32())
 	if _AddressesSize > 16 {
 		return xdr.ErrElementSizeExceeded

+ 4 - 15
gui/app.js

@@ -410,7 +410,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
 
         $('#editNode').modal('hide');
         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(); });
 
         done = false;
@@ -711,7 +711,7 @@ function randomString(len, bits)
         newStr = Math.random().toString(bits).slice(2);
         outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
     }
-    return outStr.toUpperCase();
+    return outStr.toLowerCase();
 }
 
 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 () {
     return function (input) {
         if (input === undefined)
@@ -860,8 +849,8 @@ syncthing.directive('validNodeid', function() {
                     // we shouldn't validate
                     ctrl.$setValidity('validNodeid', true);
                 } 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);
                     } else {
                         ctrl.$setValidity('validNodeid', false);

+ 3 - 3
gui/index.html

@@ -418,8 +418,8 @@ found in the LICENSE file.
           </h4>
         </div>
         <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 class="modal-footer">
           <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}">
               <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>
-              <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">
                 <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>

+ 6 - 2
integration/h2/config.xml

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

+ 3 - 3
integration/test.sh

@@ -6,9 +6,9 @@
 
 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 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
 	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
 	rmut       sync.RWMutex                              // protects the above
 
@@ -59,9 +59,9 @@ type Model struct {
 
 	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
 
 	sup suppressor
@@ -86,14 +86,14 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
 		clientVersion: clientVersion,
 		repoCfgs:      make(map[string]config.RepositoryConfiguration),
 		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),
 		suppressor:    make(map[string]*suppressor),
 		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)},
 	}
 
@@ -182,7 +182,7 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
 			ci.Completion = int(100 * have / tot)
 		}
 
-		res[node] = ci
+		res[node.String()] = ci
 	}
 
 	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.
 // 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 {
 		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.
 // 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 {
 		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()
 }
 
-func (m *Model) repoSharedWith(repo, nodeID string) bool {
+func (m *Model) repoSharedWith(repo string, nodeID protocol.NodeID) bool {
 	m.rmut.RLock()
 	defer m.rmut.RUnlock()
 	for _, nrepo := range m.nodeRepos[nodeID] {
@@ -342,7 +342,7 @@ func (m *Model) repoSharedWith(repo, nodeID string) bool {
 	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)
 	if debug {
 		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.
 // Implements the protocol.Model interface.
-func (m *Model) Close(node string, err error) {
+func (m *Model) Close(node protocol.NodeID, err error) {
 	if debug {
 		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.
 // 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.
 	m.rmut.RLock()
 	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
 	}
 
-	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)
 	}
 	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.
-func (m *Model) ConnectedTo(nodeID string) bool {
+func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
 	m.pmut.RLock()
 	_, ok := m.protoConn[nodeID]
 	m.pmut.RUnlock()
@@ -560,7 +560,7 @@ func (m *Model) updateLocal(repo string, f scanner.File) {
 	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()
 	nc, ok := m.protoConn[nodeID]
 	m.pmut.RUnlock()
@@ -639,7 +639,7 @@ func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
 	m.repoFiles[cfg.ID] = files.NewSet()
 	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 {
 		m.repoNodes[cfg.ID][i] = node.NodeID
 		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
-func (m *Model) clusterConfig(node string) protocol.ClusterConfigMessage {
+func (m *Model) clusterConfig(node protocol.NodeID) protocol.ClusterConfigMessage {
 	cm := protocol.ClusterConfigMessage{
 		ClientName:    m.clientName,
 		ClientVersion: m.clientVersion,
@@ -819,7 +819,7 @@ func (m *Model) clusterConfig(node string) protocol.ClusterConfigMessage {
 		for _, node := range m.repoNodes[repo] {
 			// TODO: Set read only bit when relevant
 			cr.Nodes = append(cr.Nodes, protocol.Node{
-				ID:    node,
+				ID:    node[:],
 				Flags: protocol.FlagShareTrusted,
 			})
 		}

+ 29 - 22
model/model_test.go

@@ -17,6 +17,13 @@ import (
 	"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{
 	"foo": scanner.File{
 		Name:     "foo",
@@ -56,7 +63,7 @@ func TestRequest(t *testing.T) {
 	m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
 	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 {
 		t.Fatal(err)
 	}
@@ -64,7 +71,7 @@ func TestRequest(t *testing.T) {
 		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 {
 		t.Error("Unexpected nil error on insecure file read")
 	}
@@ -95,7 +102,7 @@ func BenchmarkIndex10000(b *testing.B) {
 
 	b.ResetTimer()
 	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()
 	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.ScanRepo("default")
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 	b.ResetTimer()
 	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.ScanRepo("default")
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 	ufiles := genFiles(100)
 	b.ResetTimer()
 	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.ScanRepo("default")
 	files := genFiles(10000)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 	ufiles := genFiles(1)
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		m.IndexUpdate("42", "default", ufiles)
+		m.IndexUpdate(node1, "default", ufiles)
 	}
 }
 
 type FakeConnection struct {
-	id          string
+	id          protocol.NodeID
 	requestData []byte
 }
 
@@ -161,8 +168,8 @@ func (FakeConnection) Close() error {
 	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 {
@@ -202,15 +209,15 @@ func BenchmarkRequest(b *testing.B) {
 	}
 
 	fc := FakeConnection{
-		id:          "42",
+		id:          node1,
 		requestData: []byte("some data to return"),
 	}
 	m.AddConnection(fc, fc)
-	m.Index("42", "default", files)
+	m.Index(node1, "default", files)
 
 	b.ResetTimer()
 	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 {
 			b.Error(err)
 		}
@@ -222,26 +229,26 @@ func BenchmarkRequest(b *testing.B) {
 
 func TestActivityMap(t *testing.T) {
 	cm := cid.NewMap()
-	fooID := cm.Get("foo")
+	fooID := cm.Get(node1)
 	if fooID == 0 {
 		t.Fatal("ID cannot be zero")
 	}
-	barID := cm.Get("bar")
+	barID := cm.Get(node2)
 	if barID == 0 {
 		t.Fatal("ID cannot be zero")
 	}
 
 	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)
 	}
-	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)
 	}
-	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)
 	}
-	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)
 	}
 }

+ 6 - 6
model/puller.go

@@ -21,7 +21,7 @@ import (
 )
 
 type requestResult struct {
-	node     string
+	node     protocol.NodeID
 	file     scanner.File
 	filepath string // full filepath name
 	offset   int64
@@ -39,11 +39,11 @@ type openFile struct {
 	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 selected string
+	var selected protocol.NodeID
 	for _, node := range cm.Names() {
 		id := cm.Get(node)
 		if id == cid.LocalID {
@@ -61,7 +61,7 @@ func (m activityMap) leastBusyNode(availability uint64, cm *cid.Map) string {
 	return selected
 }
 
-func (m activityMap) decrease(node string) {
+func (m activityMap) decrease(node protocol.NodeID) {
 	m[node]--
 }
 
@@ -540,7 +540,7 @@ func (p *puller) handleRequestBlock(b bqBlock) bool {
 	of.outstanding++
 	p.openFiles[f.Name] = of
 
-	go func(node string, b bqBlock) {
+	go func(node protocol.NodeID, b bqBlock) {
 		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)
 		}

+ 7 - 5
model/util.go

@@ -58,12 +58,14 @@ func fileInfoFromFile(f scanner.File) protocol.FileInfo {
 	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 {
-		m[repo.ID] = make(map[string]uint32)
+		m[repo.ID] = make(map[protocol.NodeID]uint32)
 		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
@@ -72,7 +74,7 @@ func cmMap(cm protocol.ClusterConfigMessage) map[string]map[string]uint32 {
 type ClusterConfigMismatch error
 
 // compareClusterConfig returns nil for two equivalent configurations,
-// otherwise a decriptive error
+// otherwise a descriptive error
 func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
 	lm := cmMap(local)
 	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.name = name
 	t.offset = offset
@@ -38,11 +38,11 @@ func (t *TestModel) Request(nodeID, repo, name string, offset int64, size int) (
 	return t.data, nil
 }
 
-func (t *TestModel) Close(nodeID string, err error) {
+func (t *TestModel) Close(nodeID NodeID, err error) {
 	close(t.closedCh)
 }
 
-func (t *TestModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
+func (t *TestModel) ClusterConfig(nodeID NodeID, config ClusterConfigMessage) {
 }
 
 func (t *TestModel) isClosed() bool {

+ 1 - 1
protocol/message_types.go

@@ -42,7 +42,7 @@ type Repository struct {
 }
 
 type Node struct {
-	ID         string // max:64
+	ID         []byte // max:32
 	Flags      uint32
 	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
 
 import (
@@ -337,10 +333,10 @@ func (o Node) MarshalXDR() []byte {
 }
 
 func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
-	if len(o.ID) > 64 {
+	if len(o.ID) > 32 {
 		return xw.Tot(), xdr.ErrElementSizeExceeded
 	}
-	xw.WriteString(o.ID)
+	xw.WriteBytes(o.ID)
 	xw.WriteUint32(o.Flags)
 	xw.WriteUint64(o.MaxVersion)
 	return xw.Tot(), xw.Error()
@@ -358,7 +354,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
 }
 
 func (o *Node) decodeXDR(xr *xdr.Reader) error {
-	o.ID = xr.ReadStringMax(64)
+	o.ID = xr.ReadBytesMax(32)
 	o.Flags = xr.ReadUint32()
 	o.MaxVersion = xr.ReadUint64()
 	return xr.Error()

+ 5 - 5
protocol/nativemodel_darwin.go

@@ -14,29 +14,29 @@ type nativeModel struct {
 	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 {
 		files[i].Name = norm.NFD.String(files[i].Name)
 	}
 	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 {
 		files[i].Name = norm.NFD.String(files[i].Name)
 	}
 	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)
 	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)
 }
 
-func (m nativeModel) Close(nodeID string, err error) {
+func (m nativeModel) Close(nodeID NodeID, err error) {
 	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 {
 	// 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
-	IndexUpdate(nodeID string, repo string, files []FileInfo)
+	IndexUpdate(nodeID NodeID, repo string, files []FileInfo)
 	// 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
-	ClusterConfig(nodeID string, config ClusterConfigMessage)
+	ClusterConfig(nodeID NodeID, config ClusterConfigMessage)
 	// The peer node closed the connection
-	Close(nodeID string, err error)
+	Close(nodeID NodeID, err error)
 }
 
 type Connection interface {
-	ID() string
+	ID() NodeID
 	Index(repo string, files []FileInfo)
 	Request(repo string, name string, offset int64, size int) ([]byte, error)
 	ClusterConfig(config ClusterConfigMessage)
@@ -68,7 +68,7 @@ type Connection interface {
 }
 
 type rawConnection struct {
-	id       string
+	id       NodeID
 	receiver Model
 
 	reader io.ReadCloser
@@ -102,7 +102,7 @@ const (
 	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}
 	cw := &countingWriter{Writer: writer}
 
@@ -139,7 +139,7 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
 	return wireFormatConnection{&c}
 }
 
-func (c *rawConnection) ID() string {
+func (c *rawConnection) ID() NodeID {
 	return c.id
 }
 
@@ -295,7 +295,7 @@ func (c *rawConnection) readerLoop() (err error) {
 
 type incomingIndex struct {
 	update bool
-	id     string
+	id     NodeID
 	repo   string
 	files  []FileInfo
 }

+ 17 - 12
protocol/protocol_test.go

@@ -11,6 +11,11 @@ import (
 	"testing/quick"
 )
 
+var (
+	c0ID = NewNodeID([]byte{1})
+	c1ID = NewNodeID([]byte{2})
+)
+
 func TestHeaderFunctions(t *testing.T) {
 	f := func(ver, id, typ int) bool {
 		ver = int(uint(ver) % 16)
@@ -54,8 +59,8 @@ func TestPing(t *testing.T) {
 	ar, aw := 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 {
 		t.Error("c0 ping failed")
@@ -78,8 +83,8 @@ func TestPingErr(t *testing.T) {
 			eaw := &ErrPipe{PipeWriter: *aw, max: i, 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()
 			if (i < 4 || j < 4) && res {
@@ -106,8 +111,8 @@ func TestPingErr(t *testing.T) {
 // 			eaw := &ErrPipe{PipeWriter: *aw, max: i, 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)
 // 			if err == e || err == ErrClosed {
@@ -154,8 +159,8 @@ func TestVersionErr(t *testing.T) {
 	ar, aw := 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{
 		version: 2,
@@ -176,8 +181,8 @@ func TestTypeErr(t *testing.T) {
 	ar, aw := 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{
 		version: 0,
@@ -198,8 +203,8 @@ func TestClose(t *testing.T) {
 	ar, aw := 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)
 

+ 3 - 3
protocol/wireformat.go

@@ -14,11 +14,11 @@ type wireFormatConnection struct {
 	next Connection
 }
 
-func (c wireFormatConnection) ID() string {
+func (c wireFormatConnection) ID() NodeID {
 	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))
 	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))
 	}
 
-	c.next.Index(node, myFs)
+	c.next.Index(repo, myFs)
 }
 
 func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio