fix: recognize host.docker.internal as localhost in Docker deployments (#744)

* fix: recognize host.docker.internal as localhost in Docker deployments

* chore: remove tunnel-auth test file
This commit is contained in:
Iuliia Ivashko
2026-03-23 01:12:29 +02:00
committed by GitHub
parent 64b55025e1
commit b3905e7e93
2 changed files with 100 additions and 3 deletions
@@ -56,3 +56,4 @@ This module provides OpenCode server integration utilities for the web server ru
- All file writes include automatic backup before modification.
- Config merging follows priority: custom > project > user.
- UI auth uses scrypt for password hashing with constant-time comparison.
- Tunnel auth treats `host.docker.internal` as local-only when the socket remote IP is private/loopback.
@@ -80,11 +80,107 @@ const normalizeHost = (candidate) => {
return trimmed.replace(/:\d+$/, '');
};
const isLocalHost = (host) => {
const normalizeIpCandidate = (candidate) => {
if (typeof candidate !== 'string') {
return null;
}
const trimmed = candidate.trim().toLowerCase();
if (!trimmed) {
return null;
}
const withoutBrackets = trimmed.startsWith('[') && trimmed.endsWith(']')
? trimmed.slice(1, -1)
: trimmed;
const withoutZone = withoutBrackets.split('%')[0];
if (!withoutZone) {
return null;
}
if (withoutZone.startsWith('::ffff:')) {
const mappedIpv4 = withoutZone.slice('::ffff:'.length);
if (/^\d+\.\d+\.\d+\.\d+$/.test(mappedIpv4)) {
return mappedIpv4;
}
}
return withoutZone;
};
const getSocketRemoteIp = (req) => {
const remoteAddress = req?.socket?.remoteAddress || req?.connection?.remoteAddress;
return normalizeIpCandidate(remoteAddress);
};
const isPrivateOrLoopbackIpv4 = (candidate) => {
const octets = candidate.split('.').map((part) => Number(part));
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return false;
}
const [first, second] = octets;
if (first === 127) {
return true;
}
if (first === 10) {
return true;
}
if (first === 172 && second >= 16 && second <= 31) {
return true;
}
if (first === 192 && second === 168) {
return true;
}
if (first === 169 && second === 254) {
return true;
}
return false;
};
const isPrivateOrLoopbackIpv6 = (candidate) => {
if (candidate === '::1') {
return true;
}
if (candidate.startsWith('fc') || candidate.startsWith('fd')) {
return true;
}
return candidate.startsWith('fe8')
|| candidate.startsWith('fe9')
|| candidate.startsWith('fea')
|| candidate.startsWith('feb');
};
const isPrivateOrLoopbackIp = (candidate) => {
const normalized = normalizeIpCandidate(candidate);
if (!normalized) {
return false;
}
if (normalized.includes(':')) {
return isPrivateOrLoopbackIpv6(normalized);
}
return isPrivateOrLoopbackIpv4(normalized);
};
const isLocalHost = (host, req) => {
if (!host) {
return false;
}
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
return true;
}
if (host === 'host.docker.internal') {
return isPrivateOrLoopbackIp(getSocketRemoteIp(req));
}
return false;
};
const getClientIp = (req) => {
@@ -163,7 +259,7 @@ export const createTunnelAuth = () => {
return 'tunnel';
}
if (isLocalHost(reqHost)) {
if (isLocalHost(reqHost, req)) {
return 'local';
}