Simple proxy for connecting over TCP, TELNET, SSL, WebSocket or Unix socket to serial port
https://github.com/cortexm/ser2tcp
- can serve multiple serial ports using pyserial library
- each serial port can have multiple servers
- server can use TCP, TELNET, SSL, WebSocket or SOCKET protocol
- TCP protocol just bridge whole RAW serial stream to TCP
- TELNET protocol will send every character immediately and not wait for ENTER, it is useful to use standard
telnetas serial terminal - SSL protocol provides encrypted TCP connection with optional mutual TLS (mTLS) client certificate verification
- WebSocket protocol connects through the HTTP server with binary frames for data and JSON text frames for signal control
- SOCKET protocol uses Unix domain socket for local IPC
- servers accepts multiple connections at one time
- each connected client can sent to serial port
- serial port send received data to all connected clients
- non-blocking send with configurable timeout and buffer limit
- serial signal control (RTS, DTR, CTS, DSR, RI, CD) via escape protocol or WebSocket JSON
- IP filtering with allow/deny lists (CIDR notation supported)
- built-in HTTP server with REST API for status monitoring
- web interface for viewing configured ports and connections
- web terminal clients (xterm.js VT100 terminal and raw colored view)
- authentication with session management and API tokens
- light/dark mode web UI (follows system preference)
pip install ser2tcp
or from source:
pip install .
pip uninstall ser2tcp
-h, --help show this help message and exit
-V, --version show program's version number and exit
-v, --verbose Increase verbosity
-u, --usb List USB serial devices and exit
--hash-password PASSWORD
Hash password for config file and exit
-c CONFIG, --config CONFIG
configuration in JSON format (default: ~/.config/ser2tcp/config.json)
If no config file is specified and default config doesn't exist, creates one with HTTP server on first free port from 20080.
- By default print only ERROR and WARNING messages
-v: will print INFO messages-vv: print also DEBUG messages
{
"ports": [
{
"serial": {
"port": "/dev/ttyUSB0",
"baudrate": 115200,
"parity": "NONE",
"stopbits": "ONE"
},
"servers": [
{
"address": "127.0.0.1",
"port": 10001,
"protocol": "tcp"
},
{
"address": "0.0.0.0",
"port": 10002,
"protocol": "telnet",
"send_timeout": 5.0,
"buffer_limit": 65536
}
]
}
]
}Legacy format (JSON array at root level) is still supported for backward compatibility.
serial structure pass all parameters to serial.Serial constructor from pyserial library, this allows full control of the serial port.
Instead of specifying port directly, you can use match to find device by USB attributes:
{
"serial": {
"match": {
"vid": "0x303A",
"pid": "0x4001",
"serial_number": "dcda0c2004bc0000"
},
"baudrate": 115200
}
}Use ser2tcp --usb to list available USB devices with their attributes:
$ ser2tcp --usb
/dev/cu.usbmodem1101
vid: 0x303A
pid: 0x4001
serial_number: dcda0c2004bc0000
manufacturer: Espressif Systems
product: Espressif Device
location: 1-1
Match attributes: vid, pid, serial_number, manufacturer, product, location, description, hwid
- Wildcard
*supported (e.g."product": "CP210*") - Matching is case-insensitive
- Error if multiple devices match the criteria
- Device is resolved when client connects, not at startup (device does not need to exist at startup)
baudrateis optional (default 9600, CDC devices ignore it)
| Parameter | Description | Default |
|---|---|---|
address |
Bind address (IP for tcp/telnet/ssl, path for socket) | required* |
port |
TCP port (not used for socket/websocket) | required* |
protocol |
tcp, telnet, ssl, websocket or socket |
required |
endpoint |
WebSocket URL path (websocket only), must be unique | required* |
token |
Per-server auth token (websocket only) | - |
ssl |
SSL configuration (required for ssl protocol) |
- |
data |
Forward serial data (default true), false = control-only |
true |
control |
Signal control configuration | - |
send_timeout |
Disconnect client if data cannot be sent within this time (seconds) | 5.0 |
buffer_limit |
Maximum send buffer size per client (bytes), null for unlimited |
null |
max_connections |
Maximum clients per server (0 = unlimited) | 0 |
* address/port required for tcp/telnet/ssl; address for socket; endpoint for websocket
You can also limit total connections across all servers on a port:
{
"ports": [{
"max_connections": 10,
"serial": {"port": "/dev/ttyUSB0"},
"servers": [
{"protocol": "tcp", "address": "0.0.0.0", "port": 10001, "max_connections": 5},
{"protocol": "websocket", "endpoint": "device"}
]
}]
}- Port-level
max_connections: limits total clients across all servers (default 0 = unlimited) - Server-level
max_connections: limits clients on that specific server (default 0 = unlimited) - Both limits are checked — if either is reached, new connections are rejected
WebSocket connections go through the HTTP server — no separate listening port needed:
{
"protocol": "websocket",
"endpoint": "my-device",
"control": {
"rts": true,
"signals": ["rts", "dtr", "cts", "dsr"]
}
}- Accessible at
ws://host:port/ws/my-device(orwss://for HTTPS) - Available on all configured HTTP servers
- Binary frames carry raw serial data (bidirectional)
- Text frames carry JSON control messages:
{"rts": true},{"signals": {...}} - Signal state sent automatically on connect, then only on change
- Auth: per-server
token, global user session, or both accepted - Web terminals available at
/xterm/<endpoint>(VT100) and/raw/<endpoint>(colored hex)
For socket protocol, address is the path to the Unix domain socket:
{
"address": "/tmp/ser2tcp.sock",
"protocol": "socket"
}- Socket file is created on startup and removed on shutdown
- If socket file already exists, it is replaced
- Connect with:
socat - UNIX-CONNECT:/tmp/ser2tcp.sock - Not available on Windows
For ssl protocol, add ssl object with certificate paths:
{
"address": "0.0.0.0",
"port": 10003,
"protocol": "ssl",
"ssl": {
"certfile": "/path/to/server.crt",
"keyfile": "/path/to/server.key",
"ca_certs": "/path/to/ca.crt"
}
}| Parameter | Description | Required |
|---|---|---|
certfile |
Server certificate (PEM) | yes |
keyfile |
Server private key (PEM) | yes |
ca_certs |
CA certificate for client verification (mTLS) | no |
If ca_certs is specified, clients must provide a valid certificate signed by the CA.
Restrict client connections by IP address using allow and/or deny lists:
{
"address": "0.0.0.0",
"port": 10001,
"protocol": "tcp",
"allow": ["192.168.1.0/24", "10.0.0.5"],
"deny": ["192.168.1.100"]
}| Parameter | Description |
|---|---|
allow |
List of allowed IP addresses/networks (CIDR notation supported) |
deny |
List of denied IP addresses/networks (CIDR notation supported) |
Filter logic:
- No config: all IPs allowed
- Only
deny: all IPs allowed except those in deny list - Only
allow: only IPs in allow list are allowed - Both: deny takes precedence, then allow list is checked
Works on TCP, TELNET, SSL, WebSocket and HTTP servers. Not applicable to Unix socket (no IP addresses). Rejected connections are logged.
Generate CA and server certificate for testing:
# Create CA key and certificate
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=ser2tcp CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
# Create server key and certificate signing request
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
# Sign server certificate with CA
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
# For certificate bound to specific domain/IP (SAN - Subject Alternative Name):
openssl req -new -key server.key -out server.csr -subj "/CN=myserver.example.com" -addext "subjectAltName=DNS:myserver.example.com,DNS:localhost,IP:192.168.1.100"
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -copy_extensions copy
# Clean up CSR
rm server.csrFor mTLS (mutual TLS with client certificates):
# Create client key and certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=client"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
rm client.csrTesting SSL connection:
# Without client certificate
openssl s_client -connect localhost:10003
# With client certificate (mTLS)
openssl s_client -connect localhost:10003 -cert client.crt -key client.keyOptional HTTP server for monitoring and management:
{
"http": [
{"name": "main", "address": "0.0.0.0", "port": 8080}
]
}name: optional label for the server (displayed in web UI Settings tab)- HTTP servers can be added/removed/modified via web UI without restart
With authentication (configured at root level, shared across all HTTP servers):
{
"http": [
{"address": "0.0.0.0", "port": 8080}
],
"users": [
{"login": "admin", "password": "sha256:...", "admin": true}
],
"tokens": [
{"token": "my-api-key", "name": "monitoring", "admin": false}
],
"session_timeout": 3600
}users: login credentials with optionaladminflag and per-usersession_timeouttokens: permanent API tokens for automation (no expiration)session_timeout: global default session timeout in seconds- First user added (via CLI or web UI) is automatically admin
- Cannot delete last admin (user or token) — at least one admin must exist
Generate password hash:
ser2tcp --hash-password mysecretpasswordHTTPS with SSL:
{
"http": [
{"address": "0.0.0.0", "port": 8080},
{"address": "0.0.0.0", "port": 8443, "ssl": {
"certfile": "server.crt", "keyfile": "server.key"
}}
]
}With IP filtering:
{
"http": [{
"address": "0.0.0.0",
"port": 8080,
"allow": ["192.168.0.0/16"],
"deny": ["192.168.1.100"]
}]
}| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/login |
no | Authenticate, returns session token |
| POST | /api/logout |
no | Invalidate session |
| GET | /api/status |
yes | Runtime status (serial ports, servers, connections) |
| GET | /api/detect |
yes | Available serial ports with USB/device attributes |
| GET | /api/signals |
yes | Signal states for all ports |
| GET | /api/settings |
yes | Get settings (http servers, session_timeout) |
| DELETE | /api/ports/<p>/connections/<s>/<c> |
yes | Disconnect client |
| POST | /api/ports |
admin | Add new port configuration |
| PUT | /api/ports/<index> |
admin | Update port configuration |
| DELETE | /api/ports/<index> |
admin | Delete port configuration |
| PUT | /api/ports/<index>/signals |
admin | Set RTS/DTR signals |
| GET | /api/users |
admin | List users |
| POST | /api/users |
admin | Add user |
| PUT | /api/users/<login> |
admin | Update user |
| DELETE | /api/users/<login> |
admin | Delete user |
| GET | /api/tokens |
admin | List API tokens |
| POST | /api/tokens |
admin | Add API token |
| PUT | /api/tokens/<token> |
admin | Update API token |
| DELETE | /api/tokens/<token> |
admin | Delete API token |
| PUT | /api/settings |
admin | Update session_timeout |
| POST | /api/settings/http |
admin | Add HTTP server |
| PUT | /api/settings/http/<index> |
admin | Update HTTP server |
| DELETE | /api/settings/http/<index> |
admin | Delete HTTP server |
| GET | /xterm/<endpoint> |
no | WebSocket VT100 terminal |
| GET | /raw/<endpoint> |
no | WebSocket raw terminal |
Auth levels: no = public, yes = any authenticated user, admin = admin user/token only.
Authentication: Authorization: Bearer <token> header or ?token=<token> query parameter. Without users/tokens configured, all endpoints are accessible without authentication.
ser2tcp -c ser2tcp.conf
Direct running from repository:
python run.py -c ser2tcp.conf
telnet localhost 10002
(to exit telnet press CTRL + ] and type quit)
- Copy service file:
cp ser2tcp.service ~/.config/systemd/user/ - Configuration file will be created automatically at
~/.config/ser2tcp/config.jsonon first run - Reload user systemd services:
systemctl --user daemon-reload - Start and enable service:
systemctl --user enable --now ser2tcp - To allow user services running after boot you need to enable linger (if this is not configured, then service will start after user login and stop after logout):
sudo loginctl enable-linger $USER
- Create system user:
sudo useradd -r -s /usr/sbin/nologin -G dialout ser2tcp - Copy service file:
sudo cp ser2tcp-system.service /etc/systemd/system/ser2tcp.service - Create configuration file
/etc/ser2tcp.conf - Reload systemd and start service:
sudo systemctl daemon-reload sudo systemctl enable --now ser2tcp
# Check status
systemctl --user status ser2tcp
# View logs
journalctl --user-unit ser2tcp -e
# Restart
systemctl --user restart ser2tcp
# Stop
systemctl --user stop ser2tcpFor system service, use sudo systemctl instead of systemctl --user.
- Python 3.8+
- pyserial 3.0+
- uhttp-server 2.3.2+ (for HTTP/API and WebSocket)
- Linux
- macOS
- Windows
(c) 2016-2026 by Pavel Revak
- Basic support is free over GitHub issues.
- Professional support is available over email: Pavel Revak.