//генерирует конкретные задачи из шаблона урока //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;i0) 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{}; } }