Add per-subsystem config validation for wireguard, authelia, nftables, tunnel

- wireguard: ValidateConfig checks ListenPort range, Address/LanCIDR CIDR
  format. SaveConfig now validates before writing.
- authelia: ValidateConfigOpts checks required fields (Domain, JWTSecret,
  SessionSecret) and StorageEncKey minimum length (20 chars). GenerateConfig
  now validates before generating.
- nftables: ValidateWANInterface checks interface exists via net.InterfaceByName
  before generating rules that reference it.
- tunnel: ValidateConfig checks TunnelID, PublicDomain, GatewayDomain are set
  and credentials file exists. WriteConfig now validates before generating.
This commit is contained in:
2026-07-14 11:54:58 +00:00
parent a0ab8faa1c
commit 2bb162a794
5 changed files with 97 additions and 14 deletions

View File

@@ -132,8 +132,32 @@ func (m *Manager) GenerateConfig(cfg Config, services []PublicDomain) string {
return string(data)
}
// WriteConfig generates and writes the cloudflared config to disk.
// ValidateConfig checks tunnel config for common errors before generating.
func ValidateConfig(cfg Config) error {
if !cfg.Enabled {
return nil // disabled is valid
}
if cfg.TunnelID == "" {
return fmt.Errorf("tunnel ID is required")
}
if cfg.PublicDomain == "" {
return fmt.Errorf("public domain is required")
}
if cfg.GatewayDomain == "" {
return fmt.Errorf("gateway domain is required")
}
credsFile := credentialsPath(cfg.CredentialsDir, cfg.TunnelID)
if _, err := os.Stat(credsFile); err != nil {
return fmt.Errorf("credentials file not found: %s", credsFile)
}
return nil
}
// WriteConfig validates, generates, and writes the cloudflared config to disk.
func (m *Manager) WriteConfig(cfg Config, services []PublicDomain) error {
if err := ValidateConfig(cfg); err != nil {
return fmt.Errorf("config validation: %w", err)
}
content := m.GenerateConfig(cfg, services)
if content == "" {
// Remove config if tunnel is disabled or no public services

View File

@@ -7,13 +7,17 @@ import (
"testing"
)
func testConfig() Config {
func testConfig(t *testing.T) Config {
t.Helper()
credsDir := filepath.Join(t.TempDir(), "creds")
os.MkdirAll(credsDir, 0755)
os.WriteFile(filepath.Join(credsDir, "abc-123.json"), []byte(`{"AccountTag":"test"}`), 0600)
return Config{
Enabled: true,
TunnelID: "abc-123",
PublicDomain: "pub.payne.io",
GatewayDomain: "payne.io",
CredentialsDir: "/tmp/creds",
CredentialsDir: credsDir,
}
}
@@ -25,7 +29,8 @@ func TestGenerateConfig_Basic(t *testing.T) {
{Name: "dashboard"},
}
out := m.GenerateConfig(testConfig(), services)
cfg := testConfig(t)
out := m.GenerateConfig(cfg, services)
if out == "" {
t.Fatal("expected non-empty config")
@@ -37,7 +42,7 @@ func TestGenerateConfig_Basic(t *testing.T) {
}
// Check credentials file
if !strings.Contains(out, "credentials-file: /tmp/creds/abc-123.json") {
if !strings.Contains(out, "abc-123.json") {
t.Errorf("expected credentials file, got:\n%s", out)
}
@@ -75,7 +80,7 @@ func TestGenerateConfig_SubdomainOverride(t *testing.T) {
{Name: "internal-name", Subdomain: "public-name"},
}
out := m.GenerateConfig(testConfig(), services)
out := m.GenerateConfig(testConfig(t), services)
if !strings.Contains(out, "hostname: public-name.pub.payne.io") {
t.Errorf("expected subdomain override, got:\n%s", out)
@@ -85,7 +90,7 @@ func TestGenerateConfig_SubdomainOverride(t *testing.T) {
func TestGenerateConfig_Disabled(t *testing.T) {
m := NewManager(t.TempDir())
cfg := testConfig()
cfg := testConfig(t)
cfg.Enabled = false
out := m.GenerateConfig(cfg, []PublicDomain{{Name: "my-api"}})
@@ -96,7 +101,7 @@ func TestGenerateConfig_Disabled(t *testing.T) {
func TestGenerateConfig_NoServices(t *testing.T) {
m := NewManager(t.TempDir())
out := m.GenerateConfig(testConfig(), nil)
out := m.GenerateConfig(testConfig(t), nil)
if out != "" {
t.Errorf("expected empty config with no services, got:\n%s", out)
}
@@ -104,7 +109,7 @@ func TestGenerateConfig_NoServices(t *testing.T) {
func TestGenerateConfig_MissingTunnelID(t *testing.T) {
m := NewManager(t.TempDir())
cfg := testConfig()
cfg := testConfig(t)
cfg.TunnelID = ""
out := m.GenerateConfig(cfg, []PublicDomain{{Name: "my-api"}})
@@ -118,7 +123,7 @@ func TestWriteConfig(t *testing.T) {
m := NewManager(tmpDir)
services := []PublicDomain{{Name: "my-api"}}
if err := m.WriteConfig(testConfig(), services); err != nil {
if err := m.WriteConfig(testConfig(t), services); err != nil {
t.Fatalf("WriteConfig failed: %v", err)
}
@@ -139,12 +144,12 @@ func TestWriteConfig_RemovesWhenDisabled(t *testing.T) {
m := NewManager(tmpDir)
// Write config first
if err := m.WriteConfig(testConfig(), []PublicDomain{{Name: "my-api"}}); err != nil {
if err := m.WriteConfig(testConfig(t), []PublicDomain{{Name: "my-api"}}); err != nil {
t.Fatalf("WriteConfig failed: %v", err)
}
// Now disable and write again — should remove the file
cfg := testConfig()
cfg := testConfig(t)
cfg.Enabled = false
if err := m.WriteConfig(cfg, []PublicDomain{{Name: "my-api"}}); err != nil {
t.Fatalf("WriteConfig (disable) failed: %v", err)