Skip to content

Commit 4af06fd

Browse files
authored
Merge pull request #671 from ipfs/add-protocol-filtering
feat: add routing filtering to delegated routing server IPIP-484
2 parents 628b0f6 + 137d34f commit 4af06fd

File tree

9 files changed

+874
-46
lines changed

9 files changed

+874
-46
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The following emojis are used to highlight certain changes:
1616

1717
### Added
1818

19+
- `routing/http`: added support for address and protocol filtering to the delegated routing server ([IPIP-484](https://github.com/ipfs/specs/pull/484)) [#671](https://github.com/ipfs/boxo/pull/671)
20+
1921
### Changed
2022

2123
### Removed

routing/http/client/client_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,7 @@ func TestClient_FindProviders(t *testing.T) {
228228
}
229229

230230
bitswapRecord := makeBitswapRecord()
231-
bitswapProviders := []iter.Result[types.Record]{
232-
{Val: &bitswapRecord},
233-
}
231+
peerRecordFromBitswapRecord := types.FromBitswapRecord(&bitswapRecord)
234232

235233
cases := []struct {
236234
name string
@@ -254,8 +252,8 @@ func TestClient_FindProviders(t *testing.T) {
254252
},
255253
{
256254
name: "happy case (with deprecated bitswap schema)",
257-
routerResult: bitswapProviders,
258-
expResult: bitswapProviders,
255+
routerResult: []iter.Result[types.Record]{{Val: &bitswapRecord}},
256+
expResult: []iter.Result[types.Record]{{Val: peerRecordFromBitswapRecord}},
259257
expStreamingResponse: true,
260258
},
261259
{

routing/http/server/filters.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package server
2+
3+
import (
4+
"reflect"
5+
"slices"
6+
"strings"
7+
8+
"github.com/ipfs/boxo/routing/http/types"
9+
"github.com/ipfs/boxo/routing/http/types/iter"
10+
"github.com/multiformats/go-multiaddr"
11+
)
12+
13+
// filters implements IPIP-0484
14+
15+
func parseFilter(param string) []string {
16+
if param == "" {
17+
return nil
18+
}
19+
return strings.Split(strings.ToLower(param), ",")
20+
}
21+
22+
// applyFiltersToIter applies the filters to the given iterator and returns a new iterator.
23+
//
24+
// The function iterates over the input iterator, applying the specified filters to each record.
25+
// It supports both positive and negative filters for both addresses and protocols.
26+
//
27+
// Parameters:
28+
// - recordsIter: An iterator of types.Record to be filtered.
29+
// - filterAddrs: A slice of strings representing the address filter criteria.
30+
// - filterProtocols: A slice of strings representing the protocol filter criteria.
31+
func applyFiltersToIter(recordsIter iter.ResultIter[types.Record], filterAddrs, filterProtocols []string) iter.ResultIter[types.Record] {
32+
mappedIter := iter.Map(recordsIter, func(v iter.Result[types.Record]) iter.Result[types.Record] {
33+
if v.Err != nil || v.Val == nil {
34+
return v
35+
}
36+
37+
switch v.Val.GetSchema() {
38+
case types.SchemaPeer:
39+
record, ok := v.Val.(*types.PeerRecord)
40+
if !ok {
41+
logger.Errorw("problem casting find providers record", "Schema", v.Val.GetSchema(), "Type", reflect.TypeOf(v).String())
42+
// drop failed type assertion
43+
return iter.Result[types.Record]{}
44+
}
45+
46+
record = applyFilters(record, filterAddrs, filterProtocols)
47+
if record == nil {
48+
return iter.Result[types.Record]{}
49+
}
50+
v.Val = record
51+
52+
//lint:ignore SA1019 // ignore staticcheck
53+
case types.SchemaBitswap:
54+
//lint:ignore SA1019 // ignore staticcheck
55+
record, ok := v.Val.(*types.BitswapRecord)
56+
if !ok {
57+
logger.Errorw("problem casting find providers record", "Schema", v.Val.GetSchema(), "Type", reflect.TypeOf(v).String())
58+
// drop failed type assertion
59+
return iter.Result[types.Record]{}
60+
}
61+
peerRecord := types.FromBitswapRecord(record)
62+
peerRecord = applyFilters(peerRecord, filterAddrs, filterProtocols)
63+
if peerRecord == nil {
64+
return iter.Result[types.Record]{}
65+
}
66+
v.Val = peerRecord
67+
}
68+
return v
69+
})
70+
71+
// filter out nil results and errors
72+
filteredIter := iter.Filter(mappedIter, func(v iter.Result[types.Record]) bool {
73+
return v.Err == nil && v.Val != nil
74+
})
75+
76+
return filteredIter
77+
}
78+
79+
// Applies the filters. Returns nil if the provider does not pass the protocols filter
80+
// The address filter is more complicated because it potentially modifies the Addrs slice.
81+
func applyFilters(provider *types.PeerRecord, filterAddrs, filterProtocols []string) *types.PeerRecord {
82+
if len(filterAddrs) == 0 && len(filterProtocols) == 0 {
83+
return provider
84+
}
85+
86+
if !protocolsAllowed(provider.Protocols, filterProtocols) {
87+
// If the provider doesn't match any of the passed protocols, the provider is omitted from the response.
88+
return nil
89+
}
90+
91+
// return untouched if there's no filter or filterAddrsQuery contains "unknown" and provider has no addrs
92+
if len(filterAddrs) == 0 || (len(provider.Addrs) == 0 && slices.Contains(filterAddrs, "unknown")) {
93+
return provider
94+
}
95+
96+
filteredAddrs := applyAddrFilter(provider.Addrs, filterAddrs)
97+
98+
// If filtering resulted in no addrs, omit the provider
99+
if len(filteredAddrs) == 0 {
100+
return nil
101+
}
102+
103+
provider.Addrs = filteredAddrs
104+
return provider
105+
}
106+
107+
// applyAddrFilter filters a list of multiaddresses based on the provided filter query.
108+
//
109+
// Parameters:
110+
// - addrs: A slice of types.Multiaddr to be filtered.
111+
// - filterAddrsQuery: A slice of strings representing the filter criteria.
112+
//
113+
// The function supports both positive and negative filters:
114+
// - Positive filters (e.g., "tcp", "udp") include addresses that match the specified protocols.
115+
// - Negative filters (e.g., "!tcp", "!udp") exclude addresses that match the specified protocols.
116+
//
117+
// If no filters are provided, the original list of addresses is returned unchanged.
118+
// If only negative filters are provided, addresses not matching any negative filter are included.
119+
// If positive filters are provided, only addresses matching at least one positive filter (and no negative filters) are included.
120+
// If both positive and negative filters are provided, the address must match at least one positive filter and no negative filters to be included.
121+
//
122+
// Returns:
123+
// A new slice of types.Multiaddr containing only the addresses that pass the filter criteria.
124+
func applyAddrFilter(addrs []types.Multiaddr, filterAddrsQuery []string) []types.Multiaddr {
125+
if len(filterAddrsQuery) == 0 {
126+
return addrs
127+
}
128+
129+
var filteredAddrs []types.Multiaddr
130+
var positiveFilters, negativeFilters []multiaddr.Protocol
131+
132+
// Separate positive and negative filters
133+
for _, filter := range filterAddrsQuery {
134+
if strings.HasPrefix(filter, "!") {
135+
negativeFilters = append(negativeFilters, multiaddr.ProtocolWithName(filter[1:]))
136+
} else {
137+
positiveFilters = append(positiveFilters, multiaddr.ProtocolWithName(filter))
138+
}
139+
}
140+
141+
for _, addr := range addrs {
142+
protocols := addr.Protocols()
143+
144+
// Check negative filters
145+
if containsAny(protocols, negativeFilters) {
146+
continue
147+
}
148+
149+
// If no positive filters or matches a positive filter, include the address
150+
if len(positiveFilters) == 0 || containsAny(protocols, positiveFilters) {
151+
filteredAddrs = append(filteredAddrs, addr)
152+
}
153+
}
154+
155+
return filteredAddrs
156+
}
157+
158+
// Helper function to check if protocols contain any of the filters
159+
func containsAny(protocols []multiaddr.Protocol, filters []multiaddr.Protocol) bool {
160+
for _, filter := range filters {
161+
if containsProtocol(protocols, filter) {
162+
return true
163+
}
164+
}
165+
return false
166+
}
167+
168+
func containsProtocol(protos []multiaddr.Protocol, proto multiaddr.Protocol) bool {
169+
for _, p := range protos {
170+
if p.Code == proto.Code {
171+
return true
172+
}
173+
}
174+
return false
175+
}
176+
177+
// protocolsAllowed returns true if the peerProtocols are allowed by the filter protocols.
178+
func protocolsAllowed(peerProtocols []string, filterProtocols []string) bool {
179+
if len(filterProtocols) == 0 {
180+
// If no filter is passed, do not filter
181+
return true
182+
}
183+
184+
for _, filterProtocol := range filterProtocols {
185+
if filterProtocol == "unknown" && len(peerProtocols) == 0 {
186+
return true
187+
}
188+
189+
for _, peerProtocol := range peerProtocols {
190+
if strings.EqualFold(peerProtocol, filterProtocol) {
191+
return true
192+
}
193+
194+
}
195+
}
196+
return false
197+
}

0 commit comments

Comments
 (0)