added docker-traefik-proxyprotocol

This commit is contained in:
bluepuma77
2025-02-20 18:04:25 +01:00
parent 032c651672
commit c352528044
7 changed files with 485 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# Specify the base image
FROM node:lts-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json
COPY package.json package-lock.json ./
# Install only production dependencies
RUN npm install --omit=dev
# Copy the rest of application's source code
COPY src/ ./src
# Copy local certificates
COPY *.pem .
# Expose the port the app runs on
EXPOSE 3000
# Command to run your app
CMD ["node", "src/index.js"]

View File

@@ -0,0 +1,87 @@
services:
traefik:
container_name: traefik
image: traefik:v3.3
ports:
- 80:80
- 443:443
- 8000:8000
- 8001:8001
- 8002:8002
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/certificates:/certificates
- /var/log:/var/log
command:
- --api.dashboard=true
- --api.insecure=true
- --log.level=DEBUG
#- --log.filepath=/var/log/traefik.log
- --accesslog=true
#- --accesslog.filepath=/var/log/traefik-access.log
- --providers.docker.network=proxy
- --providers.docker.exposedByDefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entryPoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.asDefault=true
- --entrypoints.websecure.http.tls.certresolver=myresolver
- --entrypoints.plaintcp0.address=:8000
- --entrypoints.plaintcp1.address=:8001
- --entrypoints.plaintcp2.address=:8002
- --certificatesresolvers.myresolver.acme.email=mail@example.com
- --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.storage=/certificates/acme.json
labels:
- traefik.enable=true
- traefik.http.routers.mydashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.mydashboard.service=api@internal
- traefik.http.routers.mydashboard.middlewares=myauth
- traefik.http.middlewares.myauth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/
whoami:
container_name: whoami
image: traefik/whoami:v1.10
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.mywhoami.rule=Host(`whoami.example.com`)
- traefik.http.services.mywhoami.loadbalancer.server.port=80
echo:
container_name: echo
build:
context: .
dockerfile: Dockerfile
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.myecho.rule=Host(`echo.example.com`)
- traefik.http.routers.myecho.service=myecho
- traefik.http.services.myecho.loadbalancer.server.port=3000
- traefik.tcp.routers.myecho0.entrypoints=plaintcp0
- traefik.tcp.routers.myecho0.rule=HostSNI(`*`)
- traefik.tcp.routers.myecho0.service=myecho0
- traefik.tcp.services.myecho0.loadbalancer.server.port=3000
- traefik.tcp.routers.myecho1.entrypoints=plaintcp1
- traefik.tcp.routers.myecho1.rule=HostSNI(`*`)
- traefik.tcp.routers.myecho1.service=myecho1
- traefik.tcp.services.myecho1.loadbalancer.server.port=3000
- traefik.tcp.services.myecho1.loadbalancer.proxyprotocol.version=1
- traefik.tcp.routers.myecho2.entrypoints=plaintcp2
- traefik.tcp.routers.myecho2.rule=HostSNI(`*`)
- traefik.tcp.routers.myecho2.service=myecho2
- traefik.tcp.services.myecho2.loadbalancer.server.port=3000
- traefik.tcp.services.myecho2.loadbalancer.proxyprotocol.version=2
networks:
proxy:
name: proxy

View File

@@ -0,0 +1,13 @@
{
"name": "node-express-proxyprotocol-tls",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "node-express-proxyprotocol-tls",
"version": "1.0.0",
"license": "BSD"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "node-express-proxyprotocol-tls",
"version": "1.0.0",
"license": "BSD",
"type": "module",
"dependencies": {
}
}

View File

@@ -0,0 +1,211 @@
// index.js (ESM)
import fs from 'fs';
import net from 'net';
import tls from 'tls';
import { parseProxyProtocol } from './proxyprotocol.js';
// ---------------------------------------------------------------------
// Configuration:
const port = Number(process.env.PORT) || 3000;
// If you have certs for TLS, put them in ./certs/key.pem & ./certs/cert.pem.
// If they don't exist, TLS connections will fail to handshake.
const keyPath = './private-key.pem';
const certPath = './certificate.pem';
const haveTLSCerts = fs.existsSync(keyPath) && fs.existsSync(certPath);
let secureContext = null;
if (haveTLSCerts) {
secureContext = tls.createSecureContext({
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
});
}
// ---------------------------------------------------------------------
// Utility to parse an HTTP request from a buffer if it contains \r\n\r\n
function parseHttpRequest(buffer) {
const raw = buffer.toString('utf8');
const headersEnd = raw.indexOf('\r\n\r\n');
if (headersEnd === -1) {
// No complete HTTP header yet
return null;
}
// Separate the header part from any potential body
const headerPart = raw.slice(0, headersEnd);
const lines = headerPart.split('\r\n');
const [requestLine, ...headerLines] = lines;
const [method = '', path = '', version = ''] = requestLine.split(' ');
const headers = {};
for (const line of headerLines) {
const idx = line.indexOf(':');
if (idx > 0) {
const key = line.slice(0, idx).trim().toLowerCase();
const val = line.slice(idx + 1).trim();
headers[key] = val;
}
}
return {
http: {
method,
path,
version,
},
headers,
// rawLength is how many bytes total were in the headers portion
rawLength: headersEnd + 4, // +4 for "\r\n\r\n"
};
}
// ---------------------------------------------------------------------
// Construct the JSON echo response
function buildHttpResponse(proxyInfo, isTLS, http, headers, socket) {
const responseData = {
proxyProtocolVersion: proxyInfo?.version || 'none',
connectionSourceIp: socket.remoteAddress, // from raw or TLS socket
proxyProtocolSourceIp: proxyInfo?.sourceIp || null,
isTLS,
http,
httpHeaders: headers,
};
const json = JSON.stringify(responseData, null, 2);
return (
'HTTP/1.1 200 OK\r\n' +
'Content-Type: application/json\r\n' +
`Content-Length: ${Buffer.byteLength(json)}\r\n` +
'\r\n' +
json
);
}
// ---------------------------------------------------------------------
// Handle plain HTTP requests
function handlePlainHttp(socket, initialBuffer, proxyInfo) {
let buffer = initialBuffer;
// Immediately check if the initial buffer already contains a full request
maybeRespond();
// If not complete, listen for further data
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
maybeRespond();
});
function maybeRespond() {
const parsed = parseHttpRequest(buffer);
if (parsed) {
const { http, headers } = parsed;
const response = buildHttpResponse(proxyInfo, false, http, headers, socket);
socket.write(response);
socket.end();
// Once we respond, we don't parse further requests on the same connection
}
}
}
// ---------------------------------------------------------------------
// Handle TLS-wrapped HTTP requests
function handleTLSHttp(tlsSocket, initialBuffer, proxyInfo) {
let buffer = initialBuffer;
// Check if the initial buffer (post-handshake) has a full request
maybeRespond();
tlsSocket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
maybeRespond();
});
function maybeRespond() {
const parsed = parseHttpRequest(buffer);
if (parsed) {
const { http, headers } = parsed;
const response = buildHttpResponse(proxyInfo, true, http, headers, tlsSocket);
tlsSocket.write(response);
tlsSocket.end();
}
}
}
// ---------------------------------------------------------------------
// Main server: detect Proxy Protocol, then TLS vs. plain
function startServer() {
const server = net.createServer((rawSocket) => {
let buffer = Buffer.alloc(0);
let proxyInfo = null;
let protocolDetected = false;
rawSocket.on('error', (err) => {
console.error('Raw socket error:', err);
});
rawSocket.on('data', (chunk) => {
// Accumulate data
buffer = Buffer.concat([buffer, chunk]);
if (!protocolDetected) {
// 1) Attempt Proxy Protocol parse
const ppResult = parseProxyProtocol(buffer);
if (ppResult?.proxy) {
proxyInfo = ppResult.proxy;
buffer = buffer.slice(ppResult.bytesProcessed);
}
// If no valid PP, that's fine; we just continue
// 2) Check if remainder looks like a TLS handshake
// Typically: 0x16 (Handshake), 0x03 (TLS major), 0x01..0x03
let isTLS = false;
if (buffer.length >= 3) {
if (buffer[0] === 0x16 && buffer[1] === 0x03) {
isTLS = true;
}
}
protocolDetected = true;
// Switch to TLS or stay plain
if (isTLS && secureContext) {
// Wrap raw socket in a TLSSocket
const tlsSocket = new tls.TLSSocket(rawSocket, {
isServer: true,
secureContext,
});
// Immediately feed any data we've already read to the TLS engine
// (internal approach)
tlsSocket._handle?.receive(buffer);
// Remove all listeners from raw socket so we don't double-consume data
rawSocket.removeAllListeners('data');
tlsSocket.on('error', (err) => {
console.error('TLS socket error:', err);
});
// Once TLS handshake completes:
tlsSocket.on('secure', () => {
// Now handle the actual HTTP
handleTLSHttp(tlsSocket, Buffer.alloc(0), proxyInfo);
});
} else {
// Plain HTTP
rawSocket.removeAllListeners('data');
handlePlainHttp(rawSocket, buffer, proxyInfo);
}
}
// If protocol already detected, do nothing here
});
});
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
console.log(`TLS certs found: ${haveTLSCerts ? 'Yes' : 'No'}`);
});
}
startServer();

View File

@@ -0,0 +1,143 @@
// proxyprotocol.js (ESM)
export function parseProxyProtocol(buffer) {
// 1) Try Proxy Protocol v1 (starts with "PROXY ")
const text = buffer.toString('ascii', 0, Math.min(buffer.length, 108));
if (text.startsWith('PROXY ')) {
const lineEnd = text.indexOf('\r\n');
if (lineEnd === -1) {
return { error: 'Incomplete Proxy Protocol v1 header' };
}
const line = text.substring(0, lineEnd); // e.g. "PROXY TCP4 1.2.3.4 5.6.7.8 12345 80"
const parts = line.split(' ');
if (parts.length >= 6) {
return {
proxy: {
version: 'v1',
protocol: parts[1], // e.g. "TCP4", "TCP6"
sourceIp: parts[2],
destinationIp: parts[3],
sourcePort: parts[4],
destinationPort: parts[5],
},
bytesProcessed: lineEnd + 2, // +2 for "\r\n"
};
}
return { error: 'Malformed Proxy Protocol v1 header' };
}
// 2) Try Proxy Protocol v2
// Signature bytes: 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A
if (buffer.length >= 16) {
const sig = buffer.slice(0, 12).toString('hex');
const v2sig = '0d0a0d0a000d0a515549540a';
if (sig === v2sig) {
const verCmd = buffer[12];
const version = verCmd >> 4; // High 4 bits => version
if (version !== 2) {
return { error: 'Invalid Proxy Protocol v2 header (version != 2)' };
}
// Byte 13 => family & protocol
const familyByte = buffer[13];
// Byte 14..15 => length of remaining header data
const len = buffer.readUInt16BE(14);
const totalV2HeaderLen = 16 + len;
if (buffer.length < totalV2HeaderLen) {
return { error: 'Incomplete Proxy Protocol v2 header' };
}
// The upper 4 bits = address family; the lower 4 bits = transport protocol
// 0x1x => AF_INET (IPv4)
// 0x2x => AF_INET6 (IPv6)
// 0x0x => AF_UNSPEC
// plus the lower bits: 1 => STREAM (TCP), 2 => DGRAM (UDP), etc.
const addressFamily = (familyByte & 0xf0) >> 4; // top nibble
const transportProto = familyByte & 0x0f; // bottom nibble
let sourceIp = null;
let destinationIp = null;
let sourcePort = null;
let destinationPort = null;
// The address block starts at offset 16
// For IPv4 + TCP/UDP: 4 bytes src IP, 4 bytes dst IP, 2 bytes src port, 2 bytes dst port (total 12)
// For IPv6 + TCP/UDP: 16 bytes src IP,16 bytes dst IP, 2 bytes src port, 2 bytes dst port (total 36)
let offset = 16;
if (addressFamily === 0x1) {
// IPv4
if (len >= 12) {
// 4 bytes src IP
const srcBuf = buffer.slice(offset, offset + 4);
offset += 4;
// 4 bytes dst IP
const dstBuf = buffer.slice(offset, offset + 4);
offset += 4;
// 2 bytes src port
sourcePort = buffer.readUInt16BE(offset);
offset += 2;
// 2 bytes dst port
destinationPort = buffer.readUInt16BE(offset);
offset += 2;
sourceIp = srcBuf.join('.');
destinationIp = dstBuf.join('.');
}
} else if (addressFamily === 0x2) {
// IPv6
if (len >= 36) {
// 16 bytes src IP
const srcBuf = buffer.slice(offset, offset + 16);
offset += 16;
// 16 bytes dst IP
const dstBuf = buffer.slice(offset, offset + 16);
offset += 16;
// 2 bytes src port
sourcePort = buffer.readUInt16BE(offset);
offset += 2;
// 2 bytes dst port
destinationPort = buffer.readUInt16BE(offset + 2);
// Convert buffer to IPv6 string
sourceIp = bufToIPv6(srcBuf);
destinationIp = bufToIPv6(dstBuf);
}
} else {
// Family was 0x0 (UNSPEC) or 0x4 (AF_UNIX), etc.
// We won't parse addresses here.
// In that case, the official spec says no address information is carried.
}
return {
proxy: {
version: 'v2',
sourceIp,
destinationIp,
sourcePort,
destinationPort,
family: addressFamily, // 1 => IPv4, 2 => IPv6
protocol: transportProto, // 1 => TCP, 2 => UDP, ...
},
bytesProcessed: totalV2HeaderLen,
};
}
}
// If none recognized
return { error: 'No Proxy Protocol header' };
}
// Helper function to convert a 16-byte IPv6 buffer into a readable string
function bufToIPv6(buf) {
// Split into eight 16-bit words
const parts = [];
for (let i = 0; i < 16; i += 2) {
parts.push(buf.readUInt16BE(i).toString(16));
}
// Then compress the longest run of zeros
const ipv6Str = parts.join(':')
.replace(/(^|:)0+([0-9a-f])/g, '$1$2') // remove leading zeros in each group
.replace(/(:0+)+:/, '::'); // collapse multiple groups of zeros
return ipv6Str;
}