@apocaliss92/scrypted-reolink-native 0.1.1 → 0.1.3
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.
- package/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/pcap/network_cam.txt +108321 -0
- package/pcap/wifi0ap0-2412MHz-UnifiRuocco.pcap +0 -0
- package/src/camera-battery.ts +13 -27
- package/src/camera.ts +1 -2
- package/src/common.ts +45 -26
- package/src/connect.ts +3 -2
- package/src/debug-options.ts +105 -0
- package/src/stream-utils.ts +25 -1
- package/pcap/analyze_json.py +0 -248
- package/pcap/analyze_pcap.py +0 -135
- package/pcap/compare_pcaps.py +0 -274
- package/pcap/compare_stream_flow.py +0 -222
- package/pcap/scrypted.json +0 -307560
- package/pcap/scrypted.pcapng +0 -0
- package/pcap/simple_compare.py +0 -178
package/pcap/analyze_pcap.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Simple PCAP analyzer to compare neolink vs scrypted UDP flows"""
|
|
3
|
-
import sys
|
|
4
|
-
import struct
|
|
5
|
-
|
|
6
|
-
def read_pcap_header(f):
|
|
7
|
-
"""Read PCAP file header (24 bytes)"""
|
|
8
|
-
magic = f.read(4)
|
|
9
|
-
if magic != b'\xd4\xc3\xb2\xa1': # pcap magic number (little endian)
|
|
10
|
-
return None
|
|
11
|
-
version_major, version_minor = struct.unpack('<HH', f.read(4))
|
|
12
|
-
thiszone, sigfigs = struct.unpack('<II', f.read(8))
|
|
13
|
-
snaplen, network = struct.unpack('<II', f.read(8))
|
|
14
|
-
return {
|
|
15
|
-
'version_major': version_major,
|
|
16
|
-
'version_minor': version_minor,
|
|
17
|
-
'snaplen': snaplen,
|
|
18
|
-
'network': network
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
def read_packet_header(f):
|
|
22
|
-
"""Read PCAP packet header (16 bytes)"""
|
|
23
|
-
data = f.read(16)
|
|
24
|
-
if len(data) < 16:
|
|
25
|
-
return None
|
|
26
|
-
ts_sec, ts_usec, incl_len, orig_len = struct.unpack('<IIII', data)
|
|
27
|
-
return {
|
|
28
|
-
'ts_sec': ts_sec,
|
|
29
|
-
'ts_usec': ts_usec,
|
|
30
|
-
'incl_len': incl_len,
|
|
31
|
-
'orig_len': orig_len
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
def analyze_pcap(filename):
|
|
35
|
-
"""Basic analysis of UDP packets in PCAP"""
|
|
36
|
-
with open(filename, 'rb') as f:
|
|
37
|
-
header = read_pcap_header(f)
|
|
38
|
-
if not header:
|
|
39
|
-
print(f"Error: {filename} is not a valid PCAP file")
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
packets = []
|
|
43
|
-
packet_num = 0
|
|
44
|
-
|
|
45
|
-
while True:
|
|
46
|
-
pkt_header = read_packet_header(f)
|
|
47
|
-
if not pkt_header:
|
|
48
|
-
break
|
|
49
|
-
|
|
50
|
-
packet_data = f.read(pkt_header['incl_len'])
|
|
51
|
-
if len(packet_data) < pkt_header['incl_len']:
|
|
52
|
-
break
|
|
53
|
-
|
|
54
|
-
# Check if this is an Ethernet frame with IPv4 UDP
|
|
55
|
-
if len(packet_data) >= 42: # Min Ethernet + IP + UDP headers
|
|
56
|
-
# Skip Ethernet header (14 bytes), check IP
|
|
57
|
-
ip_header = packet_data[14:34]
|
|
58
|
-
if ip_header[0] == 0x45: # IPv4
|
|
59
|
-
protocol = ip_header[9]
|
|
60
|
-
if protocol == 17: # UDP
|
|
61
|
-
src_port = struct.unpack('>H', ip_header[20:22])[0]
|
|
62
|
-
dst_port = struct.unpack('>H', ip_header[22:24])[0]
|
|
63
|
-
udp_data = packet_data[42:]
|
|
64
|
-
|
|
65
|
-
# Check for BCUDP magic numbers
|
|
66
|
-
if len(udp_data) >= 4:
|
|
67
|
-
magic = udp_data[:4]
|
|
68
|
-
if magic == b'\x3a\xcf\x87\x2a': # Discovery
|
|
69
|
-
packets.append({
|
|
70
|
-
'num': packet_num,
|
|
71
|
-
'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
|
|
72
|
-
'src_port': src_port,
|
|
73
|
-
'dst_port': dst_port,
|
|
74
|
-
'type': 'discovery',
|
|
75
|
-
'data': udp_data[:100] # First 100 bytes
|
|
76
|
-
})
|
|
77
|
-
elif magic == b'\x20\xcf\x87\x2a': # ACK
|
|
78
|
-
packets.append({
|
|
79
|
-
'num': packet_num,
|
|
80
|
-
'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
|
|
81
|
-
'src_port': src_port,
|
|
82
|
-
'dst_port': dst_port,
|
|
83
|
-
'type': 'ack',
|
|
84
|
-
'data': udp_data[:50]
|
|
85
|
-
})
|
|
86
|
-
elif magic == b'\x10\xcf\x87\x2a': # DATA
|
|
87
|
-
packets.append({
|
|
88
|
-
'num': packet_num,
|
|
89
|
-
'ts': pkt_header['ts_sec'] + pkt_header['ts_usec'] / 1000000,
|
|
90
|
-
'src_port': src_port,
|
|
91
|
-
'dst_port': dst_port,
|
|
92
|
-
'type': 'data',
|
|
93
|
-
'data': udp_data[:50]
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
packet_num += 1
|
|
97
|
-
|
|
98
|
-
print(f"\n=== {filename} ===")
|
|
99
|
-
print(f"Total packets analyzed: {packet_num}")
|
|
100
|
-
print(f"BCUDP packets found: {len(packets)}")
|
|
101
|
-
|
|
102
|
-
# Group by type
|
|
103
|
-
by_type = {}
|
|
104
|
-
for pkt in packets:
|
|
105
|
-
by_type.setdefault(pkt['type'], []).append(pkt)
|
|
106
|
-
|
|
107
|
-
for pkt_type, pkts in by_type.items():
|
|
108
|
-
print(f"\n{pkt_type.upper()}: {len(pkts)} packets")
|
|
109
|
-
if pkts:
|
|
110
|
-
print(f" First at {pkts[0]['ts']:.3f}s, last at {pkts[-1]['ts']:.3f}s")
|
|
111
|
-
print(f" Ports: src={set(p['src_port'] for p in pkts)}, dst={set(p['dst_port'] for p in pkts)}")
|
|
112
|
-
|
|
113
|
-
# Show first few discovery packets in detail
|
|
114
|
-
discovery_pkts = [p for p in packets if p['type'] == 'discovery']
|
|
115
|
-
if discovery_pkts:
|
|
116
|
-
print(f"\nFirst 5 discovery packets:")
|
|
117
|
-
for pkt in discovery_pkts[:5]:
|
|
118
|
-
print(f" #{pkt['num']} at {pkt['ts']:.3f}s: {pkt['src_port']} -> {pkt['dst_port']}")
|
|
119
|
-
# Look for XML-like content
|
|
120
|
-
data_str = pkt['data'].decode('latin1', errors='ignore')
|
|
121
|
-
if 'C2D' in data_str or 'D2C' in data_str:
|
|
122
|
-
# Try to extract XML tag
|
|
123
|
-
start = data_str.find('<')
|
|
124
|
-
if start >= 0:
|
|
125
|
-
end = data_str.find('>', start)
|
|
126
|
-
if end > start:
|
|
127
|
-
tag = data_str[start:end+1]
|
|
128
|
-
print(f" Tag: {tag}")
|
|
129
|
-
|
|
130
|
-
return packets
|
|
131
|
-
|
|
132
|
-
if __name__ == '__main__':
|
|
133
|
-
neolink_packets = analyze_pcap('neolink.pcapng')
|
|
134
|
-
print("\n" + "="*60)
|
|
135
|
-
scrypted_packets = analyze_pcap('scrypted.pcapng')
|
package/pcap/compare_pcaps.py
DELETED
|
@@ -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
|
-
|