@apocaliss92/scrypted-reolink-native 0.1.0 → 0.1.2

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.
@@ -1,274 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Compare neolink.pcapng vs scrypted.pcapng to find differences in UDP flow.
4
- This script parses pcapng files and extracts BCUDP packets for comparison.
5
- """
6
- import struct
7
- import sys
8
- from typing import List, Dict, Any
9
-
10
- BCUDP_MAGIC_DISCOVERY = 0x3acf872a
11
- BCUDP_MAGIC_DATA = 0x10cf872a
12
- BCUDP_MAGIC_ACK = 0x20cf872a
13
-
14
- def read_pcapng_block(f) -> tuple[bytes, int] | None:
15
- """Read a pcapng block. Returns (block_type, block_data) or None if EOF."""
16
- # Read block type (4 bytes)
17
- block_type_data = f.read(4)
18
- if len(block_type_data) < 4:
19
- return None
20
- block_type = struct.unpack('<I', block_type_data)[0]
21
-
22
- # Read block length (4 bytes)
23
- block_length_data = f.read(4)
24
- if len(block_length_data) < 4:
25
- return None
26
- block_length = struct.unpack('<I', block_length_data)[0]
27
-
28
- if block_length < 12: # Minimum block size (type + len + len)
29
- return None
30
-
31
- # Read block body (length - 8, since we already read type and length)
32
- block_body = f.read(block_length - 8)
33
- if len(block_body) < block_length - 8:
34
- return None
35
-
36
- # Read trailing length (should match)
37
- trailing_length_data = f.read(4)
38
- if len(trailing_length_data) < 4:
39
- return None
40
- trailing_length = struct.unpack('<I', trailing_length_data)[0]
41
-
42
- if trailing_length != block_length:
43
- print(f"Warning: block length mismatch: {block_length} != {trailing_length}")
44
-
45
- return (block_type, block_body)
46
-
47
- def parse_enhanced_packet_block(block_body: bytes) -> Dict[str, Any] | None:
48
- """Parse an Enhanced Packet Block (type 6) from pcapng."""
49
- if len(block_body) < 16:
50
- return None
51
-
52
- # Read interface ID, timestamp, and packet data length
53
- interface_id = struct.unpack('<I', block_body[0:4])[0]
54
- timestamp_high = struct.unpack('<I', block_body[4:8])[0]
55
- timestamp_low = struct.unpack('<I', block_body[8:12])[0]
56
- captured_len = struct.unpack('<I', block_body[12:16])[0]
57
-
58
- # Packet data starts at offset 16
59
- packet_data = block_body[16:16+captured_len]
60
-
61
- return {
62
- 'interface_id': interface_id,
63
- 'timestamp': (timestamp_high << 32) | timestamp_low,
64
- 'packet_data': packet_data
65
- }
66
-
67
- def extract_udp_packet(packet_data: bytes) -> Dict[str, Any] | None:
68
- """Extract UDP packet from Ethernet frame. Returns None if not UDP."""
69
- # Skip Ethernet header (14 bytes)
70
- if len(packet_data) < 42: # Min: 14 (Ethernet) + 20 (IP) + 8 (UDP)
71
- return None
72
-
73
- # Check for IPv4 (0x45 at offset 14)
74
- if packet_data[14] != 0x45:
75
- return None
76
-
77
- # Extract IP header fields
78
- ip_header = packet_data[14:34]
79
- src_ip = '.'.join(str(b) for b in ip_header[12:16])
80
- dst_ip = '.'.join(str(b) for b in ip_header[16:20])
81
- protocol = ip_header[9]
82
-
83
- if protocol != 17: # UDP
84
- return None
85
-
86
- # Extract UDP header
87
- udp_header = packet_data[34:42]
88
- src_port = struct.unpack('>H', udp_header[0:2])[0]
89
- dst_port = struct.unpack('>H', udp_header[2:4])[0]
90
- udp_length = struct.unpack('>H', udp_header[4:6])[0]
91
-
92
- # UDP payload
93
- udp_payload = packet_data[42:42+udp_length-8]
94
-
95
- return {
96
- 'src_ip': src_ip,
97
- 'dst_ip': dst_ip,
98
- 'src_port': src_port,
99
- 'dst_port': dst_port,
100
- 'payload': udp_payload
101
- }
102
-
103
- def parse_bcudp_packet(udp_payload: bytes) -> Dict[str, Any] | None:
104
- """Parse BCUDP packet from UDP payload."""
105
- if len(udp_payload) < 4:
106
- return None
107
-
108
- magic = struct.unpack('<I', udp_payload[0:4])[0]
109
-
110
- if magic == BCUDP_MAGIC_DISCOVERY:
111
- return {
112
- 'type': 'discovery',
113
- 'magic': hex(magic),
114
- 'payload': udp_payload
115
- }
116
- elif magic == BCUDP_MAGIC_DATA:
117
- if len(udp_payload) < 20:
118
- return None
119
- connection_id = struct.unpack('<i', udp_payload[4:8])[0]
120
- packet_id = struct.unpack('<I', udp_payload[12:16])[0]
121
- payload_len = struct.unpack('<I', udp_payload[16:20])[0]
122
- return {
123
- 'type': 'data',
124
- 'magic': hex(magic),
125
- 'connection_id': connection_id,
126
- 'packet_id': packet_id,
127
- 'payload_len': payload_len,
128
- 'payload': udp_payload
129
- }
130
- elif magic == BCUDP_MAGIC_ACK:
131
- if len(udp_payload) < 28:
132
- return None
133
- connection_id = struct.unpack('<i', udp_payload[4:8])[0]
134
- group_id = struct.unpack('<I', udp_payload[12:16])[0]
135
- packet_id = struct.unpack('<I', udp_payload[16:20])[0]
136
- return {
137
- 'type': 'ack',
138
- 'magic': hex(magic),
139
- 'connection_id': connection_id,
140
- 'group_id': group_id,
141
- 'packet_id': packet_id,
142
- 'payload': udp_payload
143
- }
144
-
145
- return None
146
-
147
- def analyze_pcapng(filename: str) -> List[Dict[str, Any]]:
148
- """Analyze pcapng file and extract BCUDP packets."""
149
- packets = []
150
-
151
- with open(filename, 'rb') as f:
152
- # Read Section Header Block (first block, type 0x0A0D0D0A)
153
- first_block = read_pcapng_block(f)
154
- if not first_block or first_block[0] != 0x0A0D0D0A:
155
- print(f"Error: {filename} is not a valid pcapng file")
156
- return packets
157
-
158
- packet_num = 0
159
- while True:
160
- block = read_pcapng_block(f)
161
- if not block:
162
- break
163
-
164
- block_type, block_body = block
165
-
166
- # Enhanced Packet Block (type 6)
167
- if block_type == 6:
168
- epb = parse_enhanced_packet_block(block_body)
169
- if epb:
170
- udp_pkt = extract_udp_packet(epb['packet_data'])
171
- if udp_pkt:
172
- bcudp_pkt = parse_bcudp_packet(udp_pkt['payload'])
173
- if bcudp_pkt:
174
- packets.append({
175
- 'num': packet_num,
176
- 'timestamp': epb['timestamp'],
177
- 'src_ip': udp_pkt['src_ip'],
178
- 'dst_ip': udp_pkt['dst_ip'],
179
- 'src_port': udp_pkt['src_port'],
180
- 'dst_port': udp_pkt['dst_port'],
181
- 'bcudp': bcudp_pkt,
182
- 'raw_payload': udp_pkt['payload'].hex()[:100] # First 50 bytes hex
183
- })
184
- packet_num += 1
185
-
186
- return packets
187
-
188
- def print_packet_summary(packets: List[Dict[str, Any]], name: str):
189
- """Print summary of packets."""
190
- print(f"\n{'='*80}")
191
- print(f"{name}: {len(packets)} BCUDP packets")
192
- print(f"{'='*80}")
193
-
194
- for i, pkt in enumerate(packets[:50]): # Show first 50 packets
195
- bcudp = pkt['bcudp']
196
- direction = '→' if '192.168' in pkt['dst_ip'] else '←'
197
- pkt_type = bcudp['type'].upper()
198
-
199
- info_parts = [f"#{pkt['num']}", direction, pkt_type]
200
-
201
- if pkt_type == 'DATA':
202
- info_parts.append(f"conn={bcudp.get('connection_id', '?')}")
203
- info_parts.append(f"pid={bcudp.get('packet_id', '?')}")
204
- info_parts.append(f"len={bcudp.get('payload_len', '?')}")
205
- elif pkt_type == 'ACK':
206
- info_parts.append(f"conn={bcudp.get('connection_id', '?')}")
207
- info_parts.append(f"pid={bcudp.get('packet_id', '?')}")
208
- info_parts.append(f"gid={bcudp.get('group_id', '?')}")
209
-
210
- info_parts.append(f"{pkt['src_port']}→{pkt['dst_port']}")
211
-
212
- print(f"{' '.join(info_parts)}")
213
- if i < 10: # Show hex for first 10 packets
214
- print(f" Hex: {pkt['raw_payload']}...")
215
-
216
- if len(packets) > 50:
217
- print(f"... and {len(packets) - 50} more packets")
218
-
219
- def compare_flows(neolink_packets: List[Dict[str, Any]], scrypted_packets: List[Dict[str, Any]]):
220
- """Compare the two flows and highlight differences."""
221
- print(f"\n{'='*80}")
222
- print("COMPARISON")
223
- print(f"{'='*80}")
224
-
225
- print(f"\nNeolink: {len(neolink_packets)} BCUDP packets")
226
- print(f"Scrypted: {len(scrypted_packets)} BCUDP packets")
227
-
228
- # Find first DATA packets
229
- neolink_first_data = next((p for p in neolink_packets if p['bcudp']['type'] == 'data'), None)
230
- scrypted_first_data = next((p for p in scrypted_packets if p['bcudp']['type'] == 'data'), None)
231
-
232
- if neolink_first_data and scrypted_first_data:
233
- print(f"\nFirst DATA packet:")
234
- print(f" Neolink: conn={neolink_first_data['bcudp'].get('connection_id')}, pid={neolink_first_data['bcudp'].get('packet_id')}, {neolink_first_data['src_port']}→{neolink_first_data['dst_port']}")
235
- print(f" Scrypted: conn={scrypted_first_data['bcudp'].get('connection_id')}, pid={scrypted_first_data['bcudp'].get('packet_id')}, {scrypted_first_data['src_port']}→{scrypted_first_data['dst_port']}")
236
- print(f" Hex Neolink: {neolink_first_data['raw_payload']}")
237
- print(f" Hex Scrypted: {scrypted_first_data['raw_payload']}")
238
-
239
- if neolink_first_data['raw_payload'] != scrypted_first_data['raw_payload']:
240
- print(f" ⚠️ DIFFERENT HEX!")
241
- else:
242
- print(f" ✓ Same hex")
243
-
244
- # Count DATA/ACK packets after discovery
245
- neolink_data_count = sum(1 for p in neolink_packets if p['bcudp']['type'] == 'data')
246
- neolink_ack_count = sum(1 for p in neolink_packets if p['bcudp']['type'] == 'ack')
247
- scrypted_data_count = sum(1 for p in scrypted_packets if p['bcudp']['type'] == 'data')
248
- scrypted_ack_count = sum(1 for p in scrypted_packets if p['bcudp']['type'] == 'ack')
249
-
250
- print(f"\nPacket counts:")
251
- print(f" Neolink: {neolink_data_count} DATA, {neolink_ack_count} ACK")
252
- print(f" Scrypted: {scrypted_data_count} DATA, {scrypted_ack_count} ACK")
253
-
254
- def main():
255
- if len(sys.argv) < 3:
256
- print("Usage: python3 compare_pcaps.py <neolink.pcapng> <scrypted.pcapng>")
257
- sys.exit(1)
258
-
259
- neolink_file = sys.argv[1]
260
- scrypted_file = sys.argv[2]
261
-
262
- print("Analyzing neolink.pcapng...")
263
- neolink_packets = analyze_pcapng(neolink_file)
264
-
265
- print("Analyzing scrypted.pcapng...")
266
- scrypted_packets = analyze_pcapng(scrypted_file)
267
-
268
- print_packet_summary(neolink_packets, "NEOLINK")
269
- print_packet_summary(scrypted_packets, "SCRYPTED")
270
- compare_flows(neolink_packets, scrypted_packets)
271
-
272
- if __name__ == '__main__':
273
- main()
274
-
@@ -1,222 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Compare stream flow between neolink and scrypted after discovery/nonce request.
4
- Focus on DATA/ACK packets to identify why stream might stall.
5
- """
6
- import json
7
- import sys
8
- import struct
9
-
10
- BCUDP_MAGIC_DATA = 0x10cf872a
11
- BCUDP_MAGIC_ACK = 0x20cf872a
12
-
13
- def hex_to_bytes(hex_str: str) -> bytes:
14
- try:
15
- return bytes.fromhex(hex_str.replace(':', '').replace(' ', ''))
16
- except:
17
- return b''
18
-
19
- def parse_bcudp_data(data: bytes):
20
- """Parse BCUDP DATA packet."""
21
- if len(data) < 20:
22
- return None
23
- magic = struct.unpack('>I', data[0:4])[0]
24
- if magic != BCUDP_MAGIC_DATA:
25
- return None
26
- connection_id = int.from_bytes(data[4:8], 'little', signed=True)
27
- packet_id = int.from_bytes(data[12:16], 'little')
28
- payload_len = int.from_bytes(data[16:20], 'little')
29
- return {'type': 'data', 'connection_id': connection_id, 'packet_id': packet_id, 'payload_len': payload_len}
30
-
31
- def parse_bcudp_ack(data: bytes):
32
- """Parse BCUDP ACK packet."""
33
- if len(data) < 28:
34
- return None
35
- magic = struct.unpack('>I', data[0:4])[0]
36
- if magic != BCUDP_MAGIC_ACK:
37
- return None
38
- connection_id = int.from_bytes(data[4:8], 'little', signed=True)
39
- group_id = int.from_bytes(data[12:16], 'little')
40
- packet_id = int.from_bytes(data[16:20], 'little')
41
- return {'type': 'ack', 'connection_id': connection_id, 'group_id': group_id, 'packet_id': packet_id}
42
-
43
- def extract_stream_packets(json_file: str):
44
- """Extract DATA and ACK packets from stream."""
45
- packets = []
46
-
47
- with open(json_file, 'r') as f:
48
- data = json.load(f)
49
-
50
- for entry in data:
51
- source = entry.get('_source', {})
52
- layers = source.get('layers', {})
53
- frame = layers.get('frame', {})
54
- frame_num = int(frame.get('frame.number', '0'))
55
- frame_time = float(frame.get('frame.time_relative', '0'))
56
- udp = layers.get('udp', {})
57
- if not udp:
58
- continue
59
- src_port = int(udp.get('udp.srcport', '0'))
60
- dst_port = int(udp.get('udp.dstport', '0'))
61
- ip = layers.get('ip', {})
62
- src_ip = ip.get('ip.src', '')
63
- payload_hex = udp.get('udp.payload', '')
64
- if not payload_hex:
65
- continue
66
-
67
- bytes_data = hex_to_bytes(payload_hex)
68
- if len(bytes_data) < 4:
69
- continue
70
-
71
- # Try to parse as DATA or ACK
72
- pkt_data = parse_bcudp_data(bytes_data)
73
- if pkt_data:
74
- packets.append({
75
- 'frame_num': frame_num,
76
- 'time': frame_time,
77
- 'src_ip': src_ip,
78
- 'src_port': src_port,
79
- 'dst_port': dst_port,
80
- **pkt_data
81
- })
82
- continue
83
-
84
- pkt_ack = parse_bcudp_ack(bytes_data)
85
- if pkt_ack:
86
- packets.append({
87
- 'frame_num': frame_num,
88
- 'time': frame_time,
89
- 'src_ip': src_ip,
90
- 'src_port': src_port,
91
- 'dst_port': dst_port,
92
- **pkt_ack
93
- })
94
-
95
- return packets
96
-
97
- def analyze_stream_flow(packets, name: str):
98
- """Analyze stream flow statistics."""
99
- print(f"\n{'='*100}")
100
- print(f"{name}: Stream Flow Analysis")
101
- print(f"{'='*100}")
102
-
103
- data_packets = [p for p in packets if p['type'] == 'data']
104
- ack_packets = [p for p in packets if p['type'] == 'ack']
105
-
106
- print(f"\nTotal packets: {len(packets)} (DATA: {len(data_packets)}, ACK: {len(ack_packets)})")
107
-
108
- if not data_packets:
109
- print(" No DATA packets found!")
110
- return
111
-
112
- # Find direction: camera->client (DATA) vs client->camera (ACK)
113
- # Assuming camera IP is 192.168.x.x
114
- camera_data = [p for p in data_packets if '192.168' in p['src_ip']]
115
- client_data = [p for p in data_packets if '192.168' not in p['src_ip']]
116
-
117
- camera_ack = [p for p in ack_packets if '192.168' in p['src_ip']]
118
- client_ack = [p for p in ack_packets if '192.168' not in p['src_ip']]
119
-
120
- print(f"\nDirection breakdown:")
121
- print(f" Camera→Client DATA: {len(camera_data)}")
122
- print(f" Client→Camera DATA: {len(client_data)}")
123
- print(f" Camera→Client ACK: {len(camera_ack)}")
124
- print(f" Client→Camera ACK: {len(client_ack)}")
125
-
126
- if camera_data:
127
- print(f"\nCamera→Client DATA packets:")
128
- print(f" First: frame #{camera_data[0]['frame_num']}, time {camera_data[0]['time']:.3f}s, pid={camera_data[0]['packet_id']}")
129
- print(f" Last: frame #{camera_data[-1]['frame_num']}, time {camera_data[-1]['time']:.3f}s, pid={camera_data[-1]['packet_id']}")
130
- print(f" Duration: {camera_data[-1]['time'] - camera_data[0]['time']:.3f}s")
131
- print(f" Packet ID range: {camera_data[0]['packet_id']} → {camera_data[-1]['packet_id']}")
132
-
133
- # Check for gaps
134
- pids = sorted([p['packet_id'] for p in camera_data])
135
- gaps = []
136
- for i in range(len(pids) - 1):
137
- gap = pids[i+1] - pids[i]
138
- if gap > 1:
139
- gaps.append((pids[i], pids[i+1], gap))
140
- if gaps:
141
- print(f" ⚠️ Found {len(gaps)} gaps in packet IDs:")
142
- for start, end, gap in gaps[:10]:
143
- print(f" Missing: {start+1} to {end-1} (gap={gap-1})")
144
- else:
145
- print(f" ✓ No gaps in packet IDs")
146
-
147
- # Check packet rate
148
- if len(camera_data) > 1:
149
- duration = camera_data[-1]['time'] - camera_data[0]['time']
150
- rate = len(camera_data) / duration if duration > 0 else 0
151
- print(f" Packet rate: {rate:.2f} packets/sec")
152
-
153
- # Check ACK frequency from client
154
- if client_ack:
155
- print(f"\nClient→Camera ACK packets:")
156
- print(f" Total: {len(client_ack)}")
157
- if len(client_ack) > 1:
158
- duration = client_ack[-1]['time'] - client_ack[0]['time']
159
- rate = len(client_ack) / duration if duration > 0 else 0
160
- print(f" ACK rate: {rate:.2f} ACKs/sec")
161
- print(f" First ACK: time {client_ack[0]['time']:.3f}s, pid={client_ack[0]['packet_id']}")
162
- print(f" Last ACK: time {client_ack[-1]['time']:.3f}s, pid={client_ack[-1]['packet_id']}")
163
-
164
- def compare_streams(nl_packets, sc_packets):
165
- """Compare stream flows."""
166
- print(f"\n{'='*100}")
167
- print("COMPARISON")
168
- print(f"{'='*100}")
169
-
170
- nl_camera_data = [p for p in nl_packets if p['type'] == 'data' and '192.168' in p['src_ip']]
171
- sc_camera_data = [p for p in sc_packets if p['type'] == 'data' and '192.168' in p['src_ip']]
172
-
173
- nl_client_ack = [p for p in nl_packets if p['type'] == 'ack' and '192.168' not in p['src_ip']]
174
- sc_client_ack = [p for p in sc_packets if p['type'] == 'ack' and '192.168' not in p['src_ip']]
175
-
176
- print(f"\nCamera→Client DATA packets:")
177
- print(f" Neolink: {len(nl_camera_data)} packets")
178
- print(f" Scrypted: {len(sc_camera_data)} packets")
179
- print(f" Ratio: {len(sc_camera_data) / len(nl_camera_data) * 100 if nl_camera_data else 0:.1f}%")
180
-
181
- print(f"\nClient→Camera ACK packets:")
182
- print(f" Neolink: {len(nl_client_ack)} packets")
183
- print(f" Scrypted: {len(sc_client_ack)} packets")
184
- print(f" Ratio: {len(sc_client_ack) / len(nl_client_ack) * 100 if nl_client_ack else 0:.1f}%")
185
-
186
- if nl_camera_data and sc_camera_data:
187
- nl_duration = nl_camera_data[-1]['time'] - nl_camera_data[0]['time']
188
- sc_duration = sc_camera_data[-1]['time'] - sc_camera_data[0]['time']
189
- print(f"\nStream duration:")
190
- print(f" Neolink: {nl_duration:.2f}s")
191
- print(f" Scrypted: {sc_duration:.2f}s")
192
-
193
- nl_rate = len(nl_camera_data) / nl_duration if nl_duration > 0 else 0
194
- sc_rate = len(sc_camera_data) / sc_duration if sc_duration > 0 else 0
195
- print(f"\nPacket rate:")
196
- print(f" Neolink: {nl_rate:.2f} packets/sec")
197
- print(f" Scrypted: {sc_rate:.2f} packets/sec")
198
-
199
- if sc_rate < nl_rate * 0.5:
200
- print(f" ⚠️ Scrypted has much lower packet rate!")
201
-
202
- def main():
203
- if len(sys.argv) < 3:
204
- print("Usage: python3 compare_stream_flow.py <neolink.json> <scrypted.json>")
205
- sys.exit(1)
206
-
207
- neolink_file = sys.argv[1]
208
- scrypted_file = sys.argv[2]
209
-
210
- print("Loading neolink.json...")
211
- nl_packets = extract_stream_packets(neolink_file)
212
-
213
- print("Loading scrypted.json...")
214
- sc_packets = extract_stream_packets(scrypted_file)
215
-
216
- analyze_stream_flow(nl_packets, "NEOLINK")
217
- analyze_stream_flow(sc_packets, "SCRYPTED")
218
- compare_streams(nl_packets, sc_packets)
219
-
220
- if __name__ == '__main__':
221
- main()
222
-