refactor: support percent-encoded /unix paths

This is a PoC that aims to support
https://github.com/multiformats/multiaddr/pull/174
while not breaking existing Kubo users.

See TODO in daemon.go – likely we want to move this to
https://github.com/multiformats/go-multiaddr
This commit is contained in:
Marcin Rataj 2025-06-09 18:55:47 +02:00
parent df2d1c77ae
commit d57e6bb77a
No known key found for this signature in database
GPG Key ID: 222B6784D5A79E42
4 changed files with 97 additions and 33 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/ipfs/go-cid"
legacy "github.com/ipfs/go-ipld-legacy"
ipfs "github.com/ipfs/kubo"
daemon "github.com/ipfs/kubo/cmd/ipfs/kubo"
iface "github.com/ipfs/kubo/core/coreiface"
caopts "github.com/ipfs/kubo/core/coreiface/options"
"github.com/ipfs/kubo/misc/fsutil"
@ -109,6 +110,7 @@ func NewApi(a ma.Multiaddr) (*HttpApi, error) {
return nil, err
}
if network == "unix" {
address = daemon.NormalizeUnixMultiaddr(address)
transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", address)
}

View File

@ -9,7 +9,9 @@ import (
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
@ -704,6 +706,24 @@ take effect.
return errs
}
// TODO: should a version of this live in https://github.com/multiformats/go-multiaddr
// so we dont need to duplicate code here and in client/rpc/api.go ?
func NormalizeUnixMultiaddr(address string) string {
// Support legacy and modern /unix addrs
// https://github.com/multiformats/multiaddr/pull/174
socketPath, err := url.PathUnescape(address)
if err != nil {
return address // nil, fmt.Errorf("failed to unescape /unix socket path: %w", err)
}
// Ensure the path is absolute
if !strings.HasPrefix(socketPath, string(filepath.Separator)) {
socketPath = string(filepath.Separator) + socketPath
}
// Normalize path
socketPath = filepath.Clean(socketPath)
return socketPath
}
// serveHTTPApi collects options, creates listener, prints status message and starts serving requests.
func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error) {
cfg, err := cctx.GetConfig()
@ -730,6 +750,9 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
}
for _, addr := range apiAddrs {
if strings.HasPrefix(addr, "/unix/") {
addr = NormalizeUnixMultiaddr(addr)
}
apiMaddr, err := ma.NewMultiaddr(addr)
if err != nil {
return nil, fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", addr, err)
@ -919,6 +942,9 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
gatewayAddrs := cfg.Addresses.Gateway
for _, addr := range gatewayAddrs {
if strings.HasPrefix(addr, "/unix/") {
addr = NormalizeUnixMultiaddr(addr)
}
gatewayMaddr, err := ma.NewMultiaddr(addr)
if err != nil {
return nil, fmt.Errorf("serveHTTPGateway: invalid gateway address: %q (err: %s)", addr, err)

View File

@ -249,7 +249,7 @@ the local [Kubo RPC API](https://docs.ipfs.tech/reference/kubo/rpc/) (`/api/v0`)
Supported Transports:
* tcp/ip{4,6} - `/ipN/.../tcp/...`
* unix - `/unix/path/to/socket`
* unix - `/unix/path/to/socket` or `/unix/path%2Fto%2Fsocket`
> [!CAUTION]
> **NEVER EXPOSE UNPROTECTED ADMIN RPC TO LAN OR THE PUBLIC INTERNET**
@ -276,7 +276,7 @@ the local [HTTP gateway](https://specs.ipfs.tech/http-gateways/) (`/ipfs`, `/ipn
Supported Transports:
* tcp/ip{4,6} - `/ipN/.../tcp/...`
* unix - `/unix/path/to/socket`
* unix - `/unix/path/to/socket` or `/unix/path%2Fto%2Fsocket`
Default: `/ip4/127.0.0.1/tcp/8080`

View File

@ -2,7 +2,10 @@ package cli
import (
"context"
"net/url"
"path"
"path/filepath"
"strings"
"testing"
rpcapi "github.com/ipfs/kubo/client/rpc"
@ -13,39 +16,72 @@ import (
)
func TestRPCUnixSocket(t *testing.T) {
node := harness.NewT(t).NewNode().Init()
t.Parallel()
sockDir := node.Dir
sockAddr := path.Join("/unix", sockDir, "sock")
node.UpdateConfig(func(cfg *config.Config) {
//cfg.Addresses.API = append(cfg.Addresses.API, sockPath)
cfg.Addresses.API = []string{sockAddr}
})
t.Log("Starting daemon with unix socket:", sockAddr)
node.StartDaemon()
unixMaddr, err := multiaddr.NewMultiaddr(sockAddr)
require.NoError(t, err)
apiClient, err := rpcapi.NewApi(unixMaddr)
require.NoError(t, err)
var ver struct {
Version string
testCases := []struct {
name string
getSockMultiaddr func(sockPath string) (unixMultiaddr string)
}{
{
name: "Legacy /unix: unescaped socket path",
getSockMultiaddr: func(sockDir string) string {
return path.Join("/unix", sockDir, "sock")
},
},
{
name: "Spec-compliant /unix: percent-encoded socket path without leading slash",
getSockMultiaddr: func(sockDir string) string {
sockPath := path.Join(sockDir, "sock")
pathWithoutLeadingSlash := strings.TrimPrefix(sockPath, string(filepath.Separator))
escapedPath := url.PathEscape(pathWithoutLeadingSlash)
return path.Join("/unix", escapedPath)
},
},
{
name: "Spec-compliant /unix: percent-encoded socket path with leading slash",
getSockMultiaddr: func(sockDir string) string {
sockPath := path.Join(sockDir, "sock")
escapedPath := url.PathEscape(sockPath)
return path.Join("/unix", escapedPath)
},
},
}
err = apiClient.Request("version").Exec(context.Background(), &ver)
require.NoError(t, err)
require.NotEmpty(t, ver)
t.Log("Got version:", ver.Version)
var res struct {
ID string
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
node := harness.NewT(t).NewNode().Init()
sockDir := node.Dir
sockAddr := tc.getSockMultiaddr(sockDir)
node.UpdateConfig(func(cfg *config.Config) {
//cfg.Addresses.API = append(cfg.Addresses.API, sockPath)
cfg.Addresses.API = []string{sockAddr}
})
t.Log("Starting daemon with unix socket:", sockAddr)
node.StartDaemon()
unixMaddr, err := multiaddr.NewMultiaddr(sockAddr)
require.NoError(t, err)
apiClient, err := rpcapi.NewApi(unixMaddr)
require.NoError(t, err)
var ver struct {
Version string
}
err = apiClient.Request("version").Exec(context.Background(), &ver)
require.NoError(t, err)
require.NotEmpty(t, ver)
t.Log("Got version:", ver.Version)
var res struct {
ID string
}
err = apiClient.Request("id").Exec(context.Background(), &res)
require.NoError(t, err)
require.NotEmpty(t, res)
t.Log("Got ID:", res.ID)
node.StopDaemon()
})
}
err = apiClient.Request("id").Exec(context.Background(), &res)
require.NoError(t, err)
require.NotEmpty(t, res)
t.Log("Got ID:", res.ID)
node.StopDaemon()
}