Sfoglia il codice sorgente

Merge branch 'main' into v2

* main:
  chore(fs): speed up case normalization (#10013)
  chore(config): remove discontinued secondary STUN servers (fixes #10011) (#10012)
  chore(gui, man, authors): update docs, translations, and contributors
  fix(stun): better error handling (ref #10008) (#10010)
  fix(config): remove discontinued primary STUN server (fixes #10008) (#10009)
  fix(gui): validate device ID in canonical form (fixes #7291) (#10006)
Jakob Borg 7 mesi fa
parent
commit
2953630bc3

+ 1 - 0
AUTHORS

@@ -223,6 +223,7 @@ Marc Laporte (marclaporte) <[email protected]> <[email protected]>
 Marc Pujol (kilburn) <[email protected]>
 Marcin Dziadus (marcindziadus) <[email protected]>
 marco-m <[email protected]>
+Marcus B Spencer <[email protected]>
 Marcus Legendre <[email protected]>
 Mario Majila <[email protected]>
 Mark Pulford (mpx) <[email protected]>

+ 2 - 0
gui/default/assets/lang/lang-bg.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Разрешени мрежи",
     "Alphabetic": "Азбучен ред",
     "Altered by ignoring deletes.": "Промяна чрез пренебрегване на премахваните елементи.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Винаги включено, когато вида на папката е „{{foldertype}}“.",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Външна команда управлява версиите. Тя трябва да премахне файла от синхронизираната папка. Ако в пътя до приложението има интервали, то той трябва да бъде поставен в кавички.",
     "Anonymous Usage Reporting": "Анонимно отчитане на употреба",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "Форматът на данните за анонимно отчитане на употреба е променен. Желаете ли да използвате него вместо стария?",
@@ -52,6 +53,7 @@
     "Body:": "Съдържание:",
     "Bugs": "Дефекти",
     "Cancel": "Отказ",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Не мож да бъде включено, ако вида на папката е „{{foldertype}}“.",
     "Changelog": "Дневник на промените",
     "Clean out after": "Почистване след",
     "Cleaning Versions": "Почистване на версии",

+ 2 - 0
gui/default/assets/lang/lang-de.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Erlaubte Netzwerke",
     "Alphabetic": "Alphabetisch",
     "Altered by ignoring deletes.": "Weicht ab, weil Löschungen ignoriert werden.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Immer eingeschaltet, wenn der Ordnertyp „{{foldertype}}“ ist.",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Die Versionierung erfolgt über einen externen Befehl. Er muss die Datei aus dem geteilten Ordner entfernen. Wenn der Pfad zur Anwendung Leerzeichen enthält, sollte er in Anführungszeichen gesetzt werden.",
     "Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format des anonymen Nutzungsberichts hat sich geändert. Möchten Sie auf das neue Format umsteigen?",
@@ -52,6 +53,7 @@
     "Body:": "Nachrichtentext:",
     "Bugs": "Fehler",
     "Cancel": "Abbrechen",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Kann nicht aktiviert werden, wenn der Ordnertyp „{{foldertype}}“ ist.",
     "Changelog": "Änderungsprotokoll",
     "Clean out after": "Löschen nach",
     "Cleaning Versions": "Versionen bereinigen",

+ 2 - 0
gui/default/assets/lang/lang-fr.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Réseaux autorisés",
     "Alphabetic": "Alphabétique",
     "Altered by ignoring deletes.": "Protégé par \"Ignore Delete\".",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Toujours activé pour le type de partage \"{{foldertype}}\".",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé. Si le chemin contient des espaces, il doit être spécifié entre guillemets.",
     "Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "Le format du rapport anonyme d'utilisation a changé. Voulez-vous passer au nouveau format ?",
@@ -52,6 +53,7 @@
     "Body:": "Corps du message :",
     "Bugs": "Bogues",
     "Cancel": "Annuler",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Ne peut être activé pour le type de partage \"{{foldertype}}\".",
     "Changelog": "Historique des versions",
     "Clean out after": "Conserver pendant",
     "Cleaning Versions": "Purge des versions",

+ 2 - 0
gui/default/assets/lang/lang-it.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Reti Consentite",
     "Alphabetic": "Alfabetico",
     "Altered by ignoring deletes.": "Modificato ignorando le eliminazioni.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Sempre attivato quando il tipo di cartella è \"{{foldertype}}\".",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella condivisa. Se il percorso dell'applicazione contiene spazi, deve essere indicato tra virgolette.",
     "Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "Il formato delle statistiche anonime di utilizzo è cambiato. Vuoi passare al nuovo formato?",
@@ -52,6 +53,7 @@
     "Body:": "Corpo:",
     "Bugs": "Bug",
     "Cancel": "Annulla",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Non può essere abilitato se il tipo di cartella è \"{{foldertype}}\".",
     "Changelog": "Registro modifiche",
     "Clean out after": "Svuota dopo",
     "Cleaning Versions": "Pulizia Versioni",

+ 2 - 0
gui/default/assets/lang/lang-pl.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Dozwolone sieci",
     "Alphabetic": "Alfabetycznie",
     "Altered by ignoring deletes.": "Zmieniono przez ignorowanie usuniętych",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Zawsze włączone, gdy typ folderu to \"{{foldertype}}\".",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Zewnętrzne polecenie odpowiedzialne jest za wersjonowanie. Musi ono usunąć plik ze współdzielonego folderu. Jeżeli ścieżka do aplikacji zawiera spacje, to powinna ona być zamknięta w cudzysłowie.",
     "Anonymous Usage Reporting": "Anonimowe statystyki użycia",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "Format anonimowych statystyk użycia uległ zmianie. Czy chcesz przejść na nowy format?",
@@ -52,6 +53,7 @@
     "Body:": "Treść:",
     "Bugs": "Błędy",
     "Cancel": "Anuluj",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Nie można włączyć, jeśli typem folderu jest \"{{foldertype}}\".",
     "Changelog": "Historia zmian",
     "Clean out after": "Opróżnij po",
     "Cleaning Versions": "Czyszczenie wersji",

+ 2 - 0
gui/default/assets/lang/lang-pt-BR.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Redes permitidas",
     "Alphabetic": "Alfabética",
     "Altered by ignoring deletes.": "Alterado ignorando exclusões.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Sempre ativado quando o tipo de pasta for \"{{foldertype}}\".",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Um comando externo controla o controle de versão. Tem que remover o arquivo da pasta compartilhada. Se o caminho para o aplicativo contiver espaços, ele deve ser colocado entre aspas.",
     "Anonymous Usage Reporting": "Relatórios anônimos de uso",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "O formato do relatório anônimo de uso mudou. Gostaria de usar o formato novo?",
@@ -52,6 +53,7 @@
     "Body:": "Corpo:",
     "Bugs": "Erros",
     "Cancel": "Cancelar",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Não pode ser ativado se o tipo de pasta for \"{{foldertype}}\".",
     "Changelog": "Registro de alterações",
     "Clean out after": "Limpar depois de",
     "Cleaning Versions": "Limpando Versões",

+ 2 - 0
gui/default/assets/lang/lang-pt-PT.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "Redes permitidas",
     "Alphabetic": "Alfabética",
     "Altered by ignoring deletes.": "Alterada por terem sido ignoradas as eliminações.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Sempre ligado quando o tipo de pasta é \"{{foldertype}}\".",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Um comando externo gere as versões. Esse comando tem que remover o ficheiro da pasta partilhada. Se o caminho para a aplicação contiver espaços, então terá de o escrever entre aspas.",
     "Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "O formato do relatório anónimo de utilização foi alterado. Gostaria de mudar para o novo formato?",
@@ -52,6 +53,7 @@
     "Body:": "Corpo:",
     "Bugs": "Erros",
     "Cancel": "Cancelar",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Não pode ser habilitado quando o tipo de pasta é \"{{foldertype}}\".",
     "Changelog": "Registo de alterações",
     "Clean out after": "Esvaziar ao fim de",
     "Cleaning Versions": "Limpando versões",

+ 2 - 0
gui/default/assets/lang/lang-tr.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "İzin Verilen Ağlar",
     "Alphabetic": "Alfabetik",
     "Altered by ignoring deletes.": "Silmeler yoksayılarak değiştirildi.",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "Klasör türü \"{{foldertype}}\" olduğunda her zaman açıktır.",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Harici bir komut sürümlendirmeyi gerçekleştirir. Dosyayı paylaşılan klasörden kaldırmak zorundadır. Eğer uygulama yolu boşluklar içeriyorsa, tırnak içine alınmalıdır.",
     "Anonymous Usage Reporting": "İsimsiz Kullanım Bildirme",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "İsimsiz kullanım raporu biçimi değişti. Yeni biçime geçmek ister misiniz?",
@@ -52,6 +53,7 @@
     "Body:": "Gövde:",
     "Bugs": "Hatalar",
     "Cancel": "İptal",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "Klasör türü \"{{foldertype}}\" olduğunda etkinleştirilemez.",
     "Changelog": "Değişiklik Günlüğü",
     "Clean out after": "Şundan sonra temizle",
     "Cleaning Versions": "Sürümleri Temizleme",

+ 8 - 6
gui/default/assets/lang/lang-zh-CN.json

@@ -27,6 +27,7 @@
     "Allowed Networks": "允许的网络",
     "Alphabetic": "字母顺序",
     "Altered by ignoring deletes.": "通过忽略删除进行更改。",
+    "Always turned on when the folder type is \"{%foldertype%}\".": "当文件夹类型为“{{foldertype}}”时始终启用。",
     "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "外部命令处理版本控制。必须从共享文件夹中移除文件。如果应用程序的路径包含空格,应用半角引号括起来。",
     "Anonymous Usage Reporting": "匿名使用报告",
     "Anonymous usage report format has changed. Would you like to move to the new format?": "匿名使用报告格式已更改。是否要切换到新格式?",
@@ -52,6 +53,7 @@
     "Body:": "正文:",
     "Bugs": "问题反馈",
     "Cancel": "取消",
+    "Cannot be enabled when the folder type is \"{%foldertype%}\".": "当文件夹类型为“{{foldertype}}”时无法启用。",
     "Changelog": "更新日志",
     "Clean out after": "在该时间后清除",
     "Cleaning Versions": "清理版本中",
@@ -228,12 +230,12 @@
     "Listener Failures": "监听程序失败",
     "Listener Status": "监听程序状态",
     "Listeners": "监听程序",
-    "Loading data...": "正在载数据…",
-    "Loading...": "正在载…",
+    "Loading data...": "正在载数据…",
+    "Loading...": "正在载…",
     "Local Additions": "本地添加",
     "Local Discovery": "本地发现",
     "Local State": "本地状态",
-    "Local State (Total)": "本地状态汇总",
+    "Local State (Total)": "本地状态(总计)",
     "Locally Changed Items": "本地更改的项目",
     "Log": "日志",
     "Log File": "日志文件",
@@ -481,11 +483,11 @@
     "Untrusted": "不受信任",
     "Up to Date": "最新",
     "Updated {%file%}": "已更新 {{file}}",
-    "Upgrade": "更新",
-    "Upgrade To {%version%}": "升级至版本 {{version}}",
+    "Upgrade": "升级",
+    "Upgrade To {%version%}": "升级 {{version}}",
     "Upgrading": "升级中",
     "Upload Rate": "上传速率",
-    "Uptime": "启动时间",
+    "Uptime": "运行时间",
     "Usage reporting is always enabled for candidate releases.": "发布候选版始终启用使用报告。",
     "Use HTTPS for GUI": "使用 HTTPS 连接到 GUI",
     "Use notifications from the filesystem to detect changed items.": "使用来自文件系统的通知来检测更改的项目。",

File diff suppressed because it is too large
+ 0 - 0
gui/default/syncthing/core/aboutModalView.html


+ 5 - 11
gui/default/syncthing/core/validDeviceidDirective.js

@@ -5,18 +5,12 @@ angular.module('syncthing.core')
             link: function (scope, elm, attrs, ctrl) {
                 ctrl.$parsers.unshift(function (viewValue) {
                     $http.get(urlbase + '/svc/deviceid?id=' + viewValue).success(function (resp) {
-                        if (resp.error) {
-                            ctrl.$setValidity('validDeviceid', false);
-                        } else {
-                            ctrl.$setValidity('validDeviceid', true);
-                        }
+                        let isValid = !resp.error;
+                        let isUnique = !isValid || !scope.devices.hasOwnProperty(resp.id);
+
+                        ctrl.$setValidity('validDeviceid', isValid);
+                        ctrl.$setValidity('unique', isUnique);
                     });
-                    //Prevents user from adding a duplicate ID
-                    if (scope.devices.hasOwnProperty(viewValue)) {
-                        ctrl.$setValidity('unique', false);
-                    } else {
-                        ctrl.$setValidity('unique', true);
-                    }
                     return viewValue;
                 });
             }

+ 2 - 5
lib/config/config.go

@@ -71,22 +71,19 @@ var (
 
 	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
 	DefaultPrimaryStunServers = []string{
-		"stun.syncthing.net:3478",
+		// Discontinued because of misuse. See https://forum.syncthing.net/t/stun-server-misuse/23319
+		//"stun.syncthing.net:3478",
 	}
 	DefaultSecondaryStunServers = []string{
-		"stun.callwithus.com:3478",
 		"stun.counterpath.com:3478",
 		"stun.counterpath.net:3478",
 		"stun.ekiga.net:3478",
 		"stun.hitv.com:3478",
-		"stun.ideasip.com:3478",
 		"stun.internetcalls.com:3478",
 		"stun.miwifi.com:3478",
 		"stun.schlund.de:3478",
-		"stun.sipgate.net:10000",
 		"stun.sipgate.net:3478",
 		"stun.voip.aebc.com:3478",
-		"stun.voiparound.com:3478",
 		"stun.voipbuster.com:3478",
 		"stun.voipstunt.com:3478",
 		"stun.xten.com:3478",

+ 57 - 4
lib/fs/folding.go

@@ -17,6 +17,52 @@ import (
 // UnicodeLowercaseNormalized returns the Unicode lower case variant of s,
 // having also normalized it to normalization form C.
 func UnicodeLowercaseNormalized(s string) string {
+	if isASCII, isLower := isASCII(s); isASCII {
+		if isLower {
+			return s
+		}
+		return toLowerASCII(s)
+	}
+
+	return toLowerUnicode(s)
+}
+
+func isASCII(s string) (bool, bool) {
+	isLower := true
+	for _, b := range []byte(s) {
+		if b > unicode.MaxASCII {
+			return false, isLower
+		}
+		if 'A' <= b && b <= 'Z' {
+			isLower = false
+		}
+	}
+	return true, isLower
+}
+
+func toLowerASCII(s string) string {
+	var (
+		b   strings.Builder
+		pos int
+	)
+	b.Grow(len(s))
+	for i, c := range []byte(s) {
+		if c < 'A' || 'Z' < c {
+			continue
+		}
+		if pos < i {
+			b.WriteString(s[pos:i])
+		}
+		pos = i + 1
+		b.WriteByte(c + 'a' - 'A')
+	}
+	if pos != len(s) {
+		b.WriteString(s[pos:])
+	}
+	return b.String()
+}
+
+func toLowerUnicode(s string) string {
 	i := firstCaseChange(s)
 	if i == -1 {
 		return norm.NFC.String(s)
@@ -30,7 +76,11 @@ func UnicodeLowercaseNormalized(s string) string {
 	rs.WriteString(s[:i])
 
 	for _, r := range s[i:] {
-		rs.WriteRune(unicode.ToLower(unicode.ToUpper(r)))
+		if r <= unicode.MaxLatin1 && r != 'µ' {
+			rs.WriteRune(unicode.ToLower(r))
+		} else {
+			rs.WriteRune(unicode.To(unicode.LowerCase, unicode.To(unicode.UpperCase, r)))
+		}
 	}
 	return norm.NFC.String(rs.String())
 }
@@ -38,10 +88,13 @@ func UnicodeLowercaseNormalized(s string) string {
 // Byte index of the first rune r s.t. lower(upper(r)) != r.
 func firstCaseChange(s string) int {
 	for i, r := range s {
-		if r <= unicode.MaxASCII && (r < 'A' || r > 'Z') {
-			continue
+		if r <= unicode.MaxASCII {
+			if r < 'A' || r > 'Z' {
+				continue
+			}
+			return i
 		}
-		if unicode.ToLower(unicode.ToUpper(r)) != r {
+		if unicode.To(unicode.LowerCase, unicode.To(unicode.UpperCase, r)) != r {
 			return i
 		}
 	}

+ 20 - 17
lib/fs/folding_test.go

@@ -49,6 +49,18 @@ var caseCases = [][2]string{
 	{"a\xCC\x88", "\xC3\xA4"}, // ä
 }
 
+var benchmarkCases = [][2]string{
+	{"img_202401241010.jpg", "ASCII lowercase"},
+	{"IMG_202401241010.jpg", "ASCII mixedcase start"},
+	{"img_202401241010.JPG", "ASCII mixedcase end"},
+	{"wir_kinder_aus_bullerbü.epub", "Latin1 lowercase"},
+	{"Wir_Kinder_aus_Bullerbü.epub", "Latin1 mixedcase start"},
+	{"wir_kinder_aus_bullerbü.EPUB", "Latin1 mixedcase end"},
+	{"translated_ウェブの国際化.html", "Unicode lowercase"},
+	{"Translated_ウェブの国際化.html", "Unicode mixedcase start"},
+	{"translated_ウェブの国際化.HTML", "Unicode mixedcase end"},
+}
+
 func TestUnicodeLowercaseNormalized(t *testing.T) {
 	for _, tc := range caseCases {
 		res := UnicodeLowercaseNormalized(tc[0])
@@ -58,22 +70,13 @@ func TestUnicodeLowercaseNormalized(t *testing.T) {
 	}
 }
 
-func BenchmarkUnicodeLowercaseMaybeChange(b *testing.B) {
-	b.ReportAllocs()
-
-	for i := 0; i < b.N; i++ {
-		for _, s := range caseCases {
-			UnicodeLowercaseNormalized(s[0])
-		}
-	}
-}
-
-func BenchmarkUnicodeLowercaseNoChange(b *testing.B) {
-	b.ReportAllocs()
-
-	for i := 0; i < b.N; i++ {
-		for _, s := range caseCases {
-			UnicodeLowercaseNormalized(s[1])
-		}
+func BenchmarkUnicodeLowercase(b *testing.B) {
+	for _, c := range benchmarkCases {
+		b.Run(c[1], func(b *testing.B) {
+			b.ReportAllocs()
+			for i := 0; i < b.N; i++ {
+				UnicodeLowercaseNormalized(c[0])
+			}
+		})
 	}
 }

+ 25 - 23
lib/stun/stun.go

@@ -8,6 +8,8 @@ package stun
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"net"
 	"time"
 
@@ -38,6 +40,8 @@ const (
 	NATSymmetricUDPFirewall = stun.NATSymmetricUDPFirewall
 )
 
+var errNotPunchable = errors.New("not punchable")
+
 type Subscriber interface {
 	OnNATTypeChanged(natType NATType)
 	OnExternalAddressChanged(address *Host, via string)
@@ -110,10 +114,11 @@ func (s *Service) Serve(ctx context.Context) error {
 		l.Debugf("Starting stun for %s", s)
 
 		for _, addr := range s.cfg.Options().StunServers() {
-			// This blocks until we hit an exit condition or there are issues with the STUN server.
-			// This returns a boolean signifying if a different STUN server should be tried (oppose to the whole thing
-			// shutting down and this winding itself down.
-			s.runStunForServer(ctx, addr)
+			// This blocks until we hit an exit condition or there are
+			// issues with the STUN server.
+			if err := s.runStunForServer(ctx, addr); errors.Is(err, errNotPunchable) {
+				break // we will sleep for a while
+			}
 
 			// Have we been asked to stop?
 			select {
@@ -129,11 +134,6 @@ func (s *Service) Serve(ctx context.Context) error {
 				s.setExternalAddress(nil, "")
 				goto disabled
 			}
-
-			// Unpunchable NAT? Chillout for some time.
-			if !s.isCurrentNATTypePunchable() {
-				break
-			}
 		}
 
 		// We failed to contact all provided stun servers or the nat is not punchable.
@@ -142,7 +142,7 @@ func (s *Service) Serve(ctx context.Context) error {
 	}
 }
 
-func (s *Service) runStunForServer(ctx context.Context, addr string) {
+func (s *Service) runStunForServer(ctx context.Context, addr string) error {
 	l.Debugf("Running stun for %s via %s", s, addr)
 
 	// Resolve the address, so that in case the server advertises two
@@ -153,7 +153,7 @@ func (s *Service) runStunForServer(ctx context.Context, addr string) {
 	udpAddr, err := net.ResolveUDPAddr("udp", addr)
 	if err != nil {
 		l.Debugf("%s stun addr resolution on %s: %s", s, addr, err)
-		return
+		return err
 	}
 	s.client.SetServerAddr(udpAddr.String())
 
@@ -163,15 +163,18 @@ func (s *Service) runStunForServer(ctx context.Context, addr string) {
 		natType, extAddr, err = s.client.Discover()
 		return err
 	})
-	if err != nil || extAddr == nil {
-		l.Debugf("%s stun discovery on %s: %s", s, addr, err)
-		return
+	if err != nil {
+		l.Debugf("%s stun discovery on %s: %v", s, addr, err)
+		return err
+	} else if extAddr == nil {
+		l.Debugf("%s stun discovery on %s resulted in no address", s, addr)
+		return fmt.Errorf("%s: no address", addr)
 	}
 
 	// The stun server is most likely borked, try another one.
 	if natType == NATError || natType == NATUnknown || natType == NATBlocked {
 		l.Debugf("%s stun discovery on %s resolved to %s", s, addr, natType)
-		return
+		return fmt.Errorf("%s: bad result: %v", addr, natType)
 	}
 
 	s.setNATType(natType)
@@ -181,15 +184,14 @@ func (s *Service) runStunForServer(ctx context.Context, addr string) {
 	// and such, just let the caller check the nat type and work it out themselves.
 	if !s.isCurrentNATTypePunchable() {
 		l.Debugf("%s cannot punch %s, skipping", s, natType)
-		return
+		return errNotPunchable
 	}
 
 	s.setExternalAddress(extAddr, addr)
-
-	s.stunKeepAlive(ctx, addr, extAddr)
+	return s.stunKeepAlive(ctx, addr, extAddr)
 }
 
-func (s *Service) stunKeepAlive(ctx context.Context, addr string, extAddr *Host) {
+func (s *Service) stunKeepAlive(ctx context.Context, addr string, extAddr *Host) error {
 	var err error
 	nextSleep := time.Duration(s.cfg.Options().StunKeepaliveStartS) * time.Second
 
@@ -211,7 +213,7 @@ func (s *Service) stunKeepAlive(ctx context.Context, addr string, extAddr *Host)
 			minSleep := time.Duration(s.cfg.Options().StunKeepaliveMinS) * time.Second
 			if nextSleep < minSleep {
 				l.Debugf("%s keepalive aborting, sleep below min: %s < %s", s, nextSleep, minSleep)
-				return
+				return fmt.Errorf("unreasonably low keepalive: %v", minSleep)
 			}
 		}
 
@@ -238,13 +240,13 @@ func (s *Service) stunKeepAlive(ctx context.Context, addr string, extAddr *Host)
 		case <-time.After(sleepFor):
 		case <-ctx.Done():
 			l.Debugf("%s stopping, aborting stun", s)
-			return
+			return ctx.Err()
 		}
 
 		if s.cfg.Options().IsStunDisabled() {
 			// Disabled, give up
 			l.Debugf("%s disabled, aborting stun ", s)
-			return
+			return errors.New("disabled")
 		}
 
 		// Check if any writes happened while we were sleeping, if they did, sleep again
@@ -259,7 +261,7 @@ func (s *Service) stunKeepAlive(ctx context.Context, addr string, extAddr *Host)
 		extAddr, err = s.client.Keepalive()
 		if err != nil {
 			l.Debugf("%s stun keepalive on %s: %s (%v)", s, addr, err, extAddr)
-			return
+			return err
 		}
 		ourLastWrite = time.Now()
 	}

Some files were not shown because too many files changed in this diff