Stringify dns subsection of redsocks input config to dnsu2t

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2024-08-14 13:16:28 -07:00
parent e724f60beb
commit b775f8f14d
4 changed files with 232 additions and 14 deletions

View File

@ -3,7 +3,7 @@ import path from 'path';
import { isRight } from 'fp-ts/lib/Either';
import Reporter from 'io-ts-reporters';
import type { RedsocksConfig, HostProxyConfig } from './types';
import type { RedsocksConfig, HostProxyConfig, DnsInput } from './types';
import { ProxyConfig } from './types';
import { pathOnBoot, readFromBoot, writeToBoot } from '../lib/host-utils';
import { unlinkAll, mkdirp } from '../lib/fs-utils';
@ -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 {
@ -138,6 +168,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;

View File

@ -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,10 +22,11 @@ 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>;

View File

@ -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),

View File

@ -4,6 +4,7 @@ import type { SinonStub } from 'sinon';
import * as hostConfig from '~/src/host-config';
import { RedsocksConf } from '~/src/host-config/proxy';
import { DnsInput } from '~/src/host-config/types';
import type { RedsocksConfig, ProxyConfig } from '~/src/host-config/types';
import log from '~/lib/supervisor-console';
@ -124,6 +125,135 @@ 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', () => {
@ -573,4 +703,28 @@ describe('src/host-config', () => {
});
});
});
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;
});
});
});
});