Core 08 net
Core 08 - Net Module (TCP)¶
Basic Idea¶
Before HTTP , before WebSockets , before all application protocols - there's TCP net module gives you raw TCP sockets Everything from HTTP servers to Redis clients to database drivers is built on this
TCP Server with net.createServer()¶
const net = require('net')
const server = net.createServer((socket) => {
console.log('client connected from:', socket.remoteAddress)
socket.on('data', (data) => {
console.log('received:', data.toString())
socket.write('echo: ' + data.toString())
})
socket.on('end', () => {
console.log('client disconnected')
})
socket.on('error', (err) => {
console.error('socket error:', err.message)
})
})
server.listen(9000, () => {
console.log('TCP server listening on port 9000')
})
net.createServer() creates a raw TCP server Every connection gets a new Socket object No HTTP parsing , no framing - just raw bytes going in and out
TCP Client with net.connect()¶
const net = require('net')
const client = net.connect(9000, 'localhost', () => {
console.log('connected to server')
client.write('hello from client')
})
client.on('data', (data) => {
console.log('server says:', data.toString())
client.end()
})
client.on('end', () => {
console.log('disconnected from server')
})
client.on('error', (err) => {
console.error('connection failed:', err.message)
})
net.connect(port, host) creates an outgoing TCP connection Same socket API as the server side - data , end , error events The connection is established before the callback fires
Socket - The TCP Interface¶
const net = require('net')
const server = net.createServer()
server.on('connection', (socket) => {
// socket properties
console.log('remote:', socket.remoteAddress, ':', socket.remotePort)
console.log('local:', socket.localAddress, ':', socket.localPort)
// write data
socket.write('welcome\n')
// set encoding - turns Buffer into string automatically
socket.setEncoding('utf-8')
// timeout
socket.setTimeout(30000)
socket.on('timeout', () => {
console.log('socket timed out')
socket.end()
})
// keep connection alive
socket.setKeepAlive(true, 60000)
})
server.listen(9000)
Socket has address info , timeout controls , keep-alive setTimeout() for idle connection cleanup - critical for production setKeepAlive() sends TCP keep-alive probes to detect dead peers
Handling Partial Data¶
// TCP is a stream protocol - no message boundaries
// If you send "HELLO" and "WORLD" , you might receive "HELLOWORLD" or "HEL" then "LOWORLD"
const net = require('net')
// solution: frame your protocol with message length prefixes
const server = net.createServer((socket) => {
let buffer = Buffer.alloc(0)
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk])
// check if we have a complete message
while (buffer.length >= 4) {
const msgLen = buffer.readUInt32BE(0) // first 4 bytes = length
if (buffer.length < 4 + msgLen) break // not enough data yet
const message = buffer.slice(4, 4 + msgLen)
console.log('complete message:', message.toString())
buffer = buffer.slice(4 + msgLen) // remove processed data
}
})
})
server.listen(9000)
// client sends length-prefixed messages
const client = net.connect(9000, () => {
const msg = Buffer.from('ping')
const header = Buffer.alloc(4)
header.writeUInt32BE(msg.length)
client.write(Buffer.concat([header, msg]))
})
TCP doesn't preserve message boundaries - it's a stream If you send two messages , you might receive them as one chunk or twenty chunks Always implement message framing: length prefix , delimiter , or fixed-size packets
Real Example: Simple Echo Server and Chat¶
const net = require('net')
// echo server
const echoServer = net.createServer((socket) => {
socket.write('echo server ready (type quit to exit)\n')
socket.pipe(socket) // echo everything back (dangerous without framing)
})
echoServer.listen(9001)
// multi-client chat server
const clients = new Set()
const chatServer = net.createServer((socket) => {
clients.add(socket)
broadcast(`${socket.remoteAddress} joined\n`)
socket.on('data', (data) => {
const msg = data.toString().trim()
if (msg === '/quit') {
socket.end()
return
}
broadcast(`${socket.remoteAddress}: ${msg}\n`)
})
socket.on('close', () => {
clients.delete(socket)
broadcast(`${socket.remoteAddress} left\n`)
})
socket.on('error', () => {
clients.delete(socket)
})
})
function broadcast(message) {
for (const client of clients) {
client.write(message)
}
}
chatServer.listen(9002)
console.log('chat server on port 9002')
socket.pipe(socket) is the simplest echo server - reads from socket , writes back Never use pipe() without size limits in production - socket bombs are a thing Chat server broadcasts to all connected clients - shows the pattern for pub/sub over TCP
Security: Unencrypted TCP¶
// ALL DATA IS CLEARTEXT
const client = net.connect(3306, 'db.internal', () => {
// sending password as plaintext over TCP
client.write('PASSWORD hunter2\n')
// anyone on the network can read this with tcpdump
})
// MITM attack on raw TCP is trivial
// $ tcpdump -i eth0 port 9000 -X
// 0x0000: 50 41 53 53 57 4F 52 44 20 68 75 6E 74 65 72 32 PASSWORD hunter2
//
// No encryption = no security. Anyone with access to the network reads everything.
Raw TCP has zero security - no encryption , no authentication , no integrity checks Anyone with tcpdump on the network path reads all data in cleartext Always use TLS (via tls module) for anything crossing network boundaries Even on localhost , internal network segmentation isn't guaranteed in cloud environments
Security: Connection Flood DoS¶
const net = require('net')
// VULNERABLE - no connection limits
const server = net.createServer((socket) => {
// attacker opens 10000 connections - server runs out of file descriptors
})
// DEFENSE - track and limit connections
const MAX_CONNECTIONS = 1000
let connections = 0
const server = net.createServer((socket) => {
if (connections >= MAX_CONNECTIONS) {
socket.write('server busy\n')
socket.destroy()
return
}
connections++
socket.write('welcome\n')
socket.on('close', () => {
connections--
})
socket.on('error', () => {
connections--
})
})
server.listen(9000)
TCP servers are vulnerable to connection flood DoS Each connection consumes a file descriptor - default ulimit is often 1024 Also vulnerable to slow loris: open connections that never complete , keeping sockets alive Always set connection limits , timeouts , and rate limits on TCP servers
Summary¶
net.createServer()for TCP servers ,net.connect()for clients- TCP has no message boundaries - implement framing (length prefix , delimiter)
socket.pipe()is convenient but dangerous without size limits- Raw TCP has zero security - always encrypt with
tlsmodule - Connection limits prevent fd exhaustion DoS
Prerequisites¶
next -> core_09_dns.md