Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Containerfile.mdsip
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ RUN set -eux; \
`# shared tree state again, silently and only under concurrency.` \
grep -q 'fork,reuseaddr' /usr/local/bin/fdp-mdsip-server; \
grep -q 'nofork' /usr/local/bin/fdp-mdsip-server; \
`# Both listener forms must carry fork+nofork: the Unix form is what` \
`# production runs, the TCP form what the tests and the default run.` \
grep -q 'UNIX-LISTEN:\$FDP_MDSIP_SOCKET,fork,unlink-early,mode=666' /usr/local/bin/fdp-mdsip-server; \
[ "$(grep -c 'EXEC:/usr/local/bin/fdp-mdsip-connection,nofork' /usr/local/bin/fdp-mdsip-server)" = 2 ]; \
`# The Pelican-path chain resolves relative to the working directory, so` \
`# losing this line breaks every ptdata lookup and nothing else.` \
grep -q '^cd /$' /usr/local/bin/fdp-mdsip-connection; \
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ ofs.osslib ++ /path/to/libXrdOssMdsplus.so prefix=/tdi server=localhost:8000
http.exthandler mdsip /path/to/libXrdHttpMdsip.so prefix=/mdsip,host=localhost,port=8000,auth=xrootd
```

```
# Production: the sandbox is rootless, so the relay reaches it over a Unix socket
# rather than a TCP hop through pasta (which stalls replies above ~4 MB):
http.exthandler mdsip /path/to/libXrdHttpMdsip.so prefix=/mdsip,socket=/run/fdp-mdsip/mdsip.sock,auth=xrootd
```

Note that neither name in the config carries the **`-5` suffix** the files on
disk do: XRootD appends the plugin version itself, and referencing the suffixed
name makes it search for `-5-5.so` and only succeed via a fallback. The client
Expand Down Expand Up @@ -294,6 +300,12 @@ Loaded alongside the osslib, again with the **unsuffixed** name:
http.exthandler mdsip /path/to/libXrdHttpMdsip.so prefix=/mdsip,host=localhost,port=8000,auth=xrootd
```

```
# Production: the sandbox is rootless, so the relay reaches it over a Unix socket
# rather than a TCP hop through pasta (which stalls replies above ~4 MB):
http.exthandler mdsip /path/to/libXrdHttpMdsip.so prefix=/mdsip,socket=/run/fdp-mdsip/mdsip.sock,auth=xrootd
```

Parms are a single `XrdOucStream::GetWord()` token, so they **cannot contain
spaces** — comma is the separator. Add `+notls` before the library path only
when running without TLS, as the test harness does.
Expand Down
20 changes: 20 additions & 0 deletions docs/deployment-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,26 @@ its production shape:
http.exthandler mdsip /plugins/libXrdHttpMdsip.so prefix=/mdsip,host=<mdsip-host>,port=8000,auth=xrootd,authpath=/fdp-d3d/archives
```

**Unix socket (production).** `socket=/run/fdp-mdsip/mdsip.sock` replaces
`host=` and `port=`; naming both is refused at load. The `fdp-mdsip` container
listens there when `FDP_MDSIP_SOCKET` is set (`scripts/fdp-mdsip-server`) and
runs with `Network=none`; the directory is a host tmpfs bind-mounted into both
containers. Measured reason: through pasta every reply above ~4 MB waited out a
200 ms retransmission timeout (d3d-origin-admin
`docs/specs/2026-10-13-mdsip-unix-socket-design.md`).

Three things to know when operating it:

- The directive's parameters are one comma-separated token, so a socket path
cannot contain a comma or whitespace. `d3d-origin-admin` renders a fixed
value; nothing else should.
- `port=` is strict: `port=80abc`, which `atoi` used to read as 80, is now
refused at load with the text that was given.
- mdsip logs the peer of each connection into `/tmp/mdsip.access` by reading
`getpeername` into a `sockaddr_in`. An `AF_UNIX` peer fills only the family,
so those lines show a meaningless IPv4 address. The hosts-file match
(`* | MAP_TO_LOCAL`) is unaffected; do not chase that address as a client.

Note the **unsuffixed** library name: XRootD appends the plugin version itself.

### Checks worth doing before the change
Expand Down
12 changes: 12 additions & 0 deletions scripts/fdp-mdsip-server
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,17 @@
#
# Losing either flag serves shared tree state again -- silently, and only under
# concurrency. Containerfile.mdsip greps this file for both.
# FDP_MDSIP_SOCKET set: listen on that Unix socket instead of TCP. Production
# uses this because the sandbox is rootless and a TCP hop into it crosses
# pasta, which stalls every reply above ~4 MB for a retransmission timeout.
# unlink-early remove a stale socket file from a previous run before bind
# mode=666 any local process may connect -- the same reachability as the
# bridge-address TCP port had; the directory's 0711 keeps it
# unlistable (d3d-origin-admin docs/specs/2026-10-13-mdsip-unix-socket-design.md)
# The path is a socat address: it must contain no comma or colon.
if [ -n "${FDP_MDSIP_SOCKET:-}" ]; then
exec /usr/bin/socat "UNIX-LISTEN:$FDP_MDSIP_SOCKET,fork,unlink-early,mode=666" \
EXEC:/usr/local/bin/fdp-mdsip-connection,nofork
fi
exec /usr/bin/socat TCP-LISTEN:8000,fork,reuseaddr \
EXEC:/usr/local/bin/fdp-mdsip-connection,nofork
28 changes: 16 additions & 12 deletions src/HttpRelay.cc
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ std::string HeaderValue(std::map<std::string, std::string> &headers,
class HttpMdsipRelay : public XrdHttpExtHandler {
public:
HttpMdsipRelay(XrdSysError *log, const std::string &prefix,
const std::string &host, int port, int idle,
const fdp::MdsipTarget &target, int idle,
size_t max_sessions, int timeout_ms,
XrdSfsFileSystem *sfs, const std::string &authpath,
const std::string &point_prefix,
Expand All @@ -152,7 +152,7 @@ class HttpMdsipRelay : public XrdHttpExtHandler {
: log_(log), prefix_(prefix), sfs_(sfs), authpath_(authpath),
point_prefix_(point_prefix), point_authpath_(point_authpath),
points_(points),
sessions_(host, port, idle, max_sessions, timeout_ms) {}
sessions_(target, idle, max_sessions, timeout_ms) {}

bool MatchesPath(const char *verb, const char *path) {
if (!path || !verb) return false;
Expand Down Expand Up @@ -451,20 +451,21 @@ extern "C" XrdHttpExtHandler *XrdHttpGetExtHandler(XrdSysError *eDest,
(void)confg;

const std::string prefix = ParmValue(parms, "prefix", "/mdsip");
const std::string host = ParmValue(parms, "host", "localhost");
const std::string port_str = ParmValue(parms, "port", "8000");
const int port = std::atoi(port_str.c_str());
fdp::MdsipTarget target;
std::string target_error;
if (!fdp::ResolveMdsipTarget(ParmValue(parms, "host", ""),
ParmValue(parms, "port", ""),
ParmValue(parms, "socket", ""),
target, target_error)) {
eDest->Emsg("relay", "refusing to load:", target_error.c_str());
return 0;
}
const int idle = std::atoi(ParmValue(parms, "idle", "300").c_str());
const int timeout_ms =
std::atoi(ParmValue(parms, "timeout", "60").c_str()) * 1000;
const size_t max_sessions =
std::strtoul(ParmValue(parms, "maxsessions", "256").c_str(), 0, 10);

if (port <= 0 || port > 65535) {
eDest->Emsg("relay", "invalid mdsip port", port_str.c_str());
return 0;
}

// An XrdHttpExtHandler runs BEFORE XRootD's authorization. XrdHttpReq.cc
// dispatches at reqstate == 0, ahead of the file-access path where
// ofs.authorize and the SciTokens plugin live, so nothing this handler
Expand Down Expand Up @@ -599,10 +600,13 @@ extern "C" XrdHttpExtHandler *XrdHttpGetExtHandler(XrdSysError *eDest,
eDest->Say("++++++ XrdHttpMdsip point endpoint: ", pbanner.c_str());
}

const std::string banner = prefix + " -> mdsip " + host + ":" + port_str;
const std::string where = target.socket_path.empty()
? target.host + ":" + std::to_string(target.port)
: "unix:" + target.socket_path;
const std::string banner = prefix + " -> mdsip " + where;
eDest->Say("++++++ XrdHttpMdsip relay: ", banner.c_str());

return new HttpMdsipRelay(eDest, prefix, host, port, idle, max_sessions,
return new HttpMdsipRelay(eDest, prefix, target, idle, max_sessions,
timeout_ms, sfs, authpath,
point_prefix, point_authpath, points);
}
130 changes: 102 additions & 28 deletions src/MdsipSession.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <utility>

Expand Down Expand Up @@ -71,6 +73,39 @@ size_t MdsipMessageLength(const std::string &hdr) {
return len;
}

bool ResolveMdsipTarget(const std::string &host_parm,
const std::string &port_parm,
const std::string &socket_parm,
MdsipTarget &out, std::string &error) {
out = MdsipTarget();
error.clear();
if (!socket_parm.empty()) {
if (!host_parm.empty() || !port_parm.empty()) {
error = "socket= cannot be combined with host= or port=";
return false;
}
if (socket_parm.size() >= sizeof(sockaddr_un::sun_path)) {
error = "mdsip socket path too long: " + socket_parm;
return false;
}
out.socket_path = socket_parm;
return true;
}
out.host = host_parm.empty() ? std::string("localhost") : host_parm;
if (port_parm.empty()) {
out.port = 8000;
return true;
}
char *end = 0;
const long port = std::strtol(port_parm.c_str(), &end, 10);
if (!end || *end != '\0' || port <= 0 || port > 65535) {
error = "invalid mdsip port " + port_parm;
return false;
}
out.port = static_cast<int>(port);
return true;
}

bool ReadOneMessage(int fd, std::string &out, int timeout_ms) {
out.clear();

Expand All @@ -94,11 +129,25 @@ bool ReadOneMessage(int fd, std::string &out, int timeout_ms) {
return true;
}

MdsipSessions::MdsipSessions(std::string host, int port, int idle_seconds,
MdsipSessions::MdsipSessions(const MdsipTarget &target, int idle_seconds,
size_t max_sessions, int timeout_ms)
: host_(std::move(host)), port_(port), idle_seconds_(idle_seconds),
: target_(target), idle_seconds_(idle_seconds),
max_sessions_(max_sessions), timeout_ms_(timeout_ms) {}

namespace {
MdsipTarget TcpTarget(std::string host, int port) {
MdsipTarget t;
t.host = std::move(host);
t.port = port;
return t;
}
} // namespace

MdsipSessions::MdsipSessions(std::string host, int port, int idle_seconds,
size_t max_sessions, int timeout_ms)
: MdsipSessions(TcpTarget(std::move(host), port), idle_seconds,
max_sessions, timeout_ms) {}

MdsipSessions::~MdsipSessions() {
std::lock_guard<std::mutex> lock(mutex_);
for (std::map<std::string, Session>::iterator it = sessions_.begin();
Expand All @@ -125,34 +174,59 @@ std::string MdsipSessions::Open(std::string &error) {
}
}

char portbuf[16];
std::snprintf(portbuf, sizeof(portbuf), "%d", port_);

addrinfo hints;
std::memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

addrinfo *res = 0;
if (::getaddrinfo(host_.c_str(), portbuf, &hints, &res) != 0 || !res) {
error = "cannot resolve mdsip host " + host_;
return std::string();
}

int fd = -1;
for (addrinfo *ai = res; ai; ai = ai->ai_next) {
fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
::close(fd);
fd = -1;
if (!target_.socket_path.empty()) {
// One connect, no name resolution, no Nagle to disable: a Unix socket
// has neither. The path length check is the only way this can be
// misconfigured, and it is caught at load as well as here.
sockaddr_un addr;
std::memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (target_.socket_path.size() >= sizeof(addr.sun_path)) {
error = "mdsip socket path too long: " + target_.socket_path;
return std::string();
}
std::strncpy(addr.sun_path, target_.socket_path.c_str(),
sizeof(addr.sun_path) - 1);
fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (fd >= 0 && ::connect(fd, reinterpret_cast<sockaddr *>(&addr),
sizeof(addr)) != 0) {
::close(fd);
fd = -1;
}
if (fd < 0) {
error = "cannot connect to mdsip at " + target_.socket_path;
return std::string();
}
} else {
char portbuf[16];
std::snprintf(portbuf, sizeof(portbuf), "%d", target_.port);

addrinfo hints;
std::memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

addrinfo *res = 0;
if (::getaddrinfo(target_.host.c_str(), portbuf, &hints, &res) != 0 || !res) {
error = "cannot resolve mdsip host " + target_.host;
return std::string();
}
for (addrinfo *ai = res; ai; ai = ai->ai_next) {
fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
::close(fd);
fd = -1;
}
::freeaddrinfo(res);
if (fd < 0) {
error = "cannot connect to mdsip at " + target_.host;
return std::string();
}
const int one = 1;
::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
}
::freeaddrinfo(res);

if (fd < 0) { error = "cannot connect to mdsip at " + host_; return std::string(); }

const int one = 1;
::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));

const std::string token = RandomToken();
if (token.empty()) { ::close(fd); error = "cannot generate a session token"; return std::string(); }
Expand Down
27 changes: 24 additions & 3 deletions src/MdsipSession.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,26 @@

namespace fdp {

// Holds one mdsip TCP connection per client session.
// Where the relay finds mdsip: a TCP host and port, or a Unix socket path.
// The exthandler line names one form or the other. The Unix form exists
// because the production sandbox is a rootless container: a TCP hop into it
// crosses pasta, which stalls every reply above ~4 MB for a full
// retransmission timeout (measured 2026-10-13, d3d-origin-admin
// docs/specs/2026-10-13-mdsip-unix-socket-design.md).
struct MdsipTarget {
std::string host; // TCP form
int port = 0; // TCP form
std::string socket_path; // Unix form; empty means TCP
};

// Raw parameter strings as the directive gave them, "" where it did not.
// Fills `out`, or sets `error` and returns false.
bool ResolveMdsipTarget(const std::string &host_parm,
const std::string &port_parm,
const std::string &socket_parm,
MdsipTarget &out, std::string &error);

// Holds one mdsip connection (TCP or Unix socket) per client session.
//
// This exists because mdsip is STATEFUL: openTree sets the tree context on the
// connection. A relay that opened a fresh mdsip connection per HTTP request
Expand All @@ -20,6 +39,9 @@ namespace fdp {
// times out.
class MdsipSessions {
public:
MdsipSessions(const MdsipTarget &target, int idle_seconds,
size_t max_sessions, int timeout_ms);
// The TCP form, kept for callers and tests that name a host and port.
MdsipSessions(std::string host, int port, int idle_seconds,
size_t max_sessions, int timeout_ms);

Expand Down Expand Up @@ -64,8 +86,7 @@ private:
// relayer if one holds the fd.
void Retire(const std::map<std::string, Session>::iterator &it);

std::string host_;
int port_;
MdsipTarget target_;
int idle_seconds_;
size_t max_sessions_;
int timeout_ms_;
Expand Down
Loading
Loading