feat: sync tls config (#651)

* feat: implement tls config sync

* feat: implement workaround for #633

* extend feature test

* add tests
This commit is contained in:
Marc Brugger
2025-09-14 18:25:36 +02:00
committed by GitHub
parent 961cf4dbb5
commit 20fe83745f
17 changed files with 203 additions and 59 deletions

View File

@@ -164,6 +164,8 @@ linters:
disabled: true
- name: var-naming
disabled: true
- name: enforce-switch-style
disabled: true
staticcheck:
checks:
- 'all'

View File

@@ -15,6 +15,10 @@ deepcopy-gen: tb.controller-gen
@touch ./tmp/deepcopy-gen-boilerplate.go.txt
$(TB_CONTROLLER_GEN) paths=./pkg/types object
.PHONY: docs
docs:
go run docs/main.go
# Run tests
test: generate lint test-ci

106
README.md
View File

@@ -185,58 +185,60 @@ services:
## Config via environment variables
For Replicas replace `#` with the index number for the replica. E.g: `REPLICA#_URL` -> `REPLICA1_URL`
| Name | Type | Description |
|:-------------------------------------|--------|:----------------------------------------------------------|
| ORIGIN_URL (string) | string | URL of adguardhome instance |
| ORIGIN_WEB_URL (string) | string | Web URL of adguardhome instance |
| ORIGIN_API_PATH (string) | string | API Path |
| ORIGIN_USERNAME (string) | string | Adguardhome username |
| ORIGIN_PASSWORD (string) | string | Adguardhome password |
| ORIGIN_COOKIE (string) | string | Adguardhome cookie |
| ORIGIN_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| ORIGIN_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| ORIGIN_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| ORIGIN_INTERFACE_NAME (string) | string | Network interface name |
| ORIGIN_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| REPLICA#_URL (string) | string | URL of adguardhome instance |
| REPLICA#_WEB_URL (string) | string | Web URL of adguardhome instance |
| REPLICA#_API_PATH (string) | string | API Path |
| REPLICA#_USERNAME (string) | string | Adguardhome username |
| REPLICA#_PASSWORD (string) | string | Adguardhome password |
| REPLICA#_COOKIE (string) | string | Adguardhome cookie |
| REPLICA#_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| REPLICA#_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| REPLICA#_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| REPLICA#_INTERFACE_NAME (string) | string | Network interface name |
| REPLICA#_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| CRON (string) | string | Cron expression for the sync interval |
| RUN_ON_START (bool) | bool | Run the sung on startup |
| PRINT_CONFIG_ONLY (bool) | bool | Print current config only and stop the application |
| CONTINUE_ON_ERROR (bool) | bool | Continue sync on errors |
| API_PORT (int) | int | API port |
| API_USERNAME (string) | string | API username |
| API_PASSWORD (string) | string | API password |
| API_DARK_MODE (bool) | bool | API dark mode |
| API_METRICS_ENABLED (bool) | bool | Enable metrics |
| API_METRICS_SCRAPE_INTERVAL (int64) | int64 | Interval for metrics scraping |
| API_METRICS_QUERY_LOG_LIMIT (int) | int | Metrics log query limit |
| API_TLS_CERT_DIR (string) | string | API TLS certificate directory |
| API_TLS_CERT_NAME (string) | string | API TLS certificate file name |
| API_TLS_KEY_NAME (string) | string | API TLS key file name |
| FEATURES_DNS_ACCESS_LISTS (bool) | bool | Sync DNS access lists |
| FEATURES_DNS_SERVER_CONFIG (bool) | bool | Sync DNS server config |
| FEATURES_DNS_REWRITES (bool) | bool | Sync DNS rewrites |
| FEATURES_DHCP_SERVER_CONFIG (bool) | bool | Sync DHCP server config |
| FEATURES_DHCP_STATIC_LEASES (bool) | bool | Sync DHCP static leases |
| FEATURES_GENERAL_SETTINGS (bool) | bool | Sync general settings |
| FEATURES_QUERY_LOG_CONFIG (bool) | bool | Sync query log config |
| FEATURES_STATS_CONFIG (bool) | bool | Sync stats config |
| FEATURES_CLIENT_SETTINGS (bool) | bool | Sync client settings |
| FEATURES_SERVICES (bool) | bool | Sync services |
| FEATURES_FILTERS (bool) | bool | Sync filters |
| FEATURES_THEME (bool) | bool | Sync the weg UI theme |
For Replicas replace `#` with the index number for the replica. E.g.: `REPLICA#_URL` -> `REPLICA1_URL`
<!-- env-doc-start -->
| Name | Type | Description |
| :--- | ---- |:----------- |
| ORIGIN_URL (string) | string | URL of adguardhome instance |
| ORIGIN_WEB_URL (string) | string | Web URL of adguardhome instance |
| ORIGIN_API_PATH (string) | string | API Path |
| ORIGIN_USERNAME (string) | string | Adguardhome username |
| ORIGIN_PASSWORD (string) | string | Adguardhome password |
| ORIGIN_COOKIE (string) | string | Adguardhome cookie |
| ORIGIN_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| ORIGIN_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| ORIGIN_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| ORIGIN_INTERFACE_NAME (string) | string | Network interface name |
| ORIGIN_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| REPLICA#_URL (string) | string | URL of adguardhome instance |
| REPLICA#_WEB_URL (string) | string | Web URL of adguardhome instance |
| REPLICA#_API_PATH (string) | string | API Path |
| REPLICA#_USERNAME (string) | string | Adguardhome username |
| REPLICA#_PASSWORD (string) | string | Adguardhome password |
| REPLICA#_COOKIE (string) | string | Adguardhome cookie |
| REPLICA#_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| REPLICA#_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| REPLICA#_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| REPLICA#_INTERFACE_NAME (string) | string | Network interface name |
| REPLICA#_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| CRON (string) | string | Cron expression for the sync interval |
| RUN_ON_START (bool) | bool | Run the sung on startup |
| PRINT_CONFIG_ONLY (bool) | bool | Print current config only and stop the application |
| CONTINUE_ON_ERROR (bool) | bool | Continue sync on errors |
| API_PORT (int) | int | API port |
| API_USERNAME (string) | string | API username |
| API_PASSWORD (string) | string | API password |
| API_DARK_MODE (bool) | bool | API dark mode |
| API_METRICS_ENABLED (bool) | bool | Enable metrics |
| API_METRICS_SCRAPE_INTERVAL (int64) | int64 | Interval for metrics scraping |
| API_METRICS_QUERY_LOG_LIMIT (int) | int | Metrics log query limit |
| API_TLS_CERT_DIR (string) | string | API TLS certificate directory |
| API_TLS_CERT_NAME (string) | string | API TLS certificate file name |
| API_TLS_KEY_NAME (string) | string | API TLS key file name |
| FEATURES_DNS_ACCESS_LISTS (bool) | bool | Sync DNS access lists |
| FEATURES_DNS_SERVER_CONFIG (bool) | bool | Sync DNS server config |
| FEATURES_DNS_REWRITES (bool) | bool | Sync DNS rewrites |
| FEATURES_DHCP_SERVER_CONFIG (bool) | bool | Sync DHCP server config |
| FEATURES_DHCP_STATIC_LEASES (bool) | bool | Sync DHCP static leases |
| FEATURES_GENERAL_SETTINGS (bool) | bool | Sync general settings |
| FEATURES_QUERY_LOG_CONFIG (bool) | bool | Sync query log config |
| FEATURES_STATS_CONFIG (bool) | bool | Sync stats config |
| FEATURES_CLIENT_SETTINGS (bool) | bool | Sync client settings |
| FEATURES_SERVICES (bool) | bool | Sync services |
| FEATURES_FILTERS (bool) | bool | Sync filters |
| FEATURES_THEME (bool) | bool | Sync the web UI theme |
| FEATURES_TLS_CONFIG (bool) | bool | Sync the TLS config |
<!-- env-doc-end -->
### Unraid

View File

@@ -67,6 +67,7 @@ func init() {
doCmd.PersistentFlags().Bool(config.FlagFeatureClient, true, "Enable client settings feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureServices, true, "Enable services sync feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureFilters, true, "Enable filters sync feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureTLSConfig, true, "Enable TLS config sync feature")
doCmd.PersistentFlags().String(config.FlagOriginURL, "", "Origin instance url")
doCmd.PersistentFlags().

View File

@@ -3,6 +3,9 @@ package main
import (
"fmt"
"io"
"log"
"os"
"reflect"
"strings"
@@ -10,9 +13,47 @@ import (
)
func main() {
_, _ = fmt.Println("| Name | Type | Description |")
_, _ = fmt.Println("| :--- | ---- |:----------- |")
// Read the README.md file
content, err := os.ReadFile("README.md")
if err != nil {
log.Fatal(err)
}
// Convert to string for easier manipulation
fileContent := string(content)
// Generate the environment variables documentation
var buf strings.Builder
_, _ = buf.WriteString("| Name | Type | Description |\n")
_, _ = buf.WriteString("| :--- | ---- |:----------- |\n")
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
printEnvTags(reflect.TypeOf(types.Config{}), "")
_ = w.Close()
envDoc, _ := io.ReadAll(r)
os.Stdout = oldStdout
_, _ = buf.Write(envDoc)
// Find the markers and replace content between them
startMarker := "<!-- env-doc-start -->"
endMarker := "<!-- env-doc-end -->"
start := strings.Index(fileContent, startMarker)
end := strings.Index(fileContent, endMarker)
if start == -1 || end == -1 {
log.Fatal("Could not find markers in README.md")
}
// Construct new content
newContent := fileContent[:start+len(startMarker)] + "\n" + buf.String() + fileContent[end:]
// Write back to README.md
err = os.WriteFile("README.md", []byte(newContent), 0o644)
if err != nil {
log.Fatal(err)
}
}
// printEnvTags recursively prints all fields with `env` tags.
@@ -36,8 +77,6 @@ func printEnvTags(t reflect.Type, prefix string) {
envTag = "ORIGIN"
case "Replica":
envTag = "REPLICA#"
default:
continue
}
}
combinedTag := envTag

View File

@@ -147,6 +147,8 @@ type Client interface {
SetDhcpConfig(status *model.DhcpStatus) error
AddDHCPStaticLease(lease model.DhcpStaticLease) error
DeleteDHCPStaticLease(lease model.DhcpStaticLease) error
TLSConfig() (*model.TlsConfig, error)
SetTLSConfig(tls *model.TlsConfig) error
}
type client struct {
@@ -466,3 +468,14 @@ func (cl *client) SetProfileInfo(profile *model.ProfileInfo) error {
cl.log.With("language", profile.Language, "theme", profile.Theme).Info("Set profile")
return cl.doPut(cl.client.R().EnableTrace().SetBody(profile), "/profile/update")
}
func (cl *client) TLSConfig() (*model.TlsConfig, error) {
tlsc := &model.TlsConfig{}
err := cl.doGet(cl.client.R().EnableTrace().SetResult(tlsc), "/tls/status")
return tlsc, err
}
func (cl *client) SetTLSConfig(tlsc *model.TlsConfig) error {
cl.log.With("enabled", tlsc.Enabled).Info("Set TLS config")
return cl.doPost(cl.client.R().EnableTrace().SetBody(tlsc), "/tls/configure")
}

View File

@@ -490,3 +490,7 @@ func sumUp(t, o *[]int) *[]int {
}
return t
}
func (c *TlsConfig) Equals(config *TlsConfig) bool {
return utils.JSONEquals(c, config)
}

View File

@@ -150,6 +150,9 @@
},
"theme": {
"type": "boolean"
},
"tlsConfig": {
"type": "boolean"
}
},
"type": "object"

View File

@@ -22,6 +22,7 @@ const (
FlagFeatureClient = "feature-client-settings"
FlagFeatureServices = "feature-services"
FlagFeatureFilters = "feature-filters"
FlagFeatureTLSConfig = "feature-tls-config"
FlagOriginURL = "origin-url"
FlagOriginWebURL = "origin-web-url"

View File

@@ -173,8 +173,13 @@ func (fr *flagReader) readFeatureFlags() error {
}); err != nil {
return err
}
return fr.setBoolFlag(FlagFeatureFilters, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagFeatureFilters, func(cgf *types.Config, value bool) {
fr.cfg.Features.Filters = value
}); err != nil {
return err
}
return fr.setBoolFlag(FlagFeatureTLSConfig, func(cgf *types.Config, value bool) {
fr.cfg.Features.TLSConfig = value
})
}

View File

@@ -509,6 +509,20 @@ func (mr *MockClientMockRecorder) SetStatsConfig(sc any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), sc)
}
// SetTLSConfig mocks base method.
func (m *MockClient) SetTLSConfig(tls *model.TlsConfig) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetTLSConfig", tls)
ret0, _ := ret[0].(error)
return ret0
}
// SetTLSConfig indicates an expected call of SetTLSConfig.
func (mr *MockClientMockRecorder) SetTLSConfig(tls any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTLSConfig", reflect.TypeOf((*MockClient)(nil).SetTLSConfig), tls)
}
// Setup mocks base method.
func (m *MockClient) Setup() error {
m.ctrl.T.Helper()
@@ -568,6 +582,21 @@ func (mr *MockClientMockRecorder) Status() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status))
}
// TLSConfig mocks base method.
func (m *MockClient) TLSConfig() (*model.TlsConfig, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TLSConfig")
ret0, _ := ret[0].(*model.TlsConfig)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TLSConfig indicates an expected call of TLSConfig.
func (mr *MockClientMockRecorder) TLSConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSConfig", reflect.TypeOf((*MockClient)(nil).TLSConfig))
}
// ToggleFiltering mocks base method.
func (m *MockClient) ToggleFiltering(enabled bool, interval int) error {
m.ctrl.T.Helper()

View File

@@ -236,6 +236,22 @@ var (
}
return nil
}
tlsConfig = func(ac *actionContext) error {
tlsc, err := ac.client.TLSConfig()
if err != nil {
return err
}
if !tlsc.Equals(ac.origin.tlsConfig) {
if err := ac.client.SetTLSConfig(ac.origin.tlsConfig); err != nil {
ac.rl.With("enabled", ac.origin.tlsConfig.Enabled, "error", err).Error("error setting tls config")
if !ac.cfg.ContinueOnError {
return err
}
}
}
return nil
}
)
func syncFilterType(

View File

@@ -68,6 +68,11 @@ func setupActions(cfg *types.Config) (actions []syncAction) {
action("DHCP static leases", actionDHCPStaticLeases),
)
}
if cfg.Features.TLSConfig {
actions = append(actions,
action("TLS config", tlsConfig),
)
}
return actions
}

View File

@@ -264,6 +264,14 @@ func (w *worker) sync() {
}
}
if w.cfg.Features.TLSConfig {
o.tlsConfig, err = oc.TLSConfig()
if err != nil {
sl.With("error", err).Error("Error getting tls config")
return
}
}
w.actions = setupActions(w.cfg)
replicas := w.cfg.UniqueReplicas()
@@ -369,4 +377,5 @@ type origin struct {
safeSearch *model.SafeSearchConfig
profileInfo *model.ProfileInfo
safeBrowsing bool
tlsConfig *model.TlsConfig
}

View File

@@ -594,6 +594,7 @@ var _ = Describe("Sync", func() {
GeneralSettings: true,
StatsConfig: true,
QueryLogConfig: true,
TLSConfig: true,
},
}
})
@@ -614,6 +615,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil)
cl.EXPECT().TLSConfig().Return(&model.TlsConfig{}, nil)
// replica
cl.EXPECT().Host()
@@ -633,6 +635,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil)
cl.EXPECT().TLSConfig().Return(&model.TlsConfig{}, nil)
w.sync()
})
It("should not sync DHCP", func() {
@@ -653,6 +656,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().StatsConfig().Return(&model.PutStatsConfigUpdateRequest{}, nil)
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().TLSConfig().Return(&model.TlsConfig{}, nil)
// replica
cl.EXPECT().Host()
@@ -671,6 +675,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().Clients().Return(&model.Clients{}, nil)
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().TLSConfig().Return(&model.TlsConfig{}, nil)
w.sync()
})
It("origin version is too small", func() {
@@ -696,6 +701,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil)
cl.EXPECT().TLSConfig().Return(&model.TlsConfig{}, nil)
// replica
cl.EXPECT().Host().Times(2)

View File

@@ -22,6 +22,7 @@ func NewFeatures(enabled bool) Features {
Services: enabled,
Filters: enabled,
Theme: enabled,
TLSConfig: enabled,
}
}
@@ -35,7 +36,8 @@ type Features struct {
ClientSettings bool `json:"clientSettings" yaml:"clientSettings" documentation:"Sync client settings" env:"FEATURES_CLIENT_SETTINGS"`
Services bool `json:"services" yaml:"services" documentation:"Sync services" env:"FEATURES_SERVICES"`
Filters bool `json:"filters" yaml:"filters" documentation:"Sync filters" env:"FEATURES_FILTERS"`
Theme bool `json:"theme" yaml:"theme" documentation:"Sync the weg UI theme" env:"FEATURES_THEME"`
Theme bool `json:"theme" yaml:"theme" documentation:"Sync the web UI theme" env:"FEATURES_THEME"`
TLSConfig bool `json:"tlsConfig" yaml:"tlsConfig" documentation:"Sync the TLS config" env:"FEATURES_TLS_CONFIG"`
}
// DHCP features.
@@ -95,5 +97,8 @@ func (f *Features) collectDisabled() []string {
if !f.Filters {
features = append(features, "Filters")
}
if !f.TLSConfig {
features = append(features, "TLSConfig")
}
return features
}

View File

@@ -102,7 +102,7 @@ var _ = Describe("Types", func() {
Context("LogDisabled", func() {
It("should log all features", func() {
f := NewFeatures(false)
Ω(f.collectDisabled()).Should(HaveLen(11))
Ω(f.collectDisabled()).Should(HaveLen(12))
})
It("should log no features", func() {
f := NewFeatures(true)