distributed-state-network 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/PKG-INFO +1 -1
  2. distributed_state_network-0.3.0/documentation/ds-node-server.md +192 -0
  3. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/documentation/protocol.md +1 -0
  4. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/pyproject.toml +1 -1
  5. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/dsnode.py +6 -5
  6. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/handler.py +13 -11
  7. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/tests/connections.py +4 -12
  8. distributed_state_network-0.2.0/documentation/ds-node-server.md +0 -58
  9. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/.github/workflows/publish.yml +0 -0
  10. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/.gitignore +0 -0
  11. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/LICENSE +0 -0
  12. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/README.md +0 -0
  13. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/coverage.sh +0 -0
  14. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/documentation/ds-node-config.md +0 -0
  15. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/documentation/ds-node.md +0 -0
  16. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/documentation/usage.md +0 -0
  17. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/__init__.py +0 -0
  18. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/create_key.py +0 -0
  19. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/config.py +0 -0
  20. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/endpoint.py +0 -0
  21. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/hello_packet.py +0 -0
  22. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/msg_types.py +0 -0
  23. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/peers_packet.py +0 -0
  24. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/signed_packet.py +0 -0
  25. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/objects/state_packet.py +0 -0
  26. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/__init__.py +0 -0
  27. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/aes.py +0 -0
  28. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/byte_helper.py +0 -0
  29. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/ecdsa.py +0 -0
  30. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/https.py +0 -0
  31. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/src/distributed_state_network/util/key_manager.py +0 -0
  32. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/technical.md +0 -0
  33. {distributed_state_network-0.2.0 → distributed_state_network-0.3.0}/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed-state-network
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A tool to distribute the state of a network device to other devices on the network
5
5
  Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
6
6
  Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
@@ -0,0 +1,192 @@
1
+ ## DSNodeServer
2
+
3
+ UDP server wrapper for DSNode that handles incoming network requests.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNodeServer:
12
+ config: DSNodeConfig
13
+ node: DSNode
14
+ socket: socket.socket
15
+ running: bool
16
+ thread: threading.Thread
17
+ ```
18
+
19
+ ### Constructor
20
+
21
+ **Parameters:**
22
+ - `config` (`DSNodeConfig`): Node configuration
23
+ - `sock` (`Optional[socket.socket]`): Custom UDP socket to use (optional, creates new socket if not provided)
24
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
25
+ - `update_callback` (`Optional[Callable]`): Callback for state update events
26
+
27
+ **Example:**
28
+ ```python
29
+ # Create with default socket
30
+ server = DSNodeServer(config)
31
+
32
+ # Or with custom socket
33
+ custom_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
34
+ custom_socket.bind(("0.0.0.0", 8000))
35
+ server = DSNodeServer(config, sock=custom_socket)
36
+ ```
37
+
38
+ ### Static Methods
39
+
40
+ ### `start(config: DSNodeConfig, sock: Optional[socket.socket] = None, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> DSNodeServer`
41
+ Creates and starts a new DSNodeServer instance with UDP socket.
42
+
43
+ ```python
44
+ server = DSNodeServer.start(config)
45
+ ```
46
+
47
+ **Parameters:**
48
+ - `config` (`DSNodeConfig`): Node configuration
49
+ - `sock` (`Optional[socket.socket]`): Custom UDP socket to use (optional)
50
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events (no parameters)
51
+ - `update_callback` (`Optional[Callable]`): Callback for state update events (no parameters)
52
+
53
+ **Returns:**
54
+ - `DSNodeServer`: Running server instance
55
+
56
+ **Example with bootstrap:**
57
+ ```python
58
+ # Bootstrap node (first node in network)
59
+ bootstrap_config = DSNodeConfig(
60
+ node_id="bootstrap",
61
+ port=8000,
62
+ aes_key_file="network.key",
63
+ bootstrap_nodes=[]
64
+ )
65
+ bootstrap = DSNodeServer.start(bootstrap_config)
66
+
67
+ # Connector node (joins existing network)
68
+ connector_config = DSNodeConfig(
69
+ node_id="connector",
70
+ port=8001,
71
+ aes_key_file="network.key",
72
+ bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
73
+ )
74
+ connector = DSNodeServer.start(connector_config)
75
+ ```
76
+
77
+ ### `generate_key(out_file_path: str) -> None`
78
+ Generates a new AES key file for network encryption. All nodes in the same network must share the same AES key.
79
+
80
+ **Parameters:**
81
+ - `out_file_path` (`str`): Path where the key file will be saved
82
+
83
+ **Example:**
84
+ ```python
85
+ DSNodeServer.generate_key("/path/to/network.key")
86
+ ```
87
+
88
+ ### Instance Methods
89
+
90
+ ### `stop() -> None`
91
+ Gracefully shuts down the server and cleans up resources.
92
+
93
+ **Example:**
94
+ ```python
95
+ server.stop()
96
+ ```
97
+
98
+ ### `serve_forever() -> None`
99
+ Main UDP server loop. Continuously listens for incoming packets and routes them to appropriate handlers. This method is typically called automatically by `start()` in a daemon thread.
100
+
101
+ **Note:** This method blocks until the server is stopped.
102
+
103
+ ## Network Protocol
104
+
105
+ The server uses UDP sockets with the following characteristics:
106
+
107
+ - **Port**: Configurable via DSNodeConfig
108
+ - **Encryption**: All packets encrypted with AES
109
+ - **Authentication**: ECDSA signatures for message verification
110
+ - **Max Packet Size**: 65507 bytes (UDP limit)
111
+ - **Timeout**: 2 seconds with automatic retry (up to 3 attempts)
112
+
113
+ ## Message Types
114
+
115
+ The server handles four types of messages:
116
+
117
+ 1. **HELLO (1)**: Node introduction and public key exchange
118
+ 2. **PEERS (2)**: Request/response for peer list
119
+ 3. **UPDATE (3)**: State synchronization
120
+ 4. **PING (4)**: Connection health check
121
+
122
+ ## Automatic IP Detection
123
+
124
+ When a client connects to a bootstrap server:
125
+
126
+ 1. Server extracts client's IP from UDP packet source address
127
+ 2. Server includes detected IP in HELLO response via `detected_address` field
128
+ 3. Client updates its own address book with the detected IP
129
+ 4. Correct IP propagates to other nodes through peer discovery
130
+
131
+ This eliminates the need for manual IP configuration and works correctly behind NAT.
132
+
133
+ ## Thread Safety
134
+
135
+ The server spawns separate threads for:
136
+ - Main UDP receive loop (`serve_forever`)
137
+ - Packet handling (one thread per packet)
138
+ - Node network tick (periodic health checks)
139
+
140
+ All shared state is protected with appropriate locks.
141
+
142
+ ## Example: Multi-Node Setup
143
+
144
+ ```python
145
+ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
146
+
147
+ # Generate shared AES key (only once)
148
+ DSNodeServer.generate_key("shared.key")
149
+
150
+ # Start bootstrap node
151
+ bootstrap = DSNodeServer.start(DSNodeConfig(
152
+ node_id="bootstrap",
153
+ port=8000,
154
+ aes_key_file="shared.key",
155
+ bootstrap_nodes=[]
156
+ ))
157
+
158
+ # Start additional nodes
159
+ node1 = DSNodeServer.start(DSNodeConfig(
160
+ node_id="node1",
161
+ port=8001,
162
+ aes_key_file="shared.key",
163
+ bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
164
+ ))
165
+
166
+ node2 = DSNodeServer.start(DSNodeConfig(
167
+ node_id="node2",
168
+ port=8002,
169
+ aes_key_file="shared.key",
170
+ bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
171
+ ))
172
+
173
+ # Nodes automatically discover each other through bootstrap
174
+ ```
175
+
176
+ ## Troubleshooting
177
+
178
+ ### Server won't start
179
+ - Check if port is already in use
180
+ - Verify AES key file exists and is readable
181
+ - Ensure firewall allows UDP traffic on specified port
182
+
183
+ ### Nodes can't connect
184
+ - Verify all nodes use the same AES key
185
+ - Check bootstrap node is running and reachable
186
+ - Verify network connectivity (ping bootstrap address)
187
+ - Check firewall rules for UDP ports
188
+
189
+ ### Connection drops
190
+ - Increase timeout if on slow networks
191
+ - Check network stability
192
+ - Verify nodes aren't being stopped unexpectedly
@@ -19,6 +19,7 @@ All UDP packets use a message type byte prefix to identify the packet type:
19
19
  Each UDP packet follows this structure:
20
20
  1. **AES Encrypted Payload** containing:
21
21
  - Message type (1 byte)
22
+ - Response bit (1 byte): 0 for requests, 1 for responses
22
23
  - Message payload (variable length)
23
24
 
24
25
  ### Security
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "distributed-state-network"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
9
  description = "A tool to distribute the state of a network device to other devices on the network"
10
10
  keywords = ["distributed", "networking"]
@@ -108,8 +108,8 @@ class DSNode:
108
108
  raise Exception("Socket not set. Cannot send UDP request.")
109
109
 
110
110
  try:
111
- # Prepend message type to payload
112
- data_with_type = bytes([msg_type]) + payload
111
+ # Prepend message type and response bit (0 for request) to payload
112
+ data_with_type = bytes([msg_type, 0]) + payload
113
113
  encrypted_data = self.encrypt_data(data_with_type)
114
114
 
115
115
  # Create a unique ID for this request based on endpoint and msg type
@@ -159,12 +159,13 @@ class DSNode:
159
159
  # Decrypt the data
160
160
  decrypted = self.decrypt_data(data)
161
161
 
162
- if len(decrypted) == 0:
162
+ if len(decrypted) < 2:
163
163
  return
164
164
 
165
- # First byte is message type
165
+ # First byte is message type, second byte is response bit
166
166
  msg_type = decrypted[0]
167
- body = decrypted[1:]
167
+ is_response = decrypted[1]
168
+ body = decrypted[2:]
168
169
 
169
170
  # Find matching pending request
170
171
  request_id = (addr[0], addr[1], msg_type)
@@ -7,7 +7,7 @@ from distributed_state_network.objects.config import DSNodeConfig
7
7
  from distributed_state_network.util.aes import generate_aes_key
8
8
  from distributed_state_network.util import stop_thread
9
9
 
10
- VERSION = "0.2.0"
10
+ VERSION = "0.3.0"
11
11
  logging.basicConfig(level=logging.INFO)
12
12
 
13
13
  # Message type constants
@@ -53,19 +53,20 @@ class DSNodeServer:
53
53
  # Decrypt the data
54
54
  decrypted = self.node.decrypt_data(data)
55
55
 
56
- if len(decrypted) == 0:
56
+ if len(decrypted) < 2:
57
57
  return
58
58
 
59
- # First byte is message type
59
+ # First byte is message type, second byte is response bit
60
60
  msg_type = decrypted[0]
61
- body = decrypted[1:]
61
+ is_response = decrypted[1]
62
+ body = decrypted[2:]
62
63
 
63
- # Check if this is a response to a pending request
64
+ # Check if this is a response (either by bit flag or by pending request)
64
65
  request_id = (addr[0], addr[1], msg_type)
65
66
  with self.node.response_lock:
66
- is_response = request_id in self.node.pending_responses
67
+ has_pending_request = request_id in self.node.pending_responses
67
68
 
68
- if is_response:
69
+ if is_response == 1 or has_pending_request:
69
70
  # This is a response to our request - route to response handler
70
71
  self.node.handle_response(data, addr)
71
72
  else:
@@ -87,8 +88,8 @@ class DSNodeServer:
87
88
 
88
89
  # Send response if handler returned data
89
90
  if response is not None:
90
- # Prepend message type to response
91
- response_with_type = bytes([msg_type]) + response
91
+ # Prepend message type and response bit (1 for response) to response
92
+ response_with_type = bytes([msg_type, 1]) + response
92
93
  encrypted_response = self.node.encrypt_data(response_with_type)
93
94
  self.socket.sendto(encrypted_response, addr)
94
95
 
@@ -104,8 +105,9 @@ class DSNodeServer:
104
105
  try:
105
106
  # Receive UDP packet (max 65507 bytes for UDP)
106
107
  data, addr = self.socket.recvfrom(65507)
107
- # Handle packet in separate thread to avoid blocking
108
- threading.Thread(target=self.handle_packet, args=(data, addr), daemon=True).start()
108
+ if self.running:
109
+ # Handle packet in separate thread to avoid blocking
110
+ threading.Thread(target=self.handle_packet, args=(data, addr), daemon=True).start()
109
111
  except OSError:
110
112
  # Socket was closed
111
113
  if not self.running:
@@ -17,14 +17,12 @@ cb_test = 0
17
17
 
18
18
  current_port = 5000
19
19
 
20
- def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional[Callable] = None, update_cb: Optional[Callable] = None, https: bool = False):
20
+ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional[Callable] = None, update_cb: Optional[Callable] = None):
21
21
  global current_port
22
22
  args = {
23
23
  "node_id": node_id,
24
24
  "port": current_port,
25
- "aes_key_file": KEY_FILE,
26
- "https": https,
27
- "network_ip": "127.0.0.1"
25
+ "aes_key_file": KEY_FILE
28
26
  }
29
27
  current_port += 1
30
28
  if bootstrap_port is not None:
@@ -35,7 +33,7 @@ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional
35
33
  else:
36
34
  args["bootstrap_nodes"] = []
37
35
 
38
- return DSNodeServer.start(DSNodeConfig(**args), disconnect_cb, update_cb)
36
+ return DSNodeServer.start(DSNodeConfig(**args), None, disconnect_cb, update_cb)
39
37
 
40
38
  class ConnectionsTest(unittest.TestCase):
41
39
  def test_one(self):
@@ -48,12 +46,6 @@ class ConnectionsTest(unittest.TestCase):
48
46
  self.assertEqual(["node-1", "node-2"], sorted(node1.node.peers()))
49
47
  self.assertEqual(["node-1", "node-2"], sorted(node2.node.peers()))
50
48
 
51
- def test_https(self):
52
- node1 = start_node("node-1", https=True)
53
- node2 = start_node("node-2", node1.config.port, https=True)
54
- self.assertEqual(["node-1", "node-2"], sorted(node1.node.peers()))
55
- self.assertEqual(["node-1", "node-2"], sorted(node2.node.peers()))
56
-
57
49
  def test_many(self):
58
50
  bootstrap_node = start_node("bootstrap")
59
51
  nodes = []
@@ -92,7 +84,7 @@ class ConnectionsTest(unittest.TestCase):
92
84
  node2 = start_node("node-2", node1.config.port)
93
85
  time.sleep(1)
94
86
  node2.stop()
95
- time.sleep(5)
87
+ time.sleep(10)
96
88
  self.assertEqual(cb_test, 1)
97
89
 
98
90
  def test_update_cb(self):
@@ -1,58 +0,0 @@
1
- ## DSNodeServer
2
-
3
- HTTPS server wrapper for DSNode that handles incoming network requests.
4
-
5
- ```python
6
- from distributed_state_network import DSNodeServer
7
- ```
8
-
9
- ### Class Definition
10
- ```python
11
- class DSNodeServer(HTTPServer):
12
- config: DSNodeConfig
13
- node: DSNode
14
- ```
15
-
16
- ### Constructor
17
-
18
- **Parameters:**
19
- - `config` (`DSNodeConfig`): Node configuration
20
- - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
21
- - `update_callback` (`Optional[Callable]`): Callback for state update events
22
-
23
- ### Static Methods
24
-
25
- ### `start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> DSNodeServer`
26
- Creates and starts a new DSNodeServer instance with SSL configuration.
27
- ```python
28
- server = DSNodeServer.start(config)
29
- ```
30
-
31
- **Parameters:**
32
- - `config` (`DSNodeConfig`): Node configuration
33
- - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events (no parameters)
34
- - `update_callback` (`Optional[Callable]`): Callback for state update events (no parameters)
35
-
36
- **Returns:**
37
- - `DSNodeServer`: Running server instance
38
-
39
- ### `generate_key(out_file_path: str) -> None`
40
- Generates a new AES key file for network encryption.
41
-
42
- **Parameters:**
43
- - `out_file_path` (`str`): Path where the key file will be saved
44
-
45
- **Example:**
46
- ```python
47
- DSNodeServer.generate_key("/path/to/network.key")
48
- ```
49
-
50
- ### Instance Methods
51
-
52
- ### `stop() -> None`
53
- Gracefully shuts down the server and cleans up resources.
54
-
55
- **Example:**
56
- ```python
57
- server.stop()
58
- ```