| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "path/filepath" |
| 6 | "slices" |
| 7 | "sync" |
| 8 | |
| 9 | "github.com/ipfs/boxo/autoconf" |
| 10 | logging "github.com/ipfs/go-log/v2" |
| 11 | version "github.com/ipfs/kubo" |
| 12 | ) |
| 13 | |
| 14 | var autoconfLog = logging.Logger("autoconf") |
| 15 | |
| 16 | // Singleton state for autoconf client |
| 17 | var ( |
| 18 | clientOnce sync.Once |
| 19 | clientCache *autoconf.Client |
| 20 | clientErr error |
| 21 | ) |
| 22 | |
| 23 | // GetAutoConfClient returns a cached autoconf client or creates a new one. |
| 24 | // This is thread-safe and uses a singleton pattern. |
| 25 | func GetAutoConfClient(cfg *Config) (*autoconf.Client, error) { |
| 26 | clientOnce.Do(func() { |
| 27 | clientCache, clientErr = newAutoConfClient(cfg) |
| 28 | }) |
| 29 | return clientCache, clientErr |
| 30 | } |
| 31 | |
| 32 | // newAutoConfClient creates a new autoconf client with the given config |
| 33 | func newAutoConfClient(cfg *Config) (*autoconf.Client, error) { |
| 34 | // Get repo path for cache directory |
| 35 | repoPath, err := PathRoot() |
| 36 | if err != nil { |
| 37 | return nil, fmt.Errorf("failed to get repo path: %w", err) |
| 38 | } |
| 39 | |
| 40 | // Prepare refresh interval with nil check |
| 41 | refreshInterval := cfg.AutoConf.RefreshInterval |
| 42 | if refreshInterval == nil { |
| 43 | refreshInterval = &OptionalDuration{} |
| 44 | } |
| 45 | |
| 46 | // Use default URL if not specified |
| 47 | url := cfg.AutoConf.URL.WithDefault(DefaultAutoConfURL) |
| 48 | |
| 49 | // Build client options |
| 50 | options := []autoconf.Option{ |
| 51 | autoconf.WithCacheDir(filepath.Join(repoPath, "autoconf")), |
| 52 | autoconf.WithUserAgent(version.GetUserAgentVersion()), |
| 53 | autoconf.WithCacheSize(DefaultAutoConfCacheSize), |
| 54 | autoconf.WithTimeout(DefaultAutoConfTimeout), |
| 55 | autoconf.WithRefreshInterval(refreshInterval.WithDefault(DefaultAutoConfRefreshInterval)), |
| 56 | autoconf.WithFallback(autoconf.GetMainnetFallbackConfig), |
| 57 | autoconf.WithURL(url), |
| 58 | } |
| 59 | |
| 60 | return autoconf.NewClient(options...) |
| 61 | } |
| 62 | |
| 63 | // ValidateAutoConfWithRepo validates that autoconf setup is correct at daemon startup with repo access |
| 64 | func ValidateAutoConfWithRepo(cfg *Config, swarmKeyExists bool) error { |
| 65 | if !cfg.AutoConf.Enabled.WithDefault(DefaultAutoConfEnabled) { |
| 66 | // AutoConf is disabled, check for "auto" values and warn |
| 67 | return validateAutoConfDisabled(cfg) |
| 68 | } |
| 69 | |
| 70 | // Check for private network with default mainnet URL |
| 71 | url := cfg.AutoConf.URL.WithDefault(DefaultAutoConfURL) |
| 72 | if swarmKeyExists && url == DefaultAutoConfURL { |
| 73 | return fmt.Errorf("AutoConf cannot use the default mainnet URL (%s) on a private network (swarm.key or LIBP2P_FORCE_PNET detected). Either disable AutoConf by setting AutoConf.Enabled=false, or configure AutoConf.URL to point to a configuration service specific to your private swarm", DefaultAutoConfURL) |
| 74 | } |
| 75 | |
| 76 | // Further validation will happen lazily when config is accessed |
| 77 | return nil |
| 78 | } |
| 79 | |
| 80 | // validateAutoConfDisabled checks for "auto" values when AutoConf is disabled and logs errors |
| 81 | func validateAutoConfDisabled(cfg *Config) error { |
| 82 | hasAutoValues := false |
| 83 | var errors []string |
| 84 | |
| 85 | // Check Bootstrap |
| 86 | if slices.Contains(cfg.Bootstrap, AutoPlaceholder) { |
| 87 | hasAutoValues = true |
| 88 | errors = append(errors, "Bootstrap contains 'auto' but AutoConf.Enabled=false") |
| 89 | } |
| 90 | |
| 91 | // Check DNS.Resolvers |
| 92 | if cfg.DNS.Resolvers != nil { |
| 93 | for _, resolver := range cfg.DNS.Resolvers { |
| 94 | if resolver == AutoPlaceholder { |
| 95 | hasAutoValues = true |
| 96 | errors = append(errors, "DNS.Resolvers contains 'auto' but AutoConf.Enabled=false") |
| 97 | break |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // Check Routing.DelegatedRouters |
| 103 | if slices.Contains(cfg.Routing.DelegatedRouters, AutoPlaceholder) { |
| 104 | hasAutoValues = true |
| 105 | errors = append(errors, "Routing.DelegatedRouters contains 'auto' but AutoConf.Enabled=false") |
| 106 | } |
| 107 | |
| 108 | // Check Ipns.DelegatedPublishers |
| 109 | if slices.Contains(cfg.Ipns.DelegatedPublishers, AutoPlaceholder) { |
| 110 | hasAutoValues = true |
| 111 | errors = append(errors, "Ipns.DelegatedPublishers contains 'auto' but AutoConf.Enabled=false") |
| 112 | } |
| 113 | |
| 114 | // Log all errors |
| 115 | for _, errMsg := range errors { |
| 116 | autoconfLog.Error(errMsg) |
| 117 | } |
| 118 | |
| 119 | // If only auto values exist and no static ones, fail to start |
| 120 | if hasAutoValues { |
| 121 | if len(cfg.Bootstrap) == 1 && cfg.Bootstrap[0] == AutoPlaceholder { |
| 122 | autoconfLog.Error("Kubo cannot start with only 'auto' Bootstrap values when AutoConf.Enabled=false") |
| 123 | return fmt.Errorf("no usable bootstrap peers: AutoConf is disabled (AutoConf.Enabled=false) but 'auto' placeholder is used in Bootstrap config. Either set AutoConf.Enabled=true to enable automatic configuration, or replace 'auto' with specific Bootstrap peer addresses") |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | return nil |
| 128 | } |