HH-635: migrate legacy managed configs
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+129
-10
@@ -1,6 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -18,8 +19,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxSubscriptionSize = 16 << 20
|
||||
minAdminPasswordLength = 12
|
||||
maxSubscriptionSize = 16 << 20
|
||||
minAdminPasswordLength = 12
|
||||
managedConfigVersion = "1"
|
||||
managedConfigVersionFile = ".mohomo-docker-config-version"
|
||||
managedChinaIPProvider = " ChinaIp: {type: file, behavior: ipcidr, format: yaml, path: /usr/local/share/ssclash/rules/ChinaIp.yaml}\n"
|
||||
managedChinaIPRule = " - RULE-SET,ChinaIp,🎯 全球直连"
|
||||
legacyChinaIPRule = " - GEOIP,CN,🎯 全球直连"
|
||||
defaultControllerURL = "http://127.0.0.1:9090"
|
||||
)
|
||||
|
||||
var errMihomoStateUncertain = errors.New("Mihomo subscription state could not be restored")
|
||||
@@ -55,6 +62,7 @@ type Config struct {
|
||||
type Result struct {
|
||||
CoreInitialized bool
|
||||
ConfigInitialized bool
|
||||
ConfigMigrated bool
|
||||
ServerSettingsChanged bool
|
||||
}
|
||||
|
||||
@@ -65,6 +73,7 @@ type RuntimeConfig struct {
|
||||
ConfigSource string
|
||||
RuntimeDir string
|
||||
SubscriptionURL string
|
||||
ControllerURL string
|
||||
UpdateInterval time.Duration
|
||||
}
|
||||
|
||||
@@ -104,9 +113,13 @@ func Prepare(config Config) (Result, error) {
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("initialize Mihomo core: %w", err)
|
||||
}
|
||||
result.ConfigInitialized, err = copyIfAbsent(config.ConfigSource, filepath.Join(root, "config.yaml"), 0o644)
|
||||
result.ConfigInitialized, result.ConfigMigrated, err = prepareManagedConfig(
|
||||
config.ConfigSource,
|
||||
filepath.Join(root, "config.yaml"),
|
||||
filepath.Join(root, managedConfigVersionFile),
|
||||
)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("initialize config: %w", err)
|
||||
return result, fmt.Errorf("prepare config: %w", err)
|
||||
}
|
||||
result.ServerSettingsChanged, err = enforceServerSettings(filepath.Join(root, ".ssclash", "settings"))
|
||||
if err != nil {
|
||||
@@ -294,11 +307,15 @@ func Run(ctx context.Context, config RuntimeConfig) error {
|
||||
}
|
||||
activeSubscription := filepath.Join(runtimeDir, "subscription.yaml")
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
controllerURL := strings.TrimRight(config.ControllerURL, "/")
|
||||
if controllerURL == "" {
|
||||
controllerURL = defaultControllerURL
|
||||
}
|
||||
validate := func(candidate string) error {
|
||||
return validateSubscription(ctx, config, runtimeConfig, candidate)
|
||||
}
|
||||
reload := func(ctx context.Context) error {
|
||||
return reloadSubscription(ctx, client)
|
||||
return reloadSubscription(ctx, client, controllerURL)
|
||||
}
|
||||
if err := updateSubscription(ctx, client, config.SubscriptionURL, activeSubscription, validate); err != nil {
|
||||
return fmt.Errorf("initial subscription update failed: %w", err)
|
||||
@@ -335,7 +352,7 @@ func Run(ctx context.Context, config RuntimeConfig) error {
|
||||
}
|
||||
return fmt.Errorf("SSClash exited: %w", err)
|
||||
case <-ticker.C:
|
||||
running := mihomoRunning(ctx, client)
|
||||
running := mihomoRunning(ctx, client, controllerURL)
|
||||
var err error
|
||||
if running {
|
||||
err = updateAndReload(ctx, client, config, activeSubscription, validate, reload)
|
||||
@@ -489,8 +506,8 @@ func updateAndReload(ctx context.Context, client *http.Client, config RuntimeCon
|
||||
return errors.New("new subscription reload failed; previous subscription restored")
|
||||
}
|
||||
|
||||
func reloadSubscription(ctx context.Context, client *http.Client) error {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://127.0.0.1:9090/providers/proxies/subscription", nil)
|
||||
func reloadSubscription(ctx context.Context, client *http.Client, controllerURL string) error {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, controllerURL+"/providers/proxies/subscription", nil)
|
||||
if err != nil {
|
||||
return errors.New("create Mihomo reload request")
|
||||
}
|
||||
@@ -506,8 +523,8 @@ func reloadSubscription(ctx context.Context, client *http.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mihomoRunning(ctx context.Context, client *http.Client) bool {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:9090/version", nil)
|
||||
func mihomoRunning(ctx context.Context, client *http.Client, controllerURL string) bool {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, controllerURL+"/version", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -600,6 +617,108 @@ func copyIfAbsent(source, target string, mode os.FileMode) (bool, error) {
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func prepareManagedConfig(source, target, versionPath string) (bool, bool, error) {
|
||||
if err := validateManagedConfigVersion(versionPath); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
current, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("read managed config source: %w", err)
|
||||
}
|
||||
|
||||
info, err := os.Lstat(target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err := writeManagedConfig(target, current); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
if err := writeManagedConfigVersion(versionPath); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return true, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("inspect config %q: %w", target, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return false, false, fmt.Errorf("config %q is not a regular file", target)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return false, false, fmt.Errorf("config %q is empty", target)
|
||||
}
|
||||
existing, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("read config %q: %w", target, err)
|
||||
}
|
||||
if bytes.Equal(existing, current) {
|
||||
return false, false, writeManagedConfigVersion(versionPath)
|
||||
}
|
||||
|
||||
legacy, legacyErr := legacyManagedConfig(current)
|
||||
if legacyErr == nil && bytes.Equal(existing, legacy) {
|
||||
if err := writeManagedConfig(target, current); err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
if err := writeManagedConfigVersion(versionPath); err != nil {
|
||||
return false, true, err
|
||||
}
|
||||
return false, true, nil
|
||||
}
|
||||
if bytes.Contains(existing, []byte("GEOIP,CN")) {
|
||||
return false, false, fmt.Errorf("custom config uses GEOIP,CN and was preserved; replace it with the packaged local ChinaIp rule before retrying")
|
||||
}
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
func legacyManagedConfig(current []byte) ([]byte, error) {
|
||||
text := string(current)
|
||||
if strings.Count(text, managedChinaIPProvider) != 1 || strings.Count(text, managedChinaIPRule) != 1 {
|
||||
return nil, errors.New("packaged config is missing the managed ChinaIp rule")
|
||||
}
|
||||
text = strings.Replace(text, managedChinaIPProvider, "", 1)
|
||||
text = strings.Replace(text, managedChinaIPRule, legacyChinaIPRule, 1)
|
||||
return []byte(text), nil
|
||||
}
|
||||
|
||||
func validateManagedConfigVersion(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect managed config version: %w", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return errors.New("managed config version marker is not a regular file")
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read managed config version: %w", err)
|
||||
}
|
||||
if string(content) != managedConfigVersion+"\n" {
|
||||
return fmt.Errorf("unsupported managed config version %q", strings.TrimSpace(string(content)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeManagedConfig(path string, content []byte) error {
|
||||
return atomicWrite(path, 0o644, func(output *os.File) error {
|
||||
_, err := output.Write(content)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func writeManagedConfigVersion(path string) error {
|
||||
if _, err := os.Lstat(path); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("inspect managed config version: %w", err)
|
||||
}
|
||||
return atomicWrite(path, 0o644, func(output *os.File) error {
|
||||
_, err := output.WriteString(managedConfigVersion + "\n")
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func enforceServerSettings(path string) (bool, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
|
||||
Reference in New Issue
Block a user