Skip to content

Commit 1a566c4

Browse files
committed
unix socket (file) support
1 parent ae211b4 commit 1a566c4

7 files changed

Lines changed: 100 additions & 25 deletions

File tree

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# Ser2tcp
22

3-
Simple proxy for connecting over TCP, TELNET or SSL to serial port
3+
Simple proxy for connecting over TCP, TELNET, SSL or Unix socket to serial port
44

55
https://github.com/cortexm/ser2tcp
66

77
## Features
88

99
- can serve multiple serial ports using pyserial library
1010
- each serial port can have multiple servers
11-
- server can use TCP, TELNET or SSL protocol
11+
- server can use TCP, TELNET, SSL or SOCKET protocol
1212
- TCP protocol just bridge whole RAW serial stream to TCP
1313
- TELNET protocol will send every character immediately and not wait for ENTER, it is useful to use standard `telnet` as serial terminal
1414
- SSL protocol provides encrypted TCP connection with optional mutual TLS (mTLS) client certificate verification
15+
- SOCKET protocol uses Unix domain socket for local IPC
1516
- servers accepts multiple connections at one time
1617
- each connected client can sent to serial port
1718
- serial port send received data to all connected clients
@@ -131,13 +132,29 @@ Match attributes: `vid`, `pid`, `serial_number`, `manufacturer`, `product`, `loc
131132

132133
| Parameter | Description | Default |
133134
|-----------|-------------|---------|
134-
| `address` | Bind address | required |
135-
| `port` | TCP port | required |
136-
| `protocol` | `tcp`, `telnet` or `ssl` | required |
135+
| `address` | Bind address (IP for tcp/telnet/ssl, path for socket) | required |
136+
| `port` | TCP port (not used for socket) | required |
137+
| `protocol` | `tcp`, `telnet`, `ssl` or `socket` | required |
137138
| `ssl` | SSL configuration (required for `ssl` protocol) | - |
138139
| `send_timeout` | Disconnect client if data cannot be sent within this time (seconds) | 5.0 |
139140
| `buffer_limit` | Maximum send buffer size per client (bytes), `null` for unlimited | null |
140141

142+
#### Socket configuration
143+
144+
For `socket` protocol, `address` is the path to the Unix domain socket:
145+
146+
```json
147+
{
148+
"address": "/tmp/ser2tcp.sock",
149+
"protocol": "socket"
150+
}
151+
```
152+
153+
- Socket file is created on startup and removed on shutdown
154+
- If socket file already exists, it is replaced
155+
- Connect with: `socat - UNIX-CONNECT:/tmp/ser2tcp.sock`
156+
- Not available on Windows
157+
141158
#### SSL configuration
142159

143160
For `ssl` protocol, add `ssl` object with certificate paths:

ser2tcp/connection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def close(self):
3838
if self._socket:
3939
self._socket.close()
4040
self._socket = None
41-
self._log.info("Client disconnected: %s:%d", *self._addr)
41+
self._log.info("Client disconnected: %s", self.address_str())
4242

4343
def fileno(self):
4444
"""emulate fileno method of socket"""
@@ -48,6 +48,10 @@ def get_address(self):
4848
"""Return address"""
4949
return self._addr
5050

51+
def address_str(self):
52+
"""Return formatted address string"""
53+
return "%s:%d" % self._addr
54+
5155
def send(self, data):
5256
"""Add data to output buffer, return number of bytes added"""
5357
if not self._socket:

ser2tcp/connection_socket.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Connection Socket (Unix domain socket)"""
2+
3+
import ser2tcp.connection as _connection
4+
5+
6+
class ConnectionSocket(_connection.Connection):
7+
"""Unix domain socket connection"""
8+
9+
def __init__(
10+
self, connection, ser, send_timeout=None, buffer_limit=None,
11+
log=None):
12+
super().__init__(connection, send_timeout, buffer_limit, log)
13+
self._serial = ser
14+
self._log.info("Client connected: %s SOCKET", self._addr[0])
15+
16+
def address_str(self):
17+
"""Return formatted address string"""
18+
return self._addr[0]
19+
20+
def close(self):
21+
"""Close connection"""
22+
if self._socket:
23+
addr = self._addr[0]
24+
self._socket.close()
25+
self._socket = None
26+
self._log.info("Client disconnected: %s", addr)
27+
28+
def on_received(self, data):
29+
"""Received data from client"""
30+
if data:
31+
self._serial.send(data)

ser2tcp/connection_ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ def __init__(
2626
(ssl_sock, addr), ser, send_timeout, buffer_limit, log)
2727

2828
def _log_connected(self):
29-
self._log.info("Client connected: %s:%d SSL", *self._addr)
29+
self._log.info("Client connected: %s SSL", self.address_str())

ser2tcp/connection_tcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def __init__(
1414
self._log_connected()
1515

1616
def _log_connected(self):
17-
self._log.info("Client connected: %s:%d TCP", *self._addr)
17+
self._log.info("Client connected: %s TCP", self.address_str())
1818

1919
def on_received(self, data):
2020
"""Received data from client"""

ser2tcp/server.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
# pylint: disable=C0209
44

55
import logging as _logging
6+
import os as _os
67
import socket as _socket
78
import ssl as _ssl
89

10+
import ser2tcp.connection_socket as _connection_socket
911
import ser2tcp.connection_ssl as _connection_ssl
1012
import ser2tcp.connection_tcp as _connection_tcp
1113
import ser2tcp.connection_telnet as _connection_telnet
@@ -22,6 +24,7 @@ class Server():
2224
'TCP': _connection_tcp.ConnectionTcp,
2325
'TELNET': _connection_telnet.ConnectionTelnet,
2426
'SSL': _connection_ssl.ConnectionSsl,
27+
'SOCKET': _connection_socket.ConnectionSocket,
2528
}
2629

2730
def __init__(self, config, ser, log=None):
@@ -34,19 +37,32 @@ def __init__(self, config, ser, log=None):
3437
self._buffer_limit = self._config.get('buffer_limit')
3538
self._ssl_context = None
3639
self._socket = None
37-
self._log.info(
38-
" Server: %s %d %s",
39-
self._config['address'],
40-
self._config['port'],
41-
self._protocol)
4240
if self._protocol not in self.CONNECTIONS:
4341
raise ConfigError('Unknown protocol %s' % self._protocol)
44-
if self._protocol == 'SSL':
45-
self._ssl_context = self._create_ssl_context()
46-
self._socket = _socket.socket(
47-
_socket.AF_INET, _socket.SOCK_STREAM, _socket.IPPROTO_TCP)
48-
self._socket.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
49-
self._socket.bind((config['address'], config['port']))
42+
if self._protocol == 'SOCKET':
43+
self._log.info(
44+
" Server: %s %s",
45+
self._config['address'],
46+
self._protocol)
47+
self._socket = _socket.socket(
48+
_socket.AF_UNIX, _socket.SOCK_STREAM)
49+
sock_path = config['address']
50+
if _os.path.exists(sock_path):
51+
_os.unlink(sock_path)
52+
self._socket.bind(sock_path)
53+
else:
54+
self._log.info(
55+
" Server: %s %d %s",
56+
self._config['address'],
57+
self._config['port'],
58+
self._protocol)
59+
if self._protocol == 'SSL':
60+
self._ssl_context = self._create_ssl_context()
61+
self._socket = _socket.socket(
62+
_socket.AF_INET, _socket.SOCK_STREAM, _socket.IPPROTO_TCP)
63+
self._socket.setsockopt(
64+
_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
65+
self._socket.bind((config['address'], config['port']))
5066
self._socket.listen(1)
5167

5268
def __del__(self):
@@ -70,6 +86,8 @@ def _create_ssl_context(self):
7086
def _client_connect(self):
7187
"""connect to client, will accept waiting connection"""
7288
sock, addr = self._socket.accept()
89+
if self._protocol == 'SOCKET':
90+
addr = (self._config['address'],)
7391
kwargs = {
7492
'connection': (sock, addr),
7593
'ser': self._serial,
@@ -82,7 +100,8 @@ def _client_connect(self):
82100
try:
83101
connection = self.CONNECTIONS[self._protocol](**kwargs)
84102
except _connection_ssl.SslHandshakeError as err:
85-
self._log.info("Client rejected: %s:%d (%s)", *addr, err)
103+
self._log.info(
104+
"Client rejected: %s:%d (%s)", addr[0], addr[1], err)
86105
if not self._connections:
87106
self._serial.disconnect()
88107
return
@@ -102,6 +121,10 @@ def close(self):
102121
self.close_connections()
103122
self._socket.close()
104123
self._socket = None
124+
if self._protocol == 'SOCKET':
125+
sock_path = self._config['address']
126+
if _os.path.exists(sock_path):
127+
_os.unlink(sock_path)
105128

106129
def has_connections(self):
107130
"""True if server has some connections"""
@@ -138,9 +161,9 @@ def process_read(self, read_sockets):
138161
data = b''
139162
try:
140163
data = con.socket().recv(4096)
141-
self._log.debug("(%s:%d): %s", *con.get_address(), data)
164+
self._log.debug("(%s): %s", con.address_str(), data)
142165
except (ConnectionResetError, _ssl.SSLError) as err:
143-
self._log.info("(%s:%d): %s", *con.get_address(), err)
166+
self._log.info("(%s): %s", con.address_str(), err)
144167
if not data:
145168
self._remove_connection(con)
146169
continue
@@ -153,15 +176,15 @@ def process_write(self, write_sockets):
153176
result = con.flush()
154177
if result is None:
155178
self._log.info(
156-
"(%s:%d): write error", *con.get_address())
179+
"(%s): write error", con.address_str())
157180
self._remove_connection(con)
158181

159182
def process_stale(self):
160183
"""Remove stale connections (send timeout expired)"""
161184
for con in list(self._connections):
162185
if con.is_stale():
163186
self._log.info(
164-
"(%s:%d): send timeout", *con.get_address())
187+
"(%s): send timeout", con.address_str())
165188
self._remove_connection(con)
166189

167190
def send(self, data):

tests/test_connection_ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_log_connected_shows_ssl(self):
6767
self.assertEqual(
6868
first_call,
6969
unittest.mock.call(
70-
"Client connected: %s:%d SSL", '127.0.0.1', 12345))
70+
"Client connected: %s SSL", '127.0.0.1:12345'))
7171
conn.close()
7272

7373
def test_on_received_forwards_to_serial(self):

0 commit comments

Comments
 (0)