Skip to content

Commit ec76365

Browse files
authored
feat: autoconf support (#123)
* Use autoconf to configure settings * autoconf bootstrap peers * autoconf delegated routing endpoints * no delegated endpoint filtering * add autoconftest * set delegated routing endpoint using environ var * fix autoconf disabled error message * git ignore .autoconf-cache * update README and changelog for autoconf * fix(autoconf): add fallback configuration - ensures autoconf client uses mainnet fallback config when server is unreachable or cache is empty * fix(autoconf): use autoconf.FallbackBootstrapPeers for fallback - replaces dht.GetDefaultBootstrapPeerAddrInfos with autoconf.FallbackBootstrapPeers to align with kubo's approach * docs: fix typo in README * refactor(autoconf): implement path-based routing for delegated endpoints - set all three endpoint types (provider, peer, IPNS) to default to 'auto' - expand and validate endpoint URLs per flag to support custom configurations - accept both base URLs (https://example.com) and full URLs with routing paths - strip routing paths after validation to get base URLs for HTTP client - validate path matches flag type, error on mismatches with helpful messages - update stdout to show routing paths explicitly (/routing/v1/providers) - add spec references to Routing V1 and IPFS Mainnet in docs - simplify documentation following godoc style This ensures endpoints only receive requests they support (e.g., cid.contact only gets provider requests, not peers or IPNS). * refactor(autoconf): deduplicate HTTP clients per base URL when the same base URL appears in multiple endpoint configs (provider/peer/ipns), create only ONE HTTP client instead of three separate clients with duplicate connection pools this was not an issue with cid.contact as it only supported /providers, but is necessary to future-proof if autoconf provides more routing systems in the future implementation: - client_delegated_routing.go: new file for delegated routing client logic - collectEndpoints: deduplicates URLs and aggregates capabilities - createDelegatedHTTPRouters: creates one client per unique base URL and registers metrics once for all delegated HTTP clients - combineRouters: simplified to combine routers without client creation - clientRouter: moved from server_routers.go to co-locate with client creation same client instance is reused across routing types when base URL matches, significantly reducing memory and connection overhead * chore: improve test coverage and documentation - add file-level godoc to autoconf.go - fix parameter shadowing in createAutoConfClient - add validation for empty strings in collectEndpoints - add tests for collectEndpoints deduplication logic - add trailing slash normalization tests * refactor: improve HTTP client creation and file organization - inline autoconfDisabledError (single use function) - add newDelegatedRoutingClient helper for consistent HTTP client creation - use helper in both server and CLI code to ensure consistent options - rename client_delegated_routing.go to server_delegated_routing.go - add file-level documentation explaining server-side delegated routing
1 parent d89ef3d commit ec76365

14 files changed

+1434
-83
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
someguy
2+
.autoconf-cache

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ The following emojis are used to highlight certain changes:
1515

1616
### Added
1717

18+
- AutoConf support: automatic configuration of bootstrap peers and delegated routing endpoints ([#123](https://github.com/ipfs/someguy/pull/123)). When enabled (default), the `auto` placeholder is replaced with network-recommended values.
19+
- All endpoint flags (`--provider-endpoints`, `--peer-endpoints`, `--ipns-endpoints`) default to `auto`
20+
- See [environment-variables.md](docs/environment-variables.md#someguy_autoconf) for configuration details
21+
1822
### Changed
1923

2024
- [go-libp2p v0.45.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.45.0)

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ If you don't want to run a server yourself, but want to query some other server,
6363

6464
For more details run `someguy ask --help`.
6565

66+
### AutoConf
67+
68+
Automatic configuration of bootstrap peers and delegated routing endpoints. When enabled (default), the `auto` placeholder is replaced with network-recommended values fetched from a remote URL.
69+
70+
Configuration:
71+
- `--autoconf` / [`SOMEGUY_AUTOCONF`](docs/environment-variables.md#someguy_autoconf)
72+
- `--autoconf-url` / [`SOMEGUY_AUTOCONF_URL`](docs/environment-variables.md#someguy_autoconf_url)
73+
- `--autoconf-refresh` / [`SOMEGUY_AUTOCONF_REFRESH`](docs/environment-variables.md#someguy_autoconf_refresh)
74+
75+
Endpoint flags (default to `auto`):
76+
- `--provider-endpoints` / [`SOMEGUY_PROVIDER_ENDPOINTS`](docs/environment-variables.md#someguy_provider_endpoints)
77+
- `--peer-endpoints` / [`SOMEGUY_PEER_ENDPOINTS`](docs/environment-variables.md#someguy_peer_endpoints)
78+
- `--ipns-endpoints` / [`SOMEGUY_IPNS_ENDPOINTS`](docs/environment-variables.md#someguy_ipns_endpoints)
79+
80+
To use custom endpoints instead of `auto`:
81+
```bash
82+
someguy start --ipns-endpoints https://example.com
83+
```
84+
85+
See [environment-variables.md](docs/environment-variables.md) for URL formats and configuration details.
86+
6687
## Deployment
6788

6889
Suggested method for self-hosting is to run a [prebuilt Docker image](#docker).

autoconf.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// autoconf.go implements automatic configuration for someguy.
2+
//
3+
// Autoconf fetches network configuration from a remote JSON endpoint to automatically
4+
// configure bootstrap peers and delegated routing endpoints.
5+
//
6+
// The autoconf system:
7+
// - Fetches configuration from a remote URL (configurable)
8+
// - Caches configuration locally and refreshes periodically
9+
// - Falls back to embedded defaults if fetching fails
10+
// - Expands "auto" placeholder in endpoint configuration
11+
// - Filters out endpoints for systems running natively (e.g., DHT)
12+
// - Validates and normalizes endpoint URLs
13+
//
14+
// See https://github.com/ipfs/someguy/blob/main/docs/environment-variables.md
15+
// for configuration options and defaults.
16+
package main
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"path/filepath"
22+
"slices"
23+
"strings"
24+
"time"
25+
26+
"github.com/ipfs/boxo/autoconf"
27+
"github.com/libp2p/go-libp2p/core/peer"
28+
"github.com/multiformats/go-multiaddr"
29+
)
30+
31+
// autoConfConfig contains the configuration for the autoconf subsystem.
32+
type autoConfConfig struct {
33+
// enabled determines whether to use autoconf
34+
// Default: true
35+
enabled bool
36+
37+
// url is the HTTP(S) URL to fetch the autoconf.json from
38+
// Default: https://conf.ipfs-mainnet.org/autoconf.json
39+
url string
40+
41+
// refreshInterval is how often to refresh autoconf data
42+
// Default: 24h
43+
refreshInterval time.Duration
44+
45+
// cacheDir is the directory to cache autoconf data
46+
// Default: $SOMEGUY_DATADIR/.autoconf-cache
47+
cacheDir string
48+
}
49+
50+
func startAutoConf(ctx context.Context, cfg *config) (*autoconf.Config, error) {
51+
var autoConf *autoconf.Config
52+
if cfg.autoConf.enabled && cfg.autoConf.url != "" {
53+
client, err := createAutoConfClient(cfg.autoConf)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to create autoconf client: %w", err)
56+
}
57+
// Start primes cache and starts background updater
58+
// Note: Start() always returns a config (using fallback if needed)
59+
autoConf, err = client.Start(ctx)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to start autoconf updater: %w", err)
62+
}
63+
}
64+
return autoConf, nil
65+
}
66+
67+
func getBootstrapPeerAddrInfos(cfg *config, autoConf *autoconf.Config) []peer.AddrInfo {
68+
if autoConf != nil {
69+
nativeSystems := getNativeSystems(cfg.dhtType)
70+
return stringsToPeerAddrInfos(autoConf.GetBootstrapPeers(nativeSystems...))
71+
}
72+
// Fallback to autoconf fallback bootstrappers.
73+
return stringsToPeerAddrInfos(autoconf.FallbackBootstrapPeers)
74+
}
75+
76+
// normalizeEndpointURL validates and normalizes a single endpoint URL for a specific routing type.
77+
// Returns the base URL (with routing path stripped if present) and an error if the URL has a mismatched path.
78+
func normalizeEndpointURL(url, expectedPath, flagName string) (string, error) {
79+
// "auto" placeholder passes through unchanged
80+
if url == autoconf.AutoPlaceholder {
81+
return url, nil
82+
}
83+
84+
// Check if URL has the expected routing path
85+
if strings.HasSuffix(url, expectedPath) {
86+
// Strip the expected path to get base URL
87+
return strings.TrimSuffix(url, expectedPath), nil
88+
}
89+
90+
// Check if URL has a different routing path (potential misconfiguration)
91+
routingPaths := [...]string{
92+
autoconf.RoutingV1ProvidersPath,
93+
autoconf.RoutingV1PeersPath,
94+
autoconf.RoutingV1IPNSPath,
95+
}
96+
for _, path := range routingPaths {
97+
if strings.HasSuffix(url, path) {
98+
return "", fmt.Errorf("URL %q has path %q which doesn't match %s (expected %q or no path)", url, path, flagName, expectedPath)
99+
}
100+
}
101+
102+
// URL has no routing path or unknown path - treat as base URL
103+
return url, nil
104+
}
105+
106+
// validateEndpointURLs validates and normalizes a list of endpoint URLs for a specific routing type
107+
func validateEndpointURLs(urls []string, expectedPath, flagName, envVar string) ([]string, error) {
108+
normalized := make([]string, 0, len(urls))
109+
for _, url := range urls {
110+
baseURL, err := normalizeEndpointURL(url, expectedPath, flagName)
111+
if err != nil {
112+
return nil, fmt.Errorf("%s: %w. Use %s or %s to fix", envVar, err, envVar, flagName)
113+
}
114+
normalized = append(normalized, baseURL)
115+
}
116+
return normalized, nil
117+
}
118+
119+
// expandDelegatedRoutingEndpoints expands autoconf placeholders and categorizes endpoints by path
120+
func expandDelegatedRoutingEndpoints(cfg *config, autoConf *autoconf.Config) error {
121+
// Validate and normalize each flag's URLs separately
122+
normalizedProviders, err := validateEndpointURLs(cfg.contentEndpoints, autoconf.RoutingV1ProvidersPath, "--provider-endpoints", "SOMEGUY_PROVIDER_ENDPOINTS")
123+
if err != nil {
124+
return err
125+
}
126+
127+
normalizedPeers, err := validateEndpointURLs(cfg.peerEndpoints, autoconf.RoutingV1PeersPath, "--peer-endpoints", "SOMEGUY_PEER_ENDPOINTS")
128+
if err != nil {
129+
return err
130+
}
131+
132+
normalizedIPNS, err := validateEndpointURLs(cfg.ipnsEndpoints, autoconf.RoutingV1IPNSPath, "--ipns-endpoints", "SOMEGUY_IPNS_ENDPOINTS")
133+
if err != nil {
134+
return err
135+
}
136+
137+
if !cfg.autoConf.enabled {
138+
// Check for "auto" placeholder when autoconf is disabled
139+
if slices.Contains(normalizedProviders, autoconf.AutoPlaceholder) ||
140+
slices.Contains(normalizedPeers, autoconf.AutoPlaceholder) ||
141+
slices.Contains(normalizedIPNS, autoconf.AutoPlaceholder) {
142+
return fmt.Errorf("'auto' placeholder found in endpoint option but autoconf is disabled. Set explicit endpoint option with SOMEGUY_PROVIDER_ENDPOINTS/SOMEGUY_PEER_ENDPOINTS/SOMEGUY_IPNS_ENDPOINTS or --provider-endpoints/--peer-endpoints/--ipns-endpoints, or re-enable autoconf")
143+
}
144+
// No autoconf, keep normalized endpoints as configured
145+
cfg.contentEndpoints = deduplicateEndpoints(normalizedProviders)
146+
cfg.peerEndpoints = deduplicateEndpoints(normalizedPeers)
147+
cfg.ipnsEndpoints = deduplicateEndpoints(normalizedIPNS)
148+
return nil
149+
}
150+
151+
nativeSystems := getNativeSystems(cfg.dhtType)
152+
153+
// Expand each routing type separately to maintain category information
154+
expandedProviders := autoconf.ExpandDelegatedEndpoints(
155+
normalizedProviders,
156+
autoConf,
157+
nativeSystems,
158+
autoconf.RoutingV1ProvidersPath,
159+
)
160+
161+
expandedPeers := autoconf.ExpandDelegatedEndpoints(
162+
normalizedPeers,
163+
autoConf,
164+
nativeSystems,
165+
autoconf.RoutingV1PeersPath,
166+
)
167+
168+
expandedIPNS := autoconf.ExpandDelegatedEndpoints(
169+
normalizedIPNS,
170+
autoConf,
171+
nativeSystems,
172+
autoconf.RoutingV1IPNSPath,
173+
)
174+
175+
// Strip routing paths from expanded URLs to get base URLs
176+
cfg.contentEndpoints = stripRoutingPaths(expandedProviders, autoconf.RoutingV1ProvidersPath)
177+
cfg.peerEndpoints = stripRoutingPaths(expandedPeers, autoconf.RoutingV1PeersPath)
178+
cfg.ipnsEndpoints = stripRoutingPaths(expandedIPNS, autoconf.RoutingV1IPNSPath)
179+
180+
logger.Debugf("expanded endpoints - providers: %v, peers: %v, IPNS: %v",
181+
cfg.contentEndpoints, cfg.peerEndpoints, cfg.ipnsEndpoints)
182+
183+
return nil
184+
}
185+
186+
// stripRoutingPaths strips the routing path from URLs and deduplicates
187+
// URLs without the expected path are kept as base URLs (from custom config)
188+
// Handles trailing slashes by normalizing before comparison
189+
func stripRoutingPaths(urls []string, expectedPath string) []string {
190+
result := make([]string, 0, len(urls))
191+
for _, url := range urls {
192+
// Trim trailing slash for comparison
193+
normalized := strings.TrimSuffix(url, "/")
194+
// Strip path from autoconf-expanded URL or keep normalized base URL.
195+
result = append(result, strings.TrimSuffix(normalized, expectedPath))
196+
}
197+
return deduplicateEndpoints(result)
198+
}
199+
200+
// deduplicateEndpoints removes duplicate endpoints from a list
201+
func deduplicateEndpoints(endpoints []string) []string {
202+
if len(endpoints) == 0 {
203+
return endpoints
204+
}
205+
slices.Sort(endpoints)
206+
return slices.Compact(endpoints)
207+
}
208+
209+
func stringsToPeerAddrInfos(addrs []string) []peer.AddrInfo {
210+
addrInfos := make([]peer.AddrInfo, 0, len(addrs))
211+
212+
for _, s := range addrs {
213+
ma, err := multiaddr.NewMultiaddr(s)
214+
if err != nil {
215+
logger.Error("bad multiaddr in bootstrapper autoconf data", "err", err)
216+
continue
217+
}
218+
219+
info, err := peer.AddrInfoFromP2pAddr(ma)
220+
if err != nil {
221+
logger.Errorw("failed to convert bootstrapper address to peer addr info", "address", ma.String(), err, "err")
222+
continue
223+
}
224+
addrInfos = append(addrInfos, *info)
225+
}
226+
227+
return addrInfos
228+
}
229+
230+
// createAutoConfClient creates an autoconf client with the given configuration
231+
func createAutoConfClient(cfg autoConfConfig) (*autoconf.Client, error) {
232+
if cfg.cacheDir == "" {
233+
cfg.cacheDir = filepath.Join(".", ".autoconf-cache")
234+
}
235+
if cfg.refreshInterval == 0 {
236+
cfg.refreshInterval = autoconf.DefaultRefreshInterval
237+
}
238+
if cfg.url == "" {
239+
cfg.url = autoconf.MainnetAutoConfURL
240+
}
241+
242+
return autoconf.NewClient(
243+
autoconf.WithCacheDir(cfg.cacheDir),
244+
autoconf.WithUserAgent("someguy/"+version),
245+
autoconf.WithCacheSize(autoconf.DefaultCacheSize),
246+
autoconf.WithTimeout(autoconf.DefaultTimeout),
247+
autoconf.WithURL(cfg.url),
248+
autoconf.WithRefreshInterval(cfg.refreshInterval),
249+
autoconf.WithFallback(autoconf.GetMainnetFallbackConfig),
250+
)
251+
}
252+
253+
// getNativeSystems returns the list of systems that should be used natively based on routing type
254+
func getNativeSystems(routingType string) []string {
255+
switch routingType {
256+
case "dht", "accelerated", "standard", "auto":
257+
return []string{autoconf.SystemAminoDHT}
258+
case "disabled", "off", "none", "delegated", "custom":
259+
return []string{}
260+
default:
261+
logger.Warnf("getNativeSystems: unknown routing type %q, assuming no native systems", routingType)
262+
return []string{}
263+
}
264+
}

0 commit comments

Comments
 (0)