diff --git a/VERSION b/VERSION
index 3e9c4a6..73b4678 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.50
+1.0.51
diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go
index 7808ed1..7bd6a53 100644
--- a/cmd/edgeguard-api/main.go
+++ b/cmd/edgeguard-api/main.go
@@ -48,7 +48,7 @@ import (
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
)
-var version = "1.0.50"
+var version = "1.0.51"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")
diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go
index b95fd04..fda2eb4 100644
--- a/cmd/edgeguard-ctl/main.go
+++ b/cmd/edgeguard-ctl/main.go
@@ -9,7 +9,7 @@ import (
"os"
)
-var version = "1.0.50"
+var version = "1.0.51"
const usage = `edgeguard-ctl — EdgeGuard CLI
diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go
index 57d567b..91d56bc 100644
--- a/cmd/edgeguard-scheduler/main.go
+++ b/cmd/edgeguard-scheduler/main.go
@@ -24,7 +24,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
-var version = "1.0.50"
+var version = "1.0.51"
const (
// renewTickInterval — how often we re-evaluate expiring certs.
diff --git a/internal/database/migrations/0017_backend_websocket.sql b/internal/database/migrations/0017_backend_websocket.sql
new file mode 100644
index 0000000..d9640f3
--- /dev/null
+++ b/internal/database/migrations/0017_backend_websocket.sql
@@ -0,0 +1,28 @@
+-- +goose Up
+-- +goose StatementBegin
+
+-- Per-Backend WebSocket-Toggle. Wenn aktiv rendert der HAProxy-Renderer
+-- `timeout tunnel 1h` IN dem Backend-Block, sodass HTTP-Upgrade-
+-- Verbindungen (noVNC, VS-Code-SSH, AsyncAPI-Streams) nicht vom
+-- defaults-Timeout-Server=60s gekappt werden. Backends ohne WS-Workload
+-- behalten kurze Timeouts — gut für Connection-Hygiene.
+
+ALTER TABLE backends
+ ADD COLUMN IF NOT EXISTS websocket BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- Erhalte das alte Verhalten für bekannte WebSocket-Pools: alles was
+-- Proxmox / vm-Pool / Konsole im Namen hat bekommt websocket=true.
+-- Falls der Operator das nicht will → einfach im UI ausschalten.
+UPDATE backends
+ SET websocket = TRUE
+ WHERE name ILIKE '%vm%pool%'
+ OR name ILIKE '%proxmox%'
+ OR name ILIKE '%console%'
+ OR name ILIKE '%vnc%';
+
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+ALTER TABLE backends DROP COLUMN IF EXISTS websocket;
+-- +goose StatementEnd
diff --git a/internal/haproxy/haproxy.cfg.tpl b/internal/haproxy/haproxy.cfg.tpl
index aa728fb..aa86b86 100644
--- a/internal/haproxy/haproxy.cfg.tpl
+++ b/internal/haproxy/haproxy.cfg.tpl
@@ -21,10 +21,9 @@ defaults
timeout client 60s
timeout server 60s
timeout http-request 10s
- # WebSocket/long-poll: nach HTTP-Upgrade greift `timeout tunnel`
- # statt client/server-timeout. 1h hält Proxmox-noVNC + VS-Code-
- # SSH-Tunnel + andere langlebige Streams offen.
- timeout tunnel 1h
+ # timeout tunnel wird PER BACKEND aktiviert (websocket-Flag),
+ # damit nur WS-Workloads die lange Idle-Toleranz haben und normale
+ # HTTP-Backends saubere Connection-Hygiene behalten.
# ── Public :80 ─────────────────────────────────────────────────────────
# ACME-01 challenges proxy to edgeguard-api which serves the webroot.
@@ -91,6 +90,9 @@ backend api_backend
backend eg_backend_{{$b.ID}}
balance {{$b.LBAlgorithm}}
+ {{- if $b.WebSocket}}
+ timeout tunnel 1h
+ {{- end}}
{{- if $b.HealthCheckPath}}
option httpchk
http-check send meth GET uri {{$b.HealthCheckPath}}
diff --git a/internal/haproxy/haproxy_test.go b/internal/haproxy/haproxy_test.go
index 586e59e..91a7aa5 100644
--- a/internal/haproxy/haproxy_test.go
+++ b/internal/haproxy/haproxy_test.go
@@ -102,6 +102,42 @@ func TestRender_HealthCheckPathAddsCheckInter(t *testing.T) {
}
}
+func TestRender_WebSocketEmitsTunnelTimeout(t *testing.T) {
+ v := View{
+ Backends: []BackendView{
+ {
+ Backend: models.Backend{ID: 7, Name: "vmm", Scheme: "https",
+ LBAlgorithm: "source", WebSocket: true, Active: true},
+ Servers: []models.BackendServer{
+ {BackendID: 7, Name: "vmm-1", Address: "10.0.5.14", Port: 8006, Weight: 100, Active: true},
+ },
+ },
+ {
+ Backend: models.Backend{ID: 8, Name: "api", Scheme: "http",
+ LBAlgorithm: "roundrobin", WebSocket: false, Active: true},
+ Servers: []models.BackendServer{
+ {BackendID: 8, Name: "api-1", Address: "10.0.5.20", Port: 8080, Weight: 100, Active: true},
+ },
+ },
+ },
+ }
+ out := renderView(t, v)
+ // vmm soll tunnel-Timeout haben, api nicht.
+ idxVmm := strings.Index(out, "backend eg_backend_7")
+ idxApi := strings.Index(out, "backend eg_backend_8")
+ if idxVmm < 0 || idxApi < 0 {
+ t.Fatalf("backend sections missing in output:\n%s", out)
+ }
+ vmmBlock := out[idxVmm:idxApi]
+ apiBlock := out[idxApi:]
+ if !strings.Contains(vmmBlock, "timeout tunnel 1h") {
+ t.Errorf("vmm-Block sollte `timeout tunnel 1h` enthalten:\n%s", vmmBlock)
+ }
+ if strings.Contains(apiBlock, "timeout tunnel") {
+ t.Errorf("api-Block soll KEIN `timeout tunnel` enthalten:\n%s", apiBlock)
+ }
+}
+
func TestRender_MultiServerPool(t *testing.T) {
v := View{
Backends: []BackendView{
diff --git a/internal/models/backend.go b/internal/models/backend.go
index fe9bd4a..771e325 100644
--- a/internal/models/backend.go
+++ b/internal/models/backend.go
@@ -10,6 +10,7 @@ type Backend struct {
Scheme string `gorm:"column:scheme" json:"scheme"`
HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"`
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
+ WebSocket bool `gorm:"column:websocket" json:"websocket"`
Active bool `gorm:"column:active" json:"active"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
diff --git a/internal/services/backends/backends.go b/internal/services/backends/backends.go
index 12afa8e..d877f65 100644
--- a/internal/services/backends/backends.go
+++ b/internal/services/backends/backends.go
@@ -1,7 +1,7 @@
// Package backends implements CRUD against the `backends` table.
-// Ein Backend ist ein Pool — Name, Scheme, Healthcheck, LB-Algorithm.
-// Die konkreten Upstream-Server liegen in backend_servers (siehe
-// services/backendservers).
+// Ein Backend ist ein Pool — Name, Scheme, Healthcheck, LB-Algorithm,
+// WebSocket-Flag. Die konkreten Upstream-Server liegen in
+// backend_servers (siehe services/backendservers).
package backends
import (
@@ -23,7 +23,7 @@ type Repo struct {
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = `
-SELECT id, name, scheme, health_check_path, lb_algorithm, active,
+SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, active,
created_at, updated_at
FROM backends
`
@@ -62,11 +62,11 @@ func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, e
b.LBAlgorithm = "roundrobin"
}
row := r.Pool.QueryRow(ctx, `
-INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, active)
-VALUES ($1, $2, $3, $4, $5)
-RETURNING id, name, scheme, health_check_path, lb_algorithm, active,
+INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, active)
+VALUES ($1, $2, $3, $4, $5, $6)
+RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active,
created_at, updated_at`,
- b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.Active)
+ b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active)
return scanBackend(row)
}
@@ -80,12 +80,13 @@ UPDATE backends SET
scheme = $2,
health_check_path = $3,
lb_algorithm = $4,
- active = $5,
+ websocket = $5,
+ active = $6,
updated_at = NOW()
-WHERE id = $6
-RETURNING id, name, scheme, health_check_path, lb_algorithm, active,
+WHERE id = $7
+RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active,
created_at, updated_at`,
- b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.Active, id)
+ b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active, id)
out, err := scanBackend(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
@@ -111,7 +112,7 @@ func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
var b models.Backend
if err := row.Scan(
&b.ID, &b.Name, &b.Scheme,
- &b.HealthCheckPath, &b.LBAlgorithm, &b.Active,
+ &b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.Active,
&b.CreatedAt, &b.UpdatedAt,
); err != nil {
return nil, err
diff --git a/management-ui/src/components/Layout/Sidebar.tsx b/management-ui/src/components/Layout/Sidebar.tsx
index 5ce9cc8..b866f83 100644
--- a/management-ui/src/components/Layout/Sidebar.tsx
+++ b/management-ui/src/components/Layout/Sidebar.tsx
@@ -77,7 +77,7 @@ const NAV: NavSection[] = [
},
]
-const VERSION = '1.0.50'
+const VERSION = '1.0.51'
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
const { t } = useTranslation()
diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json
index d2149b8..7696599 100644
--- a/management-ui/src/i18n/locales/de/common.json
+++ b/management-ui/src/i18n/locales/de/common.json
@@ -217,6 +217,8 @@
"selectDomains": "Domains wählen",
"lbAlgo": "Load-Balancing",
"lbAlgoHint": "roundrobin = gleichmäßig, leastconn = an den Server mit wenigsten Verbindungen, source = sticky per Client-IP (für stateful Apps ohne shared session).",
+ "websocket": "WebSocket-Support",
+ "websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
"servers": "Server",
"noServers": "kein Server",
"nServers": "{{n}} Server",
diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json
index 06fcd43..e6a01de 100644
--- a/management-ui/src/i18n/locales/en/common.json
+++ b/management-ui/src/i18n/locales/en/common.json
@@ -217,6 +217,8 @@
"selectDomains": "Select domains",
"lbAlgo": "Load balancing",
"lbAlgoHint": "roundrobin = evenly, leastconn = pick the server with fewest active connections, source = sticky per client-IP hash (for stateful apps without shared session).",
+ "websocket": "WebSocket support",
+ "websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
"servers": "Servers",
"noServers": "no server",
"nServers": "{{n}} servers",
diff --git a/management-ui/src/pages/Backends/index.tsx b/management-ui/src/pages/Backends/index.tsx
index 9640559..a41fa17 100644
--- a/management-ui/src/pages/Backends/index.tsx
+++ b/management-ui/src/pages/Backends/index.tsx
@@ -22,6 +22,7 @@ interface Backend {
scheme: string
health_check_path?: string | null
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
+ websocket: boolean
active: boolean
created_at: string
updated_at: string
@@ -43,6 +44,7 @@ interface BackendFormValues {
scheme: 'http' | 'https'
health_check_path?: string
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
+ websocket: boolean
active: boolean
domain_ids?: number[]
}
@@ -193,7 +195,12 @@ export default function BackendsPage() {
},
{
title: t('backends.lbAlgo'), dataIndex: 'lb_algorithm', key: 'lb',
- render: (v: string) =>