Files
learning_network_protocols/src/lib/utils/taskGenerator.js
T

365 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//генерирует конкретные задачи из шаблона урока
//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<lines.length;i++){
if(!lines[i].trim()) break;
const c=lines[i].indexOf(':');
if(c>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;i<lines.length;i++){
if(!lines[i].trim()){bs=i+1;break;}
const c=lines[i].indexOf(':');
if(c>0) 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{};
}
}