Skip to content

Commit e2c1a0e

Browse files
committed
Add PPP implementation and supporting interfaces
Change-Id: I8addeceb3fd491ecf30ee72fbbd6ea220477b9a4
1 parent 56058b1 commit e2c1a0e

File tree

7 files changed

+1546
-0
lines changed

7 files changed

+1546
-0
lines changed

src/Interface.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import * as stream from 'stream';
2+
3+
import Interface from './Interface';
4+
import { encode as encodeFrame } from './framing/encoder';
5+
import PPPFrame from './ppp/PPPFrame';
6+
import { encode } from './encodingUtil';
7+
8+
let intf: Interface;
9+
let sink: BufferSink;
10+
11+
class BufferSink extends stream.Duplex {
12+
public data: Buffer[] = [];
13+
14+
constructor() {
15+
super({ allowHalfOpen: false });
16+
}
17+
18+
// eslint-disable-next-line @typescript-eslint/no-empty-function
19+
_read(): void {}
20+
21+
_write(chunk: Buffer, _: string, done: (err?: Error) => void): void {
22+
this.data.push(chunk);
23+
done();
24+
}
25+
26+
getData(): Promise<Buffer[]> {
27+
this.push(null);
28+
return new Promise((resolve) =>
29+
sink.once('close', () => resolve(this.data)),
30+
);
31+
}
32+
33+
waitForClose(): Promise<void> {
34+
return new Promise((resolve) => sink.once('close', () => resolve()));
35+
}
36+
}
37+
38+
beforeEach(() => {
39+
jest.useFakeTimers();
40+
sink = new BufferSink();
41+
intf = Interface.create(sink);
42+
});
43+
44+
afterEach(() => {
45+
jest.useRealTimers();
46+
});
47+
48+
it('sends a packet', async () => {
49+
const packet = Buffer.from('data');
50+
intf.sendPacket(0x8889, packet);
51+
52+
return expect(sink.getData()).resolves.toContainEqual(
53+
encodeFrame(PPPFrame.build(0x8889, packet)),
54+
);
55+
});
56+
57+
it('sends from a socket', () => {
58+
const socket = intf.connect(0xf0f1);
59+
const packet = Buffer.from('data');
60+
socket.send(packet);
61+
return expect(sink.getData()).resolves.toContainEqual(
62+
encodeFrame(PPPFrame.build(0xf0f1, packet)),
63+
);
64+
});
65+
66+
it('receives a packet', async () => {
67+
const socket = intf.connect(0xf0f1);
68+
const packetHandler = jest.fn();
69+
socket.on('data', packetHandler);
70+
sink.push(encodeFrame(PPPFrame.build(0xf0f1, Buffer.from('hello world!'))));
71+
sink.push(null);
72+
await sink.waitForClose();
73+
expect(packetHandler).toBeCalledWith(encode('hello world!'));
74+
});
75+
76+
it('closing interface closes sockets and underlying stream', () => {
77+
const socketA = intf.connect(0xf0f1);
78+
const socketB = intf.connect(0xf0f3);
79+
80+
intf.close();
81+
82+
expect(socketA.closed).toEqual(true);
83+
expect(socketB.closed).toEqual(true);
84+
expect(intf.destroyed).toEqual(true);
85+
});
86+
87+
it('ending underlying stream closes sockets and interface', async () => {
88+
const socket = intf.connect(0xf0f1);
89+
90+
sink.destroy();
91+
jest.runAllTimers();
92+
await sink.waitForClose();
93+
94+
expect(socket.closed).toEqual(true);
95+
expect(intf.destroyed).toEqual(true);
96+
});
97+
98+
it('throws if opening two sockets for same protocol', () => {
99+
intf.connect(0xf0f1);
100+
expect(() => intf.connect(0xf0f1)).toThrowError(
101+
'A socket is already bound to protocol 0xf0f1',
102+
);
103+
});
104+
105+
it('closing one socket allows another to be opened for the same protocol', () => {
106+
const socketA = intf.connect(0xf0f1);
107+
socketA.close();
108+
const socketB = intf.connect(0xf0f1);
109+
expect(socketA).not.toBe(socketB);
110+
});
111+
112+
it('throws if sending on a closed interface', () => {
113+
intf.close();
114+
115+
expect(intf.closed).toEqual(true);
116+
expect(() => intf.sendPacket(0x8889, Buffer.from('data'))).toThrowError(
117+
'I/O operation on closed interface',
118+
);
119+
});
120+
121+
it('ignores corrupted PPP frames', () => {
122+
expect(() => intf.write(encode('?'))).not.toThrowError();
123+
});

src/Interface.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as stream from 'stream';
2+
3+
import InterfaceSocket from './InterfaceSocket';
4+
import PPPFrame from './ppp/PPPFrame';
5+
6+
import { FrameDecoder } from './framing/decoder';
7+
import { FrameEncoder } from './framing/encoder';
8+
import { FrameSplitter } from './framing/splitter';
9+
10+
export default class Interface extends stream.Duplex {
11+
static create(phy: stream.Duplex): Interface {
12+
const intf = new Interface();
13+
const splitter = new FrameSplitter();
14+
const decoder = new FrameDecoder();
15+
const encoder = new FrameEncoder();
16+
17+
stream.pipeline([phy, splitter, decoder, intf, encoder, phy], () => {
18+
intf.down();
19+
});
20+
21+
return intf;
22+
}
23+
24+
constructor() {
25+
super({ objectMode: true, allowHalfOpen: false });
26+
}
27+
28+
public closed = false;
29+
private sockets: Record<number, InterfaceSocket> = {};
30+
31+
// eslint-disable-next-line @typescript-eslint/no-empty-function
32+
_read(): void {}
33+
34+
_write(chunk: Buffer, _: string, callback: (err?: Error) => void): void {
35+
let frame: PPPFrame;
36+
try {
37+
frame = PPPFrame.parse(chunk);
38+
} catch {
39+
console.warn(`Received malformed PPP frame: ${chunk.toString('hex')}`);
40+
callback();
41+
return;
42+
}
43+
44+
console.log(
45+
`[PHY recv] [protocol:0x${frame.protocol.toString(
46+
16,
47+
)}] ${frame.information.toString('hex')}`,
48+
);
49+
50+
const socket: InterfaceSocket | undefined = this.sockets[frame.protocol];
51+
if (socket !== undefined) {
52+
socket.handlePacket(frame.information);
53+
} else {
54+
// Protocol-reject
55+
}
56+
57+
callback();
58+
}
59+
60+
/*
61+
Open a link-layer socket for sending and receiving packets
62+
of a specific protocol number.
63+
*/
64+
connect(protocol: number): InterfaceSocket {
65+
if (this.sockets[protocol] !== undefined) {
66+
throw new Error(
67+
`A socket is already bound to protocol 0x${protocol.toString(16)}`,
68+
);
69+
}
70+
71+
return (this.sockets[protocol] = new InterfaceSocket(this, protocol));
72+
}
73+
74+
/*
75+
Used by InterfaceSocket objets to unregister themselves when closing.
76+
*/
77+
unregisterSocket(protocol: number): void {
78+
delete this.sockets[protocol];
79+
}
80+
81+
sendPacket(protocol: number, packet: Buffer): void {
82+
if (this.closed) throw new Error('I/O operation on closed interface');
83+
console.log(
84+
`[PHY send] [protocol:0x${protocol.toString(16)}] ${packet.toString(
85+
'hex',
86+
)}`,
87+
);
88+
const datagram = PPPFrame.build(protocol, packet);
89+
this.push(datagram);
90+
}
91+
92+
closeAllSockets(): void {
93+
for (const socket of Object.values(this.sockets)) {
94+
socket.close();
95+
}
96+
}
97+
98+
public close(): void {
99+
if (this.closed) return;
100+
this.closeAllSockets();
101+
this.down();
102+
}
103+
104+
/*
105+
The lower layer (iostream) is down. Bring down the interface.
106+
*/
107+
private down(): void {
108+
this.closed = true;
109+
this.closeAllSockets();
110+
this.destroy();
111+
}
112+
}

src/InterfaceSocket.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Interface from './Interface';
2+
import InterfaceSocket from './InterfaceSocket';
3+
4+
jest.mock('./Interface');
5+
6+
let intf: Interface;
7+
let intfSocket: InterfaceSocket;
8+
9+
beforeEach(() => {
10+
intf = new Interface();
11+
intfSocket = new InterfaceSocket(intf, 0xf2f1);
12+
});
13+
14+
it('is not closed after instanciation', () => {
15+
expect(intfSocket.closed).toEqual(false);
16+
});
17+
18+
it('sends data', () => {
19+
const data = Buffer.from('data');
20+
intfSocket.send(data);
21+
expect(intf.sendPacket).toBeCalledWith(0xf2f1, data);
22+
});
23+
24+
it('marks as close once closed', () => {
25+
intfSocket.close();
26+
expect(intfSocket.closed).toEqual(true);
27+
});
28+
29+
it('unregisters socket from interface upon close', () => {
30+
intfSocket.close();
31+
expect(intf.unregisterSocket).toBeCalledWith(0xf2f1);
32+
});
33+
34+
it('calls close handler on close', () => {
35+
const closeHandler = jest.fn();
36+
intfSocket.once('close', closeHandler);
37+
intfSocket.close();
38+
expect(closeHandler).toBeCalledTimes(1);
39+
});
40+
41+
it('throws when sending after closed', () => {
42+
intfSocket.close();
43+
expect(() => intfSocket.send(Buffer.from('data'))).toThrowError(
44+
'I/O operation on closed socket',
45+
);
46+
});
47+
48+
it('handles a packet', () => {
49+
const dataHandler = jest.fn();
50+
const packet = Buffer.from('data');
51+
intfSocket.once('data', dataHandler);
52+
intfSocket.handlePacket(packet);
53+
expect(dataHandler).toBeCalledWith(packet);
54+
});
55+
56+
it("doesn't emit event for packet if closed", () => {
57+
const dataHandler = jest.fn();
58+
intfSocket.once('data', dataHandler);
59+
intfSocket.close();
60+
intfSocket.handlePacket(Buffer.from('data'));
61+
expect(dataHandler).not.toBeCalled();
62+
});
63+
64+
it('close() is idempotent', () => {
65+
const closeHandler = jest.fn();
66+
intfSocket.once('close', closeHandler);
67+
intfSocket.close();
68+
intfSocket.close();
69+
expect(closeHandler).toBeCalledTimes(1);
70+
expect(intf.unregisterSocket).toBeCalledTimes(1);
71+
});

src/InterfaceSocket.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import EventEmitter from 'events';
2+
3+
import Interface from './Interface';
4+
5+
/*
6+
A socket for sending and receiving link-layer packets over a
7+
PULSE interface.
8+
9+
Available events:
10+
- close
11+
- data
12+
*/
13+
export default class InterfaceSocket extends EventEmitter {
14+
public closed = false;
15+
16+
constructor(private intf: Interface, private protocol: number) {
17+
super();
18+
}
19+
20+
public send(packet: Buffer): void {
21+
if (this.closed) throw new Error('I/O operation on closed socket');
22+
this.intf.sendPacket(this.protocol, packet);
23+
}
24+
25+
public handlePacket(packet: Buffer): void {
26+
if (!this.closed) this.emit('data', packet);
27+
}
28+
29+
public close(): void {
30+
if (this.closed) return;
31+
this.closed = true;
32+
this.emit('close');
33+
this.intf.unregisterSocket(this.protocol);
34+
this.removeAllListeners();
35+
}
36+
}

0 commit comments

Comments
 (0)