mirror of
https://github.com/bluepuma77/traefik-best-practice.git
synced 2025-12-21 13:23:10 +01:00
added docker-traefik-proxyprotocol
This commit is contained in:
23
docker-traefik-proxyprotocol/Dockerfile
Normal file
23
docker-traefik-proxyprotocol/Dockerfile
Normal 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"]
|
||||
87
docker-traefik-proxyprotocol/docker-compose.yml
Normal file
87
docker-traefik-proxyprotocol/docker-compose.yml
Normal 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
|
||||
13
docker-traefik-proxyprotocol/package-lock.json
generated
Normal file
13
docker-traefik-proxyprotocol/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
docker-traefik-proxyprotocol/package.json
Normal file
8
docker-traefik-proxyprotocol/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "node-express-proxyprotocol-tls",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
0
docker-traefik-proxyprotocol/src/README.md
Normal file
0
docker-traefik-proxyprotocol/src/README.md
Normal file
211
docker-traefik-proxyprotocol/src/index.js
Normal file
211
docker-traefik-proxyprotocol/src/index.js
Normal 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();
|
||||
143
docker-traefik-proxyprotocol/src/proxyprotocol.js
Normal file
143
docker-traefik-proxyprotocol/src/proxyprotocol.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user