-
Структура кадра (64 байта):
-
-
- Destination MAC
- 6 bytes
-
-
- Source MAC
- 6 bytes
-
-
- EtherType
- 2 bytes
-
-
- Data
- 46 bytes
-
-
-
FCS
-
4 bytes
+
+ {#if fieldValues.length > 0}
+
+
Структура пакета ({totalBytesCount} байт):
+
-
+ {/if}
\ No newline at end of file
diff --git a/src/lib/data/lessons.js b/src/lib/data/lessons.js
index adf7da0..e204f6d 100644
--- a/src/lib/data/lessons.js
+++ b/src/lib/data/lessons.js
@@ -1,3 +1,5 @@
+import { theory } from './theory.js';
+
export const lessons = [
{
id: 1,
@@ -7,162 +9,18 @@ export const lessons = [
difficulty: 'Начинающий',
component: 'BitEditor',
additionalComponents: ['HexViewer'],
- theory: `
-
-
- Бит и байт - основа сетевых протоколов
-
-
-
-
Что такое бит?
-
- - Бит - минимальная единица информации (0 или 1)
- - Основа всей компьютерной техники
-
-
-
-
-
Что такое байт?
-
- - Байт = 8 битов
- - Пример:
01011011 = 1 байт
-
-
-
-
-
Двоичная арифметика:
-
Каждая позиция бита - это степень двойки:
-
- 10101010 = 1×2⁷ + 0×2⁶ + 1×2⁵ + 0×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 1×128 + 0×64 + 1×32 + 0×16 + 1×8 + 0×4 + 1×2 + 0×1 = 170
-
-
-
-
-
Шестнадцатеричная система (Hex):
-
В обычной жизни мы используем десятичную систему (0-9). В шестнадцатеричной системе 16 цифр:
-
-
-
- | Десятичная | Hex | Двоичная |
- | 0 | 0 | 0000 |
- | 1 | 1 | 0001 |
- | 2 | 2 | 0010 |
- | 3 | 3 | 0011 |
- | 4 | 4 | 0100 |
- | 5 | 5 | 0101 |
- | 6 | 6 | 0110 |
- | 7 | 7 | 0111 |
- | 8 | 8 | 1000 |
-
-
-
- | Десятичная | Hex | Двоичная |
- | 9 | 9 | 1001 |
- | 10 | A | 1010 |
- | 11 | B | 1011 |
- | 12 | C | 1100 |
- | 13 | D | 1101 |
- | 14 | E | 1110 |
- | 15 | F | 1111 |
-
-
-
-
-
-
Как преобразовать двоичное в Hex:
-
Разбиваем байт на две половинки по 4 бита и преобразуем каждую отдельно:
-
-
-
Пример: 11010011
-
- - Разбиваем на две группы:
1101 и 0011
- 1101 = 1×8 + 1×4 + 0×2 + 1×1 = 13 = D
- 0011 = 0×8 + 0×4 + 1×2 + 1×1 = 3 = 3
- - Результат:
0xD3
-
-
-
-
Удобнее читать 0xA1, чем 10100001!
-
-
-`,
-
- objective: 'Установите биты так, чтобы получить число 184 (0xB8)',
- initialBuffer: new Uint8Array([0x00]),
- validate: (buffer) => {
- return buffer[0] === 0xB8;
- },
- hints: [
- '184 в двоичной системе = 10111000',
- 'Каждый бит представляет степень двойки',
- 'Попробуйте установить биты в позициях 7,5,4,3'
- ]
+ theory: theory['bit-manipulation'],
+ taskTemplate: { type: 'bit-set' },
},
{
id: 2,
- slug: 'mac-address-edit',
+ slug: 'mac-address',
title: 'MAC адреса - редактирование 6 байт',
category: 'Ethernet',
difficulty: 'Начинающий',
component: 'HexEditor',
-
- theory: `
-
-
- MAC адреса
-
-
-
-
Что такое MAC адрес?
-
- - MAC адрес - уникальный идентификатор сетевого устройства
- - Формат: XX:XX:XX:XX:XX:XX (6 байтов)
- - Пример:
12:34:56:78:9A:BC
- - Первые 3 байта - идентификатор производителя (OUI)
- - Последние 3 байта - серийный номер устройства
-
-
-
-
-
Как редактировать MAC-адрес?
-
- Форматы представления чисел:
-
- - Hex (16-ричный): 0xFF, 0x1A3B
- - Decimal (10-ричный): 255, 6715
- - Binary (2-ичный): 11111111, 110100011011
- - Octal (8-ричный): 377, 15073
-
-
- Endianness (порядок байт):
-
- - Big Endian: старший байт по младшему адресу (сетевой порядок)
- - Little Endian: младший байт по младшему адресу (x86, ARM)
- - Пример: 0x12345678
-
- - Big Endian: 12 34 56 78
- - Little Endian: 78 56 34 12
-
-
-
-
-
-`,
-
- objective: 'Установите MAC адрес 12:34:56:78:9A:BC',
-
- initialBuffer: new Uint8Array(6),
-
- validate: (buffer) => {
- const expectedMAC = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
- return expectedMAC.every((expected, i) => buffer[i] === expected);
- },
-
- hints: [
- 'Для HEX: разделите байт на две половинки по 4 бита. 0x34 = 00110100 → 0011(3) 0100(4)',
- 'Кликайте на байты в HexView чтобы редактировать их',
- 'Степени двойки по битам: 128,64,32,16,8,4,2,1'
- ]
+ theory: theory['mac-address'],
+ taskTemplate: { type: 'mac-set' },
},
{
id: 3,
@@ -179,82 +37,355 @@ export const lessons = [
{ name: 'Data', start: 14, length: 46, format: 'hex', color: '#f3e5f5' },
{ name: 'FCS', start: 60, length: 4, format: 'hex', color: '#ffebee' }
],
+ wiresharkTitle: 'Структура Ethernet кадра',
readOnlyRanges: [
{ start: 14, end: 59 }, //Data (46 байт)
{ start: 60, end: 63 } //FCS (4 байта) - рассчитывается автоматически
],
-
- theory: `
-
-
- Ethernet кадр
-
-
-
-
Ethernet II и IEEE 802.3 - это два стандарта Ethernet-кадров. Они идентичны по структуре, кроме одного поля: в Ethernet II это поле Type (указывает тип протокола), а в IEEE 802.3 это поле Length (указывает длину поля данных в байтах).
-
На практике Ethernet II используется чаще, потому что поле Type более полезно для определения, какой протокол находится внутри кадра.
-
-
-
-
Структура кадра Ethernet II:
-
-
-
- Destination MAC:
- 6 байт | MAC адрес получателя
-
-
- Source MAC:
- 6 байт | MAC адрес отправителя
-
-
- Type:
- 2 байта | Тип протокола
-
-
- Data:
- 46-1500 байт | Полезная нагрузка
-
-
- FCS:
- 4 байта | Контрольная сумма
-
-
-
-
-
-
Типы протоколов (EtherType):
-
-
-
- | Код | Протокол |
- 0x0800 | IPv4 |
- 0x0806 | ARP |
- 0x86DD | IPv6 |
-
-
-
-
-`,
-
- objective: 'Отправьте широковещательный ARP запрос с MAC адресом вашего компьютера 12:34:56:78:9A:BC',
- initialBuffer: new Uint8Array(64),
- validate: (buffer) => {
- const isBroadcast = buffer.slice(0, 6).every(byte => byte === 0xFF);
-
- const expectedSource = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
- const isSourceCorrect = buffer.slice(6, 12).every((byte, i) => byte === expectedSource[i]);
-
- const etherType = (buffer[12] << 8) | buffer[13];
- const isARP = etherType === 0x0806;
-
- return isBroadcast && isSourceCorrect && isARP;
- },
-
- hints: [
- 'Используйте широковещательный адрес FF:FF:FF:FF:FF:FF',
- 'Установите EtherType 0x0806 для ARP',
- 'MAC адрес отправителя должен быть 12:34:56:78:9A:BC'
- ]
- }
+ theory: theory['ethernet-frame'],
+ taskTemplate: { type: 'ethernet-frame' },
+ },
+ {
+ id: 4,
+ slug: 'ipv4-header',
+ title: 'Структура IPv4-заголовка',
+ category: 'IPv4',
+ difficulty: 'Средний',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'Структура IPv4-заголовка',
+ wiresharkFields: [
+ {
+ name: 'Version + IHL',
+ start: 0, length: 1,
+ format: 'version_ihl',
+ color: '#e3f2fd',
+ description: 'Версия протокола (4) и длина заголовка в 32-битных словах'
+ },
+ {
+ name: 'DSCP + ECN',
+ start: 1, length: 1,
+ format: 'hex',
+ color: '#e8eaf6',
+ description: 'Приоритет трафика и уведомление о перегрузке'
+ },
+ {
+ name: 'Total Length',
+ start: 2, length: 2,
+ format: 'decimal',
+ color: '#e8f5e9',
+ description: 'Общая длина пакета (заголовок + данные), байт'
+ },
+ {
+ name: 'Identification',
+ start: 4, length: 2,
+ format: 'hex',
+ color: '#fff3e0',
+ description: 'Идентификатор пакета (для сборки фрагментов)'
+ },
+ {
+ name: 'Flags + Fragment Offset',
+ start: 6, length: 2,
+ format: 'flags_fragment',
+ color: '#fce4ec',
+ description: 'Флаги фрагментации DF/MF и смещение фрагмента'
+ },
+ {
+ name: 'TTL',
+ start: 8, length: 1,
+ format: 'decimal',
+ color: '#f3e5f5',
+ description: 'Время жизни пакета — уменьшается на 1 на каждом маршрутизаторе'
+ },
+ {
+ name: 'Protocol',
+ start: 9, length: 1,
+ format: 'ip_protocol',
+ color: '#e0f7fa',
+ description: 'Протокол вышележащего уровня (TCP=6, UDP=17, ICMP=1)'
+ },
+ {
+ name: 'Header Checksum',
+ start: 10, length: 2,
+ format: 'hex',
+ color: '#f1f8e9',
+ description: 'Контрольная сумма заголовка (пересчитывается автоматически)'
+ },
+ {
+ name: 'Source IP',
+ start: 12, length: 4,
+ format: 'ip',
+ color: '#fff8e1',
+ description: 'IP-адрес отправителя'
+ },
+ {
+ name: 'Destination IP',
+ start: 16, length: 4,
+ format: 'ip',
+ color: '#fbe9e7',
+ description: 'IP-адрес получателя'
+ },
+ ],
+ //read-only диапазоны
+ //байты 0-7: version, IHL, DSCP, total length, identification, flags (установлены заранее)
+ //байты 10-11: checksum (пересчитывается автоматически в bufferProcessor)
+ readOnlyRanges: [
+ { start: 0, end: 7 },
+ { start: 10, end: 11 },
+ ],
+ theory: theory['ipv4-header'],
+ taskTemplate: { type: 'ipv4-addresses' },
+ },
+ {
+ id: 5,
+ slug: 'ipv4-ttl',
+ title: 'TTL — время жизни пакета',
+ category: 'IPv4',
+ difficulty: 'Средний',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'IPv4: поле TTL',
+ wiresharkFields: [
+ { name: 'Version + IHL', start: 0, length: 1, format: 'version_ihl', color: '#e3f2fd', description: '' },
+ { name: 'DSCP + ECN', start: 1, length: 1, format: 'hex', color: '#e8eaf6', description: '' },
+ { name: 'Total Length', start: 2, length: 2, format: 'decimal', color: '#e8f5e9', description: 'Общая длина пакета' },
+ { name: 'Identification', start: 4, length: 2, format: 'hex', color: '#fff3e0', description: '' },
+ { name: 'Flags + Frag. Offset', start: 6, length: 2, format: 'flags_fragment', color: '#fce4ec', description: '' },
+ { name: 'TTL', start: 8, length: 1, format: 'decimal', color: '#f3e5f5', description: 'Уменьшается на 1 на каждом маршрутизаторе' },
+ { name: 'Protocol', start: 9, length: 1, format: 'ip_protocol', color: '#e0f7fa', description: '' },
+ { name: 'Header Checksum', start: 10, length: 2, format: 'hex', color: '#f1f8e9', description: 'Рассчитывается автоматически' },
+ { name: 'Source IP', start: 12, length: 4, format: 'ip', color: '#fff8e1', description: '' },
+ { name: 'Destination IP', start: 16, length: 4, format: 'ip', color: '#fbe9e7', description: '' },
+ ],
+ readOnlyRanges: [
+ { start: 0, end: 7 }, //фиксированные поля заголовка
+ { start: 9, end: 11 }, //protocol + checksum
+ { start: 12, end: 19 }, //IP-адреса (уже заданы)
+ ],
+ theory: theory['ipv4-ttl'],
+ taskTemplate: { type: 'ipv4-ttl' },
+ },
+ {
+ id: 6,
+ slug: 'ipv4-fragmentation',
+ title: 'Фрагментация IPv4',
+ category: 'IPv4',
+ difficulty: 'Продвинутый',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+
+ wiresharkTitle: 'IPv4: флаги и фрагментация',
+ wiresharkFields: [
+ { name: 'Version + IHL', start: 0, length: 1, format: 'version_ihl', color: '#e3f2fd', description: '' },
+ { name: 'DSCP + ECN', start: 1, length: 1, format: 'hex', color: '#e8eaf6', description: '' },
+ { name: 'Total Length', start: 2, length: 2, format: 'decimal', color: '#e8f5e9', description: 'Длина этого фрагмента' },
+ { name: 'Identification', start: 4, length: 2, format: 'hex', color: '#fff3e0', description: 'Одинаковый у всех фрагментов одного пакета' },
+ { name: 'Flags + Frag.Offset', start: 6, length: 2, format: 'flags_fragment', color: '#fce4ec', description: 'DF — не фрагментировать, MF — есть ещё фрагменты' },
+ { name: 'TTL', start: 8, length: 1, format: 'decimal', color: '#f3e5f5', description: '' },
+ { name: 'Protocol', start: 9, length: 1, format: 'ip_protocol', color: '#e0f7fa', description: '' },
+ { name: 'Header Checksum', start: 10, length: 2, format: 'hex', color: '#f1f8e9', description: 'Рассчитывается автоматически' },
+ { name: 'Source IP', start: 12, length: 4, format: 'ip', color: '#fff8e1', description: '' },
+ { name: 'Destination IP', start: 16, length: 4, format: 'ip', color: '#fbe9e7', description: '' },
+ ],
+ //редактируемы только байты 6-7 (Flags + Fragment Offset)
+ readOnlyRanges: [
+ { start: 0, end: 5 },
+ { start: 8, end: 19 },
+ ],
+ theory: theory['ipv4-fragmentation'],
+ taskTemplate: { type: 'ipv4-fragmentation' },
+ },
+ {
+ id: 7,
+ slug: 'tcp-header',
+ title: 'Структура TCP-заголовка',
+ category: 'TCP',
+ difficulty: 'Средний',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+
+ wiresharkTitle: 'Структура TCP-заголовка (20 байт)',
+ wiresharkFields: [
+ { name: 'Source Port', start: 0, length: 2, format: 'port', color: '#e3f2fd', description: 'Порт отправителя (1–65535)' },
+ { name: 'Destination Port', start: 2, length: 2, format: 'port', color: '#e8f5e9', description: 'Порт получателя' },
+ { name: 'Sequence Number', start: 4, length: 4, format: 'seq_ack', color: '#fff3e0', description: 'Порядковый номер первого байта данных в сегменте' },
+ { name: 'Ack Number', start: 8, length: 4, format: 'seq_ack', color: '#fce4ec', description: 'Следующий ожидаемый байт (если ACK=1)' },
+ { name: 'Data Offset', start: 12, length: 1, format: 'tcp_data_offset', color: '#e8eaf6', description: 'Длина заголовка в 32-битных словах (минимум 5)' },
+ { name: 'Flags', start: 13, length: 1, format: 'tcp_flags', color: '#f3e5f5', description: 'CWR ECE URG ACK PSH RST SYN FIN' },
+ { name: 'Window Size', start: 14, length: 2, format: 'window_size', color: '#e0f7fa', description: 'Размер окна приёма (буфера получателя)' },
+ { name: 'Checksum', start: 16, length: 2, format: 'hex', color: '#f1f8e9', description: 'Контрольная сумма (с псевдозаголовком IPv4)' },
+ { name: 'Urgent Pointer', start: 18, length: 2, format: 'hex', color: '#fff8e1', description: 'Актуален только при URG=1' },
+ ],
+ //checksum (16-17) и Data Offset (12) — только чтение
+ readOnlyRanges: [
+ { start: 12, end: 12 },
+ { start: 16, end: 17 },
+ { start: 18, end: 19 },
+ ],
+ theory: theory['tcp-header'],
+ taskTemplate: { type: 'tcp-header' },
+ },
+ {
+ id: 8,
+ slug: 'tcp-flags',
+ title: 'TCP-флаги и трёхстороннее рукопожатие',
+ category: 'TCP',
+ difficulty: 'Средний',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'TCP: байт флагов',
+ wiresharkFields: [
+ { name: 'Source Port', start: 0, length: 2, format: 'port', color: '#e3f2fd', description: '' },
+ { name: 'Destination Port', start: 2, length: 2, format: 'port', color: '#e8f5e9', description: '' },
+ { name: 'Sequence Number', start: 4, length: 4, format: 'seq_ack', color: '#fff3e0', description: '' },
+ { name: 'Ack Number', start: 8, length: 4, format: 'seq_ack', color: '#fce4ec', description: '' },
+ { name: 'Data Offset', start: 12, length: 1, format: 'tcp_data_offset', color: '#e8eaf6', description: '' },
+ { name: 'Flags', start: 13, length: 1, format: 'tcp_flags', color: '#f3e5f5', description: '' },
+ { name: 'Window Size', start: 14, length: 2, format: 'window_size', color: '#e0f7fa', description: '' },
+ { name: 'Checksum', start: 16, length: 2, format: 'hex', color: '#f1f8e9', description: 'Рассчитывается автоматически' },
+ { name: 'Urgent Pointer', start: 18, length: 2, format: 'hex', color: '#fff8e1', description: '' },
+ ],
+ //редактируется только байт флагов (13)
+ readOnlyRanges: [
+ { start: 0, end: 7 }, //порты + sequence number (заданы)
+ { start: 8, end: 12 }, //Ack Number и data offset
+ { start: 14, end: 19 }, //window, checksum, urgent
+ ],
+ theory: theory['tcp-flags'],
+ taskTemplate: { type: 'tcp-flags' },
+ },
+ {
+ id: 9,
+ slug: 'udp-header',
+ title: 'Структура UDP-заголовка',
+ category: 'UDP',
+ difficulty: 'Начинающий',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'Структура UDP-заголовка (8 байт)',
+ wiresharkFields: [
+ { name: 'Source Port', start: 0, length: 2, format: 'port', color: '#e3f2fd', description: 'Порт отправителя' },
+ { name: 'Destination Port', start: 2, length: 2, format: 'port', color: '#e8f5e9', description: 'Порт получателя' },
+ { name: 'Length', start: 4, length: 2, format: 'udp_length', color: '#fff3e0', description: 'Длина UDP-заголовка + данных в байтах (мин. 8)' },
+ { name: 'Checksum', start: 6, length: 2, format: 'hex', color: '#f1f8e9', description: 'Контрольная сумма' },
+ ],
+ //Length (4-5) и Checksum (6-7) пересчитываются автоматически
+ readOnlyRanges: [
+ { start: 4, end: 7 },
+ ],
+ theory: theory['udp-header'],
+ taskTemplate: { type: 'udp-ports' },
+ },
+ {
+ id: 10,
+ slug: 'http-get-request',
+ title: 'HTTP GET-запрос',
+ category: 'HTTP',
+ difficulty: 'Средний',
+ component: 'HttpEditor',
+ additionalComponents: [], //HttpEditor содержит разбор внутри себя
+ theory: theory['http-get-request'],
+ taskTemplate: { type: 'http-get' },
+ },
+ {
+ id: 11,
+ slug: 'http-post-request',
+ title: 'HTTP POST-запрос с JSON',
+ category: 'HTTP',
+ difficulty: 'Средний',
+ component: 'HttpEditor',
+ additionalComponents: [],
+ theory: theory['http-post-request'],
+ taskTemplate: { type: 'http-post' },
+ },
+ {
+ id: 12,
+ slug: 'dns-header',
+ title: 'Структура DNS-заголовка',
+ category: 'DNS',
+ difficulty: 'Средний',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'DNS-заголовок (12 байт)',
+ wiresharkFields: [
+ {
+ name: 'Transaction ID',
+ start: 0, length: 2,
+ format: 'hex',
+ color: '#e3f2fd',
+ description: 'Произвольный идентификатор для сопоставления запроса и ответа',
+ },
+ {
+ name: 'Flags',
+ start: 2, length: 2,
+ format: 'dns_flags',
+ color: '#f3e5f5',
+ description: 'QR, Opcode, AA, TC, RD, RA, RCODE',
+ },
+ {
+ name: 'QDCOUNT',
+ start: 4, length: 2,
+ format: 'decimal',
+ color: '#e8f5e9',
+ description: 'Количество вопросов в секции Question',
+ },
+ {
+ name: 'ANCOUNT',
+ start: 6, length: 2,
+ format: 'decimal',
+ color: '#fff3e0',
+ description: 'Количество записей в секции Answer',
+ },
+ {
+ name: 'NSCOUNT',
+ start: 8, length: 2,
+ format: 'decimal',
+ color: '#fce4ec',
+ description: 'Количество записей в секции Authority',
+ },
+ {
+ name: 'ARCOUNT',
+ start: 10, length: 2,
+ format: 'decimal',
+ color: '#e8eaf6',
+ description: 'Количество записей в секции Additional',
+ },
+ ],
+ //Flags и счётчики ANCOUNT/NSCOUNT/ARCOUNT — только чтение
+ //пользователь меняет Transaction ID, RD-бит в Flags и QDCOUNT
+ readOnlyRanges: [
+ { start: 6, end: 11 }, //ANCOUNT, NSCOUNT, ARCOUNT
+ ],
+ theory: theory['dns-header'],
+ taskTemplate: { type: 'dns-header' },
+ },
+ {
+ id: 13,
+ slug: 'dns-query',
+ title: 'Кодирование доменного имени в DNS',
+ category: 'DNS',
+ difficulty: 'Продвинутый',
+ component: 'HexEditor',
+ additionalComponents: ['WiresharkView'],
+ wiresharkTitle: 'DNS-запрос: заголовок + Question',
+ wiresharkFields: [
+ //заголовок
+ { name: 'Transaction ID', start: 0, length: 2, format: 'hex', color: '#e3f2fd', description: 'Идентификатор запроса' },
+ { name: 'Flags', start: 2, length: 2, format: 'dns_flags', color: '#f3e5f5', description: '' },
+ { name: 'QDCOUNT', start: 4, length: 2, format: 'decimal', color: '#e8f5e9', description: 'Число вопросов' },
+ { name: 'ANCOUNT', start: 6, length: 2, format: 'decimal', color: '#fff3e0', description: '' },
+ { name: 'NSCOUNT', start: 8, length: 2, format: 'decimal', color: '#fce4ec', description: '' },
+ { name: 'ARCOUNT', start: 10, length: 2, format: 'decimal', color: '#e8eaf6', description: '' },
+ //Question
+ { name: 'QNAME', start: 12, length: 13, format: 'dns_qname', color: '#fff8e1', description: 'Доменное имя в DNS-кодировке (длина-метки)' },
+ { name: 'QTYPE', start: 25, length: 2, format: 'dns_qtype', color: '#e0f7fa', description: 'Тип запроса: A=1, AAAA=28, MX=15, NS=2 ...' },
+ { name: 'QCLASS', start: 27, length: 2, format: 'dns_qclass', color: '#f1f8e9', description: 'Класс: IN=1 (Internet)' },
+ ],
+ //всё предзаполнено кроме QTYPE и QCLASS
+ readOnlyRanges: [
+ { start: 0, end: 24 }, //заголовок + QNAME
+ ],
+ theory: theory['dns-query'],
+ taskTemplate: { type: 'dns-query' },
+ },
];
\ No newline at end of file
diff --git a/src/lib/data/theory.js b/src/lib/data/theory.js
new file mode 100644
index 0000000..d8785ec
--- /dev/null
+++ b/src/lib/data/theory.js
@@ -0,0 +1,463 @@
+const B = 'background:#f9fcff;padding:20px;border-radius:8px;margin:20px 0;';
+const H2 = 'color:#2196F3;border-bottom:2px solid #2196F3;padding-bottom:10px;margin-top:0;';
+const H3 = 'color:#1976D2;margin-top:0;';
+const TH = 'background:#e3f2fd;padding:8px;border:1px solid #ccc;text-align:left;';
+const TD = 'padding:6px 8px;border:1px solid #ddd;';
+const TD2 = 'padding:6px 8px;border:1px solid #ddd;background:#f5f9ff;';
+const TBL = 'border-collapse:collapse;width:100%;margin-top:8px;font-size:0.92em;';
+const DARK = 'background:#1e1e1e;color:#d4d4d4;padding:15px;border-radius:6px;font-family:\'Courier New\',monospace;font-size:0.88em;line-height:1.9;margin:10px 0;';
+const WARN = 'background:#fff8e1;padding:16px 20px;border-radius:8px;margin:20px 0;border-left:4px solid #FFC107;';
+
+export const theory = {
+
+'bit-manipulation': `
+
+
Бит и байт — основа сетевых протоколов
+
+
+
Бит и байт
+
Бит — минимальная единица информации, принимает значение 0 или 1.
+
Байт = 8 бит. Один байт хранит число от 0 до 255. Все сетевые пакеты — это последовательность байт. Зная позицию и длину поля в байтах, можно прочитать или изменить любое поле заголовка вручную.
+
+
+
+
Двоичная и шестнадцатеричная запись
+
Один байт записывают в разных системах счисления:
+
+
Десятичная: 184
+
Двоичная: 1011 1000
+
Шестнадцатеричная: 0xB8
+
+
Шестнадцатеричная запись удобна: каждая цифра (0–F) точно соответствует 4 битам.
+
+
+
+
Нумерация битов
+
Биты нумеруются справа налево: бит 7 — старший (вес 128), бит 0 — младший (вес 1).
+
+
+ | Бит: |
+ ${[7,6,5,4,3,2,1,0].map(n =>
+ `${n} | `
+ ).join('')}
+
+
+ | Вес: |
+ ${[128,64,32,16,8,4,2,1].map(n =>
+ `${n} | `
+ ).join('')}
+
+
+
Чтобы получить значение байта, сложите веса всех битов равных 1.
+ Например, биты 7, 4, 3 = 128 + 16 + 8 = 152 = 0x98.
+
+
+
+
Почему это важно?
+
В заголовках протоколов многие поля занимают несколько битов. Например, флаги TCP — каждый флаг один бит. Умение работать с отдельными битами — базовый навык при анализе сетевых пакетов.
+
+
+`,
+
+'mac-address': `
+
+
MAC-адрес
+
+
+
Что такое MAC-адрес?
+
MAC-адрес (Media Access Control) — идентификатор сетевого интерфейса на канальном уровне. Используется в Ethernet и Wi-Fi для адресации внутри одной сети. Длина: 6 байт (48 бит).
+
+
+
+
Структура MAC-адреса
+
+
00:50:56 : AB:CD:EF
+
└───┬───┘ └───┬───┘
+
OUI Номер интерфейса
+
(производитель) (назначает производитель)
+
+
Первые 3 байта — OUI, выдаются IEEE производителям. Примеры: 00:00:0C — Cisco, 00:02:B3 — Intel.
+
+
+
+
Типы MAC-адресов
+
+ | Тип | Бит 0 первого байта | Пример |
+ | Unicast | 0 (чётный байт) | 00:1A:2B:3C:4D:5E |
+ | Multicast | 1 (нечётный байт) | 01:00:5E:00:00:01 |
+ | Broadcast | все биты = 1 | FF:FF:FF:FF:FF:FF |
+
+
Бит 1 первого байта: 0 — назначен производителем, 1 — локально администратором.
+
+
+`,
+
+'ethernet-frame': `
+
+
Структура Ethernet-кадра
+
+
+
Ethernet — основа проводных сетей
+
Ethernet — самая популярная технология проводных сетей. Данные передаются в виде кадров (frames). На практике используется стандарт Ethernet II.
+
+
+
+
Структура кадра Ethernet II
+
+
+----------+----------+----------+------------------+------+
+
| Dst MAC | Src MAC |EtherType | Data | FCS |
+
| 6 байт | 6 байт | 2 байта | 46-1500 байт | 4 б |
+
+----------+----------+----------+------------------+------+
+
+
Dst MAC — получатель. Src MAC — отправитель.
+
EtherType — тип протокола: 0x0800 — IPv4, 0x0806 — ARP, 0x86DD — IPv6.
+
FCS (Frame Check Sequence) — контрольная сумма, рассчитывается автоматически.
+
+
+
+
MTU
+
Максимальный размер данных — 1500 байт (MTU). Если IP-пакет больше MTU, он фрагментируется. Минимум данных — 46 байт.
+
+
+`,
+
+'ipv4-header': `
+
+
IPv4 — протокол сетевого уровня
+
+
+
Что делает IP?
+
IP обеспечивает доставку пакетов через составную сеть — объединяет разные технологии канального уровня и выполняет маршрутизацию. Работает без гарантии доставки и без соединения.
+
+
+
+
Структура заголовка IPv4 (минимум 20 байт)
+
+
Байт 0: Version (4 бит) + IHL (4 бит)
+
Байт 1: DSCP/ECN
+
Байты 2–3: Total Length — полная длина пакета
+
Байты 4–5: Identification — ID для сборки фрагментов
+
Байты 6–7: Flags (3 бит) + Fragment Offset (13 бит)
+
Байт 8: TTL — уменьшается на 1 на каждом маршрутизаторе
+
Байт 9: Protocol — TCP=6, UDP=17, ICMP=1
+
Байты 10–11: Header Checksum (пересчитывается автоматически)
+
Байты 12–15: Source IP
+
Байты 16–19: Destination IP
+
+
+
+
+
Специальные диапазоны IPv4
+
+ | Диапазон | Назначение |
+ 10.x.x.x, 172.16-31.x.x, 192.168.x.x | Частные (не маршрутизируются в интернете) |
+ 127.0.0.1 | Loopback (localhost) |
+ 169.254.x.x | Link-local (без DHCP) |
+
+
+
+`,
+
+'ipv4-ttl': `
+
+
TTL — время жизни пакета
+
+
+
Зачем нужен TTL?
+
TTL (Time To Live) — счётчик прыжков. Каждый маршрутизатор уменьшает TTL на 1. При TTL = 0 пакет отбрасывается и отправителю уходит ICMP «Time Exceeded». Защита от бесконечной петли маршрутизации.
+
+
+
+
Стандартные значения TTL
+
+ | ОС / устройство | TTL | Hex |
+ | Linux, macOS, Android | 64 | 0x40 |
+ | Windows | 128 | 0x80 |
+ | Cisco IOS, сетевые устройства | 255 | 0xFF |
+
+
По TTL в ответе можно примерно определить ОС удалённого хоста.
+
+
+
+
traceroute и TTL
+
traceroute отправляет пакеты с TTL=1, 2, 3… Каждый маршрутизатор отвечает ICMP «Time Exceeded» — так строится полная цепочка узлов до цели.
+
+
+`,
+
+'ipv4-fragmentation': `
+
+
Фрагментация IPv4
+
+
+
MTU и фрагментация
+
MTU — максимальный размер данных кадра. Для Ethernet это 1500 байт. Если IP-пакет больше MTU, маршрутизатор фрагментирует его на части. Получатель собирает фрагменты по Identification, Flags и Fragment Offset.
+
+
+
+
Поля байтов 6–7 (Flags + Fragment Offset)
+
+
Бит 15: Reserved (всегда 0)
+
Бит 14: DF — Don't Fragment (1 = запрет фрагментации)
+
Бит 13: MF — More Fragments (1 = есть ещё фрагменты)
+
Биты 12–0: Fragment Offset (смещение в 8-байтовых блоках)
+
+
+ | Ситуация | Байт 6 | Байт 7 |
+ | DF=1, не фрагментировать | 0x40 | 0x00 |
+ | Первый фрагмент (MF=1) | 0x20 | 0x00 |
+ | Последний фрагмент, Offset=185 | 0x00 | 0xB9 |
+
+
Fragment Offset = байты ÷ 8. Смещение 1480 байт: 1480 ÷ 8 = 185 = 0xB9.
+
+
+`,
+
+'tcp-header': `
+
+
TCP — протокол надёжной передачи
+
+
+
Что делает TCP?
+
TCP обеспечивает надёжность: подтверждения (ACK), повторную отправку при потере, сохранение порядка. TCP нумерует не сегменты, а байты потока — Sequence Number это номер первого байта в сегменте.
+
+
+
+
Структура заголовка TCP (минимум 20 байт)
+
+
Байты 0–1: Source Port
+
Байты 2–3: Destination Port
+
Байты 4–7: Sequence Number — номер первого байта данных
+
Байты 8–11: Acknowledgment Number — следующий ожидаемый байт
+
Байт 12: Data Offset (длина заголовка, мин. 5 × 4 = 20 байт)
+
Байт 13: Flags — SYN, ACK, FIN, RST, PSH...
+
Байты 14–15: Window Size — размер буфера приёма
+
Байты 16–17: Checksum (авто)
+
Байты 18–19: Urgent Pointer
+
+
+
+
+
Популярные TCP-порты
+
+ | Порт | Протокол | Порт | Протокол |
+ | 22 | SSH | 443 | HTTPS |
+ | 25 | SMTP | 3306 | MySQL |
+ | 80 | HTTP | 5432 | PostgreSQL |
+
+
Ephemeral-порты клиента: 49152–65535.
+
+
+`,
+
+'tcp-flags': `
+
+
TCP-флаги и трёхстороннее рукопожатие
+
+
+
Байт флагов (байт 13 заголовка)
+
+ | Бит | Флаг | Назначение |
+ | 4 | ACK | Поле Acknowledgment Number значимо |
+ | 3 | PSH | Доставить данные приложению немедленно |
+ | 2 | RST | Немедленный сброс соединения |
+ | 1 | SYN | Установка соединения |
+ | 0 | FIN | Завершение соединения |
+
+
Несколько флагов через OR: SYN+ACK = 0x02 | 0x10 = 0x12.
+
+
+
+
Трёхстороннее рукопожатие (3-Way Handshake)
+
+
Клиент → Сервер: SYN Seq=X
+
Сервер → Клиент: SYN + ACK Seq=Y, Ack=X+1
+
Клиент → Сервер: ACK Seq=X+1, Ack=Y+1
+
↑ соединение установлено
+
+
+
+`,
+
+'udp-header': `
+
+
UDP — быстрый протокол без гарантий
+
+
+
Что такое UDP?
+
UDP — транспортный протокол без соединения. Не гарантирует доставку и порядок. Заголовок всего 8 байт, нет накладных расходов на рукопожатие. Применяется в DNS, DHCP, NTP, VoIP, онлайн-играх.
+
+
+
+
Структура заголовка UDP (8 байт)
+
+
Байты 0–1: Source Port
+
Байты 2–3: Destination Port
+
Байты 4–5: Length — длина датаграммы (минимум 8)
+
Байты 6–7: Checksum (авто, в IPv4 необязателен)
+
+
+
+
+
Популярные UDP-порты
+
+ | Порт | Протокол | Порт | Протокол |
+ | 53 | DNS | 161 | SNMP |
+ | 67/68 | DHCP | 514 | Syslog |
+ | 123 | NTP | 5353 | mDNS |
+
+
+
+`,
+
+'http-get-request': `
+
+
HTTP — протокол передачи гипертекста
+
+
+
Что такое HTTP?
+
HTTP — текстовый протокол прикладного уровня. Работает поверх TCP, порт 80 (HTTPS — 443). Режим работы: запрос–ответ. В отличие от Ethernet/IP — не бинарный, а текстовый.
+
+
+
+
Структура HTTP-запроса
+
+
МЕТОД /путь HTTP/версия ← строка запроса (обязательна)
+
Заголовок1: значение ← заголовки
+
Заголовок2: значение
+
← пустая строка (ОБЯЗАТЕЛЬНА!)
+
[тело запроса] ← для GET обычно отсутствует
+
+
+
+
+
Основные методы
+
+ | Метод | Назначение | Тело |
+ | GET | Получить ресурс | Нет |
+ | POST | Создать / передать данные | Да |
+ | PUT | Заменить ресурс целиком | Да |
+ | DELETE | Удалить ресурс | Нет |
+
+
+
+
+
⚠ Пустая строка после заголовков — обязательна!
+
Нажмите Enter дважды после последнего заголовка.
+
+
+`,
+
+'http-post-request': `
+
+
HTTP POST — отправка данных
+
+
+
POST vs GET
+
GET получает данные. POST отправляет данные в теле запроса — для создания ресурсов, передачи форм, авторизации.
+
+
+
+
Структура POST-запроса
+
+
POST /api/users HTTP/1.1
+
Host: api.example.com
+
Content-Type: application/json
+
Content-Length: 18
+
← пустая строка
+
{"name":"Student"}
+
+
+
+
+
Заголовки при наличии тела
+
Content-Type — формат тела: application/json, application/x-www-form-urlencoded.
+
Content-Length — размер тела в байтах. Для ASCII 1 символ = 1 байт.
+
+
+
+
Коды ответа HTTP
+
+ | Код | Смысл |
+ | 200 OK | Успешно |
+ | 201 Created | Ресурс создан (ответ на POST) |
+ | 400 Bad Request | Ошибка в запросе |
+ | 404 Not Found | Ресурс не найден |
+ | 500 Internal Server Error | Ошибка сервера |
+
+
+
+`,
+
+'dns-header': `
+
+
DNS — система доменных имён
+
+
+
Зачем нужен DNS?
+
DNS переводит имена в IP: www.yandex.ru → 77.88.55.66. Бинарный протокол, работает поверх UDP, порт 53. При ответах >512 байт использует TCP.
+
+
+
+
Структура заголовка DNS (12 байт)
+
+
Байты 0–1: Transaction ID — одинаковый в запросе и ответе
+
Байты 2–3: Flags
+
Байты 4–5: QDCOUNT — число вопросов
+
Байты 6–7: ANCOUNT — число ответов (в запросе = 0)
+
Байты 8–9: NSCOUNT — авторитативные серверы
+
Байты 10–11: ARCOUNT — дополнительные записи
+
+
+
+
+
Поле Flags (байты 2–3)
+
+ | Биты | Поле | Значение |
+ | 15 | QR | 0 = запрос, 1 = ответ |
+ | 8 | RD | 1 = просить рекурсию у сервера |
+ | 3–0 | RCODE | 0=OK, 3=NXDOMAIN |
+
+
0x0100 = рекурсивный запрос (RD=1). 0x0000 = итеративный.
+
+
+`,
+
+'dns-query': `
+
+
DNS: кодирование доменного имени
+
+
+
Секция Question
+
После 12-байтного заголовка: QNAME + QTYPE + QCLASS.
+
+
+
+
Кодирование QNAME
+
Каждая метка: 1 байт длины + байты символов. В конце — нулевой байт.
+
+
example.com →
+
07 65 78 61 6D 70 6C 65 (длина 7 + "example")
+
03 63 6F 6D (длина 3 + "com")
+
00 (конец имени)
+
Итого: 13 байт
+
+
+
+
+
QTYPE и QCLASS
+
+ | QTYPE | Тип | Что запрашивает |
+ | 1 | A | IPv4-адрес |
+ | 28 | AAAA | IPv6-адрес |
+ | 15 | MX | Почтовый сервер |
+ | 2 | NS | Авторитативный DNS-сервер |
+ | 5 | CNAME | Псевдоним |
+ | 16 | TXT | Текстовая запись |
+
+
QCLASS = 1 = IN (Internet) — единственный используемый класс.
+
+
+`,
+
+};
\ No newline at end of file
diff --git a/src/lib/utils/bufferProcessor.js b/src/lib/utils/bufferProcessor.js
index 9bd73f0..ac8414b 100644
--- a/src/lib/utils/bufferProcessor.js
+++ b/src/lib/utils/bufferProcessor.js
@@ -1,9 +1,22 @@
import { updateEthernetBuffer } from './protocols/ethernet.js';
+import { updateIPv4Buffer } from './protocols/ipv4.js';
+import { updateTCPBuffer } from './protocols/tcp.js';
+import { updateUDPBuffer } from './protocols/udp.js';
export function processBuffer(buffer, lessonId) {
- switch(lessonId) {
- case 3: //Ethernet frame lesson
+ switch (lessonId) {
+ case 3: // Ethernet frame
return updateEthernetBuffer(buffer);
+ case 4: // IPv4 header structure
+ case 5: // IPv4 TTL
+ case 6: // IPv4 fragmentation
+ return updateIPv4Buffer(buffer);
+ case 7: // TCP header structure
+ case 8: // TCP flags
+ return updateTCPBuffer(buffer);
+ case 9: // UDP header
+ return updateUDPBuffer(buffer);
+ // id 10 = HTTP GET, id 11 = HTTP POST — текстовые, не трогаем буфер
default:
return buffer;
}
diff --git a/src/lib/utils/protocols/dns.js b/src/lib/utils/protocols/dns.js
new file mode 100644
index 0000000..e9f29fc
--- /dev/null
+++ b/src/lib/utils/protocols/dns.js
@@ -0,0 +1,65 @@
+export function formatDNSFlags(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const raw = (bytes[0] << 8) | bytes[1];
+
+ const qr = (raw >> 15) & 1;
+ const opcode = (raw >> 11) & 0xf;
+ const aa = (raw >> 10) & 1;
+ const tc = (raw >> 9) & 1;
+ const rd = (raw >> 8) & 1;
+ const ra = (raw >> 7) & 1;
+ const rcode = raw & 0xf;
+
+ const opcodeNames = { 0: 'QUERY', 1: 'IQUERY', 2: 'STATUS' };
+ const rcodeNames = { 0: 'NOERROR', 1: 'FORMERR', 2: 'SERVFAIL', 3: 'NXDOMAIN', 5: 'REFUSED' };
+
+ const parts = [
+ `QR=${qr} (${qr === 0 ? 'Query' : 'Response'})`,
+ `Opcode=${opcodeNames[opcode] ?? opcode}`,
+ `RD=${rd}`,
+ `RA=${ra}`,
+ ];
+ if (aa) parts.push('AA=1');
+ if (tc) parts.push('TC=1');
+ if (rcode) parts.push(`RCODE=${rcodeNames[rcode] ?? rcode}`);
+
+ return parts.join(', ');
+}
+
+export function formatDNSQType(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const val = (bytes[0] << 8) | bytes[1];
+ const types = {
+ 1: 'A (IPv4-адрес)',
+ 2: 'NS (Name Server)',
+ 5: 'CNAME (Canonical Name)',
+ 12: 'PTR (Pointer)',
+ 15: 'MX (Mail Exchange)',
+ 16: 'TXT (Text)',
+ 28: 'AAAA (IPv6-адрес)',
+ 255: 'ANY',
+ };
+ return types[val] ?? `Unknown (${val})`;
+}
+
+export function formatDNSQClass(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const val = (bytes[0] << 8) | bytes[1];
+ return val === 1 ? 'IN (Internet)' : `Unknown (${val})`;
+}
+
+export function formatDNSQName(bytes) {
+ if (!bytes || bytes.length === 0) return 'N/A';
+ const labels = [];
+ let i = 0;
+ while (i < bytes.length) {
+ const len = bytes[i];
+ if (len === 0) break;
+ //защита от выхода за границы
+ if (i + 1 + len > bytes.length) return labels.join('.') + ' (truncated)';
+ const label = String.fromCharCode(...bytes.slice(i + 1, i + 1 + len));
+ labels.push(label);
+ i += 1 + len;
+ }
+ return labels.length ? labels.join('.') : '(empty)';
+}
\ No newline at end of file
diff --git a/src/lib/utils/protocols/ethernet.js b/src/lib/utils/protocols/ethernet.js
index 30329a9..7f03cbb 100644
--- a/src/lib/utils/protocols/ethernet.js
+++ b/src/lib/utils/protocols/ethernet.js
@@ -1,5 +1,5 @@
//функции для работы с Ethernet
-export function calculateFCS(headerBuffer) {
+function calculateFCS(headerBuffer) {
//упрощенная имитация CRC32
let sum = 0;
for (let i = 0; i < headerBuffer.length; i++) {
@@ -18,16 +18,8 @@ export function calculateFCS(headerBuffer) {
];
}
-export function updateEthernetBuffer(buffer, changes = {}) {
+export function updateEthernetBuffer(buffer) {
const updatedBuffer = new Uint8Array(buffer);
-
- Object.keys(changes).forEach(key => {
- const index = parseInt(key);
- if (index >= 0 && index < updatedBuffer.length) {
- updatedBuffer[index] = changes[key];
- }
- });
-
const headerBuffer = updatedBuffer.slice(0, 14);
const fcs = calculateFCS(headerBuffer);
fcs.forEach((byte, i) => {
diff --git a/src/lib/utils/protocols/ipv4.js b/src/lib/utils/protocols/ipv4.js
new file mode 100644
index 0000000..15f9e42
--- /dev/null
+++ b/src/lib/utils/protocols/ipv4.js
@@ -0,0 +1,63 @@
+function calculateIPv4Checksum(buffer) {
+ const ihl = (buffer[0] & 0x0f) * 4;
+ let sum = 0;
+
+ for (let i = 0; i < ihl; i += 2) {
+ if (i === 10) continue;
+ sum += (buffer[i] << 8) | buffer[i + 1];
+ }
+
+ while (sum >> 16) {
+ sum = (sum & 0xffff) + (sum >> 16);
+ }
+
+ return (~sum) & 0xffff;
+}
+
+export function updateIPv4Buffer(buffer) {
+ const updated = new Uint8Array(buffer);
+
+ updated[10] = 0x00;
+ updated[11] = 0x00;
+
+ const checksum = calculateIPv4Checksum(updated);
+ updated[10] = (checksum >> 8) & 0xff;
+ updated[11] = checksum & 0xff;
+
+ return updated;
+}
+
+export function formatIPv4Address(bytes) {
+ if (bytes.length < 4) return 'N/A';
+ return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;
+}
+
+export function formatIPv4Protocol(proto) {
+ const protocols = {
+ 1: 'ICMP',
+ 6: 'TCP',
+ 17: 'UDP',
+ 47: 'GRE',
+ 50: 'ESP',
+ 51: 'AH',
+ 89: 'OSPF',
+ 132: 'SCTP',
+ };
+ const name = protocols[proto] ?? 'Unknown';
+ return `${name} (${proto} / 0x${proto.toString(16).padStart(2, '0').toUpperCase()})`;
+}
+
+export function formatVersionIHL(byte) {
+ const version = (byte >> 4) & 0x0f;
+ const ihl = (byte & 0x0f) * 4;
+ return `IPv${version}, Header Length: ${ihl} bytes`;
+}
+
+export function formatFlagsFragment(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const raw = (bytes[0] << 8) | bytes[1];
+ const df = (raw >> 14) & 1;
+ const mf = (raw >> 13) & 1;
+ const offset = raw & 0x1fff;
+ return `DF=${df}, MF=${mf}, Offset=${offset}`;
+}
\ No newline at end of file
diff --git a/src/lib/utils/protocols/tcp.js b/src/lib/utils/protocols/tcp.js
new file mode 100644
index 0000000..9c2589d
--- /dev/null
+++ b/src/lib/utils/protocols/tcp.js
@@ -0,0 +1,80 @@
+//контрольная сумма TCP считается с псевдозаголовком IPv4 (src IP, dst IP,
+//protocol=6, длина TCP-сегмента). В уроках IP-адреса фиксированы:
+const LESSON_SRC_IP = [192, 168, 1, 1];
+const LESSON_DST_IP = [192, 168, 1, 2];
+
+export function calculateTCPChecksum(tcpBuffer, srcIP = LESSON_SRC_IP, dstIP = LESSON_DST_IP) {
+ const tcpLen = tcpBuffer.length;
+
+ //псевдозаголовок: src(4) + dst(4) + zero(1) + proto(1) + tcpLen(2) = 12 байт
+ const pseudo = new Uint8Array(12 + tcpLen);
+ pseudo.set(srcIP, 0);
+ pseudo.set(dstIP, 4);
+ pseudo[8] = 0x00;
+ pseudo[9] = 0x06; //протокол TCP
+ pseudo[10] = (tcpLen >> 8) & 0xff;
+ pseudo[11] = tcpLen & 0xff;
+ pseudo.set(tcpBuffer, 12);
+
+ pseudo[28] = 0x00;
+ pseudo[29] = 0x00;
+
+ let sum = 0;
+ for (let i = 0; i < pseudo.length - 1; i += 2) {
+ sum += (pseudo[i] << 8) | pseudo[i + 1];
+ }
+ //дополняем нечётный байт
+ if (pseudo.length % 2 !== 0) {
+ sum += pseudo[pseudo.length - 1] << 8;
+ }
+ while (sum >> 16) {
+ sum = (sum & 0xffff) + (sum >> 16);
+ }
+ return (~sum) & 0xffff;
+}
+
+export function updateTCPBuffer(buffer) {
+ const updated = new Uint8Array(buffer);
+ updated[16] = 0x00;
+ updated[17] = 0x00;
+ const cs = calculateTCPChecksum(updated);
+ updated[16] = (cs >> 8) & 0xff;
+ updated[17] = cs & 0xff;
+ return updated;
+}
+
+export function formatPort(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const port = (bytes[0] << 8) | bytes[1];
+ const wellKnown = {
+ 20: 'FTP-data', 21: 'FTP', 22: 'SSH', 23: 'Telnet',
+ 25: 'SMTP', 53: 'DNS', 80: 'HTTP', 110: 'POP3',
+ 143: 'IMAP', 443: 'HTTPS', 3306: 'MySQL', 5432: 'PostgreSQL',
+ 6379: 'Redis', 8080: 'HTTP-alt',
+ };
+ const name = wellKnown[port];
+ return name ? `${port} (${name})` : String(port);
+}
+
+export function formatSeqAck(bytes) {
+ if (bytes.length < 4) return 'N/A';
+ const val = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0;
+ return String(val);
+}
+
+export function formatDataOffset(byte) {
+ const offset = (byte >> 4) & 0x0f;
+ return `${offset} (${offset * 4} bytes)`;
+}
+
+export function formatTCPFlags(byte) {
+ const names = ['CWR', 'ECE', 'URG', 'ACK', 'PSH', 'RST', 'SYN', 'FIN'];
+ const active = names.filter((_, i) => byte & (1 << (7 - i)));
+ return active.length ? active.join(' | ') : 'none';
+}
+
+export function formatWindowSize(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const val = (bytes[0] << 8) | bytes[1];
+ return `${val} bytes`;
+}
\ No newline at end of file
diff --git a/src/lib/utils/protocols/udp.js b/src/lib/utils/protocols/udp.js
new file mode 100644
index 0000000..27f4089
--- /dev/null
+++ b/src/lib/utils/protocols/udp.js
@@ -0,0 +1,56 @@
+const LESSON_SRC_IP = [192, 168, 1, 1];
+const LESSON_DST_IP = [192, 168, 1, 2];
+
+export function calculateUDPChecksum(udpBuffer, srcIP = LESSON_SRC_IP, dstIP = LESSON_DST_IP) {
+ const udpLen = udpBuffer.length;
+
+ const pseudo = new Uint8Array(12 + udpLen);
+ pseudo.set(srcIP, 0);
+ pseudo.set(dstIP, 4);
+ pseudo[8] = 0x00;
+ pseudo[9] = 0x11; //протокол UDP = 17
+ pseudo[10] = (udpLen >> 8) & 0xff;
+ pseudo[11] = udpLen & 0xff;
+ pseudo.set(udpBuffer, 12);
+
+ pseudo[18] = 0x00;
+ pseudo[19] = 0x00;
+
+ let sum = 0;
+ for (let i = 0; i < pseudo.length - 1; i += 2) {
+ sum += (pseudo[i] << 8) | pseudo[i + 1];
+ }
+ if (pseudo.length % 2 !== 0) {
+ sum += pseudo[pseudo.length - 1] << 8;
+ }
+ while (sum >> 16) {
+ sum = (sum & 0xffff) + (sum >> 16);
+ }
+ const result = (~sum) & 0xffff;
+ //по RFC 768: если вычисленная сумма = 0, передаётся 0xFFFF
+ return result === 0 ? 0xffff : result;
+}
+
+export function updateUDPBuffer(buffer) {
+ const updated = new Uint8Array(buffer);
+
+ //Length = заголовок (8) + данные
+ const len = buffer.length;
+ updated[4] = (len >> 8) & 0xff;
+ updated[5] = len & 0xff;
+
+ //Checksum
+ updated[6] = 0x00;
+ updated[7] = 0x00;
+ const cs = calculateUDPChecksum(updated);
+ updated[6] = (cs >> 8) & 0xff;
+ updated[7] = cs & 0xff;
+
+ return updated;
+}
+
+export function formatUDPLength(bytes) {
+ if (bytes.length < 2) return 'N/A';
+ const val = (bytes[0] << 8) | bytes[1];
+ return `${val} bytes (заголовок 8 + данные ${val - 8})`;
+}
\ No newline at end of file
diff --git a/src/lib/utils/storage.js b/src/lib/utils/storage.js
index d277133..7a912a7 100644
--- a/src/lib/utils/storage.js
+++ b/src/lib/utils/storage.js
@@ -1,5 +1,5 @@
export const progressStorage = {
- getProgress() {
+ _getAll() {
if (typeof window === 'undefined') return {};
try {
return JSON.parse(localStorage.getItem('networking-progress') || '{}');
@@ -8,15 +8,44 @@ export const progressStorage = {
}
},
- setProgress(lessonId, completed) {
+ _save(data) {
if (typeof window === 'undefined') return;
- const progress = this.getProgress();
- progress[lessonId] = completed;
- localStorage.setItem('networking-progress', JSON.stringify(progress));
+ localStorage.setItem('networking-progress', JSON.stringify(data));
+ },
+
+ _getLessonData(lessonId) {
+ const all = this._getAll();
+ const raw = all[String(lessonId)];
+ if (raw === undefined || raw === null) return { completed: false, tasksCompleted: 0 };
+ if (typeof raw === 'boolean') return { completed: raw, tasksCompleted: raw ? 3 : 0 };
+ return { completed: !!raw.completed, tasksCompleted: raw.tasksCompleted ?? 0 };
},
isCompleted(lessonId) {
- const progress = this.getProgress();
- return !!progress[lessonId];
- }
+ return this._getLessonData(lessonId).completed;
+ },
+
+ getTasksCompleted(lessonId) {
+ return this._getLessonData(lessonId).tasksCompleted;
+ },
+
+ //сохраняет новое значение счётчика; автоматически выставляет completed
+ saveTasksCompleted(lessonId, tasksCompleted, tasksRequired = 3) {
+ const all = this._getAll();
+ const completed = tasksCompleted >= tasksRequired;
+ all[String(lessonId)] = { completed, tasksCompleted };
+ this._save(all);
+ return completed;
+ },
+
+ //используется на главной странице (список уроков)
+ getProgress() {
+ const all = this._getAll();
+ const result = {};
+ for (const [id, val] of Object.entries(all)) {
+ if (typeof val === 'boolean') result[id] = val;
+ else result[id] = !!val?.completed;
+ }
+ return result;
+ },
};
\ No newline at end of file
diff --git a/src/lib/utils/taskGenerator.js b/src/lib/utils/taskGenerator.js
new file mode 100644
index 0000000..b177c22
--- /dev/null
+++ b/src/lib/utils/taskGenerator.js
@@ -0,0 +1,365 @@
+//генерирует конкретные задачи из шаблона урока
+//AI не участвует — числа выбираются случайно в заданном диапазоне
+
+function rnd(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
+function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
+
+function randomUnicastMAC() {
+ const m = Array.from({ length: 6 }, () => rnd(0, 255));
+ m[0] = m[0] & 0xFE; // бит 0 = 0 (unicast)
+ if (m[0] === 0) m[0] = 0x02;
+ return m;
+}
+function randomMulticastMAC() {
+ const m = Array.from({ length: 6 }, () => rnd(0, 255));
+ m[0] = (m[0] | 0x01) & 0xFD;
+ if (m[0] === 0xFF) m[0] = 0x01;
+ return m;
+}
+function macStr(m) { return m.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(':'); }
+
+function randomPublicIP() {
+ let ip;
+ do {
+ ip = [rnd(1,223), rnd(0,255), rnd(0,255), rnd(1,254)];
+ } while (
+ ip[0]===0 || ip[0]===10 || ip[0]===127 ||
+ (ip[0]===172 && ip[1]>=16 && ip[1]<=31) ||
+ (ip[0]===192 && ip[1]===168) ||
+ (ip[0]===169 && ip[1]===254) ||
+ (ip[0]===100 && ip[1]>=64 && ip[1]<=127) ||
+ ip[0]>=224
+ );
+ return ip;
+}
+function randomPrivateIP() {
+ const t = rnd(0,2);
+ if (t===0) return [10, rnd(0,255), rnd(0,255), rnd(1,254)];
+ if (t===1) return [172, rnd(16,31), rnd(0,255), rnd(1,254)];
+ return [192, 168, rnd(0,255), rnd(1,254)];
+}
+function randomEphemeralPort() { return rnd(49152, 65535); }
+
+function randomHostname() {
+ const subs = ['api','cdn','app','mail','news','shop','auth','www','dev','data','files'];
+ const names = ['example','service','platform','network','cloud','tech','store','media','hub'];
+ const tlds = ['com','net','io','org','ru'];
+ return `${pick(subs)}.${pick(names)}.${pick(tlds)}`;
+}
+function randomPath() {
+ const a = ['users','products','orders','articles','posts','files','events','reports','tasks'];
+ const b = ['list','search','latest','popular','archive','create','delete'];
+ return Math.random()>0.5 ? '/'+pick(a) : '/'+pick(a)+'/'+pick(b);
+}
+function randomJSONBody() {
+ const bodies = [
+ { name: pick(['Alice','Bob','Carol','Dave','Eve','Max']) },
+ { user: pick(['admin','guest','student','teacher']), active: true },
+ { title: pick(['Hello','Update','Report','Notice','Draft']) },
+ { id: rnd(1,9999), status: pick(['active','pending','done']) },
+ { email: `user${rnd(1,99)}@example.com` },
+ { count: rnd(1,100), page: rnd(1,10) },
+ ];
+ return JSON.stringify(pick(bodies));
+}
+
+
+export function generateTask(taskTemplate) {
+ switch (taskTemplate.type) {
+
+ case 'bit-set': {
+ const target = rnd(1, 255);
+ const hex = '0x' + target.toString(16).toUpperCase().padStart(2,'0');
+ return {
+ objective: `Установите биты так, чтобы получить число ${target} (${hex})`,
+ initialBuffer: new Uint8Array([0]),
+ validate: buf => buf[0] === target,
+ aiContext: { type:'bit-set', target, hex, binary: target.toString(2).padStart(8,'0'),
+ description: `Выставить биты байта = ${target} (${hex})` },
+ };
+ }
+
+ case 'mac-set': {
+ const v = rnd(0,2);
+ if (v===0) {
+ const m = randomUnicastMAC(), s = macStr(m);
+ return { objective:`Установите unicast MAC-адрес: ${s}`,
+ initialBuffer: new Uint8Array(6),
+ validate: buf => m.every((b,i) => buf[i]===b),
+ aiContext:{ type:'mac-set', mac:s, description:`unicast MAC ${s}` }};
+ }
+ if (v===1) return { objective:'Установите broadcast MAC-адрес',
+ initialBuffer: new Uint8Array(6),
+ validate: buf => Array.from(buf.slice(0,6)).every(b=>b===0xFF),
+ aiContext:{ type:'mac-set', mac:'FF:FF:FF:FF:FF:FF', description:'broadcast' }};
+ const m = randomMulticastMAC(), s = macStr(m);
+ return { objective:`Установите multicast MAC-адрес: ${s}`,
+ initialBuffer: new Uint8Array(6),
+ validate: buf => m.every((b,i) => buf[i]===b),
+ aiContext:{ type:'mac-set', mac:s, description:`multicast MAC ${s}` }};
+ }
+
+ case 'ethernet-frame': {
+ const etTypes = [
+ { et:[0x08,0x00], name:'IPv4' },
+ { et:[0x08,0x06], name:'ARP' },
+ { et:[0x86,0xDD], name:'IPv6' },
+ ];
+ const chosen = pick(etTypes);
+ const dv = rnd(0,2);
+ let dst, dstDesc;
+ if (dv===0) { dst=[0xFF,0xFF,0xFF,0xFF,0xFF,0xFF]; dstDesc='broadcast'; }
+ else if (dv===1) { dst=randomMulticastMAC(); dstDesc=macStr(dst)+' (multicast)'; }
+ else { dst=randomUnicastMAC(); dstDesc=macStr(dst)+' (unicast)'; }
+ const src=randomUnicastMAC();
+ const etHex='0x'+chosen.et.map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join('');
+ return {
+ objective:`Соберите Ethernet-кадр: Dst MAC=${dstDesc}, Src MAC=${macStr(src)}, EtherType=${chosen.name}`,
+ initialBuffer: new Uint8Array(14+46+4),
+ validate: buf => buf.length>=14 && dst.every((b,i)=>buf[i]===b)
+ && src.every((b,i)=>buf[6+i]===b) && buf[12]===chosen.et[0] && buf[13]===chosen.et[1],
+ aiContext:{ type:'ethernet-frame', dstMAC:macStr(dst), srcMAC:macStr(src),
+ etherType:etHex, protoName:chosen.name,
+ description:`dst=${macStr(dst)} src=${macStr(src)} et=${etHex}` },
+ };
+ }
+
+ case 'ipv4-addresses': {
+ const src=randomPrivateIP(), dst=randomPublicIP();
+ const ttl=pick([64,128,255]);
+ const p=pick([{val:6,name:'TCP'},{val:17,name:'UDP'},{val:1,name:'ICMP'}]);
+ return {
+ objective:`Соберите IPv4 заголовок: Src=${src.join('.')}, Dst=${dst.join('.')}, Protocol=${p.name}, TTL=${ttl}`,
+ initialBuffer: new Uint8Array([0x45,0x00,0x00,0x14,0,0,0x40,0,0,0,0,0,0,0,0,0,0,0,0,0]),
+ validate: buf => buf.length>=20 && buf[8]===ttl && buf[9]===p.val
+ && src.every((b,i)=>buf[12+i]===b) && dst.every((b,i)=>buf[16+i]===b),
+ aiContext:{ type:'ipv4-addresses', srcIP:src.join('.'), dstIP:dst.join('.'),
+ ttl, protocol:p.val, protoName:p.name,
+ description:`src=${src.join('.')} dst=${dst.join('.')} ttl=${ttl} proto=${p.name}` },
+ };
+ }
+
+ case 'ipv4-ttl': {
+ const ttl = Math.random()>0.4 ? pick([64,128,255,32]) : rnd(1,254);
+ const label={64:' (Linux/macOS)',128:' (Windows)',255:' (сетевые устройства)'}[ttl]??'';
+ const src=randomPrivateIP(), dst=randomPublicIP();
+ return {
+ objective:`Установите TTL = ${ttl}${label}`,
+ initialBuffer: new Uint8Array([0x45,0x00,0x00,0x14,0xAB,0xCD,0x40,0x00,
+ 0x00,0x06,0x00,0x00,...src,...dst]),
+ validate: buf => buf[8]===ttl,
+ aiContext:{ type:'ipv4-ttl', ttl, hex:'0x'+ttl.toString(16).toUpperCase().padStart(2,'0'),
+ description:`TTL=${ttl} байт 8` },
+ };
+ }
+
+ case 'ipv4-fragmentation': {
+ const scenarios=[
+ {b:[0x20,0x00], desc:'первый фрагмент: MF=1, DF=0, Offset=0'},
+ {b:[0x40,0x00], desc:"Don't Fragment (DF=1): фрагментация запрещена, MF=0, Offset=0"},
+ {b:[0x00,0xB9], desc:'последний фрагмент: MF=0, DF=0, Offset=185 (1480 байт ÷ 8)'},
+ {b:[0x20,0xB9], desc:'промежуточный фрагмент: MF=1, DF=0, Offset=185'},
+ {b:[0x00,0x2E], desc:'фрагмент с Offset=46 (368 байт ÷ 8): MF=0, DF=0'},
+ ];
+ const s=pick(scenarios);
+ const src=randomPrivateIP(), dst=randomPublicIP();
+ return {
+ objective: 'Установите ' + s.desc,
+ initialBuffer:new Uint8Array([0x45,0x00,0x00,0x14,rnd(0,255),rnd(0,255),
+ 0x40,0x00,0x40,0x11,0x00,0x00,...src,...dst]),
+ validate:buf=>buf[6]===s.b[0]&&buf[7]===s.b[1],
+ aiContext:{type:'ipv4-fragmentation',b6:s.b[0],b7:s.b[1],description:s.desc},
+ };
+ }
+
+ case 'tcp-header': {
+ const svcs=[{p:80,n:'HTTP'},{p:443,n:'HTTPS'},{p:22,n:'SSH'},
+ {p:25,n:'SMTP'},{p:3306,n:'MySQL'},{p:5432,n:'PostgreSQL'}];
+ const svc=pick(svcs), sp=randomEphemeralPort(), seq=rnd(0,0xFFFFFF);
+ const win=pick([8192,16384,32768,65535]);
+ return {
+ objective:`Соберите TCP SYN к ${svc.n}: Src Port=${sp}, Dst Port=${svc.p}, Seq=${seq}, SYN (0x02), Window=${win}`,
+ initialBuffer:new Uint8Array(20).fill(0).map((_,i)=>i===12?0x50:0),
+ validate:buf=>buf.length>=20
+ &&((buf[0]<<8)|buf[1])===sp&&((buf[2]<<8)|buf[3])===svc.p
+ &&(((buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7])>>>0)===seq
+ &&buf[13]===0x02&&((buf[14]<<8)|buf[15])===win,
+ aiContext:{type:'tcp-header',srcPort:sp,dstPort:svc.p,seq,flags:0x02,window:win,service:svc.n,
+ description:`TCP SYN sp=${sp} dp=${svc.p} seq=${seq}`},
+ };
+ }
+
+ case 'tcp-flags': {
+ const combos=[
+ {f:0x02,n:'SYN', d:'инициация соединения (1-й шаг handshake)'},
+ {f:0x10,n:'ACK', d:'подтверждение получения данных'},
+ {f:0x12,n:'SYN+ACK', d:'ответ сервера (2-й шаг handshake)'},
+ {f:0x01,n:'FIN', d:'инициация завершения соединения'},
+ {f:0x11,n:'FIN+ACK', d:'завершение с подтверждением'},
+ {f:0x04,n:'RST', d:'немедленный сброс соединения'},
+ {f:0x18,n:'PSH+ACK', d:'передача данных без буферизации'},
+ ];
+ const c=pick(combos), sp=randomEphemeralPort(), dp=pick([80,443,22,25,3306]);
+ const seq=rnd(0,0xFFFFFF);
+ return {
+ objective:`Установите TCP-флаги: ${c.n} — ${c.d}. Байт флагов = 0x${c.f.toString(16).toUpperCase().padStart(2,'0')}`,
+ initialBuffer:new Uint8Array([(sp>>8)&0xFF,sp&0xFF,(dp>>8)&0xFF,dp&0xFF,
+ (seq>>24)&0xFF,(seq>>16)&0xFF,(seq>>8)&0xFF,seq&0xFF,0,0,0,0,
+ 0x50,0x00,0xFF,0xFF,0,0,0,0]),
+ validate:buf=>buf[13]===c.f,
+ aiContext:{type:'tcp-flags',flagName:c.n,
+ flagHex:'0x'+c.f.toString(16).toUpperCase().padStart(2,'0'),
+ description:`TCP флаги ${c.n} байт 13`},
+ };
+ }
+
+ case 'udp-ports': {
+ const svcs=[{d:53,n:'DNS'},{d:123,n:'NTP'},{d:67,n:'DHCP'},
+ {d:161,n:'SNMP'},{d:514,n:'Syslog'},{d:5353,n:'mDNS'},
+ {d:1194,n:'OpenVPN'},{d:4500,n:'IPSec NAT-T'}];
+ const svc=pick(svcs), sp=randomEphemeralPort();
+ return {
+ objective:`Соберите UDP для ${svc.n}: Src Port=${sp}, Dst Port=${svc.d}`,
+ initialBuffer:new Uint8Array(8),
+ validate:buf=>((buf[0]<<8)|buf[1])===sp&&((buf[2]<<8)|buf[3])===svc.d,
+ aiContext:{type:'udp-ports',srcPort:sp,dstPort:svc.d,service:svc.n,
+ description:`UDP src=${sp} dst=${svc.d} (${svc.n})`},
+ };
+ }
+
+ case 'http-get': {
+ const host=randomHostname(), path=randomPath();
+ const accept=pick(['application/json','text/html','application/xml','text/plain']);
+ return {
+ objective:`Отправьте GET-запрос: ресурс ${path} с сервера ${host}, Accept: ${accept}`,
+ initialBuffer:new TextEncoder().encode('GET / HTTP/1.1\n'),
+ validate:buf=>{
+ const txt=new TextDecoder().decode(buf).replace(/\r\n/g,'\n');
+ const lines=txt.split('\n');
+ const rl=lines[0]?.match(/^(\S+)\s+(\S+)\s+(\S+)$/);
+ if(!rl||rl[1]!=='GET'||rl[2]!==path||rl[3]!=='HTTP/1.1') return false;
+ const h={};
+ for(let i=1;i
0) h[lines[i].slice(0,c).trim().toLowerCase()]=lines[i].slice(c+1).trim();
+ }
+ return h['host']===host&&(h['accept']??'').includes(accept)&&txt.includes('\n\n');
+ },
+ aiContext:{type:'http-get',host,path,accept,description:`GET ${path} от ${host}`},
+ };
+ }
+
+ case 'http-post': {
+ const host=randomHostname(), path=randomPath();
+ const body=randomJSONBody(), bl=new TextEncoder().encode(body).length;
+ return {
+ objective:`Отправьте POST на ${host}${path}: тело ${body}, Content-Type: application/json`,
+ initialBuffer:new TextEncoder().encode('POST / HTTP/1.1\n'),
+ validate:buf=>{
+ const txt=new TextDecoder().decode(buf).replace(/\r\n/g,'\n');
+ const lines=txt.split('\n');
+ const rl=lines[0]?.match(/^(\S+)\s+(\S+)\s+(\S+)$/);
+ if(!rl||rl[1]!=='POST'||rl[2]!==path||rl[3]!=='HTTP/1.1') return false;
+ const h={};let bs=lines.length;
+ for(let i=1;i0) h[lines[i].slice(0,c).trim().toLowerCase()]=lines[i].slice(c+1).trim();
+ }
+ if(h['host']!==host) return false;
+ if(!(h['content-type']??'').includes('application/json')) return false;
+ const bodyText=lines.slice(bs).join('\n').trim();
+ try{ if(JSON.stringify(JSON.parse(bodyText))!==JSON.stringify(JSON.parse(body))) return false; }
+ catch{ return false; }
+ return parseInt(h['content-length']??'',10)===new TextEncoder().encode(bodyText).length
+ &&txt.includes('\n\n');
+ },
+ aiContext:{type:'http-post',host,path,body,bodyLength:bl,
+ description:`POST ${path}→${host}`},
+ };
+ }
+
+ case 'dns-header': {
+ const txId=rnd(1,0xFFFE);
+ const wantRD=Math.random()>0.4;
+ const flags=wantRD?0x0100:0x0000;
+ const fdesc=wantRD?'рекурсивный запрос (RD=1)':'итеративный запрос (RD=0)';
+ return {
+ objective:`Отправьте DNS-запрос: ID=${txId}, ${fdesc}, QDCOUNT=1`,
+ initialBuffer:new Uint8Array(12),
+ validate:buf=>buf.length>=6
+ &&((buf[0]<<8)|buf[1])===txId
+ &&((buf[2]<<8)|buf[3])===flags
+ &&((buf[4]<<8)|buf[5])===1,
+ aiContext:{type:'dns-header',txId,flags,flagsDesc:fdesc,
+ txH:(txId>>8)&0xFF,txL:txId&0xFF,
+ description:`DNS ID=${txId} Flags=0x${flags.toString(16).padStart(4,'0')}`},
+ };
+ }
+
+ case 'dns-query': {
+ const qtypes=[
+ {val:1, n:'A', d:'IPv4-адрес'},
+ {val:28,n:'AAAA', d:'IPv6-адрес'},
+ {val:15,n:'MX', d:'почтовый сервер'},
+ {val:2, n:'NS', d:'DNS-сервер зоны'},
+ {val:5, n:'CNAME', d:'псевдоним'},
+ {val:16,n:'TXT', d:'текстовая запись'},
+ ];
+ const qt=pick(qtypes), txId=rnd(1,0xFFFE);
+ return {
+ objective:`Отправьте запрос на example.com: QTYPE=${qt.n}, QCLASS=IN(1)`,
+ initialBuffer:new Uint8Array([
+ (txId>>8)&0xFF,txId&0xFF,0x01,0x00,0x00,0x01,0,0,0,0,0,0,
+ 0x07,0x65,0x78,0x61,0x6D,0x70,0x6C,0x65,
+ 0x03,0x63,0x6F,0x6D,0x00,
+ 0x00,0x00,0x00,0x00,
+ ]),
+ validate:buf=>buf.length>=29&&((buf[25]<<8)|buf[26])===qt.val&&((buf[27]<<8)|buf[28])===1,
+ aiContext:{type:'dns-query',qtypeName:qt.n,qtypeVal:qt.val,
+ description:`QTYPE=${qt.n}(${qt.val}), QCLASS=IN`},
+ };
+ }
+
+ default:
+ throw new Error(`Unknown task type: ${template.type}`);
+ }
+}
+
+export function buildCurrentState(buf, ctx) {
+ if (!buf||!ctx) return {};
+ switch(ctx.type){
+ case 'bit-set':
+ return{value:buf[0],binary:buf[0].toString(2).padStart(8,'0'),
+ hex:'0x'+buf[0].toString(16).toUpperCase().padStart(2,'0')};
+ case 'mac-set': case 'ethernet-frame':
+ return{dst:Array.from(buf.slice(0,6)).map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join(':'),
+ src:Array.from(buf.slice(6,12)).map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join(':')};
+ case 'ipv4-addresses': case 'ipv4-fragmentation':
+ return{srcIP:Array.from(buf.slice(12,16)).join('.'),dstIP:Array.from(buf.slice(16,20)).join('.'),
+ ttl:buf[8],proto:buf[9],
+ b6:'0x'+(buf[6]??0).toString(16).padStart(2,'0').toUpperCase(),
+ b7:'0x'+(buf[7]??0).toString(16).padStart(2,'0').toUpperCase()};
+ case 'ipv4-ttl':
+ return{ttl:buf[8],hex:'0x'+buf[8].toString(16).toUpperCase().padStart(2,'0')};
+ case 'tcp-header':
+ return{srcPort:(buf[0]<<8)|buf[1],dstPort:(buf[2]<<8)|buf[3],
+ seq:((buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7])>>>0,
+ flags:'0x'+buf[13].toString(16).padStart(2,'0').toUpperCase(),
+ window:(buf[14]<<8)|buf[15]};
+ case 'tcp-flags':
+ return{flags:buf[13],hex:'0x'+buf[13].toString(16).toUpperCase().padStart(2,'0')};
+ case 'udp-ports':
+ return{srcPort:(buf[0]<<8)|buf[1],dstPort:(buf[2]<<8)|buf[3]};
+ case 'http-get': case 'http-post':
+ return{text:new TextDecoder().decode(buf).slice(0,300)};
+ case 'dns-header':
+ return{id:(buf[0]<<8)|buf[1],
+ flags:'0x'+((buf[2]<<8)|buf[3]).toString(16).padStart(4,'0').toUpperCase(),
+ qdcount:(buf[4]<<8)|buf[5]};
+ case 'dns-query':
+ return{qtype:(buf[25]<<8)|buf[26],qclass:(buf[27]<<8)|buf[28]};
+ default: return{};
+ }
+}
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index c473550..b6c71e8 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -4,104 +4,385 @@
import { onMount } from 'svelte';
let progress = {};
+ let searchQuery = '';
+ let activeCategory = 'Все';
+ let activeDifficulty = 'Все';
onMount(() => {
progress = progressStorage.getProgress();
});
+
+ //уникальные категории и сложности из уроков
+ $: categories = ['Все', ...new Set(lessons.map(l => l.category))];
+ $: difficulties = ['Все', ...new Set(lessons.map(l => l.difficulty))];
+
+ //фильтрация
+ $: filtered = lessons.filter(l => {
+ const q = searchQuery.trim().toLowerCase();
+ const matchSearch = !q
+ || l.title.toLowerCase().includes(q)
+ || l.category.toLowerCase().includes(q)
+ || l.difficulty.toLowerCase().includes(q);
+ const matchCat = activeCategory === 'Все' || l.category === activeCategory;
+ const matchDiff = activeDifficulty === 'Все' || l.difficulty === activeDifficulty;
+ return matchSearch && matchCat && matchDiff;
+ });
+
+ //счётчик завершённых уроков
+ $: completedCount = lessons.filter(l => progress[l.id]).length;
+
+ function clearFilters() {
+ searchQuery = '';
+ activeCategory = 'Все';
+ activeDifficulty = 'Все';
+ }
+
+ //цвет тэга сложности
+ function difficultyColor(d) {
+ return { 'Начинающий': '#4caf50', 'Средний': '#ff9800', 'Продвинутый': '#f44336' }[d] ?? '#2196f3';
+ }
-
-Обучение сетевым протоколам
-
-
- {#each lessons as lesson}
-
-