mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
fix(desktop): reach split-horizon instances on their LAN IP (#3271)
This commit is contained in:
1 parent
3071bc7525
commit
d00389ab30
2 files changed
+74
-55
No files matched your search
@@ -115,19 +115,59 @@ describe('DesktopOutboundHTTP origin anchoring', () => {
|
|||||||
outboundHTTP.cleanup();
|
outboundHTTP.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an anchor that resolves to a refused scope never falls back to the remembered requirement', async () => {
|
test('an anchor that resolves to no usable address never falls back to the remembered requirement', async () => {
|
||||||
const outboundHTTP = new DesktopOutboundHTTP({
|
const outboundHTTP = new DesktopOutboundHTTP({
|
||||||
resolveHostAddresses: stubResolver({'mixed.example': ['10.0.0.1', '93.184.216.34']}),
|
resolveHostAddresses: stubResolver({'unusable.example': ['not-an-address']}),
|
||||||
});
|
});
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
outboundHTTP.registerAnchoredOrigins({
|
outboundHTTP.registerAnchoredOrigins({
|
||||||
anchorOrigin: 'https://mixed.example',
|
anchorOrigin: 'https://unusable.example',
|
||||||
origins: [],
|
origins: [],
|
||||||
unresolvedAnchorRequirement: 'any',
|
unresolvedAnchorRequirement: 'any',
|
||||||
}),
|
}),
|
||||||
{name: 'DesktopOutboundHTTPMixedAddressScopeError'},
|
{name: 'DesktopOutboundHTTPEmptyResolutionError'},
|
||||||
|
);
|
||||||
|
assert.equal(outboundHTTP.isRegisteredOrigin('https://unusable.example'), false);
|
||||||
|
outboundHTTP.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a split-horizon anchor resolving to private and public addresses is anchored on its private address', async () => {
|
||||||
|
const outboundHTTP = new DesktopOutboundHTTP({
|
||||||
|
resolveHostAddresses: stubResolver({'chat.home.example': ['2606:4700:4700::1111', '192.168.1.20']}),
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
await outboundHTTP.registerAnchoredOrigins({anchorOrigin: 'http://chat.home.example', origins: []}),
|
||||||
|
'any',
|
||||||
|
);
|
||||||
|
assert.equal(await outboundHTTP.requireCleartextTransportAddress('http://chat.home.example'), '192.168.1.20');
|
||||||
|
outboundHTTP.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a publicly reachable anchor refuses an advertised origin that also resolves privately', async () => {
|
||||||
|
const outboundHTTP = new DesktopOutboundHTTP({
|
||||||
|
resolveHostAddresses: stubResolver({
|
||||||
|
'public.example': ['93.184.216.34'],
|
||||||
|
'media.mixed.example': ['93.184.216.35', '10.0.0.1'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await outboundHTTP.registerAnchoredOrigins({
|
||||||
|
anchorOrigin: 'https://public.example',
|
||||||
|
origins: ['https://media.mixed.example'],
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
outboundHTTP.request({
|
||||||
|
body: null,
|
||||||
|
expectedOrigin: 'https://media.mixed.example',
|
||||||
|
headers: null,
|
||||||
|
method: 'GET',
|
||||||
|
originTrust: DesktopOriginTrust.REGISTERED,
|
||||||
|
serviceName: 'test',
|
||||||
|
signal: null,
|
||||||
|
timeoutMs: 500,
|
||||||
|
url: 'https://media.mixed.example/avatars/1.webp',
|
||||||
|
}),
|
||||||
|
/cannot authorize the privately resolving origin/,
|
||||||
);
|
);
|
||||||
assert.equal(outboundHTTP.isRegisteredOrigin('https://mixed.example'), false);
|
|
||||||
outboundHTTP.cleanup();
|
outboundHTTP.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,29 +480,28 @@ describe('DesktopOutboundHTTP origin anchoring', () => {
|
|||||||
outboundHTTP.cleanup();
|
outboundHTTP.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an origin resolving to both public and non-public addresses is refused outright', async () => {
|
test('a chosen origin resolving to both public and non-public addresses is reached on its non-public address', async () => {
|
||||||
const {port, requested, server} = await startServer({
|
const {port, requested, server} = await startServer({
|
||||||
'/ping': (_request, response) => sendImage(response, 'text/plain', 'ok'),
|
'/ping': (_request, response) => sendImage(response, 'text/plain', 'ok'),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const outboundHTTP = new DesktopOutboundHTTP({
|
const outboundHTTP = new DesktopOutboundHTTP({
|
||||||
resolveHostAddresses: stubResolver({'mixed.example': ['127.0.0.1', '93.184.216.34']}),
|
resolveHostAddresses: stubResolver({'mixed.example': ['93.184.216.34', '127.0.0.1']}),
|
||||||
});
|
});
|
||||||
await assert.rejects(
|
const response = await outboundHTTP.request({
|
||||||
outboundHTTP.request({
|
body: null,
|
||||||
body: null,
|
expectedOrigin: `http://mixed.example:${port}`,
|
||||||
expectedOrigin: `http://mixed.example:${port}`,
|
headers: null,
|
||||||
headers: null,
|
method: 'GET',
|
||||||
method: 'GET',
|
originTrust: DesktopOriginTrust.BOUND,
|
||||||
originTrust: DesktopOriginTrust.BOUND,
|
serviceName: 'test',
|
||||||
serviceName: 'test',
|
signal: null,
|
||||||
signal: null,
|
timeoutMs: 2000,
|
||||||
timeoutMs: 2000,
|
url: `http://mixed.example:${port}/ping`,
|
||||||
url: `http://mixed.example:${port}/ping`,
|
});
|
||||||
}),
|
response.message.resume();
|
||||||
/resolved to both public and non-public addresses/,
|
assert.equal(response.status, 200);
|
||||||
);
|
assert.deepEqual(requested, ['/ping']);
|
||||||
assert.deepEqual(requested, [], 'a mixed-scope origin must never be contacted');
|
|
||||||
outboundHTTP.cleanup();
|
outboundHTTP.cleanup();
|
||||||
} finally {
|
} finally {
|
||||||
server.close();
|
server.close();
|
||||||
|
|||||||
@@ -594,17 +594,12 @@ async function lookupAllAddresses(hostname: string): Promise<ReadonlyArray<strin
|
|||||||
return records.map((record) => record.address);
|
return records.map((record) => record.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPinnedAddress(
|
function selectPinnedAddress(candidates: ReadonlyArray<PinnedAddress>): PinnedAddress | null {
|
||||||
candidates: ReadonlyArray<PinnedAddress>,
|
const nonPublic = candidates.filter((candidate) => !isPublicPinnedAddress(candidate));
|
||||||
scope: DesktopOriginAddressScope | null,
|
if (nonPublic.length === 0) {
|
||||||
): PinnedAddress | null {
|
return candidates[0] ?? null;
|
||||||
if (candidates.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (scope === DesktopOriginAddressScope.NON_PUBLIC) {
|
return nonPublic.find((candidate) => candidate.family === 4) ?? nonPublic[0];
|
||||||
return candidates.find((candidate) => candidate.family === 4) ?? candidates[0];
|
|
||||||
}
|
|
||||||
return candidates[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addressScope(address: PinnedAddress): DesktopOriginAddressScope {
|
function addressScope(address: PinnedAddress): DesktopOriginAddressScope {
|
||||||
@@ -1038,16 +1033,14 @@ export class DesktopOutboundHTTP {
|
|||||||
throw blocked(reason, context, hostname);
|
throw blocked(reason, context, hostname);
|
||||||
}
|
}
|
||||||
const candidates = addresses.map((address) => parseIPAddress(address)).filter((value) => value != null);
|
const candidates = addresses.map((address) => parseIPAddress(address)).filter((value) => value != null);
|
||||||
if (candidates.length === 0) {
|
const pinned = selectPinnedAddress(candidates);
|
||||||
|
if (pinned == null) {
|
||||||
throw blocked(DesktopOutboundBlockReason.NO_USABLE_ADDRESS, context, hostname);
|
throw blocked(DesktopOutboundBlockReason.NO_USABLE_ADDRESS, context, hostname);
|
||||||
}
|
}
|
||||||
if (
|
if (requirement === DesktopAddressRequirement.PUBLIC && !isPublicPinnedAddress(pinned)) {
|
||||||
requirement === DesktopAddressRequirement.PUBLIC &&
|
|
||||||
candidates.some((candidate) => !isPublicPinnedAddress(candidate))
|
|
||||||
) {
|
|
||||||
throw blocked(DesktopOutboundBlockReason.NON_PUBLIC_ADDRESS, context, hostname);
|
throw blocked(DesktopOutboundBlockReason.NON_PUBLIC_ADDRESS, context, hostname);
|
||||||
}
|
}
|
||||||
return candidates[0];
|
return pinned;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveHost(hostname: string): Promise<ReadonlyArray<string>> {
|
private async resolveHost(hostname: string): Promise<ReadonlyArray<string>> {
|
||||||
@@ -1177,25 +1170,12 @@ export class DesktopOutboundHTTP {
|
|||||||
return {address: literal, origin, scope: addressScope(literal), unreachable: false};
|
return {address: literal, origin, scope: addressScope(literal), unreachable: false};
|
||||||
}
|
}
|
||||||
const rawAddresses = await this.resolveOriginHost(absoluteLookupHostname(hostname), origin);
|
const rawAddresses = await this.resolveOriginHost(absoluteLookupHostname(hostname), origin);
|
||||||
const candidates: Array<PinnedAddress> = [];
|
const candidates = rawAddresses.map((address) => parseIPAddress(address)).filter((value) => value != null);
|
||||||
let scope: DesktopOriginAddressScope | null = null;
|
const pinned = selectPinnedAddress(candidates);
|
||||||
for (const rawAddress of rawAddresses) {
|
if (pinned == null) {
|
||||||
const address = parseIPAddress(rawAddress);
|
|
||||||
if (address == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const currentScope = addressScope(address);
|
|
||||||
if (scope != null && scope !== currentScope) {
|
|
||||||
throw new DesktopOutboundHTTPMixedAddressScopeError(origin);
|
|
||||||
}
|
|
||||||
scope = currentScope;
|
|
||||||
candidates.push(address);
|
|
||||||
}
|
|
||||||
const pinned = selectPinnedAddress(candidates, scope);
|
|
||||||
if (pinned == null || scope == null) {
|
|
||||||
throw new DesktopOutboundHTTPEmptyResolutionError(origin);
|
throw new DesktopOutboundHTTPEmptyResolutionError(origin);
|
||||||
}
|
}
|
||||||
return {address: pinned, origin, scope, unreachable: false};
|
return {address: pinned, origin, scope: addressScope(pinned), unreachable: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
private markBindingUnreachable(binding: DesktopOriginAddressBinding): void {
|
private markBindingUnreachable(binding: DesktopOriginAddressBinding): void {
|
||||||
|
|||||||
Reference in new issue
Block a user