1
0
Эх сурвалжийг харах

Merge branch 'main' into v2

* main:
  feat(config): add option for audit file (fixes #9481) (#10066)
  chore(api): log X-Forwarded-For (#10035)
  chore(gui): update dependency copyrights, add script for periodic maintenance (#10067)
  chore(gui, man, authors): update docs, translations, and contributors
  chore(syncthing): remove support for TLS 1.2 sync connections (#10064)
  fix(osutil): give threads same I/O priority on Linux (#10063)
  chore(stun): switch lookup warning to debug level
  chore(gui, man, authors): update docs, translations, and contributors
Jakob Borg 5 сар өмнө
parent
commit
abe34fc1f6

+ 2 - 0
AUTHORS

@@ -277,6 +277,7 @@ Oyebanji Jacob Mayowa <[email protected]>
 Pablo <[email protected]>
 Pascal Jungblut (pascalj) <[email protected]> <[email protected]>
 Paul Brit <[email protected]>
+Paul Donald <[email protected]>
 Pawel Palenica (qepasa) <[email protected]>
 Paweł Rozlach <[email protected]>
 perewa <[email protected]>
@@ -327,6 +328,7 @@ Syncthing Release Automation <[email protected]>
 Sébastien WENSKE <[email protected]>
 Taylor Khan (nelsonkhan) <[email protected]>
 Terrance <[email protected]>
+TheCreeper <[email protected]>
 Thomas <[email protected]>
 Thomas Hipp <[email protected]>
 Tim Abell (timabell) <[email protected]>

+ 1 - 0
build.sh

@@ -23,6 +23,7 @@ case "${1:-default}" in
 
 	prerelease)
 		script authors
+		script copyrights
 		build weblate
 		pushd man ; ./refresh.sh ; popd
 		git add -A gui man AUTHORS

+ 13 - 2
cmd/syncthing/main.go

@@ -533,8 +533,19 @@ func (c *serveCmd) syncthingMain() {
 		Verbose:               c.Verbose,
 		DBMaintenanceInterval: c.DBMaintenanceInterval,
 	}
-	if c.Audit {
-		appOpts.AuditWriter = auditWriter(c.AuditFile)
+
+	if c.Audit || cfgWrapper.Options().AuditEnabled {
+		l.Infoln("Auditing is enabled.")
+
+		auditFile := cfgWrapper.Options().AuditFile
+
+		// Ignore config option if command-line option is set
+		if c.AuditFile != "" {
+			l.Debugln("Using the audit file from the command-line parameter.")
+			auditFile = c.AuditFile
+		}
+
+		appOpts.AuditWriter = auditWriter(auditFile)
 	}
 
 	app, err := syncthing.New(cfgWrapper, sdb, evLogger, cert, appOpts)

+ 6 - 0
gui/default/assets/lang/lang-et.json

@@ -9,9 +9,15 @@
     "Add Folder": "Lisa kaust",
     "Add new folder?": "Lisa uus kaust?",
     "Address": "Aadress",
+    "Addresses": "Aadressid",
+    "All Data": "Kõik andmed",
+    "All Time": "Kõik ajad",
+    "Allowed Networks": "Lubatud võrgud",
     "Alphabetic": "Tähestikuline",
     "Automatic upgrades": "Automaatsed uuendused",
     "Be careful!": "Ettevaatust!",
+    "Cancel": "Loobu",
+    "Changelog": "Muudatuste nimekiri",
     "Close": "Sulge",
     "Configured": "Seadistatud",
     "Connection Error": "Ühenduse viga",

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
gui/default/syncthing/core/aboutModalView.html


+ 48 - 8
lib/api/api_auth.go

@@ -18,6 +18,7 @@ import (
 	ldap "github.com/go-ldap/ldap/v3"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/events"
+	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/rand"
 )
 
@@ -27,15 +28,54 @@ const (
 	randomTokenLength  = 64
 )
 
-func emitLoginAttempt(success bool, username, address string, evLogger events.Logger) {
-	evLogger.Log(events.LoginAttempt, map[string]interface{}{
+func emitLoginAttempt(success bool, username string, r *http.Request, evLogger events.Logger) {
+	remoteAddress, proxy := remoteAddress(r)
+	evData := map[string]any{
 		"success":       success,
 		"username":      username,
-		"remoteAddress": address,
-	})
-	if !success {
-		l.Infof("Wrong credentials supplied during API authorization from %s", address)
+		"remoteAddress": remoteAddress,
 	}
+	if proxy != "" {
+		evData["proxy"] = proxy
+	}
+	evLogger.Log(events.LoginAttempt, evData)
+
+	if success {
+		return
+	}
+	if proxy != "" {
+		l.Infof("Wrong credentials supplied during API authorization from %s proxied by %s", remoteAddress, proxy)
+	} else {
+		l.Infof("Wrong credentials supplied during API authorization from %s", remoteAddress)
+	}
+}
+
+func remoteAddress(r *http.Request) (remoteAddr, proxy string) {
+	remoteAddr = r.RemoteAddr
+	remoteIP := osutil.IPFromString(r.RemoteAddr)
+
+	// parse X-Forwarded-For only if the proxy connects via unix socket, localhost or a LAN IP
+	var localProxy bool
+	if remoteIP != nil {
+		remoteAddr = remoteIP.String()
+		localProxy = remoteIP.IsLoopback() || remoteIP.IsPrivate() || remoteIP.IsLinkLocalUnicast()
+	} else if remoteAddr == "@" {
+		localProxy = true
+	}
+
+	if !localProxy {
+		return
+	}
+
+	forwardedAddr, _, _ := strings.Cut(r.Header.Get("X-Forwarded-For"), ",")
+	forwardedAddr = strings.TrimSpace(forwardedAddr)
+	forwardedIP := osutil.IPFromString(forwardedAddr)
+
+	if forwardedIP != nil {
+		proxy = remoteAddr
+		remoteAddr = forwardedIP.String()
+	}
+	return
 }
 
 func antiBruteForceSleep() {
@@ -152,7 +192,7 @@ func (m *basicAuthAndSessionMiddleware) passwordAuthHandler(w http.ResponseWrite
 		return
 	}
 
-	emitLoginAttempt(false, req.Username, r.RemoteAddr, m.evLogger)
+	emitLoginAttempt(false, req.Username, r, m.evLogger)
 	antiBruteForceSleep()
 	forbidden(w)
 }
@@ -175,7 +215,7 @@ func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg c
 		return usernameFromIso, true
 	}
 
-	emitLoginAttempt(false, username, r.RemoteAddr, evLogger)
+	emitLoginAttempt(false, username, r, evLogger)
 	antiBruteForceSleep()
 	return "", false
 }

+ 1 - 1
lib/api/tokenmanager.go

@@ -189,7 +189,7 @@ func (m *tokenCookieManager) createSession(username string, persistent bool, w h
 		Path:   "/",
 	})
 
-	emitLoginAttempt(true, username, r.RemoteAddr, m.evLogger)
+	emitLoginAttempt(true, username, r, m.evLogger)
 }
 
 func (m *tokenCookieManager) hasValidSession(r *http.Request) bool {

+ 4 - 0
lib/config/config_test.go

@@ -92,6 +92,8 @@ func TestDefaultValues(t *testing.T) {
 			RawStunServers:            []string{"default"},
 			AnnounceLANAddresses:      true,
 			FeatureFlags:              []string{},
+			AuditEnabled:              false,
+			AuditFile:                 "",
 			ConnectionPriorityTCPLAN:  10,
 			ConnectionPriorityQUICLAN: 20,
 			ConnectionPriorityTCPWAN:  30,
@@ -295,6 +297,8 @@ func TestOverriddenValues(t *testing.T) {
 		StunKeepaliveMinS:         900,
 		RawStunServers:            []string{"foo"},
 		FeatureFlags:              []string{"feature"},
+		AuditEnabled:              true,
+		AuditFile:                 "nggyu",
 		ConnectionPriorityTCPLAN:  40,
 		ConnectionPriorityQUICLAN: 45,
 		ConnectionPriorityTCPWAN:  50,

+ 10 - 11
lib/config/optionsconfiguration.go

@@ -67,22 +67,21 @@ type OptionsConfiguration struct {
 	AnnounceLANAddresses        bool     `json:"announceLANAddresses" xml:"announceLANAddresses" default:"true"`
 	SendFullIndexOnUpgrade      bool     `json:"sendFullIndexOnUpgrade" xml:"sendFullIndexOnUpgrade"`
 	FeatureFlags                []string `json:"featureFlags" xml:"featureFlag"`
+	AuditEnabled                bool     `json:"auditEnabled" xml:"auditEnabled" default:"false"`
+	AuditFile                   string   `json:"auditFile" xml:"auditFile"`
 	// The number of connections at which we stop trying to connect to more
 	// devices, zero meaning no limit. Does not affect incoming connections.
 	ConnectionLimitEnough int `json:"connectionLimitEnough" xml:"connectionLimitEnough"`
 	// The maximum number of connections which we will allow in total, zero
 	// meaning no limit. Affects incoming connections and prevents
 	// attempting outgoing connections.
-	ConnectionLimitMax int `json:"connectionLimitMax" xml:"connectionLimitMax"`
-	// When set, this allows TLS 1.2 on sync connections, where we otherwise
-	// default to TLS 1.3+ only.
-	InsecureAllowOldTLSVersions        bool `json:"insecureAllowOldTLSVersions" xml:"insecureAllowOldTLSVersions"`
-	ConnectionPriorityTCPLAN           int  `json:"connectionPriorityTcpLan" xml:"connectionPriorityTcpLan" default:"10"`
-	ConnectionPriorityQUICLAN          int  `json:"connectionPriorityQuicLan" xml:"connectionPriorityQuicLan" default:"20"`
-	ConnectionPriorityTCPWAN           int  `json:"connectionPriorityTcpWan" xml:"connectionPriorityTcpWan" default:"30"`
-	ConnectionPriorityQUICWAN          int  `json:"connectionPriorityQuicWan" xml:"connectionPriorityQuicWan" default:"40"`
-	ConnectionPriorityRelay            int  `json:"connectionPriorityRelay" xml:"connectionPriorityRelay" default:"50"`
-	ConnectionPriorityUpgradeThreshold int  `json:"connectionPriorityUpgradeThreshold" xml:"connectionPriorityUpgradeThreshold" default:"0"`
+	ConnectionLimitMax                 int `json:"connectionLimitMax" xml:"connectionLimitMax"`
+	ConnectionPriorityTCPLAN           int `json:"connectionPriorityTcpLan" xml:"connectionPriorityTcpLan" default:"10"`
+	ConnectionPriorityQUICLAN          int `json:"connectionPriorityQuicLan" xml:"connectionPriorityQuicLan" default:"20"`
+	ConnectionPriorityTCPWAN           int `json:"connectionPriorityTcpWan" xml:"connectionPriorityTcpWan" default:"30"`
+	ConnectionPriorityQUICWAN          int `json:"connectionPriorityQuicWan" xml:"connectionPriorityQuicWan" default:"40"`
+	ConnectionPriorityRelay            int `json:"connectionPriorityRelay" xml:"connectionPriorityRelay" default:"50"`
+	ConnectionPriorityUpgradeThreshold int `json:"connectionPriorityUpgradeThreshold" xml:"connectionPriorityUpgradeThreshold" default:"0"`
 	// Legacy deprecated
 	DeprecatedUPnPEnabled        bool     `json:"-" xml:"upnpEnabled,omitempty"`        // Deprecated: Do not use.
 	DeprecatedUPnPLeaseM         int      `json:"-" xml:"upnpLeaseMinutes,omitempty"`   // Deprecated: Do not use.
@@ -187,7 +186,7 @@ func (opts OptionsConfiguration) StunServers() []string {
 		case "default":
 			_, records, err := net.LookupSRV("stun", "udp", "syncthing.net")
 			if err != nil {
-				l.Warnln("Unable to resolve primary STUN servers via DNS:", err)
+				l.Debugf("Unable to resolve primary STUN servers via DNS:", err)
 			}
 
 			for _, record := range records {

+ 2 - 0
lib/config/testdata/overridenvalues.xml

@@ -45,6 +45,8 @@
         <unackedNotificationID>asdfasdf</unackedNotificationID>
         <announceLANAddresses>false</announceLANAddresses>
         <featureFlag>feature</featureFlag>
+        <auditEnabled>true</auditEnabled>
+        <auditFile>nggyu</auditFile>
         <connectionPriorityTcpLan>40</connectionPriorityTcpLan>
         <connectionPriorityQuicLan>45</connectionPriorityQuicLan>
         <connectionPriorityTcpWan>50</connectionPriorityTcpWan>

+ 5 - 32
lib/osutil/lowprio_linux.go

@@ -15,32 +15,6 @@ import (
 	"syscall"
 )
 
-const ioprioClassShift = 13
-
-type ioprioClass int
-
-const (
-	ioprioClassRT ioprioClass = iota + 1
-	ioprioClassBE
-	ioprioClassIdle
-)
-
-const (
-	ioprioWhoProcess = iota + 1
-	ioprioWhoPGRP
-	ioprioWhoUser
-)
-
-func ioprioSet(class ioprioClass, value int) error {
-	res, _, err := syscall.Syscall(syscall.SYS_IOPRIO_SET,
-		uintptr(ioprioWhoProcess), 0,
-		uintptr(class)<<ioprioClassShift|uintptr(value))
-	if res == 0 {
-		return nil
-	}
-	return err
-}
-
 // SetLowPriority lowers the process CPU scheduling priority, and possibly
 // I/O priority depending on the platform and OS.
 func SetLowPriority() error {
@@ -89,14 +63,13 @@ func SetLowPriority() error {
 		}
 	}
 
+	// For any new process, the default is to be assigned the IOPRIO_CLASS_BE
+	// scheduling class. This class directly maps the BE prio level to the
+	// niceness of a process, determined as: io_nice = (cpu_nice + 20) / 5.
+	// For example, a niceness of 11 results in an I/O priority of B6.
+	// https://www.kernel.org/doc/Documentation/block/ioprio.txt
 	if err := syscall.Setpriority(syscall.PRIO_PGRP, pidSelf, wantNiceLevel); err != nil {
 		return fmt.Errorf("set niceness: %w", err)
 	}
-
-	// Best effort, somewhere to the end of the scale (0 through 7 being the
-	// range).
-	if err := ioprioSet(ioprioClassBE, 5); err != nil {
-		return fmt.Errorf("set I/O priority: %w", err)
-	}
 	return nil
 }

+ 12 - 0
lib/osutil/net.go

@@ -8,6 +8,7 @@ package osutil
 
 import (
 	"net"
+	"strings"
 )
 
 // GetInterfaceAddrs returns the IP networks of all interfaces that are up.
@@ -46,6 +47,17 @@ func GetInterfaceAddrs(includePtP bool) ([]*net.IPNet, error) {
 	return nets, nil
 }
 
+func IPFromString(addr string) net.IP {
+	// strip the port
+	host, _, err := net.SplitHostPort(addr)
+	if err != nil {
+		host = addr
+	}
+	// strip IPv6 zone identifier
+	host, _, _ = strings.Cut(host, "%")
+	return net.ParseIP(host)
+}
+
 func IPFromAddr(addr net.Addr) (net.IP, error) {
 	switch a := addr.(type) {
 	case *net.TCPAddr:

+ 32 - 0
lib/osutil/osutil_test.go

@@ -135,3 +135,35 @@ func TestRenameOrCopy(t *testing.T) {
 		}
 	}
 }
+
+func TestIPFromString(t *testing.T) {
+	t.Parallel()
+
+	cases := []struct {
+		in  string
+		out string
+	}{
+		{"192.168.178.1", "192.168.178.1"},
+		{"192.168.178.1:8384", "192.168.178.1"},
+		{"fe80::20c:29ff:fe9a:46d2", "fe80::20c:29ff:fe9a:46d2"},
+		{"[fe80::20c:29ff:fe9a:46d2]:8384", "fe80::20c:29ff:fe9a:46d2"},
+		{"[fe80::20c:29ff:fe9a:46d2%eno1]:8384", "fe80::20c:29ff:fe9a:46d2"},
+		{"google.com", ""},
+		{"1.1.1.1.1", ""},
+		{"", ""},
+	}
+
+	for _, c := range cases {
+		ip := osutil.IPFromString(c.in)
+		var address string
+		if ip != nil {
+			address = ip.String()
+		} else {
+			address = ""
+		}
+
+		if c.out != address {
+			t.Fatalf("result should be %s != %s", c.out, address)
+		}
+	}
+}

+ 1 - 7
lib/syncthing/syncthing.go

@@ -251,13 +251,7 @@ func (a *App) startup() error {
 	// The TLS configuration is used for both the listening socket and outgoing
 	// connections.
 
-	var tlsCfg *tls.Config
-	if a.cfg.Options().InsecureAllowOldTLSVersions {
-		l.Infoln("TLS 1.2 is allowed on sync connections. This is less than optimally secure.")
-		tlsCfg = tlsutil.SecureDefaultWithTLS12()
-	} else {
-		tlsCfg = tlsutil.SecureDefaultTLS13()
-	}
+	tlsCfg := tlsutil.SecureDefaultTLS13()
 	tlsCfg.Certificates = []tls.Certificate{a.cert}
 	tlsCfg.NextProtos = []string{bepProtocolName}
 	tlsCfg.ClientAuth = tls.RequestClientCert

+ 1 - 1
man/stdiscosrv.1

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "STDISCOSRV" "1" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "STDISCOSRV" "1" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 stdiscosrv \- Syncthing Discovery Server
 .SH SYNOPSIS

+ 1 - 1
man/strelaysrv.1

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "STRELAYSRV" "1" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "STRELAYSRV" "1" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 strelaysrv \- Syncthing Relay Server
 .SH SYNOPSIS

+ 1 - 1
man/syncthing-bep.7

@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-BEP" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-BEP" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-bep \- Block Exchange Protocol v1
 .SH INTRODUCTION AND DEFINITIONS

+ 6 - 9
man/syncthing-config.5

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-CONFIG" "5" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-CONFIG" "5" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-config \- Syncthing Configuration
 .SH SYNOPSIS
@@ -221,7 +221,6 @@ may no longer correspond to the defaults.
         <sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
         <connectionLimitEnough>0</connectionLimitEnough>
         <connectionLimitMax>0</connectionLimitMax>
-        <insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
     </options>
     <remoteIgnoredDevice time=\(dq2022\-01\-09T20:02:01Z\(dq id=\(dq5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU\(dq name=\(dqbugger\(dq address=\(dq192.168.0.20:22000\(dq></remoteIgnoredDevice>
     <defaults>
@@ -1077,6 +1076,11 @@ header do not need this setting.
 When this setting is disabled, the GUI will not send 401 responses so users
 won’t see browser popups prompting for username and password.
 .UNINDENT
+.INDENT 0.0
+.TP
+.B metricsWithoutAuth
+If true, this allows access to the ‘/metrics’ without authentication.
+.UNINDENT
 .SH LDAP ELEMENT
 .INDENT 0.0
 .INDENT 3.5
@@ -1196,7 +1200,6 @@ Search filter for user searches.
     <sendFullIndexOnUpgrade>false</sendFullIndexOnUpgrade>
     <connectionLimitEnough>0</connectionLimitEnough>
     <connectionLimitMax>0</connectionLimitMax>
-    <insecureAllowOldTLSVersions>false</insecureAllowOldTLSVersions>
 </options>
 .EE
 .UNINDENT
@@ -1533,12 +1536,6 @@ no limit.  Affects incoming connections and prevents attempting outgoing
 connections.  The mechanism is described in detail in a \fI\%separate
 chapter\fP\&.
 .UNINDENT
-.INDENT 0.0
-.TP
-.B insecureAllowOldTLSVersions
-Only for compatibility with old versions of Syncthing on remote devices, as
-detailed in \fI\%insecureAllowOldTLSVersions\fP\&.
-.UNINDENT
 .SH DEFAULTS ELEMENT
 .INDENT 0.0
 .INDENT 3.5

+ 1 - 1
man/syncthing-device-ids.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-DEVICE-IDS" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-DEVICE-IDS" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-device-ids \- Understanding Device IDs
 .sp

+ 1 - 1
man/syncthing-event-api.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-EVENT-API" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-EVENT-API" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-event-api \- Event API
 .SH DESCRIPTION

+ 1 - 1
man/syncthing-faq.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-FAQ" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-FAQ" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-faq \- Frequently Asked Questions
 .INDENT 0.0

+ 1 - 1
man/syncthing-globaldisco.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-GLOBALDISCO" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-GLOBALDISCO" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-globaldisco \- Global Discovery Protocol v3
 .SH ANNOUNCEMENTS

+ 1 - 1
man/syncthing-localdisco.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-LOCALDISCO" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-LOCALDISCO" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-localdisco \- Local Discovery Protocol v4
 .SH MODE OF OPERATION

+ 1 - 1
man/syncthing-networking.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-NETWORKING" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-NETWORKING" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-networking \- Firewall Setup
 .SH ROUTER SETUP

+ 1 - 1
man/syncthing-relay.7

@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-RELAY" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-RELAY" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-relay \- Relay Protocol v1
 .SH WHAT IS A RELAY?

+ 2 - 3
man/syncthing-rest-api.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-REST-API" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-REST-API" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-rest-api \- REST API
 .sp
@@ -279,8 +279,7 @@ Returns the current configuration.
     \(dqsendFullIndexOnUpgrade\(dq: false,
     \(dqfeatureFlags\(dq: [],
     \(dqconnectionLimitEnough\(dq: 0,
-    \(dqconnectionLimitMax\(dq: 0,
-    \(dqinsecureAllowOldTLSVersions\(dq: false
+    \(dqconnectionLimitMax\(dq: 0
   },
   \(dqremoteIgnoredDevices\(dq: [
     {

+ 1 - 1
man/syncthing-security.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-SECURITY" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-SECURITY" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-security \- Security Principles
 .sp

+ 1 - 1
man/syncthing-stignore.5

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-STIGNORE" "5" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-STIGNORE" "5" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-stignore \- Prevent files from being synchronized to other nodes
 .SH SYNOPSIS

+ 1 - 1
man/syncthing-versioning.7

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING-VERSIONING" "7" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING-VERSIONING" "7" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing-versioning \- Keep automatic backups of deleted files by other nodes
 .sp

+ 1 - 1
man/syncthing.1

@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "SYNCTHING" "1" "Apr 04, 2025" "v1.29.3" "Syncthing"
+.TH "SYNCTHING" "1" "Apr 21, 2025" "v1.29.3" "Syncthing"
 .SH NAME
 syncthing \- Syncthing
 .SH SYNOPSIS

+ 489 - 0
script/copyrights.go

@@ -0,0 +1,489 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//go:build ignore
+// +build ignore
+
+// Updates the list of software copyrights in aboutModalView.html based on the
+// output of `go mod graph`.
+
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"regexp"
+	"slices"
+	"strconv"
+	"strings"
+	"time"
+
+	"golang.org/x/net/html"
+)
+
+var copyrightMap = map[string]string{
+	// https://github.com/aws/aws-sdk-go/blob/main/NOTICE.txt#L2
+	"aws/aws-sdk-go": "Copyright &copy; 2015 Amazon.com, Inc. or its affiliates, Copyright 2014-2015 Stripe, Inc",
+	// https://github.com/ccding/go-stun/blob/master/main.go#L1
+	"ccding/go-stun": "Copyright &copy; 2016 Cong Ding",
+	// https://github.com/search?q=repo%3Acertifi%2Fgocertifi%20copyright&type=code
+	// "certifi/gocertifi": "No copyrights found",
+	// https://github.com/search?q=repo%3Aebitengine%2Fpurego%20copyright&type=code
+	"ebitengine/purego": "Copyright &copy; 2022 The Ebitengine Authors",
+	// https://github.com/search?q=repo%3Agoogle%2Fpprof%20copyright&type=code
+	"google/pprof": "Copyright &copy; 2016 Google Inc",
+	// https://github.com/greatroar/blobloom/blob/master/README.md?plain=1#L74
+	"greatroar/blobloom": "Copyright &copy; 2020-2024 the Blobloom authors",
+	// https://github.com/jmespath/go-jmespath/blob/master/NOTICE#L2
+	"jmespath/go-jmespath": "Copyright &copy; 2015 James Saryerwinnie",
+	// https://github.com/maxmind/geoipupdate/blob/main/README.md?plain=1#L140
+	"maxmind/geoipupdate": "Copyright &copy; 2018-2024 by MaxMind, Inc",
+	// https://github.com/search?q=repo%3Apuzpuzpuz%2Fxsync%20copyright&type=code
+	// "puzpuzpuz/xsync": "No copyrights found",
+	// https://github.com/search?q=repo%3Atklauser%2Fnumcpus%20copyright&type=code
+	"tklauser/numcpus": "Copyright &copy; 2018-2024 Tobias Klauser",
+	// https://github.com/search?q=repo%3Auber-go%2Fmock%20copyright&type=code
+	"go.uber.org/mock": "Copyright &copy; 2010-2022 Google LLC",
+}
+
+var urlMap = map[string]string{
+	"fontawesome.io":             "https://github.com/FortAwesome/Font-Awesome",
+	"go.uber.org/automaxprocs":   "https://github.com/uber-go/automaxprocs",
+	"go.uber.org/mock":           "https://github.com/uber-go/mock",
+	"google.golang.org/protobuf": "https://github.com/protocolbuffers/protobuf-go",
+	"gopkg.in/yaml.v2":           "", // ignore, as gopkg.in/yaml.v3 supersedes
+	"gopkg.in/yaml.v3":           "https://github.com/go-yaml/yaml",
+	"sigs.k8s.io/yaml":           "https://github.com/kubernetes-sigs/yaml",
+}
+
+const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
+
+type Type int
+
+const (
+	// TypeJS defines non-Go copyright notices
+	TypeJS Type = iota
+	// TypeKeep defines Go copyright notices for packages that are still used.
+	TypeKeep
+	// TypeToss defines Go copyright notices for packages that are no longer used.
+	TypeToss
+	// TypeNew defines Go copyright notices for new packages found via `go mod graph`.
+	TypeNew
+)
+
+type CopyrightNotice struct {
+	Type           Type
+	Name           string
+	HTML           string
+	Module         string
+	URL            string
+	Copyright      string
+	RepoURL        string
+	RepoCopyrights []string
+}
+
+var copyrightRe = regexp.MustCompile(`(?s)id="copyright-notices">(.+?)</ul>`)
+
+func main() {
+	bs := readAll(htmlFile)
+	matches := copyrightRe.FindStringSubmatch(string(bs))
+
+	if len(matches) <= 1 {
+		log.Fatal("Cannot find id copyright-notices in ", htmlFile)
+	}
+
+	modules := getModules()
+
+	notices := parseCopyrightNotices(matches[1])
+	old := len(notices)
+
+	// match up modules to notices
+	matched := map[string]bool{}
+	removes := 0
+	for i, notice := range notices {
+		if notice.Type == TypeJS {
+			continue
+		}
+		found := ""
+		for _, module := range modules {
+			if strings.Contains(module, notice.Name) {
+				found = module
+
+				break
+			}
+		}
+		if found != "" {
+			matched[found] = true
+			notices[i].Module = found
+
+			continue
+		}
+		removes++
+		fmt.Printf("Removing: %-40s %-55s %s\n", notice.Name, notice.URL, notice.Copyright)
+		notices[i].Type = TypeToss
+	}
+
+	// add new modules to notices
+	adds := 0
+	for _, module := range modules {
+		_, ok := matched[module]
+		if ok {
+			continue
+		}
+
+		adds++
+		notice := CopyrightNotice{}
+		notice.Name = module
+		if strings.HasPrefix(notice.Name, "github.com/") {
+			notice.Name = strings.ReplaceAll(notice.Name, "github.com/", "")
+		}
+		notice.Type = TypeNew
+
+		url, ok := urlMap[module]
+		if ok {
+			notice.URL = url
+			notice.RepoURL = url
+		} else {
+			notice.URL = "https://" + module
+			notice.RepoURL = "https://" + module
+		}
+		notices = append(notices, notice)
+	}
+
+	if removes == 0 && adds == 0 {
+		// authors.go is quiet, so let's be quiet too.
+		// fmt.Printf("No changes detected in %d modules and %d notices\n", len(modules), len(notices))
+		os.Exit(0)
+	}
+
+	// get copyrights via Github API for new modules
+	notfound := 0
+	for i, n := range notices {
+		if n.Type != TypeNew {
+			continue
+		}
+		copyright, ok := copyrightMap[n.Name]
+		if ok {
+			notices[i].Copyright = copyright
+
+			continue
+		}
+		notices[i].Copyright = defaultCopyright(n)
+
+		if strings.Contains(n.URL, "github.com/") {
+			notices[i].RepoURL = notices[i].URL
+			owner, repo := parseGitHubURL(n.URL)
+			licenseText := getLicenseText(owner, repo)
+			notices[i].RepoCopyrights = extractCopyrights(licenseText, n)
+
+			if len(notices[i].RepoCopyrights) > 0 {
+				notices[i].Copyright = notices[i].RepoCopyrights[0]
+			}
+
+			notices[i].HTML = fmt.Sprintf("<li><a href=\"%s\">%s</a>, %s.</li>", n.URL, n.Name, notices[i].Copyright)
+			if len(notices[i].RepoCopyrights) > 0 {
+				continue
+			}
+		}
+		fmt.Printf("Copyright not found: %-30s : using %q\n", n.Name, notices[i].Copyright)
+		notfound++
+	}
+
+	replacements := write(notices, bs)
+	fmt.Printf("Removed:              %3d\n", removes)
+	fmt.Printf("Added:                %3d\n", adds)
+	fmt.Printf("Copyrights not found: %3d\n", notfound)
+	fmt.Printf("Old package count:    %3d\n", old)
+	fmt.Printf("New package count:    %3d\n", replacements)
+}
+
+func write(notices []CopyrightNotice, bs []byte) int {
+	keys := make([]string, 0, len(notices))
+
+	noticeMap := make(map[string]CopyrightNotice, 0)
+
+	for _, n := range notices {
+		if n.Type != TypeKeep && n.Type != TypeNew {
+			continue
+		}
+		if n.Type == TypeNew {
+			fmt.Printf("Adding: %-40s %-55s %s\n", n.Name, n.URL, n.Copyright)
+		}
+		keys = append(keys, n.Name)
+		noticeMap[n.Name] = n
+	}
+
+	slices.Sort(keys)
+
+	indent := "          "
+	replacements := []string{}
+	for _, n := range notices {
+		if n.Type != TypeJS {
+			continue
+		}
+		replacements = append(replacements, indent+n.HTML)
+	}
+
+	for _, k := range keys {
+		n := noticeMap[k]
+		line := fmt.Sprintf("%s<li><a href=\"%s\">%s</a>, %s.</li>", indent, n.URL, n.Name, n.Copyright)
+		replacements = append(replacements, line)
+	}
+	replacement := strings.Join(replacements, "\n")
+
+	bs = copyrightRe.ReplaceAll(bs, []byte("id=\"copyright-notices\">\n"+replacement+"\n        </ul>"))
+	writeFile(htmlFile, string(bs))
+
+	return len(replacements)
+}
+
+func readAll(path string) []byte {
+	fd, err := os.Open(path)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer fd.Close()
+
+	bs, err := io.ReadAll(fd)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return bs
+}
+
+func writeFile(path string, data string) {
+	err := os.WriteFile(path, []byte(data), 0o644)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func getModules() []string {
+	cmd := exec.Command("go", "mod", "graph")
+	output, err := cmd.Output()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	seen := make(map[string]struct{})
+	scanner := bufio.NewScanner(bytes.NewReader(output))
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		fields := strings.Fields(line)
+		if len(fields) == 0 {
+			continue
+		}
+
+		if !strings.HasPrefix(fields[0], "github.com/syncthing/syncthing") {
+			continue
+		}
+
+		// Get left-hand side of dependency pair (before '@')
+		mod := strings.SplitN(fields[1], "@", 2)[0]
+
+		// Keep only first 3 path components
+		parts := strings.Split(mod, "/")
+		if len(parts) == 1 {
+			continue
+		}
+		short := strings.Join(parts[:min(len(parts), 3)], "/")
+
+		if strings.HasPrefix(short, "golang.org/x") ||
+			strings.HasPrefix(short, "github.com/prometheus") ||
+			short == "go" {
+
+			continue
+		}
+
+		seen[short] = struct{}{}
+	}
+
+	adds := make([]string, 0)
+	for k := range seen {
+		adds = append(adds, k)
+	}
+
+	slices.Sort(adds)
+
+	return adds
+}
+
+func parseCopyrightNotices(input string) []CopyrightNotice {
+	doc, err := html.Parse(strings.NewReader("<ul>" + input + "</ul>"))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	var notices []CopyrightNotice
+
+	typ := TypeJS
+
+	var f func(*html.Node)
+	f = func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "li" {
+			var notice CopyrightNotice
+			var aFound bool
+
+			for c := n.FirstChild; c != nil; c = c.NextSibling {
+				if c.Type == html.ElementNode && c.Data == "a" {
+					aFound = true
+					for _, attr := range c.Attr {
+						if attr.Key == "href" {
+							notice.URL = attr.Val
+						}
+					}
+					if c.FirstChild != nil && c.FirstChild.Type == html.TextNode {
+						notice.Name = strings.TrimSpace(c.FirstChild.Data)
+					}
+				} else if c.Type == html.TextNode && aFound {
+					// Anything after <a> is considered the copyright
+					notice.Copyright = strings.TrimSpace(html.UnescapeString(c.Data))
+					notice.Copyright = strings.Trim(notice.Copyright, "., ")
+				}
+				if typ == TypeJS && strings.Contains(notice.URL, "AudriusButkevicius") {
+					typ = TypeKeep
+				}
+				notice.Type = typ
+				var buf strings.Builder
+				_ = html.Render(&buf, n)
+				notice.HTML = buf.String()
+			}
+
+			notice.Copyright = strings.ReplaceAll(notice.Copyright, "©", "&copy;")
+			notice.HTML = strings.ReplaceAll(notice.HTML, "©", "&copy;")
+			notices = append(notices, notice)
+		}
+		for c := n.FirstChild; c != nil; c = c.NextSibling {
+			f(c)
+		}
+	}
+
+	f(doc)
+
+	return notices
+}
+
+func parseGitHubURL(u string) (string, string) {
+	parsed, err := url.Parse(u)
+	if err != nil {
+		log.Fatal(err)
+	}
+	parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
+	if len(parts) < 2 {
+		log.Fatal(fmt.Errorf("invalid GitHub URL: %q", parsed.Path))
+	}
+
+	return parts[0], parts[1]
+}
+
+func getLicenseText(owner, repo string) string {
+	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/license", owner, repo)
+	req, _ := http.NewRequest("GET", url, nil)
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	var result struct {
+		Content  string `json:"content"`
+		Encoding string `json:"encoding"`
+	}
+	body, _ := io.ReadAll(resp.Body)
+	err = json.Unmarshal(body, &result)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if result.Encoding != "base64" {
+		log.Fatal(fmt.Sprintf("unexpected encoding: %s", result.Encoding))
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(result.Content)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return string(decoded)
+}
+
+func extractCopyrights(license string, notice CopyrightNotice) []string {
+	lines := strings.Split(license, "\n")
+
+	re := regexp.MustCompile(`(?i)^\s*(copyright\s*(?:©|\(c\)|&copy;|19|20).*)$`)
+
+	copyrights := []string{}
+
+	for _, line := range lines {
+		if matches := re.FindStringSubmatch(strings.TrimSpace(line)); len(matches) == 2 {
+			copyright := strings.TrimSpace(matches[1])
+			re := regexp.MustCompile(`(?i)all rights reserved`)
+			copyright = re.ReplaceAllString(copyright, "")
+			copyright = strings.ReplaceAll(copyright, "©", "&copy;")
+			copyright = strings.ReplaceAll(copyright, "(C)", "&copy;")
+			copyright = strings.ReplaceAll(copyright, "(c)", "&copy;")
+			copyright = strings.Trim(copyright, "., ")
+			copyrights = append(copyrights, copyright)
+		}
+	}
+
+	if len(copyrights) > 0 {
+		return copyrights
+	}
+
+	return []string{}
+}
+
+func defaultCopyright(n CopyrightNotice) string {
+	year := time.Now().Format("2006")
+
+	return fmt.Sprintf("Copyright &copy; %v, the %s authors", year, n.Name)
+}
+
+func writeNotices(path string, notices []CopyrightNotice) {
+	s := ""
+	for i, n := range notices {
+		s += "#        : " + strconv.Itoa(i) + "\n" + n.String()
+	}
+	writeFile(path, s)
+}
+
+func (n CopyrightNotice) String() string {
+	return fmt.Sprintf("Type     : %v\nHTML     : %v\nName     : %v\nModule   : %v\nURL      : %v\nCopyright: %v\nRepoURL  : %v\nRepoCopys: %v\n\n",
+		n.Type, n.HTML, n.Name, n.Module, n.URL, n.Copyright, n.RepoURL, strings.Join(n.RepoCopyrights, ","))
+}
+
+func (t Type) String() string {
+	switch t {
+	case TypeJS:
+		return "TypeJS"
+	case TypeKeep:
+		return "TypeKeep"
+	case TypeToss:
+		return "TypeToss"
+	case TypeNew:
+		return "TypeNew"
+	default:
+		return "unknown"
+	}
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно