Skip to content

Commit d983335

Browse files
committed
feat: 🎸 add demo server
1 parent 811316b commit d983335

File tree

4 files changed

+368
-0
lines changed

4 files changed

+368
-0
lines changed

src/nfs/v4/__demos__/README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# NFSv4 TCP Server Demo
2+
3+
This demo shows how to create a simple NFSv4 server that listens on a TCP socket and decodes incoming NFSv4 packets.
4+
5+
## What it does
6+
7+
1. Starts a TCP server on `127.0.0.1:2049` (default NFS port)
8+
2. Accepts incoming connections
9+
3. Receives TCP data and prints it in hexadecimal format
10+
4. Decodes RPC record marking (RM) frames
11+
5. Decodes RPC call messages
12+
6. Decodes NFSv4 COMPOUND procedure calls
13+
7. Pretty-prints all decoded information to the console, including individual operations within COMPOUND requests
14+
15+
## Running the demo
16+
17+
```bash
18+
# Build the project first
19+
npm run build
20+
21+
# Run the demo
22+
node lib/nfs/v4/__demos__/tcp-server.js
23+
```
24+
25+
Or run directly with ts-node:
26+
27+
```bash
28+
npx ts-node src/nfs/v4/__demos__/tcp-server.ts
29+
```
30+
31+
You can also specify a custom port:
32+
33+
```bash
34+
PORT=8585 npx ts-node src/nfs/v4/__demos__/tcp-server.ts
35+
```
36+
37+
## NFSv4 Protocol Structure
38+
39+
NFSv4 differs from NFSv3 in that it uses COMPOUND procedures to bundle multiple operations:
40+
41+
- **NULL (procedure 0)**: Standard no-op procedure
42+
- **COMPOUND (procedure 1)**: Container for one or more NFSv4 operations
43+
44+
Each COMPOUND request contains:
45+
- `tag`: Client-defined string for request identification
46+
- `minorversion`: NFSv4 minor version number (0 for NFSv4.0)
47+
- `argarray`: Array of operations to execute
48+
49+
## Supported Operations
50+
51+
The demo can decode all NFSv4 operations including:
52+
53+
- **File access**: ACCESS, GETATTR, GETFH, LOOKUP, LOOKUPP, READ, READDIR, READLINK
54+
- **File modification**: WRITE, CREATE, REMOVE, RENAME, LINK, SETATTR, COMMIT
55+
- **File handles**: PUTFH, PUTPUBFH, PUTROOTFH, SAVEFH, RESTOREFH
56+
- **State management**: OPEN, CLOSE, LOCK, LOCKT, LOCKU, OPEN_CONFIRM, OPEN_DOWNGRADE
57+
- **Client/Session**: SETCLIENTID, SETCLIENTID_CONFIRM, RENEW, RELEASE_LOCKOWNER
58+
- **Delegations**: DELEGPURGE, DELEGRETURN
59+
- **Other**: VERIFY, NVERIFY, SECINFO, OPENATTR
60+
61+
## Example Output
62+
63+
When a client sends a COMPOUND request, you'll see output like:
64+
65+
```
66+
================================================================================
67+
[2023-10-09T12:34:56.789Z] Received 128 bytes
68+
HEX: 80000078000000011b8b45f200000000...
69+
--------------------------------------------------------------------------------
70+
71+
RPC Record (120 bytes):
72+
HEX: 000000011b8b45f200000000...
73+
74+
RPC Message:
75+
RpcCallMessage {
76+
xid: 463701234,
77+
rpcvers: 2,
78+
prog: 100003,
79+
vers: 4,
80+
proc: 1,
81+
...
82+
}
83+
84+
NFS Procedure: COMPOUND
85+
86+
NFS COMPOUND Request:
87+
Tag: "nfs4_client"
88+
Minor Version: 0
89+
Operations (3):
90+
[0] PUTFH
91+
{
92+
"op": 22,
93+
"fh": <Buffer 00 01 02 ...>
94+
}
95+
[1] LOOKUP
96+
{
97+
"op": 15,
98+
"name": "file.txt"
99+
}
100+
[2] GETFH
101+
{
102+
"op": 10
103+
}
104+
================================================================================
105+
```
106+
107+
## Testing the server
108+
109+
You can test the server using:
110+
111+
1. **Real NFS clients**: Configure an NFSv4 client to connect to `127.0.0.1:2049`
112+
2. **Custom test scripts**: Create TypeScript/JavaScript clients using the `FullNfsv4Encoder`
113+
3. **Network tools**: Use tools like `tcpreplay` to replay captured NFSv4 traffic
114+
115+
## Notes
116+
117+
- This is a **demo/debugging tool** only - it does not respond to requests or implement a full NFS server
118+
- The server only decodes and displays incoming requests
119+
- Port 2049 may require root/admin privileges on some systems
120+
- Use a custom port (e.g., `PORT=8585`) to avoid privilege requirements

src/nfs/v4/__demos__/tcp-client.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as net from 'net';
2+
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
3+
import {FullNfsv4Encoder} from '../FullNfsv4Encoder';
4+
import {Nfsv4CompoundRequest, Nfsv4PutfhRequest, Nfsv4LookupRequest, Nfsv4GetfhRequest} from '../messages';
5+
import {Nfsv4Fh} from '../structs';
6+
import {Nfsv4Proc} from '../constants';
7+
8+
/* tslint:disable:no-console */
9+
10+
const PORT = Number(process.env.PORT) || 2049;
11+
const HOST = '127.0.0.1';
12+
13+
const createTestCompoundRequest = (): Nfsv4CompoundRequest => {
14+
const fhData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
15+
const fh = new Nfsv4Fh(fhData);
16+
const putfh = new Nfsv4PutfhRequest(fh);
17+
const lookup = new Nfsv4LookupRequest('testfile.txt');
18+
const getfh = new Nfsv4GetfhRequest();
19+
return new Nfsv4CompoundRequest('nfs4_client', 0, [putfh, lookup, getfh]);
20+
};
21+
22+
const createTestCred = () => {
23+
return {
24+
flavor: 0,
25+
body: new Reader(new Uint8Array()),
26+
};
27+
};
28+
29+
const createTestVerf = () => {
30+
return {
31+
flavor: 0,
32+
body: new Reader(new Uint8Array()),
33+
};
34+
};
35+
36+
console.log('Connecting to NFSv4 server...');
37+
38+
const client = net.connect({port: PORT, host: HOST}, () => {
39+
console.log(`Connected to ${HOST}:${PORT}`);
40+
console.log('Sending COMPOUND request (PUTFH + LOOKUP + GETFH)...\n');
41+
const encoder = new FullNfsv4Encoder();
42+
const request = createTestCompoundRequest();
43+
const xid = 0x1b8b45f2;
44+
const proc = Nfsv4Proc.COMPOUND;
45+
const cred = createTestCred();
46+
const verf = createTestVerf();
47+
const encoded = encoder.encodeCall(xid, proc, cred, verf, request);
48+
console.log(`Sending ${encoded.length} bytes`);
49+
console.log(
50+
'HEX:',
51+
Array.from(encoded)
52+
.map((b) => b.toString(16).padStart(2, '0'))
53+
.join(' '),
54+
);
55+
console.log('');
56+
client.write(encoded);
57+
setTimeout(() => {
58+
console.log('Closing connection...');
59+
client.end();
60+
}, 100);
61+
});
62+
63+
client.on('data', (data) => {
64+
console.log('Received response:', data.length, 'bytes');
65+
console.log('(This demo server does not send responses)');
66+
});
67+
68+
client.on('end', () => {
69+
console.log('Connection closed');
70+
process.exit(0);
71+
});
72+
73+
client.on('error', (err) => {
74+
console.error('Connection error:', err.message);
75+
process.exit(1);
76+
});

src/nfs/v4/__demos__/tcp-server.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as net from 'net';
2+
import {RmRecordDecoder} from '../../../rm';
3+
import {RpcMessageDecoder, RpcCallMessage} from '../../../rpc';
4+
import {Nfsv4Decoder} from '../Nfsv4Decoder';
5+
import * as msg from '../messages';
6+
import {Nfsv4Op} from '../constants';
7+
8+
/* tslint:disable:no-console */
9+
10+
const PORT = Number(process.env.PORT) || 2049;
11+
const HOST = '127.0.0.1';
12+
13+
const toHex = (buffer: Uint8Array | Buffer): string => {
14+
return Array.from(buffer)
15+
.map((byte) => byte.toString(16).padStart(2, '0'))
16+
.join('');
17+
};
18+
19+
const getProcName = (proc: number): string => {
20+
const names: Record<number, string> = {
21+
0: 'NULL',
22+
1: 'COMPOUND',
23+
};
24+
return names[proc] || `UNKNOWN(${proc})`;
25+
};
26+
27+
const getOpName = (op: any): string => {
28+
if (op instanceof msg.Nfsv4AccessRequest) return 'ACCESS';
29+
if (op instanceof msg.Nfsv4CloseRequest) return 'CLOSE';
30+
if (op instanceof msg.Nfsv4CommitRequest) return 'COMMIT';
31+
if (op instanceof msg.Nfsv4CreateRequest) return 'CREATE';
32+
if (op instanceof msg.Nfsv4DelegpurgeRequest) return 'DELEGPURGE';
33+
if (op instanceof msg.Nfsv4DelegreturnRequest) return 'DELEGRETURN';
34+
if (op instanceof msg.Nfsv4GetattrRequest) return 'GETATTR';
35+
if (op instanceof msg.Nfsv4GetfhRequest) return 'GETFH';
36+
if (op instanceof msg.Nfsv4LinkRequest) return 'LINK';
37+
if (op instanceof msg.Nfsv4LockRequest) return 'LOCK';
38+
if (op instanceof msg.Nfsv4LocktRequest) return 'LOCKT';
39+
if (op instanceof msg.Nfsv4LockuRequest) return 'LOCKU';
40+
if (op instanceof msg.Nfsv4LookupRequest) return 'LOOKUP';
41+
if (op instanceof msg.Nfsv4LookuppRequest) return 'LOOKUPP';
42+
if (op instanceof msg.Nfsv4NverifyRequest) return 'NVERIFY';
43+
if (op instanceof msg.Nfsv4OpenRequest) return 'OPEN';
44+
if (op instanceof msg.Nfsv4OpenattrRequest) return 'OPENATTR';
45+
if (op instanceof msg.Nfsv4OpenConfirmRequest) return 'OPEN_CONFIRM';
46+
if (op instanceof msg.Nfsv4OpenDowngradeRequest) return 'OPEN_DOWNGRADE';
47+
if (op instanceof msg.Nfsv4PutfhRequest) return 'PUTFH';
48+
if (op instanceof msg.Nfsv4PutpubfhRequest) return 'PUTPUBFH';
49+
if (op instanceof msg.Nfsv4PutrootfhRequest) return 'PUTROOTFH';
50+
if (op instanceof msg.Nfsv4ReadRequest) return 'READ';
51+
if (op instanceof msg.Nfsv4ReaddirRequest) return 'READDIR';
52+
if (op instanceof msg.Nfsv4ReadlinkRequest) return 'READLINK';
53+
if (op instanceof msg.Nfsv4RemoveRequest) return 'REMOVE';
54+
if (op instanceof msg.Nfsv4RenameRequest) return 'RENAME';
55+
if (op instanceof msg.Nfsv4RenewRequest) return 'RENEW';
56+
if (op instanceof msg.Nfsv4RestorefhRequest) return 'RESTOREFH';
57+
if (op instanceof msg.Nfsv4SavefhRequest) return 'SAVEFH';
58+
if (op instanceof msg.Nfsv4SecinfoRequest) return 'SECINFO';
59+
if (op instanceof msg.Nfsv4SetattrRequest) return 'SETATTR';
60+
if (op instanceof msg.Nfsv4SetclientidRequest) return 'SETCLIENTID';
61+
if (op instanceof msg.Nfsv4SetclientidConfirmRequest) return 'SETCLIENTID_CONFIRM';
62+
if (op instanceof msg.Nfsv4VerifyRequest) return 'VERIFY';
63+
if (op instanceof msg.Nfsv4WriteRequest) return 'WRITE';
64+
if (op instanceof msg.Nfsv4ReleaseLockOwnerRequest) return 'RELEASE_LOCKOWNER';
65+
if (op instanceof msg.Nfsv4IllegalRequest) return 'ILLEGAL';
66+
return 'UNKNOWN';
67+
};
68+
69+
const server = net.createServer((socket) => {
70+
console.log(`[${new Date().toISOString()}] Client connected from ${socket.remoteAddress}:${socket.remotePort}`);
71+
const rmDecoder = new RmRecordDecoder();
72+
const rpcDecoder = new RpcMessageDecoder();
73+
const nfsDecoder = new Nfsv4Decoder();
74+
socket.on('data', (data) => {
75+
console.log('\n' + '='.repeat(80));
76+
console.log(`[${new Date().toISOString()}] Received ${data.length} bytes`);
77+
console.log('HEX:', toHex(data));
78+
console.log('-'.repeat(80));
79+
const uint8Data = new Uint8Array(data);
80+
rmDecoder.push(uint8Data);
81+
let record = rmDecoder.readRecord();
82+
while (record) {
83+
console.log(`\nRPC Record (${record.size()} bytes):`);
84+
console.log('HEX:', toHex(record.subarray()));
85+
const rpcMessage = rpcDecoder.decodeMessage(record);
86+
if (rpcMessage) {
87+
console.log('\nRPC Message:');
88+
console.log(rpcMessage);
89+
if (rpcMessage instanceof RpcCallMessage) {
90+
const proc = rpcMessage.proc;
91+
console.log(`\nNFS Procedure: ${getProcName(proc)}`);
92+
if (rpcMessage.params) {
93+
if (proc === 1) {
94+
const compound = nfsDecoder.decodeCompound(rpcMessage.params, true);
95+
if (compound && 'argarray' in compound) {
96+
console.log('\nNFS COMPOUND Request:');
97+
console.log(` Tag: "${compound.tag}"`);
98+
console.log(` Minor Version: ${compound.minorversion}`);
99+
console.log(` Operations (${compound.argarray.length}):`);
100+
compound.argarray.forEach((op: any, idx: number) => {
101+
console.log(` [${idx}] ${getOpName(op)}`);
102+
console.log(` ${JSON.stringify(op, null, 2).split('\n').slice(1).join('\n ')}`);
103+
});
104+
} else {
105+
console.log('Could not decode COMPOUND request');
106+
}
107+
} else if (proc === 0) {
108+
console.log('NULL procedure (no parameters)');
109+
} else {
110+
console.log(`Unknown procedure: ${proc}`);
111+
}
112+
}
113+
}
114+
} else {
115+
console.log('Could not decode RPC message');
116+
}
117+
record = rmDecoder.readRecord();
118+
}
119+
console.log('='.repeat(80) + '\n');
120+
});
121+
socket.on('end', () => {
122+
console.log(`[${new Date().toISOString()}] Client disconnected`);
123+
});
124+
socket.on('error', (err) => {
125+
console.error(`[${new Date().toISOString()}] Socket error:`, err.message);
126+
});
127+
});
128+
129+
server.on('error', (err) => {
130+
console.error('Server error:', err.message);
131+
process.exit(1);
132+
});
133+
134+
server.listen(PORT, HOST, () => {
135+
console.log(`NFSv4 TCP Server listening on ${HOST}:${PORT}`);
136+
console.log('Waiting for connections...\n');
137+
});
138+
139+
process.on('SIGINT', () => {
140+
console.log('\nShutting down server...');
141+
server.close(() => {
142+
console.log('Server closed');
143+
process.exit(0);
144+
});
145+
});

src/nfs/v4/__demos__/test-demo.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
# NFSv4 TCP Demo Test Script
4+
# This script starts the server in background, runs the client, then stops the server
5+
6+
PORT=8585
7+
8+
echo "Starting NFSv4 TCP Server on port $PORT..."
9+
PORT=$PORT npx ts-node src/nfs/v4/__demos__/tcp-server.ts &
10+
SERVER_PID=$!
11+
12+
# Wait for server to start
13+
sleep 2
14+
15+
echo ""
16+
echo "Running NFSv4 TCP Client..."
17+
PORT=$PORT npx ts-node src/nfs/v4/__demos__/tcp-client.ts
18+
19+
# Give time to see output
20+
sleep 1
21+
22+
echo ""
23+
echo "Stopping server..."
24+
kill $SERVER_PID 2>/dev/null
25+
wait $SERVER_PID 2>/dev/null
26+
27+
echo "Done!"

0 commit comments

Comments
 (0)