Skip to content

Commit aca8807

Browse files
committed
Add pcap output feature
Change-Id: Ic2471c3bf7c2808d096a0aea90d49e81bdebf023
1 parent 67edec7 commit aca8807

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

src/Interface.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import PPPFrame from './ppp/PPPFrame';
66
import LCPEncapsulation from './ppp/LCPEncapsulation';
77
import { ControlCode } from './ppp/ControlProtocol';
88
import { encode } from './encodingUtil';
9+
import { PcapPacketDirection } from './PcapWriter';
10+
11+
jest.mock('./PcapWriter');
912

1013
let intf: Interface;
1114
let sink: BufferSink;
@@ -198,3 +201,25 @@ describe('getLink()', () => {
198201

199202
// TODO: check ping failure triggers LCP restart
200203
});
204+
205+
describe('pcap writing', () => {
206+
let packetWriteSpy: jest.SpyInstance;
207+
208+
beforeEach(() => {
209+
intf = Interface.create(sink, '/tmp/not/a/real/path');
210+
// eslint-disable-next-line
211+
packetWriteSpy = jest.spyOn((intf as any).pcapWriter, 'writePacket');
212+
});
213+
214+
it('writes a received packet', () => {
215+
const packet = PPPFrame.build(0xabcd, encode('hello recv!'));
216+
intf.write(packet);
217+
expect(packetWriteSpy).toBeCalledWith(PcapPacketDirection.IN, packet);
218+
});
219+
220+
it('writes a sent packet', () => {
221+
const packet = PPPFrame.build(0xabcd, encode('hello send!'));
222+
intf.sendPacket(0xabcd, encode('hello send!'));
223+
expect(packetWriteSpy).toBeCalledWith(PcapPacketDirection.OUT, packet);
224+
});
225+
});

src/Interface.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import Event from './util/event';
99
import { FrameDecoder } from './framing/decoder';
1010
import { FrameEncoder } from './framing/encoder';
1111
import { FrameSplitter } from './framing/splitter';
12+
import PcapWriter, { PcapPacketDirection } from './PcapWriter';
1213

1314
export default class Interface extends stream.Duplex {
14-
static create(phy: stream.Duplex): Interface {
15-
const intf = new Interface();
15+
static create(phy: stream.Duplex, pcapPath?: string): Interface {
16+
const intf = new Interface(pcapPath);
1617
const splitter = new FrameSplitter();
1718
const decoder = new FrameDecoder();
1819
const encoder = new FrameEncoder();
@@ -24,9 +25,13 @@ export default class Interface extends stream.Duplex {
2425
return intf;
2526
}
2627

27-
constructor() {
28+
constructor(pcapPath?: string) {
2829
super({ objectMode: true, allowHalfOpen: false });
2930

31+
if (pcapPath) {
32+
this.pcapWriter = new PcapWriter(pcapPath, PcapWriter.linkTypePPPWithDir);
33+
}
34+
3035
this.lcp.addListener('linkUp', this.onLinkUp.bind(this));
3136
this.lcp.addListener('linkDown', this.onLinkDown.bind(this));
3237

@@ -41,11 +46,16 @@ export default class Interface extends stream.Duplex {
4146
private lcp = new LinkControlProtocol(this);
4247
private link?: Link;
4348
private linkAvailable = new Event();
49+
private pcapWriter?: PcapWriter;
4450

4551
// eslint-disable-next-line @typescript-eslint/no-empty-function
4652
_read(): void {}
4753

4854
_write(chunk: Buffer, _: string, callback: (err?: Error) => void): void {
55+
if (this.pcapWriter) {
56+
this.pcapWriter.writePacket(PcapPacketDirection.IN, chunk);
57+
}
58+
4959
let frame: PPPFrame;
5060
try {
5161
frame = PPPFrame.parse(chunk);
@@ -100,6 +110,9 @@ export default class Interface extends stream.Duplex {
100110
)}`,
101111
);
102112
const datagram = PPPFrame.build(protocol, packet);
113+
if (this.pcapWriter) {
114+
this.pcapWriter.writePacket(PcapPacketDirection.OUT, datagram);
115+
}
103116
this.push(datagram);
104117
}
105118

src/PcapWriter.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
import PcapWriter, { PcapPacketDirection } from './PcapWriter';
6+
import { encode } from './encodingUtil';
7+
8+
const tmpPath = path.join(os.tmpdir(), 'test.cap');
9+
10+
let writer: PcapWriter;
11+
12+
beforeEach(() => {
13+
writer = new PcapWriter(tmpPath, 0xcafef00d);
14+
});
15+
16+
afterEach(() => {
17+
writer.close();
18+
});
19+
20+
it('writes the requested link type in the header', () => {
21+
const header = fs.readFileSync(tmpPath);
22+
expect(header).toEqual(
23+
encode(
24+
'\xa1\xb2\xc3\xd4\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xca\xfe\xf0\x0d',
25+
),
26+
);
27+
});
28+
29+
it('writes a received packet to the pcap file', () => {
30+
const data = encode('hello world!');
31+
const dateNowSpy = jest.spyOn(Date, 'now');
32+
dateNowSpy.mockReturnValueOnce(1638452221234);
33+
writer.writePacket(PcapPacketDirection.IN, data);
34+
const packet = fs.readFileSync(tmpPath).slice(24);
35+
expect(packet).toEqual(
36+
encode(
37+
'\x61\xa8\xcb\xfd\x00\x03\x92\x10\x00\x00\x00\x0c\x00\x00\x00\x0c\x00hello world!',
38+
),
39+
);
40+
});
41+
42+
it('writes a sent packet to the pcap file', () => {
43+
const data = encode('hello world!');
44+
const dateNowSpy = jest.spyOn(Date, 'now');
45+
dateNowSpy.mockReturnValueOnce(1638452221234);
46+
writer.writePacket(PcapPacketDirection.OUT, data);
47+
const packet = fs.readFileSync(tmpPath).slice(24);
48+
expect(packet).toEqual(
49+
encode(
50+
'\x61\xa8\xcb\xfd\x00\x03\x92\x10\x00\x00\x00\x0c\x00\x00\x00\x0c\x01hello world!',
51+
),
52+
);
53+
});
54+
55+
it('writing a packet larger than 65k throws', () => {
56+
expect(() =>
57+
writer.writePacket(PcapPacketDirection.OUT, Buffer.alloc(0x10000)),
58+
).toThrowError('Data too large to write to pcap file');
59+
});

src/PcapWriter.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as fs from 'fs';
2+
3+
export enum PcapPacketDirection {
4+
IN = 0,
5+
OUT = 1,
6+
}
7+
export default class PcapWriter {
8+
static linkTypePPPWithDir = 204;
9+
10+
private fd: number;
11+
12+
constructor(path: string, linkType: number) {
13+
this.fd = fs.openSync(path, 'w');
14+
this.writeHeader(linkType);
15+
}
16+
17+
public close(): void {
18+
fs.closeSync(this.fd);
19+
}
20+
21+
private writeHeader(linkType: number): void {
22+
const buf = Buffer.alloc(24);
23+
let offset = 0;
24+
25+
// magic_number
26+
buf.writeUInt32BE(0xa1b2c3d4, offset);
27+
offset += 4;
28+
29+
// version_major
30+
buf.writeUInt16BE(2, offset);
31+
offset += 2;
32+
33+
// version_minor
34+
buf.writeUInt16BE(4, offset);
35+
offset += 2;
36+
37+
// thiszone (GMT to local time correction)
38+
buf.writeInt32BE(0, offset);
39+
offset += 4;
40+
41+
// sigfigs (accuracy of timestamps)
42+
buf.writeUInt32BE(0, offset);
43+
offset += 4;
44+
45+
// snaplen (max length of captured packets, in octets)
46+
buf.writeUInt32BE(65535, offset);
47+
offset += 4;
48+
49+
// network
50+
buf.writeUInt32BE(linkType, offset);
51+
52+
fs.writeSync(this.fd, buf);
53+
}
54+
55+
writePacket(direction: PcapPacketDirection, data: Buffer): void {
56+
if (data.byteLength > 0xffff) {
57+
throw new Error('Data too large to write to pcap file');
58+
}
59+
60+
const buf = Buffer.alloc(17);
61+
62+
const timestamp = Date.now();
63+
const seconds = Math.floor(timestamp / 1000);
64+
const microseconds = (timestamp - seconds * 1000) * 1000;
65+
66+
let offset = buf.writeUInt32BE(seconds);
67+
offset = buf.writeUInt32BE(microseconds, offset);
68+
offset = buf.writeUInt32BE(data.byteLength, offset);
69+
offset = buf.writeUInt32BE(data.byteLength, offset);
70+
buf.writeUInt8(direction, offset);
71+
72+
fs.writeSync(this.fd, buf);
73+
fs.writeSync(this.fd, data);
74+
}
75+
}

0 commit comments

Comments
 (0)