master
go 425 lines 11.2 KB
Raw
1 package commands
2
3 import (
4 "errors"
5 "fmt"
6 "io"
7 "slices"
8 "strings"
9
10 cmds "github.com/ipfs/go-ipfs-cmds"
11 config "github.com/ipfs/kubo/config"
12 cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
13 repo "github.com/ipfs/kubo/repo"
14 fsrepo "github.com/ipfs/kubo/repo/fsrepo"
15 peer "github.com/libp2p/go-libp2p/core/peer"
16 ma "github.com/multiformats/go-multiaddr"
17 )
18
19 type BootstrapOutput struct {
20 Peers []string
21 }
22
23 var peerOptionDesc = "A peer to add to the bootstrap list (in the format '<multiaddr>/<peerID>')"
24
25 var BootstrapCmd = &cmds.Command{
26 Helptext: cmds.HelpText{
27 Tagline: "Show or edit the list of bootstrap peers.",
28 ShortDescription: `
29 Running 'ipfs bootstrap' with no arguments will run 'ipfs bootstrap list'.
30 ` + bootstrapSecurityWarning,
31 },
32
33 Run: bootstrapListCmd.Run,
34 Encoders: bootstrapListCmd.Encoders,
35 Type: bootstrapListCmd.Type,
36
37 Subcommands: map[string]*cmds.Command{
38 "list": bootstrapListCmd,
39 "add": bootstrapAddCmd,
40 "rm": bootstrapRemoveCmd,
41 },
42 }
43
44 var bootstrapAddCmd = &cmds.Command{
45 Helptext: cmds.HelpText{
46 Tagline: "Add peers to the bootstrap list.",
47 ShortDescription: `Outputs a list of peers that were added (that weren't already
48 in the bootstrap list).
49
50 The special values 'default' and 'auto' can be used to add the default
51 bootstrap peers. Both are equivalent and will add the 'auto' placeholder to
52 the bootstrap list, which gets resolved using the AutoConf system.
53 ` + bootstrapSecurityWarning,
54 },
55
56 Arguments: []cmds.Argument{
57 cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(),
58 },
59
60 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
61 if err := req.ParseBodyArgs(); err != nil {
62 return err
63 }
64 inputPeers := req.Arguments
65
66 if len(inputPeers) == 0 {
67 return errors.New("no bootstrap peers to add")
68 }
69
70 // Convert "default" to "auto" for backward compatibility
71 for i, peer := range inputPeers {
72 if peer == "default" {
73 inputPeers[i] = "auto"
74 }
75 }
76
77 cfgRoot, err := cmdenv.GetConfigRoot(env)
78 if err != nil {
79 return err
80 }
81
82 r, err := fsrepo.Open(cfgRoot)
83 if err != nil {
84 return err
85 }
86 defer r.Close()
87 cfg, err := r.Config()
88 if err != nil {
89 return err
90 }
91
92 // Check if trying to add "auto" when AutoConf is disabled
93 for _, peer := range inputPeers {
94 if peer == config.AutoPlaceholder && !cfg.AutoConf.Enabled.WithDefault(config.DefaultAutoConfEnabled) {
95 return errors.New("cannot add default bootstrap peers: AutoConf is disabled (AutoConf.Enabled=false). Enable AutoConf by setting AutoConf.Enabled=true in your config, or add specific peer addresses instead")
96 }
97 }
98
99 added, err := bootstrapAdd(r, cfg, inputPeers)
100 if err != nil {
101 return err
102 }
103
104 return cmds.EmitOnce(res, &BootstrapOutput{added})
105 },
106 Type: BootstrapOutput{},
107 Encoders: cmds.EncoderMap{
108 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *BootstrapOutput) error {
109 return bootstrapWritePeers(w, "added ", out.Peers)
110 }),
111 },
112 }
113
114 const (
115 bootstrapAllOptionName = "all"
116 )
117
118 var bootstrapRemoveCmd = &cmds.Command{
119 Helptext: cmds.HelpText{
120 Tagline: "Remove peers from the bootstrap list.",
121 ShortDescription: `Outputs the list of peers that were removed.
122 ` + bootstrapSecurityWarning,
123 },
124
125 Arguments: []cmds.Argument{
126 cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(),
127 },
128 Options: []cmds.Option{
129 cmds.BoolOption(bootstrapAllOptionName, "Remove all bootstrap peers. (Deprecated, use 'all' subcommand)"),
130 },
131 Subcommands: map[string]*cmds.Command{
132 "all": bootstrapRemoveAllCmd,
133 },
134 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
135 all, _ := req.Options[bootstrapAllOptionName].(bool)
136
137 cfgRoot, err := cmdenv.GetConfigRoot(env)
138 if err != nil {
139 return err
140 }
141
142 r, err := fsrepo.Open(cfgRoot)
143 if err != nil {
144 return err
145 }
146 defer r.Close()
147 cfg, err := r.Config()
148 if err != nil {
149 return err
150 }
151
152 var removed []string
153 if all {
154 removed, err = bootstrapRemoveAll(r, cfg)
155 } else {
156 if err := req.ParseBodyArgs(); err != nil {
157 return err
158 }
159 removed, err = bootstrapRemove(r, cfg, req.Arguments)
160 }
161 if err != nil {
162 return err
163 }
164
165 return cmds.EmitOnce(res, &BootstrapOutput{removed})
166 },
167 Type: BootstrapOutput{},
168 Encoders: cmds.EncoderMap{
169 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *BootstrapOutput) error {
170 return bootstrapWritePeers(w, "removed ", out.Peers)
171 }),
172 },
173 }
174
175 var bootstrapRemoveAllCmd = &cmds.Command{
176 Helptext: cmds.HelpText{
177 Tagline: "Remove all peers from the bootstrap list.",
178 ShortDescription: `Outputs the list of peers that were removed.`,
179 },
180
181 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
182 cfgRoot, err := cmdenv.GetConfigRoot(env)
183 if err != nil {
184 return err
185 }
186
187 r, err := fsrepo.Open(cfgRoot)
188 if err != nil {
189 return err
190 }
191 defer r.Close()
192 cfg, err := r.Config()
193 if err != nil {
194 return err
195 }
196
197 removed, err := bootstrapRemoveAll(r, cfg)
198 if err != nil {
199 return err
200 }
201
202 return cmds.EmitOnce(res, &BootstrapOutput{removed})
203 },
204 Type: BootstrapOutput{},
205 Encoders: cmds.EncoderMap{
206 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *BootstrapOutput) error {
207 return bootstrapWritePeers(w, "removed ", out.Peers)
208 }),
209 },
210 }
211
212 var bootstrapListCmd = &cmds.Command{
213 Helptext: cmds.HelpText{
214 Tagline: "Show peers in the bootstrap list.",
215 ShortDescription: "Peers are output in the format '<multiaddr>/<peerID>'.",
216 },
217 Options: []cmds.Option{
218 cmds.BoolOption(configExpandAutoName, "Expand 'auto' placeholders from AutoConf service."),
219 },
220
221 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
222 cfgRoot, err := cmdenv.GetConfigRoot(env)
223 if err != nil {
224 return err
225 }
226
227 r, err := fsrepo.Open(cfgRoot)
228 if err != nil {
229 return err
230 }
231 defer r.Close()
232 cfg, err := r.Config()
233 if err != nil {
234 return err
235 }
236
237 // Check if user wants to expand auto values
238 expandAuto, _ := req.Options[configExpandAutoName].(bool)
239 if expandAuto {
240 // Use the same expansion method as the daemon
241 expandedBootstrap := cfg.BootstrapWithAutoConf()
242 return cmds.EmitOnce(res, &BootstrapOutput{expandedBootstrap})
243 }
244
245 // Simply return the bootstrap config as-is, including any "auto" values
246 return cmds.EmitOnce(res, &BootstrapOutput{cfg.Bootstrap})
247 },
248 Type: BootstrapOutput{},
249 Encoders: cmds.EncoderMap{
250 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *BootstrapOutput) error {
251 return bootstrapWritePeers(w, "", out.Peers)
252 }),
253 },
254 }
255
256 func bootstrapWritePeers(w io.Writer, prefix string, peers []string) error {
257 slices.SortStableFunc(peers, func(a, b string) int {
258 return strings.Compare(a, b)
259 })
260 for _, peer := range peers {
261 _, err := w.Write([]byte(prefix + peer + "\n"))
262 if err != nil {
263 return err
264 }
265 }
266 return nil
267 }
268
269 func bootstrapAdd(r repo.Repo, cfg *config.Config, peers []string) ([]string, error) {
270 // Validate peers - skip validation for "auto" placeholder
271 for _, p := range peers {
272 if p == config.AutoPlaceholder {
273 continue // Skip validation for "auto" placeholder
274 }
275 m, err := ma.NewMultiaddr(p)
276 if err != nil {
277 return nil, err
278 }
279 tpt, p2ppart := ma.SplitLast(m)
280 if p2ppart == nil || p2ppart.Protocol().Code != ma.P_P2P {
281 return nil, fmt.Errorf("invalid bootstrap address: %s", p)
282 }
283 if tpt == nil {
284 return nil, fmt.Errorf("bootstrap address without a transport: %s", p)
285 }
286 }
287
288 addedMap := map[string]struct{}{}
289 addedList := make([]string, 0, len(peers))
290
291 // re-add cfg bootstrap peers to rm dupes
292 bpeers := cfg.Bootstrap
293 cfg.Bootstrap = nil
294
295 // add new peers
296 for _, s := range peers {
297 if _, found := addedMap[s]; found {
298 continue
299 }
300
301 cfg.Bootstrap = append(cfg.Bootstrap, s)
302 addedList = append(addedList, s)
303 addedMap[s] = struct{}{}
304 }
305
306 // add back original peers. in this order so that we output them.
307 for _, s := range bpeers {
308 if _, found := addedMap[s]; found {
309 continue
310 }
311
312 cfg.Bootstrap = append(cfg.Bootstrap, s)
313 addedMap[s] = struct{}{}
314 }
315
316 if err := r.SetConfig(cfg); err != nil {
317 return nil, err
318 }
319
320 return addedList, nil
321 }
322
323 func bootstrapRemove(r repo.Repo, cfg *config.Config, toRemove []string) ([]string, error) {
324 // Check if bootstrap contains "auto"
325 hasAuto := slices.Contains(cfg.Bootstrap, config.AutoPlaceholder)
326
327 if hasAuto && cfg.AutoConf.Enabled.WithDefault(config.DefaultAutoConfEnabled) {
328 // Cannot selectively remove peers when using "auto" bootstrap
329 // Users should either disable AutoConf or replace "auto" with specific peers
330 return nil, fmt.Errorf("cannot remove individual bootstrap peers when using 'auto' placeholder: the 'auto' value is managed by AutoConf. Either disable AutoConf by setting AutoConf.Enabled=false and replace 'auto' with specific peer addresses, or use 'ipfs bootstrap rm --all' to remove all peers")
331 }
332
333 // Original logic for non-auto bootstrap
334 removed := make([]peer.AddrInfo, 0, len(toRemove))
335 keep := make([]peer.AddrInfo, 0, len(cfg.Bootstrap))
336
337 toRemoveAddr, err := config.ParseBootstrapPeers(toRemove)
338 if err != nil {
339 return nil, err
340 }
341 toRemoveMap := make(map[peer.ID][]ma.Multiaddr, len(toRemoveAddr))
342 for _, addr := range toRemoveAddr {
343 toRemoveMap[addr.ID] = addr.Addrs
344 }
345
346 peers, err := cfg.BootstrapPeers()
347 if err != nil {
348 return nil, err
349 }
350
351 for _, p := range peers {
352 addrs, ok := toRemoveMap[p.ID]
353 // not in the remove set?
354 if !ok {
355 keep = append(keep, p)
356 continue
357 }
358 // remove the entire peer?
359 if len(addrs) == 0 {
360 removed = append(removed, p)
361 continue
362 }
363 var keptAddrs, removedAddrs []ma.Multiaddr
364 // remove specific addresses
365 filter:
366 for _, addr := range p.Addrs {
367 for _, addr2 := range addrs {
368 if addr.Equal(addr2) {
369 removedAddrs = append(removedAddrs, addr)
370 continue filter
371 }
372 }
373 keptAddrs = append(keptAddrs, addr)
374 }
375 if len(removedAddrs) > 0 {
376 removed = append(removed, peer.AddrInfo{ID: p.ID, Addrs: removedAddrs})
377 }
378
379 if len(keptAddrs) > 0 {
380 keep = append(keep, peer.AddrInfo{ID: p.ID, Addrs: keptAddrs})
381 }
382 }
383 cfg.SetBootstrapPeers(keep)
384
385 if err := r.SetConfig(cfg); err != nil {
386 return nil, err
387 }
388
389 return config.BootstrapPeerStrings(removed), nil
390 }
391
392 func bootstrapRemoveAll(r repo.Repo, cfg *config.Config) ([]string, error) {
393 // Check if bootstrap contains "auto" - if so, we need special handling
394 hasAuto := slices.Contains(cfg.Bootstrap, config.AutoPlaceholder)
395
396 var removed []string
397 if hasAuto {
398 // When "auto" is present, we can't parse it as peer.AddrInfo
399 // Just return the raw bootstrap list as strings for display
400 removed = slices.Clone(cfg.Bootstrap)
401 } else {
402 // Original logic for configs without "auto"
403 removedPeers, err := cfg.BootstrapPeers()
404 if err != nil {
405 return nil, err
406 }
407 removed = config.BootstrapPeerStrings(removedPeers)
408 }
409
410 cfg.Bootstrap = nil
411 if err := r.SetConfig(cfg); err != nil {
412 return nil, err
413 }
414 return removed, nil
415 }
416
417 const bootstrapSecurityWarning = `
418 SECURITY WARNING:
419
420 The bootstrap command manipulates the "bootstrap list", which contains
421 the addresses of bootstrap nodes. These are the *trusted peers* from
422 which to learn about other peers in the network. Only edit this list
423 if you understand the risks of adding or removing nodes from this list.
424
425 `