Skip to content
Draft
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
15 changes: 9 additions & 6 deletions packages/core/src/utils/tunnelForward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ const FORWARDED_CLIENT_HEADERS = [
'referer',
];

/**
* Sentry SaaS replaces `X-Sentry-Forwarded-For` with the connecting IP, which is the tunnel server, but keeps these
* and prefers them. Passing them through keeps the client IP, as a rewrite proxy does.
*/
const FORWARDED_CLIENT_IP_HEADERS = ['cf-connecting-ip', 'x-vercel-forwarded-for', 'x-forwarded-for'];

/**
* Reads the body up to the envelope header line. Returns the line and every byte read, or `undefined`.
*/
Expand Down Expand Up @@ -383,16 +389,13 @@ function forwardHeaders(request: Request): Record<string, string> {
// No `Content-Length`: some runtimes reject it on a streamed body (Next.js edge in dev).
const headers: Record<string, string> = { 'Content-Type': 'application/x-sentry-envelope' };

// Relay would otherwise attribute the server's IP. Same precedence Relay uses.
const clientIp =
request.headers.get('cf-connecting-ip') ||
request.headers.get('x-vercel-forwarded-for') ||
request.headers.get('x-forwarded-for');
// For self-hosted Relay, which honors this header. Same precedence Relay uses.
const clientIp = FORWARDED_CLIENT_IP_HEADERS.map(name => request.headers.get(name)).find(Boolean);
if (clientIp) {
headers['X-Sentry-Forwarded-For'] = clientIp;
}

for (const name of FORWARDED_CLIENT_HEADERS) {
for (const name of [...FORWARDED_CLIENT_IP_HEADERS, ...FORWARDED_CLIENT_HEADERS]) {
const value = request.headers.get(name);
if (value) {
headers[name] = value;
Expand Down
25 changes: 25 additions & 0 deletions packages/core/test/lib/utils/tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,31 @@ describe('handleTunnelRequest', () => {
);
});

it('passes the client IP headers through unchanged, since Sentry SaaS replaces X-Sentry-Forwarded-For', async () => {
fetchMock.mockResolvedValueOnce(new Response('ok'));
const clientIpHeaders = {
'cf-connecting-ip': '1.1.1.1',
'x-vercel-forwarded-for': '3.3.3.3',
'x-forwarded-for': '2.2.2.2, 10.0.0.1',
};

await handleTunnelRequest(
new Request('http://localhost/tunnel', {
method: 'POST',
body: `${JSON.stringify({ dsn: TEST_DSN })}\n`,
headers: { ...clientIpHeaders, 'content-length': '1' },
}),
{ allowedDsns: [TEST_DSN] },
);

expect(fetchMock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({ ...clientIpHeaders, 'X-Sentry-Forwarded-For': '1.1.1.1' }),
}),
);
});

it('forwards the client headers Relay reads the browser and allowed domains from', async () => {
fetchMock.mockResolvedValueOnce(new Response('ok'));
const clientHeaders = {
Expand Down
Loading