Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/lib/DnsOverTlsSocket.ts
1030 views
1
import { randomBytes } from 'crypto';
2
import * as dnsPacket from 'dns-packet';
3
import IResolvablePromise from '@secret-agent/interfaces/IResolvablePromise';
4
import { createPromise } from '@secret-agent/commons/utils';
5
import MitmSocket from '@secret-agent/mitm-socket/index';
6
import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';
7
import IDnsSettings from '@secret-agent/interfaces/IDnsSettings';
8
import { IBoundLog } from '@secret-agent/interfaces/ILog';
9
import Log from '@secret-agent/commons/Logger';
10
import EventSubscriber from '@secret-agent/commons/EventSubscriber';
11
import RequestSession from '../handlers/RequestSession';
12
13
const { log } = Log(module);
14
15
export default class DnsOverTlsSocket {
16
public get host(): string {
17
return this.dnsSettings.dnsOverTlsConnection?.host;
18
}
19
20
public get isActive(): boolean {
21
return this.mitmSocket.isReusable() && !this.isClosing;
22
}
23
24
private readonly dnsSettings: IDnsSettings;
25
private mitmSocket: MitmSocket;
26
private isConnected: Promise<void>;
27
28
private pending = new Map<
29
number,
30
{ host: string; resolvable: IResolvablePromise<IDnsResponse> }
31
>();
32
33
private buffer: Buffer = null;
34
private isClosing = false;
35
36
private readonly onClose?: () => void;
37
38
private requestSession: RequestSession | undefined;
39
private logger: IBoundLog;
40
private eventSubscriber = new EventSubscriber();
41
42
constructor(dnsSettings: IDnsSettings, requestSession: RequestSession, onClose?: () => void) {
43
this.requestSession = requestSession;
44
this.logger = log.createChild({ sessionId: requestSession.sessionId });
45
this.dnsSettings = dnsSettings;
46
this.onClose = onClose;
47
}
48
49
public async lookupARecords(host: string): Promise<IDnsResponse> {
50
if (!this.isConnected) {
51
this.isConnected = this.connect();
52
}
53
await this.isConnected;
54
return this.getDnsResponse(host);
55
}
56
57
public close(): void {
58
if (this.isClosing) return;
59
this.isClosing = true;
60
this.mitmSocket?.close();
61
this.eventSubscriber.close();
62
this.requestSession = null;
63
this.mitmSocket = null;
64
if (this.onClose) this.onClose();
65
}
66
67
protected async connect(): Promise<void> {
68
const { host, port, servername } = this.dnsSettings.dnsOverTlsConnection || {};
69
this.mitmSocket = new MitmSocket(this.requestSession?.sessionId, {
70
host,
71
servername,
72
port: String(port ?? 853),
73
isSsl: true,
74
keepAlive: true,
75
debug: true,
76
});
77
78
await this.mitmSocket.connect(this.requestSession.requestAgent.socketSession, 10e3);
79
80
this.eventSubscriber.on(this.mitmSocket.socket, 'data', this.onData.bind(this));
81
82
const onClose = this.eventSubscriber.on(this.mitmSocket.socket, 'close', () => {
83
this.isClosing = true;
84
if (this.onClose) this.onClose();
85
});
86
this.eventSubscriber.on(this.mitmSocket, 'eof', async () => {
87
this.eventSubscriber.off(onClose);
88
if (this.isClosing) return;
89
this.mitmSocket.close();
90
try {
91
this.isConnected = this.connect();
92
await this.isConnected;
93
// re-run pending queries
94
for (const [id, entry] of this.pending) {
95
this.pending.delete(id);
96
const newHost = this.getDnsResponse(entry.host);
97
entry.resolvable.resolve(newHost);
98
}
99
} catch (error) {
100
this.logger.info('Error re-connecting to dns', {
101
error,
102
});
103
}
104
});
105
}
106
107
private getDnsResponse(host: string): Promise<IDnsResponse> {
108
const id = this.query({
109
name: host,
110
class: 'IN',
111
type: 'A',
112
});
113
const resolvable = createPromise<IDnsResponse>(5e3);
114
this.pending.set(id, { host, resolvable });
115
return resolvable.promise;
116
}
117
118
private disconnect(): void {
119
for (const [, entry] of this.pending) {
120
entry.resolvable.reject(new CanceledPromiseError('Disconnecting Dns Socket'));
121
}
122
this.close();
123
}
124
125
private query(...questions: IQuestion[]): number {
126
const id = randomBytes(2).readUInt16BE(0);
127
const dnsQuery = dnsPacket.streamEncode({
128
flags: dnsPacket.RECURSION_DESIRED,
129
id,
130
questions,
131
type: 'query',
132
});
133
this.mitmSocket.socket.write(dnsQuery);
134
return id;
135
}
136
137
private onData(data: Buffer): void {
138
if (this.buffer === null) {
139
this.buffer = Buffer.from(data);
140
} else {
141
this.buffer = Buffer.concat([this.buffer, data]);
142
}
143
144
while (this.buffer.byteLength > 2) {
145
const messageLength = this.getMessageLength();
146
if (messageLength < 12) {
147
return this.disconnect();
148
}
149
150
if (this.buffer.byteLength < messageLength + 2) return;
151
152
// append prefixed byte length
153
const next = this.buffer.slice(0, messageLength + 2);
154
const decoded = dnsPacket.streamDecode(next) as IDnsResponse;
155
this.pending.get(decoded.id)?.resolvable?.resolve(decoded);
156
this.pending.delete(decoded.id);
157
this.buffer = this.buffer.slice(messageLength + 2);
158
}
159
}
160
161
private getMessageLength(): number | undefined {
162
if (this.buffer.byteLength >= 2) {
163
// https://tools.ietf.org/html/rfc7858#section-3.3
164
// https://tools.ietf.org/html/rfc1035#section-4.2.2
165
// The message is prefixed with a two byte length field which gives the
166
// message length, excluding the two byte length field.
167
return this.buffer.readUInt16BE(0);
168
}
169
}
170
}
171
172
interface IQuestion {
173
name: string;
174
type: string;
175
class: string;
176
}
177
178
interface IAnswer {
179
name: string;
180
type: string;
181
class: string;
182
ttl: number;
183
flush: boolean;
184
data: string;
185
}
186
187
interface IDnsResponse {
188
id: number;
189
type: string;
190
flags: number;
191
flag_qr: boolean;
192
opcode: string;
193
flag_aa: boolean;
194
flag_tc: boolean;
195
flag_rd: boolean;
196
flag_ra: boolean;
197
flag_z: boolean;
198
flag_ad: boolean;
199
flag_cd: boolean;
200
rcode: string;
201
questions: IQuestion[];
202
answers: IAnswer[];
203
authorities: string[];
204
additionals: string[];
205
}
206
207