mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-19 00:26:53 +00:00
Merge pull request #2284 from balena-os/enable-redsocks-dnsu2t
Enable redsocks dnsu2t
This commit is contained in:
commit
da6f4bdbaf
10
docs/API.md
10
docs/API.md
@ -568,7 +568,8 @@ By default, with [balenaOS 2.82.6](https://github.com/balena-os/meta-balena/blob
|
||||
"port": 8123,
|
||||
"login": "username",
|
||||
"password": "password",
|
||||
"noProxy": [ "152.10.30.4", "253.1.1.0/16" ]
|
||||
"noProxy": [ "152.10.30.4", "253.1.1.0/16" ],
|
||||
"dns": "1.1.1.1:53"
|
||||
},
|
||||
"hostname": "mynewhostname",
|
||||
"force": true
|
||||
@ -585,7 +586,9 @@ Keep in mind that, even if transparent proxy redirection will take effect immedi
|
||||
The `noProxy` setting for the proxy is an optional array of IP addresses/subnets that should not be routed through the
|
||||
proxy. Keep in mind that local/reserved subnets are already [excluded by balenaOS automatically](https://github.com/balena-os/meta-balena/blob/master/meta-balena-common/recipes-connectivity/balena-proxy-config/balena-proxy-config/balena-proxy-config).
|
||||
|
||||
If either `proxy` or `hostname` are null or empty values (i.e. `{}` for proxy or an empty string for hostname), they will be cleared to their default values (i.e. not using a proxy, and a hostname equal to the first 7 characters of the device's uuid, respectively).
|
||||
As of v16.6.0, the Supervisor supports configuring the `dnsu2t` plugin for redsocks via the `dns` subfield under `proxy`. Only the `remote_ip` and `remote_port` fields are allowed to be modified. The input must be of type `boolean` or `string`. If boolean and `true`, the remote values will be the default, `8.8.8.8:53`. If boolean and false, the configuration will be removed. If string, it should be in the format `ADDRESS:PORT`, with `ADDRESS` and `PORT` both required. You may not configure `dnsu2t` without a redsocks proxy configuration, as `dnsu2t` depends on a working proxy to function.
|
||||
|
||||
If any of `proxy`, `dns`, or `hostname` are nullish, falsey, or empty values (i.e. `{}` for proxy, `false` for dns, or an empty string for hostname), they will be cleared to their default settings (i.e. not using a proxy, not using dns, and a hostname equal to the first 7 characters of the device's uuid, respectively).
|
||||
|
||||
#### Examples:
|
||||
From an app container:
|
||||
@ -636,7 +639,8 @@ Response:
|
||||
"port":"8123",
|
||||
"type":"socks5"
|
||||
},
|
||||
"hostname":"27b0fdc"
|
||||
"hostname":"27b0fdc",
|
||||
"dns": "1.1.1.1:53"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -51,9 +51,15 @@ export function parse(
|
||||
const legacyDecoded = LegacyHostConfiguration.decode(conf);
|
||||
if (isRight(legacyDecoded)) {
|
||||
return legacyDecoded.right;
|
||||
} else {
|
||||
const formattedErrorMessage = [
|
||||
'Could not parse host config input to a valid format:',
|
||||
]
|
||||
.concat(Reporter.report(legacyDecoded))
|
||||
.join('\n');
|
||||
throw new Error(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
throw new Error('Could not parse host config input to a valid format');
|
||||
}
|
||||
|
||||
function patchProxy(
|
||||
|
@ -3,8 +3,8 @@ import path from 'path';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Reporter from 'io-ts-reporters';
|
||||
|
||||
import type { RedsocksConfig, HostProxyConfig } from './types';
|
||||
import { ProxyConfig } from './types';
|
||||
import type { RedsocksConfig, HostProxyConfig, DnsInput } from './types';
|
||||
import { ProxyConfig, DnsConfig } from './types';
|
||||
import { pathOnBoot, readFromBoot, writeToBoot } from '../lib/host-utils';
|
||||
import { unlinkAll, mkdirp } from '../lib/fs-utils';
|
||||
import { isENOENT } from '../lib/errors';
|
||||
@ -17,6 +17,9 @@ const redsocksConfPath = path.join(proxyBasePath, 'redsocks.conf');
|
||||
|
||||
const disallowedProxyFields = ['local_ip', 'local_port'];
|
||||
|
||||
const DEFAULT_REMOTE_IP = '8.8.8.8';
|
||||
const DEFAULT_REMOTE_PORT = 53;
|
||||
|
||||
const isAuthField = (field: string): boolean =>
|
||||
['login', 'password'].includes(field);
|
||||
|
||||
@ -38,18 +41,45 @@ export class RedsocksConf {
|
||||
public static stringify(config: RedsocksConfig): string {
|
||||
const blocks: string[] = [];
|
||||
|
||||
if (config.redsocks && Object.keys(config.redsocks).length > 0) {
|
||||
blocks.push(RedsocksConf.stringifyBlock('base', baseBlock));
|
||||
blocks.push(
|
||||
RedsocksConf.stringifyBlock('redsocks', {
|
||||
...config.redsocks,
|
||||
local_ip: '127.0.0.1',
|
||||
local_port: 12345,
|
||||
}),
|
||||
);
|
||||
// If no redsocks config is provided or dns is the only config, return empty string.
|
||||
// A dns-only config is not valid as it depends on proxy being configured to function.
|
||||
if (
|
||||
!config.redsocks ||
|
||||
!Object.keys(config.redsocks).length ||
|
||||
(Object.keys(config.redsocks).length === 1 &&
|
||||
Object.hasOwn(config.redsocks, 'dns'))
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return blocks.length ? blocks.join('\n') : '';
|
||||
// Add base block
|
||||
blocks.push(RedsocksConf.stringifyBlock('base', baseBlock));
|
||||
|
||||
const { dns, ...redsocks } = config.redsocks;
|
||||
// Add redsocks block
|
||||
blocks.push(
|
||||
RedsocksConf.stringifyBlock('redsocks', {
|
||||
...redsocks,
|
||||
local_ip: '127.0.0.1',
|
||||
local_port: 12345,
|
||||
}),
|
||||
);
|
||||
|
||||
// Add optional dnsu2t block if input dns config is true or a string
|
||||
if (dns != null) {
|
||||
const dnsu2t = dnsToDnsu2t(dns);
|
||||
if (dnsu2t) {
|
||||
blocks.push(
|
||||
RedsocksConf.stringifyBlock('dnsu2t', {
|
||||
...dnsu2t,
|
||||
local_ip: '127.0.0.1',
|
||||
local_port: 53,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return blocks.join('\n');
|
||||
}
|
||||
|
||||
public static parse(rawConf: string): RedsocksConfig {
|
||||
@ -59,6 +89,20 @@ export class RedsocksConf {
|
||||
return conf;
|
||||
}
|
||||
|
||||
// Extract contents of `dnsu2t {...}` using regex if exists
|
||||
let dns: DnsConfig | null = null;
|
||||
const rawDnsu2tBlockMatch = rawConf.match(blockRegexFor('dnsu2t'));
|
||||
if (rawDnsu2tBlockMatch) {
|
||||
const rawDnsu2tBlock = RedsocksConf.parseBlock(
|
||||
rawDnsu2tBlockMatch[1],
|
||||
disallowedProxyFields,
|
||||
);
|
||||
const maybeDnsConfig = DnsConfig.decode(rawDnsu2tBlock);
|
||||
if (isRight(maybeDnsConfig)) {
|
||||
dns = maybeDnsConfig.right;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract contents of `redsocks {...}` using regex
|
||||
const rawRedsocksBlockMatch = rawConf.match(blockRegexFor('redsocks'));
|
||||
// No group was captured, indicating malformed config
|
||||
@ -74,6 +118,8 @@ export class RedsocksConf {
|
||||
if (isRight(maybeProxyConfig)) {
|
||||
conf.redsocks = {
|
||||
...maybeProxyConfig.right,
|
||||
// Only add dns subfield if redsocks config is valid
|
||||
...(dns && { dns: `${dns.remote_ip}:${dns.remote_port}` }),
|
||||
};
|
||||
return conf;
|
||||
} else {
|
||||
@ -138,6 +184,25 @@ export class RedsocksConf {
|
||||
}
|
||||
}
|
||||
|
||||
function dnsToDnsu2t(
|
||||
dns: DnsInput,
|
||||
): { remote_ip: string; remote_port: number } | null {
|
||||
const dnsu2t = {
|
||||
remote_ip: DEFAULT_REMOTE_IP,
|
||||
remote_port: DEFAULT_REMOTE_PORT,
|
||||
};
|
||||
|
||||
if (typeof dns === 'boolean') {
|
||||
return dns ? dnsu2t : null;
|
||||
} else {
|
||||
// Convert dns string to config object
|
||||
const [ip, port] = dns.split(':');
|
||||
dnsu2t.remote_ip = ip;
|
||||
dnsu2t.remote_port = parseInt(port, 10);
|
||||
return dnsu2t;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readProxy(): Promise<HostProxyConfig | undefined> {
|
||||
// Get and parse redsocks.conf
|
||||
let rawConf: string | undefined;
|
||||
|
@ -1,5 +1,15 @@
|
||||
import * as t from 'io-ts';
|
||||
import { NumericIdentifier } from '../types';
|
||||
import { NumericIdentifier, shortStringWithRegex } from '../types';
|
||||
|
||||
const AddressString = shortStringWithRegex(
|
||||
'AddressString',
|
||||
/^.+:[0-9]+$/,
|
||||
"must be a string in the format 'ADDRESS:PORT'",
|
||||
);
|
||||
type AddressString = t.TypeOf<typeof AddressString>;
|
||||
|
||||
export const DnsInput = t.union([AddressString, t.boolean]);
|
||||
export type DnsInput = t.TypeOf<typeof DnsInput>;
|
||||
|
||||
export const ProxyConfig = t.intersection([
|
||||
t.type({
|
||||
@ -12,14 +22,21 @@ export const ProxyConfig = t.intersection([
|
||||
ip: t.string,
|
||||
port: NumericIdentifier,
|
||||
}),
|
||||
// login & password are optional fields
|
||||
// login, password, and dns are optional fields
|
||||
t.partial({
|
||||
login: t.string,
|
||||
password: t.string,
|
||||
dns: DnsInput,
|
||||
}),
|
||||
]);
|
||||
export type ProxyConfig = t.TypeOf<typeof ProxyConfig>;
|
||||
|
||||
export const DnsConfig = t.type({
|
||||
remote_ip: t.string,
|
||||
remote_port: NumericIdentifier,
|
||||
});
|
||||
export type DnsConfig = t.TypeOf<typeof DnsConfig>;
|
||||
|
||||
/**
|
||||
* The internal object representation of redsocks.conf, obtained
|
||||
* from RedsocksConf.parse
|
||||
@ -47,10 +64,12 @@ export type HostProxyConfig = t.TypeOf<typeof HostProxyConfig>;
|
||||
* with host-config PATCH and provided to the user with host-config GET.
|
||||
*/
|
||||
export const HostConfiguration = t.type({
|
||||
network: t.partial({
|
||||
proxy: HostProxyConfig,
|
||||
hostname: t.string,
|
||||
}),
|
||||
network: t.exact(
|
||||
t.partial({
|
||||
proxy: t.exact(HostProxyConfig),
|
||||
hostname: t.string,
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type HostConfiguration = t.TypeOf<typeof HostConfiguration>;
|
||||
|
||||
@ -61,9 +80,18 @@ export type HostConfiguration = t.TypeOf<typeof HostConfiguration>;
|
||||
* valid but has the correct shape.
|
||||
*/
|
||||
export const LegacyHostConfiguration = t.type({
|
||||
network: t.partial({
|
||||
proxy: t.record(t.string, t.any),
|
||||
hostname: t.string,
|
||||
}),
|
||||
network: t.exact(
|
||||
t.partial({
|
||||
proxy: t.intersection([
|
||||
t.record(t.string, t.any),
|
||||
// Dns was added after the initial API endpoint was introduced,
|
||||
// so we can be more strict with its type.
|
||||
t.partial({
|
||||
dns: DnsInput,
|
||||
}),
|
||||
]),
|
||||
hostname: t.string,
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type LegacyHostConfiguration = t.TypeOf<typeof LegacyHostConfiguration>;
|
||||
|
@ -93,7 +93,11 @@ const VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
*/
|
||||
const CONFIG_VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_:]*$/;
|
||||
|
||||
const shortStringWithRegex = (name: string, regex: RegExp, message: string) =>
|
||||
export const shortStringWithRegex = (
|
||||
name: string,
|
||||
regex: RegExp,
|
||||
message: string,
|
||||
) =>
|
||||
new t.Type<string, string>(
|
||||
name,
|
||||
(s: unknown): s is string => ShortString.is(s) && regex.test(s),
|
||||
|
@ -15,3 +15,10 @@ redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.2.3.4;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
|
@ -31,6 +31,18 @@ describe('host-config', () => {
|
||||
const noProxy = path.join(proxyBase, 'no_proxy');
|
||||
const hostname = pathOnRoot('/etc/hostname');
|
||||
const appLockDir = pathOnRoot(updateLock.lockPath(APP_ID));
|
||||
const defaultConf = {
|
||||
proxy: {
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
type: 'socks5',
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
dns: '1.2.3.4:54',
|
||||
noProxy: ['152.10.30.4', '253.1.1.0/16'],
|
||||
},
|
||||
hostname: 'deadbeef',
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
await config.initialized();
|
||||
@ -56,7 +68,7 @@ describe('host-config', () => {
|
||||
'test/data/mnt/boot/system-proxy/redsocks.conf',
|
||||
),
|
||||
[noProxy]: testfs.from('test/data/mnt/boot/system-proxy/no_proxy'),
|
||||
[hostname]: 'deadbeef',
|
||||
[hostname]: defaultConf.hostname,
|
||||
// Create a lock. This won't prevent host config patch unless
|
||||
// there are current apps present, in which case an updates locked
|
||||
// error will be thrown.
|
||||
@ -70,6 +82,7 @@ describe('host-config', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await tFs.enable();
|
||||
await config.set({ hostname: defaultConf.hostname });
|
||||
// Stub external dependencies
|
||||
stub(dbus, 'servicePartOf').resolves([]);
|
||||
stub(dbus, 'restartService').resolves();
|
||||
@ -85,17 +98,7 @@ describe('host-config', () => {
|
||||
|
||||
it('reads proxy configs and hostname', async () => {
|
||||
const { network } = await get();
|
||||
expect(network).to.have.property('hostname', 'deadbeef');
|
||||
expect(network).to.have.property('proxy');
|
||||
expect(network.proxy).to.have.property('ip', 'example.org');
|
||||
expect(network.proxy).to.have.property('port', 1080);
|
||||
expect(network.proxy).to.have.property('type', 'socks5');
|
||||
expect(network.proxy).to.have.property('login', 'foo');
|
||||
expect(network.proxy).to.have.property('password', 'bar');
|
||||
expect(network.proxy).to.have.deep.property('noProxy', [
|
||||
'152.10.30.4',
|
||||
'253.1.1.0/16',
|
||||
]);
|
||||
expect(network).to.deep.equal(defaultConf);
|
||||
});
|
||||
|
||||
it('prevents patch if update locks are present', async () => {
|
||||
@ -117,7 +120,13 @@ describe('host-config', () => {
|
||||
} catch (e: unknown) {
|
||||
expect.fail(`Expected hostConfig.patch to not throw, but got ${e}`);
|
||||
}
|
||||
expect(await get()).to.deep.equal({ network: { hostname: 'deadbeef' } });
|
||||
// Proxy should have been deleted as proxy was patched to empty,
|
||||
// hostname should remain unchanged
|
||||
const { network } = await get();
|
||||
expect(network).to.deep.equal({
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
expect(await config.get('hostname')).to.equal(defaultConf.hostname);
|
||||
});
|
||||
|
||||
it('patches hostname regardless of update locks', async () => {
|
||||
@ -125,44 +134,43 @@ describe('host-config', () => {
|
||||
|
||||
try {
|
||||
await patch({ network: { hostname: 'test' } });
|
||||
// /etc/hostname isn't changed until the balena-hostname service
|
||||
// is restarted by the OS.
|
||||
expect(await config.get('hostname')).to.equal('test');
|
||||
} catch (e: unknown) {
|
||||
expect.fail(`Expected hostConfig.patch to not throw, but got ${e}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('patches hostname', async () => {
|
||||
it('patches hostname without modifying other fields', async () => {
|
||||
await patch({ network: { hostname: 'test' } });
|
||||
// /etc/hostname isn't changed until the balena-hostname service
|
||||
// is restarted by the OS.
|
||||
expect(await config.get('hostname')).to.equal('test');
|
||||
// Proxy should remain unchanged as patch didn't include it
|
||||
const { network } = await get();
|
||||
expect(network.proxy).to.deep.equal(defaultConf.proxy);
|
||||
});
|
||||
|
||||
it('patches proxy', async () => {
|
||||
const newConf = {
|
||||
network: {
|
||||
proxy: {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
type: 'http-relay',
|
||||
login: 'bar',
|
||||
password: 'foo',
|
||||
noProxy: ['balena.io', '222.22.2.2'],
|
||||
},
|
||||
},
|
||||
it('patches proxy without modifying other fields', async () => {
|
||||
const newProxy = {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
type: 'http-relay',
|
||||
login: 'bar',
|
||||
password: 'foo',
|
||||
dns: '2.2.2.2:52',
|
||||
noProxy: ['balena.io', '222.22.2.2'],
|
||||
};
|
||||
await patch(newConf);
|
||||
await patch({ network: { proxy: newProxy } });
|
||||
const { network } = await get();
|
||||
expect(network).to.have.property('proxy');
|
||||
expect(network.proxy).to.have.property('ip', 'example2.org');
|
||||
expect(network.proxy).to.have.property('port', 1090);
|
||||
expect(network.proxy).to.have.property('type', 'http-relay');
|
||||
expect(network.proxy).to.have.property('login', 'bar');
|
||||
expect(network.proxy).to.have.property('password', 'foo');
|
||||
expect(network.proxy).to.have.deep.property('noProxy', [
|
||||
'balena.io',
|
||||
'222.22.2.2',
|
||||
]);
|
||||
expect(network).to.deep.equal({
|
||||
proxy: {
|
||||
...defaultConf.proxy,
|
||||
...newProxy,
|
||||
},
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
@ -182,6 +190,13 @@ describe('host-config', () => {
|
||||
password = "foo";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 2.2.2.2;
|
||||
remote_port = 52;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
|
||||
@ -192,25 +207,23 @@ describe('host-config', () => {
|
||||
});
|
||||
|
||||
it('patches proxy fields specified while leaving unspecified fields unchanged', async () => {
|
||||
const newProxyFields = {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
};
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
},
|
||||
proxy: newProxyFields,
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
expect(network).to.have.property('proxy');
|
||||
expect(network.proxy).to.have.property('ip', 'example2.org');
|
||||
expect(network.proxy).to.have.property('port', 1090);
|
||||
expect(network.proxy).to.have.property('type', 'socks5');
|
||||
expect(network.proxy).to.have.property('login', 'foo');
|
||||
expect(network.proxy).to.have.property('password', 'bar');
|
||||
expect(network.proxy).to.have.deep.property('noProxy', [
|
||||
'152.10.30.4',
|
||||
'253.1.1.0/16',
|
||||
]);
|
||||
expect(network).to.deep.equal({
|
||||
proxy: {
|
||||
...defaultConf.proxy,
|
||||
...newProxyFields,
|
||||
},
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
@ -230,6 +243,13 @@ describe('host-config', () => {
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.2.3.4;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
|
||||
@ -244,22 +264,15 @@ describe('host-config', () => {
|
||||
it('patches proxy to empty if input is empty', async () => {
|
||||
await patch({ network: { proxy: {} } });
|
||||
const { network } = await get();
|
||||
expect(network).to.not.have.property('proxy');
|
||||
expect(network).to.deep.equal({
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps current proxy if input is invalid', async () => {
|
||||
await patch({ network: { proxy: null as any } });
|
||||
const { network } = await get();
|
||||
expect(network).to.have.property('proxy');
|
||||
expect(network.proxy).to.have.property('ip', 'example.org');
|
||||
expect(network.proxy).to.have.property('port', 1080);
|
||||
expect(network.proxy).to.have.property('type', 'socks5');
|
||||
expect(network.proxy).to.have.property('login', 'foo');
|
||||
expect(network.proxy).to.have.property('password', 'bar');
|
||||
expect(network.proxy).to.have.deep.property('noProxy', [
|
||||
'152.10.30.4',
|
||||
'253.1.1.0/16',
|
||||
]);
|
||||
expect(network).to.deep.equal(defaultConf);
|
||||
});
|
||||
|
||||
it('ignores unsupported fields when patching proxy', async () => {
|
||||
@ -352,43 +365,41 @@ describe('host-config', () => {
|
||||
|
||||
it('skips restarting proxy services when part of redsocks-conf.target', async () => {
|
||||
(dbus.servicePartOf as SinonStub).resolves(['redsocks-conf.target']);
|
||||
const newProxy = {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
type: 'http-relay',
|
||||
login: 'bar',
|
||||
password: 'foo',
|
||||
dns: '4.3.2.1:52',
|
||||
noProxy: ['balena.io', '222.22.2.2'],
|
||||
};
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
ip: 'example2.org',
|
||||
port: 1090,
|
||||
type: 'http-relay',
|
||||
login: 'bar',
|
||||
password: 'foo',
|
||||
noProxy: ['balena.io', '222.22.2.2'],
|
||||
},
|
||||
proxy: newProxy,
|
||||
},
|
||||
});
|
||||
expect(dbus.restartService as SinonStub).to.not.have.been.called;
|
||||
const { network } = await get();
|
||||
expect(network).to.have.property('proxy');
|
||||
expect(network.proxy).to.have.property('ip', 'example2.org');
|
||||
expect(network.proxy).to.have.property('port', 1090);
|
||||
expect(network.proxy).to.have.property('type', 'http-relay');
|
||||
expect(network.proxy).to.have.property('login', 'bar');
|
||||
expect(network.proxy).to.have.property('password', 'foo');
|
||||
expect(network.proxy).to.have.deep.property('noProxy', [
|
||||
'balena.io',
|
||||
'222.22.2.2',
|
||||
]);
|
||||
expect(network.proxy).to.deep.equal({
|
||||
...defaultConf.proxy,
|
||||
...newProxy,
|
||||
});
|
||||
});
|
||||
|
||||
it('patches redsocks.conf to be empty if prompted', async () => {
|
||||
it('patches redsocks.conf to be empty', async () => {
|
||||
await patch({ network: { proxy: {} } });
|
||||
const { network } = await get();
|
||||
expect(network).to.not.have.property('proxy');
|
||||
expect(network).to.deep.equal({
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
expect(await fs.readdir(proxyBase)).to.not.have.members([
|
||||
'redsocks.conf',
|
||||
'no_proxy',
|
||||
]);
|
||||
});
|
||||
|
||||
it('patches no_proxy to be empty if prompted', async () => {
|
||||
it('patches no_proxy to be empty', async () => {
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
@ -398,13 +409,41 @@ describe('host-config', () => {
|
||||
});
|
||||
const { network } = await get();
|
||||
// If only noProxy is patched, redsocks.conf should remain unchanged
|
||||
expect(network).to.have.property('proxy').that.deep.includes({
|
||||
expect(network).to.have.property('proxy').that.deep.equals({
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
type: 'socks5',
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
dns: '1.2.3.4:54',
|
||||
});
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.2.3.4;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
expect(network.proxy).to.not.have.property('noProxy');
|
||||
expect(await fs.readdir(proxyBase)).to.not.have.members(['no_proxy']);
|
||||
});
|
||||
@ -415,5 +454,217 @@ describe('host-config', () => {
|
||||
const { network: newNetwork } = await get();
|
||||
expect(network.hostname).to.equal(newNetwork.hostname);
|
||||
expect(network.proxy).to.deep.equal(newNetwork.proxy);
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.2.3.4;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('patches dnsu2t config to default without modifying other fields', async () => {
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
expect(network.proxy).to.deep.equal({
|
||||
...defaultConf.proxy,
|
||||
dns: '8.8.8.8:53',
|
||||
});
|
||||
expect(await config.get('hostname')).to.equal(defaultConf.hostname);
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 8.8.8.8;
|
||||
remote_port = 53;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('patches dnsu2t config to string value', async () => {
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: '4.3.2.1:51',
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
expect(network.proxy).to.deep.equal({
|
||||
...defaultConf.proxy,
|
||||
dns: '4.3.2.1:51',
|
||||
});
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 4.3.2.1;
|
||||
remote_port = 51;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('patches dnsu2t config to empty', async () => {
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
const { dns, ...proxyWithoutDns } = defaultConf.proxy;
|
||||
expect(network.proxy).to.deep.equal(proxyWithoutDns);
|
||||
});
|
||||
|
||||
it('adds dnsu2t config to config without dnsu2t when provided valid input', async () => {
|
||||
// Delete dns config to set up test
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
expect(network.proxy).to.not.have.property('dns');
|
||||
expect(await fs.readFile(redsocksConf, 'utf-8')).to.not.contain('dnsu2t');
|
||||
|
||||
// Add valid dns config
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: '5.5.5.5:55',
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network: n2 } = await get();
|
||||
expect(n2.proxy).to.deep.equal({
|
||||
...defaultConf.proxy,
|
||||
dns: '5.5.5.5:55',
|
||||
});
|
||||
await expect(fs.readFile(redsocksConf, 'utf-8')).to.eventually.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
type = socks5;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 5.5.5.5;
|
||||
remote_port = 55;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("does not add dnsu2t config when redsocks proxy isn't configured", async () => {
|
||||
// Delete redsocks config to set up test
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {},
|
||||
},
|
||||
});
|
||||
const { network } = await get();
|
||||
expect(network).to.not.have.property('proxy');
|
||||
expect(await fs.readdir(proxyBase)).to.not.have.members([
|
||||
'redsocks.conf',
|
||||
'no_proxy',
|
||||
]);
|
||||
|
||||
// Add valid dns config
|
||||
await patch({
|
||||
network: {
|
||||
proxy: {
|
||||
dns: '1.2.3.4:54',
|
||||
},
|
||||
},
|
||||
});
|
||||
const { network: n2 } = await get();
|
||||
expect(n2).to.deep.equal({
|
||||
hostname: defaultConf.hostname,
|
||||
});
|
||||
expect(await fs.readdir(proxyBase)).to.not.have.members([
|
||||
'redsocks.conf',
|
||||
'no_proxy',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,12 @@ import type { SinonStub } from 'sinon';
|
||||
|
||||
import * as hostConfig from '~/src/host-config';
|
||||
import { RedsocksConf } from '~/src/host-config/proxy';
|
||||
import type { RedsocksConfig, ProxyConfig } from '~/src/host-config/types';
|
||||
import {
|
||||
type RedsocksConfig,
|
||||
type ProxyConfig,
|
||||
LegacyHostConfiguration,
|
||||
DnsInput,
|
||||
} from '~/src/host-config/types';
|
||||
import log from '~/lib/supervisor-console';
|
||||
|
||||
describe('RedsocksConf', () => {
|
||||
@ -124,34 +129,163 @@ describe('RedsocksConf', () => {
|
||||
const confStr = RedsocksConf.stringify(conf);
|
||||
expect(confStr).to.equal('');
|
||||
});
|
||||
|
||||
it('stringifies dns to separate dnsu2t block', () => {
|
||||
const conf: RedsocksConfig = {
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
dns: '1.1.1.1:54',
|
||||
},
|
||||
};
|
||||
const confStr = RedsocksConf.stringify(conf);
|
||||
expect(confStr).to.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.1.1.1;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('stringifies dns: true to default dnsu2t config', () => {
|
||||
const conf: RedsocksConfig = {
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
dns: true,
|
||||
},
|
||||
};
|
||||
const confStr = RedsocksConf.stringify(conf);
|
||||
expect(confStr).to.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 8.8.8.8;
|
||||
remote_port = 53;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not include dnsu2t config if dns: false', () => {
|
||||
const conf: RedsocksConfig = {
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
dns: false,
|
||||
},
|
||||
};
|
||||
const confStr = RedsocksConf.stringify(conf);
|
||||
expect(confStr).to.equal(
|
||||
stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
}
|
||||
` + '\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not stringify dnsu2t if no other fields in redsocks config', () => {
|
||||
const conf = {
|
||||
redsocks: {
|
||||
dns: '3.3.3.3:52',
|
||||
},
|
||||
} as RedsocksConfig;
|
||||
const confStr = RedsocksConf.stringify(conf);
|
||||
expect(confStr).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('parses config string into RedsocksConfig', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
|
||||
dnstc {
|
||||
test = test;
|
||||
}
|
||||
|
||||
`;
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({
|
||||
redsocks: {
|
||||
@ -166,26 +300,26 @@ describe('RedsocksConf', () => {
|
||||
|
||||
it("parses `redsocks {...}` config block no matter what position it's in or how many newlines surround it", () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
dnsu2t {
|
||||
dnstc {
|
||||
test = test;
|
||||
}
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = http-connect;
|
||||
ip = {test2}.balenadev.io;
|
||||
port = 1082;
|
||||
login = "us}{er";
|
||||
password = "p{}a}}s{{s";
|
||||
}
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = http-connect;
|
||||
ip = {test2}.balenadev.io;
|
||||
port = 1082;
|
||||
login = "us}{er";
|
||||
password = "p{}a}}s{{s";
|
||||
}
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
`;
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({
|
||||
redsocks: {
|
||||
@ -276,17 +410,17 @@ describe('RedsocksConf', () => {
|
||||
|
||||
it('parses to empty redsocks config with warnings while any values are invalid', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
redsocks {
|
||||
local_ip = 123;
|
||||
local_port = foo;
|
||||
type = socks6;
|
||||
ip = 456;
|
||||
port = bar;
|
||||
login = user;
|
||||
password = pass;
|
||||
redsocks {
|
||||
local_ip = 123;
|
||||
local_port = foo;
|
||||
type = socks6;
|
||||
ip = 456;
|
||||
port = bar;
|
||||
login = user;
|
||||
password = pass;
|
||||
invalid_field = invalid_value;
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect((log.warn as SinonStub).lastCall.args[0]).to.equal(
|
||||
@ -306,24 +440,24 @@ describe('RedsocksConf', () => {
|
||||
it('parses to empty config with warnings while some key-value pairs are malformed', () => {
|
||||
// Malformed key-value pairs are pairs that are missing a key, value, or "="
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug off;
|
||||
log_info = on
|
||||
= stderr;
|
||||
daemon = ;
|
||||
redirector = iptables;
|
||||
}
|
||||
base {
|
||||
log_debug off;
|
||||
log_info = on
|
||||
= stderr;
|
||||
daemon = ;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip 127.0.0.1;
|
||||
local_port = 12345
|
||||
= socks5;
|
||||
ip = ;
|
||||
= 1080;
|
||||
login =;
|
||||
password = "bar";
|
||||
}
|
||||
`;
|
||||
redsocks {
|
||||
local_ip 127.0.0.1;
|
||||
local_port = 12345
|
||||
= socks5;
|
||||
ip = ;
|
||||
= 1080;
|
||||
login =;
|
||||
password = "bar";
|
||||
}
|
||||
`;
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(
|
||||
@ -350,12 +484,12 @@ describe('RedsocksConf', () => {
|
||||
|
||||
it('parses to empty config with warnings when a block is empty', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
}
|
||||
base {
|
||||
}
|
||||
|
||||
redsocks {
|
||||
}
|
||||
`;
|
||||
redsocks {
|
||||
}
|
||||
`;
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(
|
||||
@ -375,6 +509,199 @@ describe('RedsocksConf', () => {
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(conf).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('parses dnsu2t to dns field in redsocks config', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.1.1.1;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
dns: '1.1.1.1:54',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses dnsu2t no matter its position', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.1.1.1;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
dns: '1.1.1.1:54',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not parse dnsu2t to dns field if dnsu2t is partial or invalid', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
test = true;
|
||||
}
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
const redsocksConfStr2 = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
type = socks5;
|
||||
ip = example.org;
|
||||
port = 1080;
|
||||
login = "foo";
|
||||
password = "bar";
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_port = 53;
|
||||
}
|
||||
`;
|
||||
const conf2 = RedsocksConf.parse(redsocksConfStr2);
|
||||
expect(conf2).to.deep.equal({
|
||||
redsocks: {
|
||||
type: 'socks5',
|
||||
ip: 'example.org',
|
||||
port: 1080,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not parse dnsu2t to dns field if missing or invalid redsocks config', () => {
|
||||
const redsocksConfStr = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.1.1.1;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
`;
|
||||
const conf = RedsocksConf.parse(redsocksConfStr);
|
||||
expect(conf).to.deep.equal({});
|
||||
|
||||
const redsocksConfStr2 = stripIndent`
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
type = socks5;
|
||||
}
|
||||
|
||||
dnsu2t {
|
||||
remote_ip = 1.1.1.1;
|
||||
remote_port = 54;
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 53;
|
||||
}
|
||||
`;
|
||||
const conf2 = RedsocksConf.parse(redsocksConfStr2);
|
||||
expect(conf2).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -394,19 +721,7 @@ describe('src/host-config', () => {
|
||||
hostname: 'balena',
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks4',
|
||||
ip: 'balena.io',
|
||||
port: 1079,
|
||||
login: '"baz"',
|
||||
password: '"foo"',
|
||||
noProxy: ['8.8.8.8'],
|
||||
},
|
||||
hostname: 'balena',
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only hostname', () => {
|
||||
@ -415,11 +730,7 @@ describe('src/host-config', () => {
|
||||
hostname: 'balena2',
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
hostname: 'balena2',
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only proxy', () => {
|
||||
@ -435,18 +746,7 @@ describe('src/host-config', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
});
|
||||
|
||||
it('parses valid HostConfiguration with only noProxy', () => {
|
||||
@ -457,13 +757,7 @@ describe('src/host-config', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
noProxy: ['1.1.1.1', '2.2.2.2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
});
|
||||
|
||||
it('parses HostConfiguration where auth fields are missing double quotes', () => {
|
||||
@ -480,18 +774,7 @@ describe('src/host-config', () => {
|
||||
},
|
||||
};
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: 'foo',
|
||||
password: 'bar',
|
||||
noProxy: ['3.3.3.3'],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
// Should not warn about missing double quotes
|
||||
expect(log.warn as SinonStub).to.not.have.been.called;
|
||||
});
|
||||
@ -541,18 +824,7 @@ describe('src/host-config', () => {
|
||||
},
|
||||
};
|
||||
(log.warn as SinonStub).resetHistory();
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'socks6',
|
||||
ip: 123,
|
||||
port: 'abc',
|
||||
login: 'user',
|
||||
password: 'pass',
|
||||
noProxy: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
expect((log.warn as SinonStub).lastCall.args[0]).to.equal(
|
||||
'Malformed host config detected, things may not behave as expected:\n' +
|
||||
'Expecting string at network.proxy.0.0.ip but instead got: 123\n' +
|
||||
@ -570,7 +842,11 @@ describe('src/host-config', () => {
|
||||
|
||||
it('throws error for HostConfiguration without network key', () => {
|
||||
expect(() => hostConfig.parse({})).to.throw(
|
||||
'Could not parse host config input to a valid format',
|
||||
'Could not parse host config input to a valid format:\n' +
|
||||
'Expecting Partial<{| ' +
|
||||
'proxy: ({ [K in string]: any } & Partial<{ dns: (AddressString | boolean) }>), ' +
|
||||
'hostname: string ' +
|
||||
'|}> at network but instead got: undefined',
|
||||
);
|
||||
});
|
||||
|
||||
@ -579,7 +855,11 @@ describe('src/host-config', () => {
|
||||
network: 123,
|
||||
};
|
||||
expect(() => hostConfig.parse(conf)).to.throw(
|
||||
'Could not parse host config input to a valid format',
|
||||
'Could not parse host config input to a valid format:\n' +
|
||||
'Expecting Partial<{| ' +
|
||||
'proxy: ({ [K in string]: any } & Partial<{ dns: (AddressString | boolean) }>), ' +
|
||||
'hostname: string ' +
|
||||
'|}> at network but instead got: 123',
|
||||
);
|
||||
});
|
||||
|
||||
@ -590,8 +870,200 @@ describe('src/host-config', () => {
|
||||
},
|
||||
};
|
||||
expect(() => hostConfig.parse(conf)).to.throw(
|
||||
'Could not parse host config input to a valid format',
|
||||
'Could not parse host config input to a valid format:\n' +
|
||||
'Expecting string at network.hostname but instead got: 123',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for HostConfiguration with invalid dns', () => {
|
||||
const invalids = [
|
||||
123, // wrong type
|
||||
'invalid-because-no-colon',
|
||||
':', // only colon
|
||||
':53', // missing address
|
||||
'1.1.1.1', // missing port
|
||||
'example.com:not-a-port', // wrong port type
|
||||
];
|
||||
for (const dns of invalids) {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
dns,
|
||||
},
|
||||
},
|
||||
};
|
||||
const formattedDns = typeof dns === 'string' ? `"${dns}"` : dns;
|
||||
expect(() => hostConfig.parse(conf)).to.throw(
|
||||
'Could not parse host config input to a valid format:\n' +
|
||||
'Expecting one of:\n' +
|
||||
' AddressString\n' +
|
||||
' boolean\n' +
|
||||
`at network.proxy.1.dns but instead got: ${formattedDns}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('parses valid dns input', () => {
|
||||
const valids = ['balena.io:53', '1.1.1.1:5', 'balena.io:65535'];
|
||||
for (const dns of valids) {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
password: '"bar"',
|
||||
dns,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal(conf);
|
||||
}
|
||||
});
|
||||
|
||||
it("strips additional inputs from HostConfiguration while not erroring if some optional inputs aren't present", () => {
|
||||
const conf = {
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
// login optional field present
|
||||
// but password optional field missing
|
||||
login: '"foo"',
|
||||
noProxy: ['2.2.2.2'],
|
||||
// local_* invalid fields present
|
||||
local_ip: '127.0.0.2',
|
||||
local_port: 1082,
|
||||
// extra key present
|
||||
extra1: 123,
|
||||
},
|
||||
// extra key present
|
||||
extra2: true,
|
||||
},
|
||||
};
|
||||
expect(hostConfig.parse(conf)).to.deep.equal({
|
||||
network: {
|
||||
proxy: {
|
||||
type: 'http-connect',
|
||||
ip: 'test.balena.io',
|
||||
port: 1081,
|
||||
login: '"foo"',
|
||||
noProxy: ['2.2.2.2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
describe('DnsInput', () => {
|
||||
it('decodes to DnsInput boolean', () => {
|
||||
expect(DnsInput.is(true)).to.be.true;
|
||||
expect(DnsInput.is(false)).to.be.true;
|
||||
});
|
||||
|
||||
it('decodes to DnsInput from string in the format "ADDRESS:PORT"', () => {
|
||||
expect(DnsInput.is('1.2.3.4:53')).to.be.true;
|
||||
expect(DnsInput.is('example.com:53')).to.be.true;
|
||||
});
|
||||
|
||||
it('does not decode to DnsInput from invalid string', () => {
|
||||
expect(DnsInput.is('')).to.be.false;
|
||||
expect(DnsInput.is(':')).to.be.false;
|
||||
expect(DnsInput.is('1.2.3.4:')).to.be.false;
|
||||
expect(DnsInput.is(':53')).to.be.false;
|
||||
expect(DnsInput.is('example.com:')).to.be.false;
|
||||
expect(DnsInput.is('1.2.3.4')).to.be.false;
|
||||
expect(DnsInput.is('example.com')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('LegacyHostConfiguration', () => {
|
||||
it('maintains strict dns typing for LegacyHostConfiguration', () => {
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: 123,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.false;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: '1.1.1.1:53',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.true;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.false;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: ':53',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.false;
|
||||
|
||||
expect(
|
||||
LegacyHostConfiguration.is({
|
||||
network: {
|
||||
proxy: {
|
||||
legacy: 'field',
|
||||
dns: '1.1.1.1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user