fix: add InlineDNSLink flag to PublicGateways config (#9328)
https://github.com/ipfs/kubo/issues/9243 Co-authored-by: Marcin Rataj <lidel@lidel.org>
Henrique Dias committed
Oct 11, 2022 at 15:45 UTC
4291d6b2369b8f1dc3cd5c191cd4f3b099df1560
5 files changed
+91
-23
config/gateway.go
+7
@@ -1,5 +1,7 @@
1
package config
2
3
+const DefaultInlineDNSLink = false
4
+
5
type GatewaySpec struct {
6
// Paths is explicit list of path prefixes that should be handled by
7
// this gateway. Example: `["/ipfs", "/ipns", "/api"]`
@@ -18,6 +20,11 @@ type GatewaySpec struct {
20
// NoDNSLink configures this gateway to _not_ resolve DNSLink for the FQDN
21
// provided in `Host` HTTP header.
22
NoDNSLink bool
23
+
24
+ // InlineDNSLink configures this gateway to always inline DNSLink names
25
+ // (FQDN) into a single DNS label in order to interop with wildcard TLS certs
26
+ // and Origin per CID isolation provided by rules like https://publicsuffix.org
27
+ InlineDNSLink Flag
28
}
29
30
// Gateway contains options for the HTTP gateway server.
core/corehttp/hostname.go
+9
-5
@@ -84,7 +84,8 @@ func HostnameOption() ServeOption {
84
if gw.UseSubdomains {
85
// Yes, redirect if applicable
86
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
87
- newURL, err := toSubdomainURL(host, r.URL.Path, r, coreAPI)
87
+ useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
88
+ newURL, err := toSubdomainURL(host, r.URL.Path, r, useInlinedDNSLink, coreAPI)
89
if err != nil {
90
http.Error(w, err.Error(), http.StatusBadRequest)
91
return
@@ -132,6 +133,9 @@ func HostnameOption() ServeOption {
133
// Assemble original path prefix.
134
pathPrefix := "/" + ns + "/" + rootID
135
136
+ // Retrieve whether or not we should inline DNSLink.
137
+ useInlinedDNSLink := gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink)
138
+
139
// Does this gateway _handle_ subdomains AND this path?
140
if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
141
// If not, resource does not exist, return 404
@@ -149,7 +153,7 @@ func HostnameOption() ServeOption {
153
}
154
if !strings.HasPrefix(r.Host, dnsCID) {
155
dnsPrefix := "/" + ns + "/" + dnsCID
152
- newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, coreAPI)
156
+ newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
157
if err != nil {
158
http.Error(w, err.Error(), http.StatusBadRequest)
159
return
@@ -165,7 +169,7 @@ func HostnameOption() ServeOption {
169
// Do we need to fix multicodec in PeerID represented as CIDv1?
170
if isPeerIDNamespace(ns) {
171
if rootCID.Type() != cid.Libp2pKey {
168
- newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, coreAPI)
172
+ newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, useInlinedDNSLink, coreAPI)
173
if err != nil {
174
http.Error(w, err.Error(), http.StatusBadRequest)
175
return
@@ -451,7 +455,7 @@ func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
455
}
456
457
// Converts a hostname/path to a subdomain-based URL, if applicable.
454
-func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI) (redirURL string, err error) {
458
+func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool, ipfs iface.CoreAPI) (redirURL string, err error) {
459
var scheme, ns, rootID, rest string
460
461
query := r.URL.RawQuery
@@ -554,7 +558,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, ipfs iface.CoreAPI)
558
// can be loaded from a subdomain gateway with a wildcard TLS cert if
559
// represented as a single DNS label:
560
// https://my-v--long-example-com.ipns.dweb.link
557
- if isHTTPS && ns == "ipns" && strings.Contains(rootID, ".") {
561
+ if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") {
562
if isDNSLinkName(r.Context(), ipfs, rootID) {
563
// my.v-long.example.com → my-v--long-example-com
564
dnsLabel, err := toDNSLinkDNSLabel(rootID)
core/corehttp/hostname_test.go
+20
-16
@@ -36,35 +36,39 @@ func TestToSubdomainURL(t *testing.T) {
36
37
for _, test := range []struct {
38
// in:
39
- request *http.Request
40
- gwHostname string
41
- path string
39
+ request *http.Request
40
+ gwHostname string
41
+ inlineDNSLink bool
42
+ path string
43
// out:
44
url string
45
err error
46
}{
47
// DNSLink
47
- {httpRequest, "localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
48
+ {httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
49
// Hostname with port
49
- {httpRequest, "localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
50
+ {httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
51
// CIDv0 → CIDv1base32
51
- {httpRequest, "localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
52
+ {httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
53
// CIDv1 with long sha512
53
- {httpRequest, "localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
54
+ {httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
55
// PeerID as CIDv1 needs to have libp2p-key multicodec
55
- {httpRequest, "localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
56
- {httpRequest, "localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
56
+ {httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
57
+ {httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
58
// PeerID: ed25519+identity multihash → CIDv1Base36
58
- {httpRequest, "localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
59
- {httpRequest, "sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
59
+ {httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
60
+ {httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
61
// HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
61
- {httpRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
62
- {httpsRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
63
- {httpsProxiedRequest, "dweb.link", "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
62
+ {httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
63
+ {httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
64
+ {httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
65
+ // HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
66
+ {httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
67
+ {httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
68
} {
65
- url, err := toSubdomainURL(test.gwHostname, test.path, test.request, coreAPI)
69
+ url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, coreAPI)
70
if url != test.url || !equalError(err, test.err) {
67
- t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.path, url, err, test.url, test.err)
71
+ t.Errorf("(%s, %v, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.inlineDNSLink, test.path, url, err, test.url, test.err)
72
}
73
}
74
}
docs/config.md
+23
-2
@@ -59,6 +59,7 @@ config file at runtime.
59
- [`Gateway.PublicGateways: Paths`](#gatewaypublicgateways-paths)
60
- [`Gateway.PublicGateways: UseSubdomains`](#gatewaypublicgateways-usesubdomains)
61
- [`Gateway.PublicGateways: NoDNSLink`](#gatewaypublicgateways-nodnslink)
62
+ - [`Gateway.PublicGateways: InlineDNSLink`](#gatewaypublicgateways-inlinednslink)
63
- [Implicit defaults of `Gateway.PublicGateways`](#implicit-defaults-of-gatewaypublicgateways)
64
- [`Gateway` recipes](#gateway-recipes)
65
- [`Identity`](#identity)
@@ -149,7 +150,7 @@ config file at runtime.
150
- [`Swarm.Transports.Network.QUIC`](#swarmtransportsnetworkquic)
151
- [`Swarm.Transports.Network.Relay`](#swarmtransportsnetworkrelay)
152
- [`Swarm.Transports.Network.WebTransport`](#swarmtransportsnetworkwebtransport)
152
- - [`How to enable WebTransport`](#how-to-enable-webtransport)
153
+ - [How to enable WebTransport](#how-to-enable-webtransport)
154
- [`Swarm.Transports.Security`](#swarmtransportssecurity)
155
- [`Swarm.Transports.Security.TLS`](#swarmtransportssecuritytls)
156
- [`Swarm.Transports.Security.SECIO`](#swarmtransportssecuritysecio)
@@ -767,6 +768,26 @@ Default: `false` (DNSLink lookup enabled by default for every defined hostname)
768
769
Type: `bool`
770
771
+#### `Gateway.PublicGateways: InlineDNSLink`
772
+
773
+An optional flag to explicitly configure whether subdomain gateway's redirects
774
+(enabled by `UseSubdomains: true`) should always inline a DNSLink name (FQDN)
775
+into a single DNS label:
776
+
777
+```
778
+//example.com/ipns/example.net → HTTP 301 → //example-net.ipns.example.com
779
+```
780
+
781
+DNSLink name inlining allows for HTTPS on public subdomain gateways with single
782
+label wildcard TLS certs (also enabled when passing `X-Forwarded-Proto: https`),
783
+and provides disjoint Origin per root CID when special rules like
784
+https://publicsuffix.org, or a custom localhost logic in browsers like Brave
785
+has to be applied.
786
+
787
+Default: `false`
788
+
789
+Type: `flag`
790
+
791
#### Implicit defaults of `Gateway.PublicGateways`
792
793
Default entries for `localhost` hostname and loopback IPs are always present.
@@ -1964,7 +1985,7 @@ Default: Disabled
1985
Type: `flag`
1986
1987
1967
-#### How to enable WebTransport
1988
+##### How to enable WebTransport
1989
1990
Thoses steps are temporary and wont be needed once we make it enabled by default.
1991
test/sharness/t0114-gateway-subdomains.sh
+32
@@ -323,6 +323,38 @@ test_localhost_gateway_response_should_contain \
323
"http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \
324
"Ref"
325
326
+## ============================================================================
327
+## Test DNSLink inlining on HTTP gateways
328
+## ============================================================================
329
+
330
+# set explicit subdomain gateway config for the hostname
331
+ipfs config --json Gateway.PublicGateways '{
332
+ "localhost": {
333
+ "UseSubdomains": true,
334
+ "InlineDNSLink": true,
335
+ "Paths": ["/ipfs", "/ipns", "/api"]
336
+ },
337
+ "example.com": {
338
+ "UseSubdomains": true,
339
+ "InlineDNSLink": true,
340
+ "Paths": ["/ipfs", "/ipns", "/api"]
341
+ }
342
+}' || exit 1
343
+# restart daemon to apply config changes
344
+test_kill_ipfs_daemon
345
+test_launch_ipfs_daemon_without_network
346
+
347
+test_localhost_gateway_response_should_contain \
348
+ "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
349
+ "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
350
+ "Location: http://en-wikipedia--on--ipfs-org.ipns.localhost:$GWAY_PORT/wiki"
351
+
352
+test_hostname_gateway_response_should_contain \
353
+ "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
354
+ "example.com" \
355
+ "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
356
+ "Location: http://en-wikipedia--on--ipfs-org.ipns.example.com/wiki"
357
+
358
## ============================================================================
359
## Test subdomain-based requests with a custom hostname config
360
## (origin per content root at http://*.example.com)