Compare commits

..

2 Commits

Author SHA1 Message Date
Yvan Wang
cd9ea51039 fix(hub): use rfcEmail validator to allow IDN/Punycode email addresses (#1935)
valibot's email() action uses a domain-label regex that disallows the
-- sequence, so Punycode ACE labels like xn--mnchen-3ya.de (the ASCII
form of münchen.de) are incorrectly rejected.

Switching to rfcEmail() applies the RFC 5321 domain-label pattern,
which allows hyphens within labels and therefore accepts both standard
and internationalized domain names.
2026-04-18 12:23:14 -04:00
Uğur Tafralı
a71617e058 feat(agent): Add EXIT_ON_DNS_ERROR environment variable (#1929)
Co-authored-by: henrygd <hank@henrygd.me>
2026-04-17 19:26:11 -04:00
6 changed files with 139 additions and 32 deletions

View File

@@ -4,11 +4,15 @@ import (
"context" "context"
"errors" "errors"
"log/slog" "log/slog"
"net"
"os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
"github.com/henrygd/beszel/agent/health" "github.com/henrygd/beszel/agent/health"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
) )
@@ -111,11 +115,34 @@ func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
_ = health.Update() _ = health.Update()
case <-sigCtx.Done(): case <-sigCtx.Done():
slog.Info("Shutting down", "cause", context.Cause(sigCtx)) slog.Info("Shutting down", "cause", context.Cause(sigCtx))
return c.stop()
}
}
}
// stop does not stop the connection manager itself, just any active connections. The manager will attempt to reconnect after stopping, so this should only be called immediately before shutting down the entire agent.
//
// If we need or want to expose a graceful Stop method in the future, do something like this to actually stop the manager:
//
// func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
// ctx, cancel := context.WithCancel(context.Background())
// c.cancel = cancel
//
// for {
// select {
// case <-ctx.Done():
// return c.stop()
// }
// }
// }
//
// func (c *ConnectionManager) Stop() {
// c.cancel()
// }
func (c *ConnectionManager) stop() error {
_ = c.agent.StopServer() _ = c.agent.StopServer()
c.closeWebSocket() c.closeWebSocket()
return health.CleanUp() return health.CleanUp()
}
}
} }
// handleEvent processes connection events and updates the connection state accordingly. // handleEvent processes connection events and updates the connection state accordingly.
@@ -185,10 +212,17 @@ func (c *ConnectionManager) connect() {
// Try WebSocket first, if it fails, start SSH server // Try WebSocket first, if it fails, start SSH server
err := c.startWebSocketConnection() err := c.startWebSocketConnection()
if err != nil && c.State == Disconnected { if err != nil {
if shouldExitOnErr(err) {
time.Sleep(2 * time.Second) // prevent tight restart loop
_ = c.stop()
os.Exit(1)
}
if c.State == Disconnected {
c.startSSHServer() c.startSSHServer()
c.startWsTicker() c.startWsTicker()
} }
}
} }
// startWebSocketConnection attempts to establish a WebSocket connection to the hub. // startWebSocketConnection attempts to establish a WebSocket connection to the hub.
@@ -224,3 +258,14 @@ func (c *ConnectionManager) closeWebSocket() {
c.wsClient.Close() c.wsClient.Close()
} }
} }
// shouldExitOnErr checks if the error is a DNS resolution failure and if the
// EXIT_ON_DNS_ERROR env var is set. https://github.com/henrygd/beszel/issues/1924.
func shouldExitOnErr(err error) bool {
if val, _ := utils.GetEnv("EXIT_ON_DNS_ERROR"); val == "true" {
if opErr, ok := errors.AsType[*net.OpError](err); ok {
return strings.Contains(opErr.Err.Error(), "lookup")
}
}
return false
}

View File

@@ -4,6 +4,7 @@ package agent
import ( import (
"crypto/ed25519" "crypto/ed25519"
"errors"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@@ -298,3 +299,65 @@ func TestConnectionManager_ConnectFlow(t *testing.T) {
cm.connect() cm.connect()
}, "Connect should not panic without WebSocket client") }, "Connect should not panic without WebSocket client")
} }
func TestShouldExitOnErr(t *testing.T) {
createDialErr := func(msg string) error {
return &net.OpError{
Op: "dial",
Net: "tcp",
Err: errors.New(msg),
}
}
tests := []struct {
name string
err error
envValue string
expected bool
}{
{
name: "no env var",
err: createDialErr("lookup lkahsdfasdf: no such host"),
envValue: "",
expected: false,
},
{
name: "env var false",
err: createDialErr("lookup lkahsdfasdf: no such host"),
envValue: "false",
expected: false,
},
{
name: "env var true, matching error",
err: createDialErr("lookup lkahsdfasdf: no such host"),
envValue: "true",
expected: true,
},
{
name: "env var true, matching error with extra context",
err: createDialErr("lookup beszel.server.lan on [::1]:53: read udp [::1]:44557->[::1]:53: read: connection refused"),
envValue: "true",
expected: true,
},
{
name: "env var true, non-matching error",
err: errors.New("connection refused"),
envValue: "true",
expected: false,
},
{
name: "env var true, dial but not lookup",
err: createDialErr("connection timeout"),
envValue: "true",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("EXIT_ON_DNS_ERROR", tt.envValue)
result := shouldExitOnErr(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -195,6 +195,6 @@ func main() {
} }
if err := a.Start(serverConfig); err != nil { if err := a.Start(serverConfig); err != nil {
log.Fatal("Failed to start server: ", err) log.Fatal("Failed to start: ", err)
} }
} }

View File

@@ -17,7 +17,7 @@ import { toast } from "../ui/use-toast"
import { OtpInputForm } from "./otp-forms" import { OtpInputForm } from "./otp-forms"
const honeypot = v.literal("") const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`)) const emailSchema = v.pipe(v.string(), v.rfcEmail(t`Invalid email address.`))
const passwordSchema = v.pipe( const passwordSchema = v.pipe(
v.string(), v.string(),
v.minLength(8, t`Password must be at least 8 characters.`), v.minLength(8, t`Password must be at least 8 characters.`),

View File

@@ -24,7 +24,7 @@ interface ShoutrrrUrlCardProps {
} }
const NotificationSchema = v.object({ const NotificationSchema = v.object({
emails: v.array(v.pipe(v.string(), v.email())), emails: v.array(v.pipe(v.string(), v.rfcEmail())),
webhooks: v.array(v.pipe(v.string(), v.url())), webhooks: v.array(v.pipe(v.string(), v.url())),
}) })

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n" "Language: it\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-04-17 09:26\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -57,7 +57,7 @@ msgstr "1 ora"
#. Load average #. Load average
#: src/components/routes/system/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 minute" msgid "1 minute"
@@ -74,7 +74,7 @@ msgstr "12 ore"
#. Load average #. Load average
#: src/components/routes/system/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -87,7 +87,7 @@ msgstr "30 giorni"
#. Load average #. Load average
#: src/components/routes/system/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 min" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -248,7 +248,7 @@ msgstr "Larghezza di banda"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Batt" msgstr ""
#: src/components/routes/system/charts/sensor-charts.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
@@ -336,7 +336,7 @@ msgstr "Attenzione - possibile perdita di dati"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -490,13 +490,13 @@ msgstr "Copia YAML"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgctxt "Core system metrics" msgctxt "Core system metrics"
msgid "Core" msgid "Core"
msgstr "Interne" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -624,7 +624,7 @@ msgstr "Utilizzo del disco di {extraFsName}"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options" msgctxt "Layout display options"
msgid "Display" msgid "Display"
msgstr "Display" msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
@@ -677,7 +677,7 @@ msgstr "Modifica {foo}"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx #: src/components/login/otp-forms.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Email notifications" msgid "Email notifications"
@@ -772,7 +772,7 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -824,7 +824,7 @@ msgstr "Impronta digitale"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "Firmware" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -858,7 +858,7 @@ msgstr "Globale"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU" msgid "GPU"
msgstr "GPU" msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
@@ -883,7 +883,7 @@ msgstr "Stato"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Hearthbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -901,7 +901,7 @@ msgstr "Comando Homebrew"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Host / IP" msgid "Host / IP"
msgstr "Host / IP" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method" msgid "HTTP Method"
@@ -1043,7 +1043,7 @@ msgstr "Istruzioni di configurazione manuale"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system/chart-card.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Max 1 min" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
@@ -1109,7 +1109,7 @@ msgstr "Unità rete"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "No" msgid "No"
msgstr "No" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
@@ -1196,7 +1196,7 @@ msgstr "Pagine / Impostazioni"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Password" msgid "Password"
msgstr "Password" msgstr ""
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Password must be at least 8 characters." msgid "Password must be at least 8 characters."
@@ -1384,7 +1384,7 @@ msgstr "Riprendi"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label" msgctxt "Root disk label"
msgid "Root" msgid "Root"
msgstr "Root" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token" msgid "Rotate token"
@@ -1615,11 +1615,11 @@ msgstr "Temperature dei sensori di sistema"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>" msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat" msgid "Test heartbeat"
msgstr "Test Heartbeat" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test notification sent" msgid "Test notification sent"
@@ -1665,7 +1665,7 @@ msgstr "Attiva/disattiva tema"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1931,4 +1931,3 @@ msgstr "Sì"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated." msgid "Your user settings have been updated."
msgstr "Le impostazioni utente sono state aggiornate." msgstr "Le impostazioni utente sono state aggiornate."