mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): dial portal targets via localhost dual-stack
Vite and other Node >=17 dev servers bind ::1 only for localhost, so a fixed 127.0.0.1 dial 502s on the default path. Use hostname localhost with family autoselection and rewrite Host to match.
This commit is contained in:
@@ -163,7 +163,7 @@ describe("portal HTTP proxy", () => {
|
||||
// Node may add its own connection-local Keep-Alive header; the upstream value must not pass.
|
||||
expect(result.headers["keep-alive"]).not.toBe("upstream-secret=17");
|
||||
expect(received).toMatchObject({
|
||||
host: `127.0.0.1:${targetPort}`,
|
||||
host: `localhost:${targetPort}`,
|
||||
cookie: "app=ok; theme=dark",
|
||||
proto: "http",
|
||||
forwardedHost: "portal.example:9999",
|
||||
@@ -218,6 +218,31 @@ describe("portal HTTP proxy", () => {
|
||||
expect(result.body).toContain('http-equiv="refresh" content="2"');
|
||||
});
|
||||
|
||||
it("reaches IPv6-only targets through the localhost dual-stack dial", async () => {
|
||||
// Node >=17 dev servers (Vite, Next.js) often bind ::1 only on "localhost".
|
||||
const v6Target = createServer((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.end("v6 proxied");
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
v6Target.once("error", reject);
|
||||
v6Target.listen(0, "::1", () => resolve());
|
||||
});
|
||||
try {
|
||||
const v6Port = (v6Target.address() as AddressInfo).port;
|
||||
const portal = await portalService().open({ targetPort: v6Port });
|
||||
const result = await httpCall({
|
||||
port: portal.listenPort,
|
||||
path: `/?${portal.tokenQuery}`,
|
||||
});
|
||||
expect(result).toMatchObject({ status: 200, body: "v6 proxied" });
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => {
|
||||
v6Target.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("splices WebSockets and destroys upgraded sockets and listeners on close", async () => {
|
||||
const service = portalService();
|
||||
const portal = await service.open({ targetPort });
|
||||
|
||||
@@ -200,14 +200,17 @@ export function handlePortalProxyRequest(params: {
|
||||
|
||||
const headers = proxyHeaders(req.headers);
|
||||
const originalHost = req.headers.host;
|
||||
headers.host = `127.0.0.1:${target.targetPort}`;
|
||||
headers.host = `localhost:${target.targetPort}`;
|
||||
headers["x-forwarded-for"] = req.socket.remoteAddress ?? "";
|
||||
headers["x-forwarded-proto"] = tls ? "https" : "http";
|
||||
if (originalHost) {
|
||||
headers["x-forwarded-host"] = originalHost;
|
||||
}
|
||||
// Dial "localhost", not a fixed loopback literal: Node >=17 dev servers (Vite,
|
||||
// Next.js) often bind ::1 only, and family autoselection reaches either stack.
|
||||
const proxyReq = requestHttp({
|
||||
hostname: "127.0.0.1",
|
||||
hostname: "localhost",
|
||||
autoSelectFamily: true,
|
||||
port: target.targetPort,
|
||||
method: req.method,
|
||||
path: authorization.requestPath,
|
||||
@@ -257,7 +260,7 @@ function websocketHeaders(req: IncomingMessage, targetPort: number, requestPath:
|
||||
lines.push(`${normalized}: ${item}`);
|
||||
}
|
||||
}
|
||||
lines.push(`host: 127.0.0.1:${targetPort}`, "", "");
|
||||
lines.push(`host: localhost:${targetPort}`, "", "");
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
@@ -283,7 +286,12 @@ export function handlePortalProxyUpgrade(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSocket: Socket = net.connect({ host: "127.0.0.1", port: target.targetPort });
|
||||
// Same localhost/dual-stack contract as the HTTP path above.
|
||||
const targetSocket: Socket = net.connect({
|
||||
host: "localhost",
|
||||
autoSelectFamily: true,
|
||||
port: target.targetPort,
|
||||
});
|
||||
upgradedSockets.add(socket);
|
||||
upgradedSockets.add(targetSocket);
|
||||
const release = (stream: Duplex) => upgradedSockets.delete(stream);
|
||||
|
||||
Reference in New Issue
Block a user