wireguard.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"time"
  10  
  11  	"github.com/dgraph-io/badger/v4"
  12  	"next.orly.dev/pkg/lol/chk"
  13  	"next.orly.dev/pkg/lol/log"
  14  
  15  	"next.orly.dev/pkg/nostr/encoders/hex"
  16  	"next.orly.dev/pkg/wireguard"
  17  )
  18  
  19  // Key prefixes for WireGuard data
  20  const (
  21  	wgServerKeyPrefix  = "wg:server:key"  // Server's WireGuard private key
  22  	wgSubnetSeedPrefix = "wg:subnet:seed" // Seed for deterministic subnet generation
  23  	wgPeerPrefix       = "wg:peer:"       // Peer data by Nostr pubkey hex
  24  	wgSequenceKey      = "wg:seq"         // Badger sequence key for subnet allocation
  25  	wgRevokedPrefix    = "wg:revoked:"    // Revoked keypairs by Nostr pubkey hex
  26  	wgAccessLogPrefix  = "wg:accesslog:"  // Access log for obsolete addresses
  27  )
  28  
  29  // WireGuardPeer stores WireGuard peer information in the database.
  30  type WireGuardPeer struct {
  31  	NostrPubkey  []byte `json:"nostr_pubkey"`   // User's Nostr pubkey (32 bytes)
  32  	WGPrivateKey []byte `json:"wg_private_key"` // WireGuard private key (32 bytes)
  33  	WGPublicKey  []byte `json:"wg_public_key"`  // WireGuard public key (32 bytes)
  34  	Sequence     uint32 `json:"sequence"`       // Sequence number for subnet derivation
  35  	CreatedAt    int64  `json:"created_at"`     // Unix timestamp
  36  }
  37  
  38  // WireGuardRevokedKey stores a revoked/old WireGuard keypair for audit purposes.
  39  type WireGuardRevokedKey struct {
  40  	NostrPubkey  []byte `json:"nostr_pubkey"`   // User's Nostr pubkey (32 bytes)
  41  	WGPublicKey  []byte `json:"wg_public_key"`  // Revoked WireGuard public key (32 bytes)
  42  	Sequence     uint32 `json:"sequence"`       // Sequence number (subnet)
  43  	CreatedAt    int64  `json:"created_at"`     // When the key was originally created
  44  	RevokedAt    int64  `json:"revoked_at"`     // When the key was revoked
  45  	AccessCount  int    `json:"access_count"`   // Number of access attempts since revocation
  46  	LastAccessAt int64  `json:"last_access_at"` // Last access attempt timestamp (0 if never)
  47  }
  48  
  49  // WireGuardAccessLog records an access attempt to an obsolete address.
  50  type WireGuardAccessLog struct {
  51  	NostrPubkey []byte `json:"nostr_pubkey"`  // User's Nostr pubkey
  52  	WGPublicKey []byte `json:"wg_public_key"` // The obsolete public key used
  53  	Sequence    uint32 `json:"sequence"`      // Subnet sequence
  54  	Timestamp   int64  `json:"timestamp"`     // When the access occurred
  55  	RemoteAddr  string `json:"remote_addr"`   // Remote IP address
  56  }
  57  
  58  // ServerIP returns the derived server IP for this peer's subnet.
  59  func (p *WireGuardPeer) ServerIP(pool *wireguard.SubnetPool) string {
  60  	subnet := pool.SubnetForSequence(p.Sequence)
  61  	return subnet.ServerIP.String()
  62  }
  63  
  64  // ClientIP returns the derived client IP for this peer's subnet.
  65  func (p *WireGuardPeer) ClientIP(pool *wireguard.SubnetPool) string {
  66  	subnet := pool.SubnetForSequence(p.Sequence)
  67  	return subnet.ClientIP.String()
  68  }
  69  
  70  // GetWireGuardServerKey retrieves the WireGuard server private key.
  71  func (d *D) GetWireGuardServerKey() (key []byte, err error) {
  72  	err = d.DB.View(func(txn *badger.Txn) error {
  73  		item, err := txn.Get([]byte(wgServerKeyPrefix))
  74  		if errors.Is(err, badger.ErrKeyNotFound) {
  75  			return err
  76  		}
  77  		if err != nil {
  78  			return err
  79  		}
  80  		return item.Value(func(val []byte) error {
  81  			key = make([]byte, len(val))
  82  			copy(key, val)
  83  			return nil
  84  		})
  85  	})
  86  	return
  87  }
  88  
  89  // SetWireGuardServerKey stores the WireGuard server private key.
  90  func (d *D) SetWireGuardServerKey(key []byte) error {
  91  	if len(key) != 32 {
  92  		return fmt.Errorf("invalid key length: %d (expected 32)", len(key))
  93  	}
  94  	return d.DB.Update(func(txn *badger.Txn) error {
  95  		return txn.Set([]byte(wgServerKeyPrefix), key)
  96  	})
  97  }
  98  
  99  // GetOrCreateWireGuardServerKey retrieves or creates the WireGuard server key.
 100  func (d *D) GetOrCreateWireGuardServerKey() (key []byte, err error) {
 101  	// Try to get existing key
 102  	if key, err = d.GetWireGuardServerKey(); err == nil && len(key) == 32 {
 103  		return key, nil
 104  	}
 105  	if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
 106  		return nil, err
 107  	}
 108  
 109  	// Generate new keypair
 110  	privateKey, publicKey, err := wireguard.GenerateKeyPair()
 111  	if err != nil {
 112  		return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err)
 113  	}
 114  
 115  	// Store the private key
 116  	if err = d.SetWireGuardServerKey(privateKey); chk.E(err) {
 117  		return nil, err
 118  	}
 119  
 120  	log.I.F("generated new WireGuard server key (pubkey=%s...)", hex.Enc(publicKey[:8]))
 121  	return privateKey, nil
 122  }
 123  
 124  // GetSubnetSeed retrieves the subnet pool seed.
 125  func (d *D) GetSubnetSeed() (seed []byte, err error) {
 126  	err = d.DB.View(func(txn *badger.Txn) error {
 127  		item, err := txn.Get([]byte(wgSubnetSeedPrefix))
 128  		if errors.Is(err, badger.ErrKeyNotFound) {
 129  			return err
 130  		}
 131  		if err != nil {
 132  			return err
 133  		}
 134  		return item.Value(func(val []byte) error {
 135  			seed = make([]byte, len(val))
 136  			copy(seed, val)
 137  			return nil
 138  		})
 139  	})
 140  	return
 141  }
 142  
 143  // SetSubnetSeed stores the subnet pool seed.
 144  func (d *D) SetSubnetSeed(seed []byte) error {
 145  	if len(seed) != 32 {
 146  		return fmt.Errorf("invalid seed length: %d (expected 32)", len(seed))
 147  	}
 148  	return d.DB.Update(func(txn *badger.Txn) error {
 149  		return txn.Set([]byte(wgSubnetSeedPrefix), seed)
 150  	})
 151  }
 152  
 153  // GetOrCreateSubnetPool creates or restores a subnet pool from the database.
 154  func (d *D) GetOrCreateSubnetPool(baseNetwork string) (*wireguard.SubnetPool, error) {
 155  	// Try to get existing seed
 156  	seed, err := d.GetSubnetSeed()
 157  	if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
 158  		return nil, err
 159  	}
 160  
 161  	var pool *wireguard.SubnetPool
 162  
 163  	if len(seed) == 32 {
 164  		// Restore pool with existing seed
 165  		pool, err = wireguard.NewSubnetPoolWithSeed(baseNetwork, seed)
 166  		if err != nil {
 167  			return nil, err
 168  		}
 169  		log.D.F("restored subnet pool with existing seed")
 170  	} else {
 171  		// Create new pool with random seed
 172  		pool, err = wireguard.NewSubnetPool(baseNetwork)
 173  		if err != nil {
 174  			return nil, err
 175  		}
 176  
 177  		// Store the new seed
 178  		if err = d.SetSubnetSeed(pool.Seed()); err != nil {
 179  			return nil, fmt.Errorf("failed to store subnet seed: %w", err)
 180  		}
 181  		log.I.F("generated new subnet pool seed")
 182  	}
 183  
 184  	// Restore existing allocations from database
 185  	peers, err := d.GetAllWireGuardPeers()
 186  	if err != nil {
 187  		return nil, fmt.Errorf("failed to load existing peers: %w", err)
 188  	}
 189  
 190  	for _, peer := range peers {
 191  		pool.RestoreAllocation(hex.Enc(peer.NostrPubkey), peer.Sequence)
 192  	}
 193  
 194  	if len(peers) > 0 {
 195  		log.D.F("restored %d subnet allocations", len(peers))
 196  	}
 197  
 198  	return pool, nil
 199  }
 200  
 201  // GetWireGuardPeer retrieves a WireGuard peer by Nostr pubkey.
 202  func (d *D) GetWireGuardPeer(nostrPubkey []byte) (peer *WireGuardPeer, err error) {
 203  	key := append([]byte(wgPeerPrefix), []byte(hex.Enc(nostrPubkey))...)
 204  
 205  	err = d.DB.View(func(txn *badger.Txn) error {
 206  		item, err := txn.Get(key)
 207  		if errors.Is(err, badger.ErrKeyNotFound) {
 208  			return err
 209  		}
 210  		if err != nil {
 211  			return err
 212  		}
 213  		return item.Value(func(val []byte) error {
 214  			peer = &WireGuardPeer{}
 215  			return json.Unmarshal(val, peer)
 216  		})
 217  	})
 218  	return
 219  }
 220  
 221  // GetOrCreateWireGuardPeer retrieves or creates a WireGuard peer.
 222  // The pool is used for subnet derivation from the sequence number.
 223  func (d *D) GetOrCreateWireGuardPeer(nostrPubkey []byte, pool *wireguard.SubnetPool) (peer *WireGuardPeer, err error) {
 224  	// Try to get existing peer
 225  	if peer, err = d.GetWireGuardPeer(nostrPubkey); err == nil {
 226  		return peer, nil
 227  	}
 228  	if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
 229  		return nil, err
 230  	}
 231  
 232  	// Generate new WireGuard keypair
 233  	privateKey, publicKey, err := wireguard.GenerateKeyPair()
 234  	if err != nil {
 235  		return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err)
 236  	}
 237  
 238  	// Get next sequence number from Badger's sequence
 239  	seq64, err := d.GetNextWGSequence()
 240  	if err != nil {
 241  		return nil, fmt.Errorf("failed to allocate sequence: %w", err)
 242  	}
 243  	seq := uint32(seq64)
 244  
 245  	// Register allocation with pool for in-memory tracking
 246  	pubkeyHex := hex.Enc(nostrPubkey)
 247  	pool.RestoreAllocation(pubkeyHex, seq)
 248  
 249  	peer = &WireGuardPeer{
 250  		NostrPubkey:  nostrPubkey,
 251  		WGPrivateKey: privateKey,
 252  		WGPublicKey:  publicKey,
 253  		Sequence:     seq,
 254  		CreatedAt:    time.Now().Unix(),
 255  	}
 256  
 257  	// Store peer data
 258  	if err = d.setWireGuardPeer(peer); err != nil {
 259  		return nil, err
 260  	}
 261  
 262  	subnet := pool.SubnetForSequence(seq)
 263  	log.I.F("created WireGuard peer: nostr=%s... -> subnet %s/%s (seq=%d)",
 264  		hex.Enc(nostrPubkey[:8]), subnet.ServerIP, subnet.ClientIP, seq)
 265  
 266  	return peer, nil
 267  }
 268  
 269  // RegenerateWireGuardPeer generates a new keypair for an existing peer.
 270  // The sequence number (and thus subnet) is preserved.
 271  // The old keypair is archived for audit purposes.
 272  func (d *D) RegenerateWireGuardPeer(nostrPubkey []byte, pool *wireguard.SubnetPool) (peer *WireGuardPeer, err error) {
 273  	// Get existing peer to preserve sequence
 274  	existing, err := d.GetWireGuardPeer(nostrPubkey)
 275  	if err != nil {
 276  		return nil, err
 277  	}
 278  
 279  	// Archive the old keypair for audit purposes
 280  	if err = d.ArchiveRevokedKey(existing); err != nil {
 281  		log.W.F("failed to archive revoked key: %v", err)
 282  		// Continue anyway - this is audit logging, not critical
 283  	}
 284  
 285  	// Generate new WireGuard keypair
 286  	privateKey, publicKey, err := wireguard.GenerateKeyPair()
 287  	if err != nil {
 288  		return nil, fmt.Errorf("failed to generate WireGuard keypair: %w", err)
 289  	}
 290  
 291  	peer = &WireGuardPeer{
 292  		NostrPubkey:  nostrPubkey,
 293  		WGPrivateKey: privateKey,
 294  		WGPublicKey:  publicKey,
 295  		Sequence:     existing.Sequence, // Keep same sequence (same subnet)
 296  		CreatedAt:    time.Now().Unix(),
 297  	}
 298  
 299  	// Store updated peer data
 300  	if err = d.setWireGuardPeer(peer); err != nil {
 301  		return nil, err
 302  	}
 303  
 304  	subnet := pool.SubnetForSequence(peer.Sequence)
 305  	log.I.F("regenerated WireGuard peer: nostr=%s... -> subnet %s/%s (old key archived)",
 306  		hex.Enc(nostrPubkey[:8]), subnet.ServerIP, subnet.ClientIP)
 307  
 308  	return peer, nil
 309  }
 310  
 311  // DeleteWireGuardPeer removes a WireGuard peer from the database.
 312  // Note: The sequence number is not recycled to prevent subnet reuse.
 313  func (d *D) DeleteWireGuardPeer(nostrPubkey []byte) error {
 314  	peerKey := append([]byte(wgPeerPrefix), []byte(hex.Enc(nostrPubkey))...)
 315  
 316  	return d.DB.Update(func(txn *badger.Txn) error {
 317  		if err := txn.Delete(peerKey); err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
 318  			return err
 319  		}
 320  		return nil
 321  	})
 322  }
 323  
 324  // GetAllWireGuardPeers returns all WireGuard peers.
 325  func (d *D) GetAllWireGuardPeers() (peers []*WireGuardPeer, err error) {
 326  	prefix := []byte(wgPeerPrefix)
 327  
 328  	err = d.DB.View(func(txn *badger.Txn) error {
 329  		opts := badger.DefaultIteratorOptions
 330  		opts.Prefix = prefix
 331  		it := txn.NewIterator(opts)
 332  		defer it.Close()
 333  
 334  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 335  			item := it.Item()
 336  			err := item.Value(func(val []byte) error {
 337  				peer := &WireGuardPeer{}
 338  				if err := json.Unmarshal(val, peer); err != nil {
 339  					return err
 340  				}
 341  				peers = append(peers, peer)
 342  				return nil
 343  			})
 344  			if err != nil {
 345  				return err
 346  			}
 347  		}
 348  		return nil
 349  	})
 350  	return
 351  }
 352  
 353  // setWireGuardPeer stores a WireGuard peer in the database.
 354  func (d *D) setWireGuardPeer(peer *WireGuardPeer) error {
 355  	data, err := json.Marshal(peer)
 356  	if err != nil {
 357  		return fmt.Errorf("failed to marshal peer: %w", err)
 358  	}
 359  
 360  	peerKey := append([]byte(wgPeerPrefix), []byte(hex.Enc(peer.NostrPubkey))...)
 361  
 362  	return d.DB.Update(func(txn *badger.Txn) error {
 363  		return txn.Set(peerKey, data)
 364  	})
 365  }
 366  
 367  // GetNextWGSequence retrieves and increments the sequence counter using Badger's Sequence.
 368  func (d *D) GetNextWGSequence() (seq uint64, err error) {
 369  	// Get a sequence with bandwidth 1 (allocate 1 number at a time)
 370  	badgerSeq, err := d.DB.GetSequence([]byte(wgSequenceKey), 1)
 371  	if err != nil {
 372  		return 0, fmt.Errorf("failed to get sequence: %w", err)
 373  	}
 374  	defer badgerSeq.Release()
 375  
 376  	seq, err = badgerSeq.Next()
 377  	if err != nil {
 378  		return 0, fmt.Errorf("failed to get next sequence number: %w", err)
 379  	}
 380  	return seq, nil
 381  }
 382  
 383  // ArchiveRevokedKey stores a revoked keypair for audit purposes.
 384  func (d *D) ArchiveRevokedKey(peer *WireGuardPeer) error {
 385  	revoked := &WireGuardRevokedKey{
 386  		NostrPubkey: peer.NostrPubkey,
 387  		WGPublicKey: peer.WGPublicKey,
 388  		Sequence:    peer.Sequence,
 389  		CreatedAt:   peer.CreatedAt,
 390  		RevokedAt:   time.Now().Unix(),
 391  		AccessCount: 0,
 392  		LastAccessAt: 0,
 393  	}
 394  
 395  	data, err := json.Marshal(revoked)
 396  	if err != nil {
 397  		return fmt.Errorf("failed to marshal revoked key: %w", err)
 398  	}
 399  
 400  	// Key: wg:revoked:<pubkey-hex>:<revoked-timestamp>
 401  	keyStr := fmt.Sprintf("%s%s:%d", wgRevokedPrefix, hex.Enc(peer.NostrPubkey), revoked.RevokedAt)
 402  
 403  	return d.DB.Update(func(txn *badger.Txn) error {
 404  		return txn.Set([]byte(keyStr), data)
 405  	})
 406  }
 407  
 408  // GetRevokedKeys returns all revoked keys for a user.
 409  func (d *D) GetRevokedKeys(nostrPubkey []byte) (keys []*WireGuardRevokedKey, err error) {
 410  	prefix := []byte(wgRevokedPrefix + hex.Enc(nostrPubkey) + ":")
 411  
 412  	err = d.DB.View(func(txn *badger.Txn) error {
 413  		opts := badger.DefaultIteratorOptions
 414  		opts.Prefix = prefix
 415  		it := txn.NewIterator(opts)
 416  		defer it.Close()
 417  
 418  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 419  			item := it.Item()
 420  			err := item.Value(func(val []byte) error {
 421  				key := &WireGuardRevokedKey{}
 422  				if err := json.Unmarshal(val, key); err != nil {
 423  					return err
 424  				}
 425  				keys = append(keys, key)
 426  				return nil
 427  			})
 428  			if err != nil {
 429  				return err
 430  			}
 431  		}
 432  		return nil
 433  	})
 434  	return
 435  }
 436  
 437  // GetAllRevokedKeys returns all revoked keys across all users (admin view).
 438  func (d *D) GetAllRevokedKeys() (keys []*WireGuardRevokedKey, err error) {
 439  	prefix := []byte(wgRevokedPrefix)
 440  
 441  	err = d.DB.View(func(txn *badger.Txn) error {
 442  		opts := badger.DefaultIteratorOptions
 443  		opts.Prefix = prefix
 444  		it := txn.NewIterator(opts)
 445  		defer it.Close()
 446  
 447  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 448  			item := it.Item()
 449  			err := item.Value(func(val []byte) error {
 450  				key := &WireGuardRevokedKey{}
 451  				if err := json.Unmarshal(val, key); err != nil {
 452  					return err
 453  				}
 454  				keys = append(keys, key)
 455  				return nil
 456  			})
 457  			if err != nil {
 458  				return err
 459  			}
 460  		}
 461  		return nil
 462  	})
 463  	return
 464  }
 465  
 466  // LogObsoleteAccess records an access attempt to an obsolete WireGuard address.
 467  func (d *D) LogObsoleteAccess(nostrPubkey, wgPubkey []byte, sequence uint32, remoteAddr string) error {
 468  	now := time.Now().Unix()
 469  
 470  	logEntry := &WireGuardAccessLog{
 471  		NostrPubkey: nostrPubkey,
 472  		WGPublicKey: wgPubkey,
 473  		Sequence:    sequence,
 474  		Timestamp:   now,
 475  		RemoteAddr:  remoteAddr,
 476  	}
 477  
 478  	data, err := json.Marshal(logEntry)
 479  	if err != nil {
 480  		return fmt.Errorf("failed to marshal access log: %w", err)
 481  	}
 482  
 483  	// Key: wg:accesslog:<pubkey-hex>:<timestamp>
 484  	keyStr := fmt.Sprintf("%s%s:%d", wgAccessLogPrefix, hex.Enc(nostrPubkey), now)
 485  
 486  	return d.DB.Update(func(txn *badger.Txn) error {
 487  		return txn.Set([]byte(keyStr), data)
 488  	})
 489  }
 490  
 491  // GetAccessLogs returns access logs for a user.
 492  func (d *D) GetAccessLogs(nostrPubkey []byte) (logs []*WireGuardAccessLog, err error) {
 493  	prefix := []byte(wgAccessLogPrefix + hex.Enc(nostrPubkey) + ":")
 494  
 495  	err = d.DB.View(func(txn *badger.Txn) error {
 496  		opts := badger.DefaultIteratorOptions
 497  		opts.Prefix = prefix
 498  		it := txn.NewIterator(opts)
 499  		defer it.Close()
 500  
 501  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 502  			item := it.Item()
 503  			err := item.Value(func(val []byte) error {
 504  				logEntry := &WireGuardAccessLog{}
 505  				if err := json.Unmarshal(val, logEntry); err != nil {
 506  					return err
 507  				}
 508  				logs = append(logs, logEntry)
 509  				return nil
 510  			})
 511  			if err != nil {
 512  				return err
 513  			}
 514  		}
 515  		return nil
 516  	})
 517  	return
 518  }
 519  
 520  // GetAllAccessLogs returns all access logs (admin view).
 521  func (d *D) GetAllAccessLogs() (logs []*WireGuardAccessLog, err error) {
 522  	prefix := []byte(wgAccessLogPrefix)
 523  
 524  	err = d.DB.View(func(txn *badger.Txn) error {
 525  		opts := badger.DefaultIteratorOptions
 526  		opts.Prefix = prefix
 527  		it := txn.NewIterator(opts)
 528  		defer it.Close()
 529  
 530  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 531  			item := it.Item()
 532  			err := item.Value(func(val []byte) error {
 533  				logEntry := &WireGuardAccessLog{}
 534  				if err := json.Unmarshal(val, logEntry); err != nil {
 535  					return err
 536  				}
 537  				logs = append(logs, logEntry)
 538  				return nil
 539  			})
 540  			if err != nil {
 541  				return err
 542  			}
 543  		}
 544  		return nil
 545  	})
 546  	return
 547  }
 548  
 549  // IncrementRevokedKeyAccess updates the access count for a revoked key.
 550  func (d *D) IncrementRevokedKeyAccess(nostrPubkey, wgPubkey []byte) error {
 551  	// Find and update the matching revoked key
 552  	prefix := []byte(wgRevokedPrefix + hex.Enc(nostrPubkey) + ":")
 553  	wgPubkeyHex := hex.Enc(wgPubkey)
 554  	now := time.Now().Unix()
 555  
 556  	return d.DB.Update(func(txn *badger.Txn) error {
 557  		opts := badger.DefaultIteratorOptions
 558  		opts.Prefix = prefix
 559  		it := txn.NewIterator(opts)
 560  		defer it.Close()
 561  
 562  		for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
 563  			item := it.Item()
 564  			key := item.KeyCopy(nil)
 565  
 566  			err := item.Value(func(val []byte) error {
 567  				revoked := &WireGuardRevokedKey{}
 568  				if err := json.Unmarshal(val, revoked); err != nil {
 569  					return err
 570  				}
 571  
 572  				// Check if this is the matching revoked key
 573  				if hex.Enc(revoked.WGPublicKey) == wgPubkeyHex {
 574  					revoked.AccessCount++
 575  					revoked.LastAccessAt = now
 576  
 577  					data, err := json.Marshal(revoked)
 578  					if err != nil {
 579  						return err
 580  					}
 581  					return txn.Set(key, data)
 582  				}
 583  				return nil
 584  			})
 585  			if err != nil {
 586  				return err
 587  			}
 588  		}
 589  		return nil
 590  	})
 591  }
 592