Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# AGENTS.md

someguy is a server implementing the [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/).
It proxies requests to the Amino DHT and other delegated routing endpoints. It
is a caching proxy, not a libp2p node.

## Build and test

```bash
go build ./...
go test ./...
```

Run `gofmt` and `go vet ./...` before committing.

## Code map

- `main.go`, `server.go`: CLI entry point, host and router wiring.
- `server_routers.go`: router composition (`composableRouter`, `parallelRouter`, `libp2pRouter`, `sanitizeRouter`).
- `server_cached_router.go`, `cached_addr_book.go`: address caching layer.
- `server_dht.go`: DHT setup (standard and accelerated).
- `server_delegated_routing.go`: delegated HTTP routing clients.

## Documentation

- [environment-variables.md](docs/environment-variables.md): all config flags and environment variables
- [peer-address-caching.md](docs/peer-address-caching.md): how `/providers` and `/peers` cache and refresh peer addresses
- [metrics.md](docs/metrics.md): Prometheus metrics
- [tracing.md](docs/tracing.md): OpenTelemetry tracing
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ The following emojis are used to highlight certain changes:

### Fixed

- `/routing/v1/peers/{peerid}` now serves addresses cache-first, the same way `/routing/v1/providers/{cid}` does. It answers from the cached address book and host peerstore before falling back to a DHT lookup, so a relay-dependent peer that is absent from peer routing but recently seen as a provider is no longer answered with an empty result. See [`docs/peer-address-caching.md`](https://github.com/ipfs/someguy/blob/main/docs/peer-address-caching.md).
- A completed identify now prunes a peer's cached addresses down to its current advertised set (signed peer record or identify listen addresses) plus any live-connection address, instead of unioning forever. This stops stale certhashes, dead relay circuits, and rotated NAT ports from accumulating across provider lookups and gossip.
- Multiaddrs in `/routing/v1` responses are returned in a stable sorted order. They previously came back in nondeterministic order, so repeated requests for the same peer or provider returned the same addresses shuffled differently.

### Security

## [v0.13.0] - 2026-05-26
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![GitHub Release](https://img.shields.io/github/v/release/ipfs/someguy?filter=!*rc*)](https://github.com/ipfs/someguy/releases)
[![Go Reference](https://pkg.go.dev/badge/github.com/ipfs/someguy.svg)](https://pkg.go.dev/github.com/ipfs/someguy)

Someguy is an [HTTP Delegated Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) server that proxies requests to the [Amino DHT](https://docs.ipfs.tech/concepts/glossary/#amino) and other Delegated Routing servers such as the [Network Indexer](https://cid.contact).
Someguy is an [HTTP Delegated Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) server that proxies requests to the [Amino DHT](https://docs.ipfs.tech/concepts/glossary/#amino) and other [delegated routing servers](https://specs.ipfs.tech/routing/http-routing-v1/).

[Shipyard](https://ipshipyard.com/) also runs a [public Someguy instance](https://docs.ipfs.tech/concepts/public-utilities/#delegated-routing-endpoint) at `https://delegated-ipfs.dev/routing/v1`.

Expand Down Expand Up @@ -53,7 +53,7 @@ Run `someguy` as a client or as a server.

### Server

Start the server with `someguy start`. By default it proxies requests to the [IPFS Amino DHT](https://blog.ipfs.tech/2023-09-amino-refactoring/) and the [cid.contact](https://cid.contact) indexer (IPNI) node.
Start the server with `someguy start`. By default it proxies requests to the [IPFS Amino DHT](https://blog.ipfs.tech/2023-09-amino-refactoring/) and other [Delegated Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) servers.

For more details, run `someguy start --help`.

Expand Down Expand Up @@ -84,6 +84,13 @@ someguy start --ipns-endpoints https://example.com

See [environment-variables.md](docs/environment-variables.md) for URL formats and configuration details.

## Documentation

- [environment-variables.md](docs/environment-variables.md): all config flags and environment variables
- [peer-address-caching.md](docs/peer-address-caching.md): how `/providers` and `/peers` cache and refresh peer addresses
- [metrics.md](docs/metrics.md): Prometheus metrics
- [tracing.md](docs/tracing.md): OpenTelemetry tracing

## Deployment

For self-hosting, run the [prebuilt Docker image](#docker).
Expand Down
123 changes: 110 additions & 13 deletions cached_addr_book.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/core/record"
"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
ma "github.com/multiformats/go-multiaddr"
Expand Down Expand Up @@ -92,7 +93,8 @@ type peerState struct {
}

type cachedAddrBook struct {
addrBook peerstore.AddrBook // memory address book
addrBook peerstore.AddrBook // someguy's own address book: durable, probed, written here
hostPeerstore peerstore.AddrBook // libp2p host peerstore, DHT-populated, read-only fallback
peerCache *lru.Cache[peer.ID, peerState] // LRU cache with additional metadata about peer
probingEnabled bool
isProbing atomic.Bool
Expand All @@ -102,6 +104,18 @@ type cachedAddrBook struct {

type AddrBookOption func(*cachedAddrBook) error

// WithHostPeerstore lets GetCachedAddrs fall back to the libp2p host peerstore,
// which go-libp2p-kad-dht populates with provider addresses during
// FindProviders (under a short TempAddrTTL). This catches peers seen very
// recently as providers that have not yet been copied into someguy's own
// longer-lived address book.
func WithHostPeerstore(ps peerstore.AddrBook) AddrBookOption {
return func(cab *cachedAddrBook) error {
cab.hostPeerstore = ps
return nil
}
}

func WithAllowPrivateIPs() AddrBookOption {
return func(cab *cachedAddrBook) error {
cab.allowPrivateIPs = true
Expand Down Expand Up @@ -185,20 +199,24 @@ func (cab *cachedAddrBook) background(ctx context.Context, host host.Host) {
peerStateSize.Set(float64(cab.peerCache.Len())) // update metric

ttl := cab.getTTL(host.Network().Connectedness(ev.Peer))
if ev.SignedPeerRecord != nil {
logger.Debug("Caching signed peer record")
cab, ok := peerstore.GetCertifiedAddrBook(cab.addrBook)
if ok {
_, err := cab.ConsumePeerRecord(ev.SignedPeerRecord, ttl)
if err != nil {
logger.Warnf("failed to consume signed peer record: %v", err)
}

// A completed identify reports the peer's current advertised
// addresses, which supersede the set accumulated from provider
// records, DHT gossip, and earlier identifies. Replace the
// stored set instead of unioning so stale certhashes, dead
// relay circuits, and rotated NAT ports do not pile up.
//
// Drop the remote addresses of inbound connections: that is the
// peer's ephemeral source port, which nobody can dial back to,
// so caching it would reintroduce exactly the junk this prune
// removes. Outbound (and direction-unknown) remotes are kept.
var connAddrs []ma.Multiaddr
for _, c := range host.Network().ConnsToPeer(ev.Peer) {
if c.Stat().Direction != network.DirInbound {
connAddrs = append(connAddrs, c.RemoteMultiaddr())
}
} else {
logger.Debug("No signed peer record, caching listen addresses")
// We don't have a signed peer record, so we use the listen addresses
cab.addrBook.AddAddrs(ev.Peer, ev.ListenAddrs, ttl)
}
cab.replacePeerAddrs(ev.Peer, ev.SignedPeerRecord, ev.ListenAddrs, connAddrs, ttl)
case event.EvtPeerConnectednessChanged:
// If the peer is not connected or limited, we update the TTL
if !hasValidConnectedness(ev.Connectedness) {
Expand All @@ -221,6 +239,52 @@ func (cab *cachedAddrBook) background(ctx context.Context, host host.Host) {
}
}

// replacePeerAddrs replaces p's stored addresses with the authoritative set
// from a completed identify: the signed peer record, else the identify listen
// addresses, plus any live-connection address so an active session is kept.
//
// Clearing first drops addresses absent from the current set (stale certhashes,
// dead relay circuits, rotated NAT ports) instead of letting them linger to TTL.
//
// libp2p/go-libp2p#3487 does the same prune inside ConsumePeerRecord (not yet
// in the pinned version); re-adding the same set here stays correct once it
// lands, so a dependency bump will not regress this.
func (cab *cachedAddrBook) replacePeerAddrs(p peer.ID, signed *record.Envelope, listenAddrs, connAddrs []ma.Multiaddr, ttl time.Duration) {
// Nothing authoritative to apply. Return before clearing so an identify that
// carried no usable addresses never wipes a peer's existing cached set.
if signed == nil && len(listenAddrs) == 0 && len(connAddrs) == 0 {
return
}

// Drop the accumulated set so addresses absent from the current advertised
// set are removed instead of unioned.
cab.addrBook.ClearAddrs(p)

accepted := false
if signed != nil {
if certBook, ok := peerstore.GetCertifiedAddrBook(cab.addrBook); ok {
ok, err := certBook.ConsumePeerRecord(signed, ttl)
if err != nil {
logger.Warnf("failed to consume signed peer record: %v", err)
}
accepted = ok
}
}
if !accepted {
// No signed record, no certified addr book, or the record was rejected
// (e.g. a sequence-number check in some go-libp2p version). Fall back to
// the identify listen addresses so the clear never leaves the peer with
// zero addresses.
cab.addrBook.AddAddrs(p, listenAddrs, ttl)
}

// Preserve live-connection addresses at the connected TTL even when absent
// from the advertised set, so an active session is never dropped.
if len(connAddrs) > 0 {
cab.addrBook.AddAddrs(p, connAddrs, ConnectedAddrTTL)
}
}

// Loops over all peers with addresses and probes them if they haven't been probed recently
func (cab *cachedAddrBook) probePeers(ctx context.Context, host host.Host) {
defer cab.isProbing.Store(false)
Expand Down Expand Up @@ -286,6 +350,13 @@ func (cab *cachedAddrBook) probePeers(ctx context.Context, host host.Host) {
func (cab *cachedAddrBook) GetCachedAddrs(p peer.ID) []types.Multiaddr {
cachedAddrs := cab.addrBook.Addrs(p)

// Fall back to the host peerstore, which the DHT fills with provider
// addresses during FindProviders (short TempAddrTTL). Lets peer routing
// serve a peer seen as a provider moments ago but absent from peer routing.
if len(cachedAddrs) == 0 && cab.hostPeerstore != nil {
cachedAddrs = cab.hostPeerstore.Addrs(p)
}

if len(cachedAddrs) == 0 {
return nil
}
Expand All @@ -297,6 +368,32 @@ func (cab *cachedAddrBook) GetCachedAddrs(p peer.ID) []types.Multiaddr {
return result
}

// CacheAddrs stores addresses observed for a peer outside of a direct
// connection (e.g. embedded in a provider record returned by FindProviders) so
// that later peer-routing lookups can serve them from the same peerbook.
// Private addresses are dropped unless explicitly allowed. These addresses are
// unverified, so they are stored with the recently-connected TTL and will be
// confirmed or evicted by the probe loop.
func (cab *cachedAddrBook) CacheAddrs(p peer.ID, addrs []types.Multiaddr) {
if len(addrs) == 0 {
return
}

maddrs := make([]ma.Multiaddr, 0, len(addrs))
for _, addr := range addrs {
if !cab.allowPrivateIPs && !manet.IsPublicAddr(addr.Multiaddr) {
continue
}
maddrs = append(maddrs, addr.Multiaddr)
}

if len(maddrs) == 0 {
return
}

cab.addrBook.AddAddrs(p, maddrs, cab.recentlyConnectedTTL)
}

// Update the peer cache with information about a failed connection
// This should be called when a connection attempt to a peer fails
func (cab *cachedAddrBook) RecordFailedConnection(p peer.ID) {
Expand Down
115 changes: 115 additions & 0 deletions cached_addr_book_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/p2p/host/eventbus"
"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -25,6 +27,114 @@ func TestCachedAddrBook(t *testing.T) {
require.NotNil(t, cab.addrBook)
}

func TestGetCachedAddrsHostPeerstoreFallback(t *testing.T) {
testPeer, err := peer.Decode("12D3KooWCZ67sU8oCvKd82Y6c9NgpqgoZYuZEUcg4upHCjK3n1aj")
require.NoError(t, err)
addr := ma.StringCast("/ip4/137.21.14.12/tcp/4001")

t.Run("falls back to host peerstore when addrBook is empty", func(t *testing.T) {
hostPeerstore := pstoremem.NewAddrBook()
hostPeerstore.AddAddrs(testPeer, []ma.Multiaddr{addr}, peerstore.TempAddrTTL)

cab, err := newCachedAddrBook(WithAllowPrivateIPs(), WithHostPeerstore(hostPeerstore))
require.NoError(t, err)

got := cab.GetCachedAddrs(testPeer)
require.Len(t, got, 1)
require.Equal(t, addr.String(), got[0].String())
})

t.Run("prefers addrBook over host peerstore", func(t *testing.T) {
ownAddr := ma.StringCast("/ip4/1.2.3.4/tcp/4001")
hostPeerstore := pstoremem.NewAddrBook()
hostPeerstore.AddAddrs(testPeer, []ma.Multiaddr{addr}, peerstore.TempAddrTTL)

cab, err := newCachedAddrBook(WithAllowPrivateIPs(), WithHostPeerstore(hostPeerstore))
require.NoError(t, err)
cab.addrBook.AddAddrs(testPeer, []ma.Multiaddr{ownAddr}, time.Hour)

got := cab.GetCachedAddrs(testPeer)
require.Len(t, got, 1)
require.Equal(t, ownAddr.String(), got[0].String())
})

t.Run("returns nil when both are empty", func(t *testing.T) {
cab, err := newCachedAddrBook(WithAllowPrivateIPs(), WithHostPeerstore(pstoremem.NewAddrBook()))
require.NoError(t, err)
require.Nil(t, cab.GetCachedAddrs(testPeer))
})
}

func TestReplacePeerAddrsPrunesStaleAddrs(t *testing.T) {
cab, err := newCachedAddrBook(WithAllowPrivateIPs())
require.NoError(t, err)

p := peer.ID("test-peer")

// Seed an accumulated set, as if learned from provider records and gossip.
stale := []ma.Multiaddr{
ma.StringCast("/ip4/1.1.1.1/tcp/4001"),
ma.StringCast("/ip4/2.2.2.2/udp/4001/quic-v1"),
}
cab.addrBook.AddAddrs(p, stale, time.Hour)
require.Len(t, cab.addrBook.Addrs(p), 2)

// A completed identify reports a different current set (no signed record),
// plus one address held by a live connection.
current := []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/4001")}
connAddr := ma.StringCast("/ip4/4.4.4.4/tcp/4001")

cab.replacePeerAddrs(p, nil, current, []ma.Multiaddr{connAddr}, time.Hour)

got := make([]string, 0)
for _, a := range cab.addrBook.Addrs(p) {
got = append(got, a.String())
}

// Stale addrs are gone; the current advertised addr and the live-connection
// addr remain.
require.ElementsMatch(t, []string{
"/ip4/3.3.3.3/tcp/4001",
"/ip4/4.4.4.4/tcp/4001",
}, got)
}

func TestReplacePeerAddrsKeepsLiveConnWhenAdvertisedSetEmpty(t *testing.T) {
cab, err := newCachedAddrBook(WithAllowPrivateIPs())
require.NoError(t, err)

p := peer.ID("test-peer")
connAddr := ma.StringCast("/ip4/4.4.4.4/tcp/4001")

// Identify reported no usable listen addrs, but we hold a live connection.
cab.replacePeerAddrs(p, nil, nil, []ma.Multiaddr{connAddr}, time.Hour)

got := make([]string, 0)
for _, a := range cab.addrBook.Addrs(p) {
got = append(got, a.String())
}
require.Equal(t, []string{"/ip4/4.4.4.4/tcp/4001"}, got)
}

func TestReplacePeerAddrsEmptyInputKeepsExistingAddrs(t *testing.T) {
cab, err := newCachedAddrBook(WithAllowPrivateIPs())
require.NoError(t, err)

p := peer.ID("test-peer")
existing := ma.StringCast("/ip4/1.1.1.1/tcp/4001")
cab.addrBook.AddAddrs(p, []ma.Multiaddr{existing}, time.Hour)

// An identify with no signed record, no listen addrs, and no live
// connection must not wipe the peer's existing cached addresses.
cab.replacePeerAddrs(p, nil, nil, nil, time.Hour)

got := make([]string, 0)
for _, a := range cab.addrBook.Addrs(p) {
got = append(got, a.String())
}
require.Equal(t, []string{"/ip4/1.1.1.1/tcp/4001"}, got)
}

func TestBackground(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down Expand Up @@ -229,6 +339,11 @@ func (mn *mockNetwork) Connectedness(p peer.ID) network.Connectedness {
return network.NotConnected
}

func (mn *mockNetwork) ConnsToPeer(p peer.ID) []network.Conn {
// No live connections in tests
return nil
}

func (mh *mockHost) EventBus() event.Bus {
return mh.eventBus
}
Loading
Loading