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
This commit is contained in:
Jakob Borg
2025-04-24 08:47:52 +07:00
31 changed files with 716 additions and 109 deletions
+48 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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