diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md index 8e499465ce..8ee7d34d1f 100644 --- a/.github/ISSUE_TEMPLATE/issue.md +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -12,7 +12,7 @@ Welcome to v86's issue tracker! We use this tracker for bug reports or feature requests. For user support, questions or general comments, use the chat at https://gitter.im/copy/v86 or the forum at https://github.com/copy/v86/discussions -Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-xp.md. +Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-nt.md and docs/windows-9x.md. Before reporting OS incompatibilities, check existing issues and the compatibility section of the readme. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20d02d3656..dd4ac97158 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: run: make rustfmt - name: Fetch kvm-unit-test cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-kvm-unit-test with: path: tests/kvm-unit-tests/ @@ -67,7 +67,7 @@ jobs: run: tests/kvm-unit-tests/run.js tests/kvm-unit-tests/x86/realmode.flat - name: Fetch namsmtests cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-nasmtests with: path: tests/nasm/build/ @@ -83,7 +83,7 @@ jobs: run: make rust-test - name: Fetch image cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-images with: path: images/ @@ -115,7 +115,7 @@ jobs: run: make expect-tests - name: Upload the artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: v86 path: | @@ -138,7 +138,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Get artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: v86 path: build diff --git a/Makefile b/Makefile index 6a9c31df75..c932dcb52c 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ CARGO_FLAGS_SAFE=\ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature=+multivalue -C target-feature=+simd128 CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \ - memory.js dma.js pit.js vga.js vga_text.js ps2.js rtc.js uart.js \ + memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \ acpi.js apic.js ioapic.js \ state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js \ bus.js log.js cpu.js debug.js \ @@ -318,10 +318,12 @@ rust-test-intensive: QUICKCHECK_TESTS=100000000 make rust-test api-tests: all-debug -# ./tests/api/clean-shutdown.js \ - ./tests/api/reset.js \ - ./tests/api/floppy-insert-eject.js \ - ./tests/api/serial.js \ + ./tests/api/clean-shutdown.js + ./tests/api/state.js + ./tests/api/reset.js + #./tests/api/floppy-insert-eject.js # disabled for now, sometimes hangs + ./tests/api/serial.js + ./tests/api/reboot.js all-tests: eslint kvm-unit-test qemutests qemutests-release jitpagingtests api-tests nasmtests nasmtests-force-jit tests expect-tests # Skipping: diff --git a/Readme.md b/Readme.md index 02eeaed549..fa47e7808a 100644 --- a/Readme.md +++ b/Readme.md @@ -62,7 +62,8 @@ list of emulated hardware: [Networking](docs/networking.md) — [Alpine Linux guest setup](tools/docker/alpine/) — [Arch Linux guest setup](docs/archlinux.md) — -[Windows 2000/XP guest setup](docs/windows-xp.md) — +[Windows NT guest setup](docs/windows-nt.md) — +[Windows 9x guest setup](docs/windows-9x.md) — [9p filesystem](docs/filesystem.md) — [Linux rootfs on 9p](docs/linux-9p-image.md) — [Profiling](docs/profiling.md) — @@ -94,8 +95,9 @@ Here's an overview of the operating systems supported in v86: - Windows 1, 3.x, 95, 98, ME, NT and 2000 work reasonably well. - In Windows 2000 and higher the PC type has to be changed from ACPI PC to Standard PC - There are some known boot issues ([#250](https://github.com/copy/v86/issues/250), [#433](https://github.com/copy/v86/issues/433), [#507](https://github.com/copy/v86/issues/507), [#555](https://github.com/copy/v86/issues/555), [#620](https://github.com/copy/v86/issues/620), [#645](https://github.com/copy/v86/issues/645)) + - See [Windows 9x guest setup](docs/windows-9x.md) - Windows XP, Vista and 8 work under certain conditions (see [#86](https://github.com/copy/v86/issues/86), [#208](https://github.com/copy/v86/issues/208)) - - See [Windows 2000/XP guest setup](docs/windows-xp.md) + - See [Windows NT guest setup](docs/windows-nt.md) - Many hobby operating systems work. - 9front works. - Plan 9 doesn't work. @@ -166,6 +168,7 @@ See [tests/Readme.md](tests/Readme.md) for more information. - [Programatically using the serial terminal](examples/serial.html) - [A Lua interpreter](examples/lua.html) - [Two instances in one window](examples/two_instances.html) +- [Networking between browser windows/tabs using the Broadcast Channel API](examples/broadcast-network.html) - [Saving and restoring emulator state](examples/save_restore.html) Using v86 for your own purposes is as easy as: diff --git a/debug.html b/debug.html index 57594cb869..829aa17ca6 100644 --- a/debug.html +++ b/debug.html @@ -1,7 +1,7 @@ -
+# This example allows network across multiple browser tabs by using BroadcastChannels. + +# Configure a static IP +ifconfig eth0 up arp 10.5.0.x + +# Ping by IP +ping 10.5.0.x + +# Run a DNS server and send a query (10.5.0.x for server, 10.5.0.y for record) +echo "anotherhost 10.5.0.y" | dnsd -c - -v - server +nslookup -type=a anotherhost 10.5.0.x - client + +# Telnet calculator +socat TCP-L:23,fork exec:bc + +# Simple HTTP server +socat TCP-L:80,crlf,fork system:'echo HTTP/1.1 200 OK;echo;lua /root/test.lua' +diff --git a/examples/nodejs.js b/examples/nodejs.js index f08b54d473..c2ec8246d0 100755 --- a/examples/nodejs.js +++ b/examples/nodejs.js @@ -38,7 +38,7 @@ process.stdin.on("data", function(c) if(c === "\u0003") { // ctrl c - emulator.stop(); + emulator.destroy(); process.stdin.pause(); } else diff --git a/examples/nodejs_state.js b/examples/nodejs_state.js index e829cb093b..229f2c1456 100755 --- a/examples/nodejs_state.js +++ b/examples/nodejs_state.js @@ -42,7 +42,7 @@ process.stdin.on("data", async function(c) if(c === "\u0003") { // ctrl c - emulator.stop(); + emulator.destroy(); process.stdin.pause(); } else if(c === "\x1b\x4f\x51") diff --git a/index.html b/index.html index ee08215592..e3e540d8ae 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ -
Arch Linux 12 MB >_ | - Complete Arch Linux with various compilers, networking and Xorg. Restored from snapshot. | ||||||||
Damn Small Linux 50 MB 💻 | - Graphical Linux with 2.4 kernel, Firefox 2.0 and more. Takes 1 minute to boot. | ||||||||
Buildroot Linux 5.0 MB >_ |
- Minimal Linux with busybox, Lua, tests, internet access, ping, telnet and curl. Exchange files through /mnt/ . | ||||||||
ReactOS 18 MB 💻 | - Windows-compatible OS with QtWeb and Breakout. Restored from snapshot. | ||||||||
Windows 2000 22 MB 💻 | - Including Pinball and Internet Explorer with internet access. Additional sectors are loaded as needed. | ||||||||
Windows 98 9.7 MB 💻 | - Including Minesweeper and Internet Explorer with internet access. Additional sectors are loaded as needed. | ||||||||
Windows 95 4.6 MB 💻 | - Restored from snapshot | ||||||||
Windows 3.1 15 MB 💻 | - Takes 15 seconds to boot | ||||||||
Windows 1.01 0.6 MB 💻 | - The first version of Microsoft Windows | ||||||||
MS-DOS 6.22 4.4 MB >_ | - With Enhanced Tools, QBasic, vim, games and demos. | ||||||||
FreeDOS 0.5 MB >_ | - With nasm, vim, debug.com, Rogue, some games and demos. | ||||||||
FreeBSD 17 MB >_ | - FreeBSD 12.0 base install. Restored from snapshot. | ||||||||
OpenBSD 12 MB >_ | - OpenBSD 6.6 base install. Restored from snapshot. | ||||||||
9front 4.4 MB 💻 | - A Plan 9 fork. | ||||||||
Haiku 38 MB 💻 | - An open-source operating system inspired by BeOS. Restored from snapshot. Includes network support. | ||||||||
SerenityOS 17 MB 💻 | - A graphical Unix-like operating system. Restored from snapshot. | ||||||||
HelenOS 7.9 MB 💻 | - A graphical operating system based on a multiserver microkernel design | ||||||||
FiwixOS 15 MB >_ | - A Unix-like OS written from scratch. Includes Doom. | ||||||||
Android-x86 42 MB 💻 | - An x86 port of the Android 1.6. Quite slow. Takes about 3 minutes to boot. | ||||||||
Oberon 1.2 MB 💻 | - Native Oberon 2.3.6 | ||||||||
KolibriOS 1.4 MB 💻 | - Fast graphical OS written in Assembly | ||||||||
QNX 1.3 MB 💻 | - QNX 4.05 Demo disk (no networking) | ||||||||
Snowdrop 0.3 MB >_ | - A homebrew operating system from scratch, written in assembly language | ||||||||
Solar OS 0.3 MB 💻 | - Simple graphical OS | ||||||||
Bootchess 512 B >_ | - A tiny chess program written in the boot sector | ||||||||
SectorLISP 512 B >_ | - A LISP interpreter that fits into the boot sector | ||||||||
Name | +Size | +UI | +Family | +Arch | +Status | +Source | +Lang | +Medium | +Notes | +
---|---|---|---|---|---|---|---|---|---|
Arch Linux | 15+ MB | +Linux | 32-bit | Modern | Open-source | C | 9pfs | Xorg, Firefox, various compilers and more | |
Damn Small Linux | 50 MB | +Linux | 32-bit | Historic | Open-source | C | CD | 4.11.rc2 with Firefox 2.0 | |
Buildroot Linux | 4.9 MB | +Linux | 32-bit | Modern | Open-source | C | bzImage | Lua, ping, curl, telnet | |
FreeBSD | 16+ MB | +BSD | 32-bit | Modern | Open-source | C | HD | FreeBSD 12.0 | |
OpenBSD | 11+ MB | +BSD | 32-bit | Modern | Open-source | C | HD | OpenBSD 6.6 | |
FiwixOS | 15+ MB | +Unix-like | 32-bit | Modern | Open-source | C | HD | With Doom | |
SerenityOS | 16+ MB | +Unix-like | 32-bit | Modern | Open-source | C++ | HD | Web browser, various games and demos | |
Haiku | 41+ MB | +BeOS | 32-bit | Modern | Open-source | C++ | HD | Networking (WebPositive), OCaml, 2048, NetHack | |
Tiny Aros | 17+ MB | +AmigaOS | 32-bit | Modern | Open-source | C | CD | AmigaOS-like graphical OS | |
ReactOS | 17+ MB | +Windows-like | 32-bit | Modern | Open-source | C++ | HD | QtWeb, LBreakout2, OpenTTD, Bochs, TCC | |
Windows 1.01 | 0.7 MB | +Windows | 16-bit | Historic | Proprietary | ASM, C | Floppy | Reversi, Paint | |
Windows 95 | 19+ MB | +Windows | 32-bit | Historic | Proprietary | ASM, C | HD | Age of Empires, FASM, POV-Ray, Hover! | |
Windows 2000 | 23+ MB | +Windows | 32-bit | Historic | Proprietary | C++ | HD | IE 5, Pinball | |
MS-DOS 6.22 | 2.4+ MB | +DOS | 16-bit | Historic | Proprietary | ASM | HD | Doom, Sim City, OCaml 1.0, Turbo C and more | |
FreeDOS | 0.6 MB | +DOS | 16-bit | Modern | Open-source | ASM, C | Floppy | nasm, vim, debug.com, Rogue, various demos | |
KolibriOS | 1.3 MB | +Custom | 32-bit | Modern | Open-source | ASM | Floppy | Various apps, games and demos | |
QNX 4.05 | 1.4 MB | +Custom | 32-bit | Historic | Proprietary | C | Floppy | 1999 demo disk |
@@ -126,7 +175,8 @@ | ||
+ | + Presets: none, public relay, wisp, fetch |
diff --git a/src/browser/dummy_screen.js b/src/browser/dummy_screen.js index 2f7757d0b0..405dd818f4 100644 --- a/src/browser/dummy_screen.js +++ b/src/browser/dummy_screen.js @@ -48,6 +48,14 @@ function DummyScreenAdapter() is_graphical = graphical; }; + this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, bitmap, bitmap_changed) + { + }; + + this.set_font_page = function(page_a, page_b) + { + }; + this.clear_screen = function() { }; diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 39ea9ba393..3e1cf7b725 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -5,7 +5,6 @@ const ETHERTYPE_IPV4 = 0x0800; const ETHERTYPE_ARP = 0x0806; const ETHERTYPE_IPV6 = 0x86DD; - const IPV4_PROTO_ICMP = 1; const IPV4_PROTO_TCP = 6; const IPV4_PROTO_UDP = 17; @@ -18,60 +17,248 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; +/* For the complete TCP state diagram see: + * + * https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg + * + * State TIME_WAIT is not needed, we can skip it and transition directly to CLOSED instead. + */ const TCP_STATE_CLOSED = "closed"; -const TCP_STATE_LISTEN = "listen"; +const TCP_STATE_SYN_RECEIVED = "syn-received"; +const TCP_STATE_SYN_SENT = "syn-sent"; +//const TCP_STATE_LISTEN = "listen"; const TCP_STATE_ESTABLISHED = "established"; const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; const TCP_STATE_CLOSE_WAIT = "close-wait"; const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; const TCP_STATE_LAST_ACK = "last-ack"; const TCP_STATE_CLOSING = "closing"; -const TCP_STATE_TIME_WAIT = "time-wait"; -const TCP_STATE_SYN_RECEIVED = "syn-received"; -const TCP_STATE_SYN_SENT = "syn-sent"; +//const TCP_STATE_TIME_WAIT = "time-wait"; + +// source: RFC6335, 6. Port Number Ranges +const TCP_DYNAMIC_PORT_START = 49152; +const TCP_DYNAMIC_PORT_END = 65535; +const TCP_DYNAMIC_PORT_RANGE = TCP_DYNAMIC_PORT_END - TCP_DYNAMIC_PORT_START; + +const ETH_HEADER_SIZE = 14; +const ETH_PAYLOAD_OFFSET = ETH_HEADER_SIZE; +const ETH_PAYLOAD_SIZE = 1500; +const ETH_TRAILER_SIZE = 4; +const ETH_FRAME_SIZE = ETH_HEADER_SIZE + ETH_PAYLOAD_SIZE + ETH_TRAILER_SIZE; +const IPV4_HEADER_SIZE = 20; +const IPV4_PAYLOAD_OFFSET = ETH_PAYLOAD_OFFSET + IPV4_HEADER_SIZE; +const IPV4_PAYLOAD_SIZE = ETH_PAYLOAD_SIZE - IPV4_HEADER_SIZE; +const UDP_HEADER_SIZE = 8; +const UDP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_SIZE; +const UDP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - UDP_HEADER_SIZE; +const TCP_HEADER_SIZE = 20; +const TCP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + TCP_HEADER_SIZE; +const TCP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - TCP_HEADER_SIZE; +const ICMP_HEADER_SIZE = 4; + +const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } -function siptolong(s) { - let parts = s.split(".").map(function(x) { return parseInt(x, 10); }); +function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } -function iptolong(parts) { - return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; +class GrowableRingbuffer +{ + /** + * @param {number} initial_capacity + * @param {number} maximum_capacity + */ + constructor(initial_capacity, maximum_capacity) + { + initial_capacity = Math.min(initial_capacity, 16); + this.maximum_capacity = maximum_capacity ? Math.max(maximum_capacity, initial_capacity) : 0; + this.tail = 0; + this.head = 0; + this.length = 0; + this.buffer = new Uint8Array(initial_capacity); + } + + /** + * @param {Uint8Array} src_array + */ + write(src_array) + { + const src_length = src_array.length; + const total_length = this.length + src_length; + let capacity = this.buffer.length; + if(capacity < total_length) { + while(capacity < total_length) { + capacity *= 2; + } + if(this.maximum_capacity && capacity > this.maximum_capacity) { + throw new Error("stream capacity overflow in GrowableRingbuffer.write(), package dropped"); + } + const new_buffer = new Uint8Array(capacity); + this.peek(new_buffer); + this.tail = 0; + this.head = this.length; + this.buffer = new_buffer; + } + const buffer = this.buffer; + + const new_head = this.head + src_length; + if(new_head > capacity) { + const i_split = capacity - this.head; + buffer.set(src_array.subarray(0, i_split), this.head); + buffer.set(src_array.subarray(i_split)); + } + else { + buffer.set(src_array, this.head); + } + this.head = new_head % capacity; + this.length += src_length; + } + + /** + * @param {Uint8Array} dst_array + */ + peek(dst_array) + { + const length = Math.min(this.length, dst_array.length); + if(length) { + const buffer = this.buffer; + const capacity = buffer.length; + const new_tail = this.tail + length; + if(new_tail > capacity) { + const buf_len_left = new_tail % capacity; + const buf_len_right = capacity - this.tail; + dst_array.set(buffer.subarray(this.tail)); + dst_array.set(buffer.subarray(0, buf_len_left), buf_len_right); + } + else { + dst_array.set(buffer.subarray(this.tail, new_tail)); + } + } + return length; + } + + /** + * @param {number} length + */ + remove(length) + { + if(length > this.length) { + length = this.length; + } + if(length) { + this.tail = (this.tail + length) % this.buffer.length; + this.length -= length; + } + return length; + } } -function handle_fake_tcp(packet, adapter) +function create_eth_encoder_buf() { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src + const eth_frame = new Uint8Array(ETH_FRAME_SIZE); + const buffer = eth_frame.buffer; + const offset = eth_frame.byteOffset; + return { + eth_frame: eth_frame, + eth_frame_view: new DataView(buffer), + eth_payload_view: new DataView(buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE), + ipv4_payload_view: new DataView(buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE), + udp_payload_view: new DataView(buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE), + text_encoder: new TextEncoder() }; +} - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); +/** + * Copy given data array into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {ArrayBuffer|ArrayBufferView} data + * @param {DataView} view + * @param {Object} out + */ +function view_set_array(offset, data, view, out) +{ + out.eth_frame.set(data, view.byteOffset + offset); + return data.length; +} + +/** + * UTF8-encode given string into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {string} str + * @param {DataView} view + * @param {Object} out + */ +function view_set_string(offset, str, view, out) +{ + return out.text_encoder.encodeInto(str, out.eth_frame.subarray(view.byteOffset + offset)).written; +} + +/** + * Calculate internet checksum for view[0 : length] and return the 16-bit result. + * Source: RFC768 and RFC1071 (chapter 4.1). + * + * @param {number} length + * @param {number} checksum + * @param {DataView} view + * @param {Object} out + */ +function calc_inet_checksum(length, checksum, view, out) +{ + const uint16_end = view.byteOffset + (length & ~1); + const eth_frame = out.eth_frame; + for(let i = view.byteOffset; i < uint16_end; i += 2) { + checksum += eth_frame[i] << 8 | eth_frame[i+1]; + } + if(length & 1) { + checksum += eth_frame[uint16_end] << 8; + } + while(checksum >> 16) { + checksum = (checksum & 0xffff) + (checksum >> 16); + } + return ~checksum & 0xffff; +} +/** + * @param {Object} out + * @param {Object} spec + */ +function make_packet(out, spec) +{ + dbg_assert(spec.eth); + out.eth_frame.fill(0); + return out.eth_frame.subarray(0, write_eth(spec, out)); +} + +function handle_fake_tcp(packet, adapter) +{ + const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { dbg_log("SYN to already opened port", LOG_FETCH); } - if(adapter.on_tcp_connection(adapter, packet, tuple)) return; + if(adapter.on_tcp_connection(packet, tuple)) { + return; + } } if(!adapter.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); + dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; + let reply = {}; + reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; + reply.ipv4 = { + proto: IPV4_PROTO_TCP, + src: packet.ipv4.dest, + dest: packet.ipv4.src + }; reply.tcp = { sport: packet.tcp.dport, dport: packet.tcp.sport, @@ -81,14 +268,14 @@ function handle_fake_tcp(packet, adapter) rst: true, ack: packet.tcp.syn }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } adapter.tcp_conn[tuple].process(packet); } -function handle_fake_dns(packet, adapter) +function handle_fake_dns_static(packet, adapter) { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; @@ -108,7 +295,7 @@ function handle_fake_dns(packet, adapter) let q = packet.dns.questions[i]; switch(q.type){ - case 1: // A recrod + case 1: // A record answers.push({ name: q.name, type: q.type, @@ -127,10 +314,52 @@ function handle_fake_dns(packet, adapter) questions: packet.dns.questions, answers: answers }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } +function handle_fake_dns_doh(packet, adapter) +{ + const fetch_url = `https://${adapter.doh_server || DEFAULT_DOH_SERVER}/dns-query`; + const fetch_opts = { + method: "POST", + headers: [["content-type", "application/dns-message"]], + body: packet.udp.data + }; + const preferred_fetch = (window.anura?.net?.fetch) || fetch; + preferred_fetch(fetch_url, fetch_opts).then(async (resp) => { + const reply = { + eth: { + ethertype: ETHERTYPE_IPV4, + src: adapter.router_mac, + dest: packet.eth.src + }, + ipv4: { + proto: IPV4_PROTO_UDP, + src: adapter.router_ip, + dest: packet.ipv4.src + }, + udp: { + sport: 53, + dport: packet.udp.sport, + data: new Uint8Array(await resp.arrayBuffer()) + } + }; + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); + }); + return true; +} + +function handle_fake_dns(packet, adapter) +{ + if(adapter.dns_method === "static") { + return handle_fake_dns_static(packet, adapter); + } + else { + return handle_fake_dns_doh(packet, adapter); + } +} + function handle_fake_ntp(packet, adapter) { let now = Date.now(); // - 1000 * 60 * 60 * 24 * 7; let now_n = now + NTP_EPOC_DIFF; @@ -158,7 +387,7 @@ function handle_fake_ntp(packet, adapter) { reply.ntp.trans_ts_f = now_n_f; reply.ntp.stratum = 2; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } @@ -213,40 +442,37 @@ function handle_fake_dhcp(packet, adapter) { options.push(new Uint8Array([255, 0])); reply.dhcp.options = options; - adapter.receive(make_packet(reply)); - + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_fake_networking(data, adapter) { let packet = {}; parse_eth(data, packet); - if(packet.tcp) { - if(handle_fake_tcp(packet, adapter)) return true; - } - - if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { - arp_whohas(packet, adapter); - } - - if(packet.dns) { - if(handle_fake_dns(packet, adapter)) return; - } - - if(packet.ntp) { - if(handle_fake_ntp(packet, adapter)) return; - } - - // ICMP Ping - if(packet.icmp && packet.icmp.type === 8) { - handle_fake_ping(packet, adapter); - } - if(packet.dhcp) { - if(handle_fake_dhcp(packet, adapter)) return; + if(packet.ipv4) { + if(packet.tcp) { + handle_fake_tcp(packet, adapter); + } + else if(packet.udp) { + if(packet.dns) { + handle_fake_dns(packet, adapter); + } + else if(packet.dhcp) { + handle_fake_dhcp(packet, adapter); + } + else if(packet.ntp) { + handle_fake_ntp(packet, adapter); + } + else if(packet.udp.dport === 8) { + handle_udp_echo(packet, adapter); + } + } + else if(packet.icmp && packet.icmp.type === 8) { + handle_fake_ping(packet, adapter); + } } - - if(packet.udp && packet.udp.dport === 8) { - handle_udp_echo(packet, adapter); + else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { + arp_whohas(packet, adapter); } } @@ -264,8 +490,8 @@ function parse_eth(data, o) { o.eth = eth; - // Remove CRC from the end of the packet maybe? - let payload = data.subarray(14, data.length); + // TODO: Remove CRC from the end of the packet maybe? + let payload = data.subarray(ETH_HEADER_SIZE, data.length); if(ethertype === ETHERTYPE_IPV4) { parse_ipv4(payload, o); @@ -281,18 +507,17 @@ function parse_eth(data, o) { } } -function write_eth(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_eth(spec, out) { + const view = out.eth_frame_view; + view_set_array(0, spec.eth.dest, view, out); + view_set_array(6, spec.eth.src, view, out); view.setUint16(12, spec.eth.ethertype); - for(let i = 0; i < 6; ++i ) view.setUint8(0 + i, spec.eth.dest[i]); - for(let i = 0; i < 6; ++i ) view.setUint8(6 + i, spec.eth.src[i]); - - let len = 14; + let len = ETH_HEADER_SIZE; if(spec.arp) { - len += write_arp(spec, data.subarray(14)); + len += write_arp(spec, out); } - if(spec.ipv4) { - len += write_ipv4(spec, data.subarray(14)); + else if(spec.ipv4) { + len += write_ipv4(spec, out); } return len; } @@ -314,26 +539,18 @@ function parse_arp(data, o) { o.arp = arp; } -function write_arp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_arp(spec, out) { + const view = out.eth_payload_view; view.setUint16(0, spec.arp.htype); view.setUint16(2, spec.arp.ptype); view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); - - for(let i = 0; i < 6; ++i) { - view.setUint8(8 + i, spec.arp.sha[i]); - view.setUint8(18 + i, spec.arp.tha[i]); - } - - for(let i = 0; i < 4; ++i) { - view.setUint8(14 + i, spec.arp.spa[i]); - view.setUint8(24 + i, spec.arp.tpa[i]); - } - + view_set_array(8, spec.arp.sha, view, out); + view_set_array(14, spec.arp.spa, view, out); + view_set_array(18, spec.arp.tha, view, out); + view_set_array(24, spec.arp.tpa, view, out); return 28; - } function parse_ipv4(data, o) { @@ -362,46 +579,37 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - if(Math.max(len, 46) !== data.length) dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + if(Math.max(len, 46) !== data.length) { + dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + } o.ipv4 = ipv4; let ipdata = data.subarray(ihl * 4, len); if(proto === IPV4_PROTO_ICMP) { parse_icmp(ipdata, o); } - if(proto === IPV4_PROTO_TCP) { + else if(proto === IPV4_PROTO_TCP) { parse_tcp(ipdata, o); } - if(proto === IPV4_PROTO_UDP) { + else if(proto === IPV4_PROTO_UDP) { parse_udp(ipdata, o); } - - - return true; } -function write_ipv4(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - let ihl = 5; // 20 byte header length normally - let version = 4; - let len = 4 * ihl; // Total Length +function write_ipv4(spec, out) { + const view = out.eth_payload_view; + const ihl = IPV4_HEADER_SIZE >> 2; // header length in 32-bit words + const version = 4; + let len = IPV4_HEADER_SIZE; if(spec.icmp) { - len += write_icmp(spec, data.subarray(ihl * 4)); - } - if(spec.udp) { - len += write_udp(spec, data.subarray(ihl * 4)); + len += write_icmp(spec, out); } - if(spec.tcp) { - len += write_tcp(spec, data.subarray(ihl * 4)); + else if(spec.udp) { + len += write_udp(spec, out); } - if(spec.tcp_data) { - // TODO(perf) - for(let i = 0; i < spec.tcp_data.length; ++i) { - view.setUint8(len + i, spec.tcp_data[i]); - } - len += spec.tcp_data.length; + else if(spec.tcp) { + len += write_tcp(spec, out); } view.setUint8(0, version << 4 | (ihl & 0x0F)); @@ -411,24 +619,10 @@ function write_ipv4(spec, data) { view.setUint8(6, 2 << 5); // DF Flag view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); - view.setUint16(10, 0); // Checksum is zero during hashing - - for(let i = 0; i < 4; ++i) { - view.setUint8(12 + i, spec.ipv4.src[i]); - view.setUint8(16 + i, spec.ipv4.dest[i]); - } - - let checksum = 0; - for(let i = 0; i < ihl * 2; ++i) { - // TODO(perf) - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(10, checksum ^ 0xFFFF); - + view.setUint16(10, 0); // checksum initially zero before calculation + view_set_array(12, spec.ipv4.src, view, out); + view_set_array(16, spec.ipv4.dest, view, out); + view.setUint16(10, calc_inet_checksum(IPV4_HEADER_SIZE, 0, view, out)); return len; } @@ -440,33 +634,18 @@ function parse_icmp(data, o) { checksum: view.getUint16(2), data: data.subarray(4) }; - o.icmp = icmp; - return true; } -function write_icmp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_icmp(spec, out) { + const view = out.ipv4_payload_view; view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); - view.setUint16(2, 0); // checksum 0 during calc - - for(let i = 0; i < spec.icmp.data.length; ++i) { - view.setUint8(i + 4, spec.icmp.data[i]); - } - - let checksum = 0; - for(let i = 0; i < 4 + spec.icmp.data.length; i += 2) { - // TODO(perf) - checksum += view.getUint16(i); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(2, checksum ^ 0xFFFF); - - return 4 + spec.icmp.data.length; + view.setUint16(2, 0); // checksum initially zero before calculation + const data_length = view_set_array(ICMP_HEADER_SIZE, spec.icmp.data, view, out); + const total_length = ICMP_HEADER_SIZE + data_length; + view.setUint16(2, calc_inet_checksum(total_length, 0, view, out)); + return total_length; } function parse_udp(data, o) { @@ -484,41 +663,45 @@ function parse_udp(data, o) { if(udp.dport === 67 || udp.sport === 67) { //DHCP parse_dhcp(data.subarray(8), o); } - if(udp.dport === 53 || udp.sport === 53) { + else if(udp.dport === 53 || udp.sport === 53) { parse_dns(data.subarray(8), o); } - if(udp.dport === 123) { + else if(udp.dport === 123) { parse_ntp(data.subarray(8), o); } o.udp = udp; - return true; } -function write_udp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - let payload_length; - +function write_udp(spec, out) { + const view = out.ipv4_payload_view; + let total_length = UDP_HEADER_SIZE; if(spec.dhcp) { - payload_length = write_dhcp(spec, data.subarray(8)); - } else if(spec.dns) { - payload_length = write_dns(spec, data.subarray(8)); - } else if(spec.ntp) { - payload_length = write_ntp(spec, data.subarray(8)); - } else { - let raw_data = spec.udp.data; - payload_length = raw_data.length; - for(let i = 0; i < raw_data.length; ++i) { - view.setUint8(8+i, raw_data[i]); - } + total_length += write_dhcp(spec, out); + } + else if(spec.dns) { + total_length += write_dns(spec, out); + } + else if(spec.ntp) { + total_length += write_ntp(spec, out); + } + else { + total_length += view_set_array(0, spec.udp.data, out.udp_payload_view, out); } view.setUint16(0, spec.udp.sport); view.setUint16(2, spec.udp.dport); - view.setUint16(4, 8 + payload_length); - view.setUint16(6, 0); // Checksum - - return 8 + payload_length; + view.setUint16(4, total_length); + view.setUint16(6, 0); // checksum initially zero before calculation + + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_UDP + + total_length; + view.setUint16(6, calc_inet_checksum(total_length, pseudo_header, view, out)); + return total_length; } function parse_dns(data, o) { @@ -572,8 +755,8 @@ function parse_dns(data, o) { o.dns = dns; } -function write_dns(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_dns(spec, out) { + const view = out.udp_payload_view; view.setUint16(0, spec.dns.id); view.setUint16(2, spec.dns.flags); view.setUint16(4, spec.dns.questions.length); @@ -583,12 +766,9 @@ function write_dns(spec, data) { for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view_set_string(offset + 1, s, view, out); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, q.type); offset += 2; @@ -598,12 +778,9 @@ function write_dns(spec, data) { function write_reply(a) { for(let s of a.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view_set_string(offset + 1, s, view, out); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, a.type); offset += 2; @@ -613,12 +790,7 @@ function write_dns(spec, data) { offset += 4; view.setUint16(offset, a.data.length); offset += 2; - - for(let ii = 0; ii < a.data.length; ++ii) { - view.setUint8(offset + ii, a.data[ii]); - } - - offset += a.data.length; + offset += view_set_array(offset, a.data, view, out); } for(let i = 0; i < spec.dns.answers.length; ++i) { @@ -662,12 +834,10 @@ function parse_dhcp(data, o) { o.dhcp = dhcp; o.dhcp_options = dhcp.options; - return true; } -function write_dhcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_dhcp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.dhcp.op); view.setUint8(1, spec.dhcp.htype); view.setUint8(2, spec.dhcp.hlen); @@ -679,21 +849,14 @@ function write_dhcp(spec, data) { view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); - - for(let i = 0; i < spec.dhcp.chaddr.length; ++i) { - view.setUint8(28+i, spec.dhcp.chaddr[i]); - } + view_set_array(28, spec.dhcp.chaddr, view, out); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { - for(let i = 0; i < o.length; ++i) { - view.setUint8(offset, o[i]); - ++offset; - } + offset += view_set_array(offset, o, view, out); } - return offset; } @@ -716,12 +879,10 @@ function parse_ntp(data, o) { trans_ts_i: view.getUint32(40), trans_ts_f: view.getUint32(44), }; - return true; } -function write_ntp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_ntp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.ntp.flags); view.setUint8(1, spec.ntp.stratum); view.setUint8(2, spec.ntp.poll); @@ -737,12 +898,12 @@ function write_ntp(spec, data) { view.setUint32(36, spec.ntp.rec_ts_f); view.setUint32(40, spec.ntp.trans_ts_i); view.setUint32(44, spec.ntp.trans_ts_f); - return 48; } function parse_tcp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); + let tcp = { sport: view.getUint16(0), dport: view.getUint16(2), @@ -769,12 +930,10 @@ function parse_tcp(data, o) { let offset = tcp.doff * 4; o.tcp_data = data.subarray(offset); - return true; } -function write_tcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_tcp(spec, out) { + const view = out.ipv4_payload_view; let flags = 0; let tcp = spec.tcp; @@ -787,7 +946,7 @@ function write_tcp(spec, data) { if(tcp.ece) flags |= 0x40; if(tcp.cwr) flags |= 0x80; - let doff = 5; + const doff = TCP_HEADER_SIZE >> 2; // header length in 32-bit words view.setUint16(0, tcp.sport); view.setUint16(2, tcp.dport); @@ -796,98 +955,23 @@ function write_tcp(spec, data) { view.setUint8(12, doff << 4); view.setUint8(13, flags); view.setUint16(14, tcp.winsize); - view.setUint16(16, 0); // Checksum is 0 during calculation + view.setUint16(16, 0); // checksum initially zero before calculation view.setUint16(18, tcp.urgent || 0); - let total_len = (doff * 4) + (spec.tcp_data ? spec.tcp_data.length : 0); - - let checksum = 0; - let psudo_header = new Uint8Array(12); - let phview = new DataView(psudo_header.buffer, psudo_header.byteOffset, psudo_header.byteLength); - for(let i = 0; i < 4; ++i) { - phview.setUint8(i, spec.ipv4.src[i]); - phview.setUint8(4 + i, spec.ipv4.dest[i]); - } - phview.setUint8(9, IPV4_PROTO_TCP); - phview.setUint16(10, total_len); - - for(let i = 0; i < 6; ++i) { - // TODO(perf) - checksum += phview.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - for(let i = 0; i < doff * 2; ++i) { - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - + let total_length = TCP_HEADER_SIZE; if(spec.tcp_data) { - for(let i = 0; i < spec.tcp_data.length; i += 2) { - checksum += spec.tcp_data[i] << 8 | spec.tcp_data[i+1]; - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } + total_length += view_set_array(TCP_HEADER_SIZE, spec.tcp_data, view, out); } - view.setUint16(16, checksum ^ 0xFFFF); - return doff * 4; -} - -function make_packet(spec) { - // TODO: Can we reuse this buffer? - let bytes = new Uint8Array(1518); // Max ethernet packet size - dbg_assert(spec.eth); - - let written = write_eth(spec, bytes); - return bytes.subarray(0, written); -} - -function fake_tcp_connect(dport, adapter) -{ - // TODO: check port collisions - let sport = 49152 + Math.floor(Math.random() * 1000); - let tuple = [ - adapter.vm_ip.join("."), - dport, - adapter.router_ip.join("."), - sport - ].join(":"); - - let reader; - let connector; - - let conn = new TCPConnection(); - conn.net = adapter; - conn.on_data = function(data) { if(reader) reader.call(handle, data); }; - conn.on_connect = function() { if(connector) connector.call(handle); }; - conn.tuple = tuple; - - conn.hsrc = adapter.router_mac; - conn.psrc = adapter.router_ip; - conn.sport = sport; - conn.hdest = adapter.vm_mac; - conn.dport = dport; - conn.pdest = adapter.vm_ip; - - adapter.tcp_conn[tuple] = conn; - conn.connect(); - - // TODO: Real event source - let handle = { - write: function(data) { conn.write(data); }, - on: function(event, cb) { - if( event === "data" ) reader = cb; - if( event === "connect" ) connector = cb; - }, - close: function() { conn.close(); } - }; - - return handle; + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_TCP + + total_length; + view.setUint16(16, calc_inet_checksum(total_length, pseudo_header, view, out)); + return total_length; } /** @@ -895,8 +979,12 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_buffer = new Uint8Array([]); - this.seq_history = []; + this.state = TCP_STATE_CLOSED; + this.send_buffer = new GrowableRingbuffer(2048, 0); + this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); + this.in_active_close = false; + this.delayed_send_fin = false; + this.delayed_state = undefined; } TCPConnection.prototype.ipv4_reply = function() { @@ -918,7 +1006,28 @@ TCPConnection.prototype.ipv4_reply = function() { return reply; }; +TCPConnection.prototype.packet_reply = function(packet, tcp_options) { + const reply_tcp = { + sport: packet.tcp.dport, + dport: packet.tcp.sport, + winsize: packet.tcp.winsize, + ackn: this.ack, + seq: this.seq + }; + if(tcp_options) { + for(const opt in tcp_options) { + reply_tcp[opt] = tcp_options[opt]; + } + } + const reply = this.ipv4_reply(); + reply.tcp = reply_tcp; + return reply; +}; + +/* +// TODO: Is this method used anywhere anymore? It used to be called from fake_tcp_connect() which was removed. TCPConnection.prototype.connect = function() { + // dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.seq = 1338; this.ack = 1; this.start_seq = 0; @@ -935,8 +1044,9 @@ TCPConnection.prototype.connect = function() { winsize: 0, syn: true, }; - this.net.receive(make_packet(reply)); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; +*/ TCPConnection.prototype.accept = function(packet) { this.seq = 1338; @@ -951,7 +1061,6 @@ TCPConnection.prototype.accept = function(packet) { this.winsize = packet.tcp.winsize; let reply = this.ipv4_reply(); - reply.tcp = { sport: this.sport, dport: this.dport, @@ -961,87 +1070,155 @@ TCPConnection.prototype.accept = function(packet) { syn: true, ack: true }; - this.net.receive(make_packet(reply)); + // dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.process = function(packet) { - - // Receive Handshake Part 2, Send Part 3 - if(packet.tcp.syn) { - dbg_assert(packet.tcp.ack); - dbg_assert(this.state === TCP_STATE_SYN_SENT); - - this.ack = packet.tcp.seq + 1; - this.start_seq = packet.tcp.seq; - this.last_received_ackn = packet.tcp.ackn; - - let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); - - this.state = TCP_STATE_ESTABLISHED; - if(this.on_connect) this.on_connect.call(this); + if(this.state === TCP_STATE_CLOSED) { + // dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } - - if(packet.tcp.fin) { - dbg_log(`All done with ${this.tuple} resetting`, LOG_FETCH); - if(this.ack !== packet.tcp.seq) { - dbg_log("Closing the connecton, but seq was wrong", LOG_FETCH); - ++this.ack; // FIN increases seq# - } - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - rst: true, - }; - delete this.net.tcp_conn[this.tuple]; - this.net.receive(make_packet(reply)); + else if(packet.tcp.rst) { + // dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); + this.on_close(); + this.release(); return; } - - if(this.ack !== packet.tcp.seq) { - dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} (${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); - - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - ack: true - }; - this.net.receive(make_packet(reply)); - + else if(packet.tcp.syn) { + if(this.state === TCP_STATE_SYN_SENT && packet.tcp.ack) { + this.ack = packet.tcp.seq + 1; + this.start_seq = packet.tcp.seq; + this.last_received_ackn = packet.tcp.ackn; + + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + // dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + if(this.on_connect) { + this.on_connect.call(this); + } + } + else { + dbg_log(`TCP[${this.tuple}]: WARNING: unexpected SYN packet dropped`, LOG_FETCH); + } + if(packet.tcp_data.length) { + dbg_log(`TCP[${this.tuple}]: WARNING: ${packet.tcp_data.length} bytes of unexpected SYN packet payload dropped`, LOG_FETCH); + } return; } - this.seq_history.push(`${packet.tcp.seq - this.start_seq}:${packet.tcp.seq + packet.tcp_data.length- this.start_seq}`); - - this.ack += packet.tcp_data.length; - - if(packet.tcp_data.length > 0) { - let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); + if(packet.tcp.ack) { + if(this.state === TCP_STATE_SYN_RECEIVED) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + if(!packet.tcp.fin) { // handle FIN+ACK in FIN_WAIT_1 separately further down below + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_2; + } + } + else if(this.state === TCP_STATE_CLOSING || this.state === TCP_STATE_LAST_ACK) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); + this.release(); + return; + } } - if(this.last_received_ackn === undefined) this.last_received_ackn = packet.tcp.ackn; - let nread = packet.tcp.ackn - this.last_received_ackn; - //console.log("Read ", nread, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) - if(nread > 0) { + if(this.last_received_ackn === undefined) { this.last_received_ackn = packet.tcp.ackn; - this.send_buffer = this.send_buffer.subarray(nread); - this.seq += nread; - this.pending = false; + } + else { + const n_ack = packet.tcp.ackn - this.last_received_ackn; + //console.log("Read ", n_ack, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) + if(n_ack > 0) { + this.last_received_ackn = packet.tcp.ackn; + this.send_buffer.remove(n_ack); + this.seq += n_ack; + this.pending = false; + + if(this.delayed_send_fin && !this.send_buffer.length) { + // dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${this.delayed_state}"`, LOG_FETCH); + this.delayed_send_fin = false; + this.state = this.delayed_state; + const reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + return; + } + } + else if(n_ack < 0) { // TODO: could this just be a 32-bit sequence number overflow? + dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.on_close(); + this.release(); + return; + } } - if(nread < 0) return; + if(packet.tcp.fin) { + if(this.ack !== packet.tcp.seq) { + dbg_log(`TCP[${this.tuple}]: WARNING: closing connection in state "${this.state}" with invalid seq (${this.ack} != ${packet.tcp.seq})`, LOG_FETCH); + } + ++this.ack; // FIN increases seqnr + const reply = this.packet_reply(packet, {}); + if(this.state === TCP_STATE_ESTABLISHED) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); + reply.tcp.ack = true; + this.state = TCP_STATE_CLOSE_WAIT; + this.on_shutdown(); + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + if(packet.tcp.ack) { + // dbg_log(`TCP[${this.tuple}]: received ACK+FIN in state "${this.state}"`, LOG_FETCH); + this.release(); + } + else { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_CLOSING}"`, LOG_FETCH); + this.state = TCP_STATE_CLOSING; + } + reply.tcp.ack = true; + } + else if(this.state === TCP_STATE_FIN_WAIT_2) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}"`, LOG_FETCH); + this.release(); + reply.tcp.ack = true; + } + else { + // dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); + this.release(); + this.on_close(); + reply.tcp.rst = true; + } + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + } + else if(this.ack !== packet.tcp.seq) { + // Handle TCP Keep-Alives silently. + // Excerpt from RFC 9293, 3.8.4. TCP Keep-Alives: + // To confirm that an idle connection is still active, these + // implementations send a probe segment designed to elicit a response + // from the TCP peer. Such a segment generally contains SEG.SEQ = + // SND.NXT-1 and may or may not contain one garbage octet of data. + if(this.ack !== packet.tcp.seq + 1) { + dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} ` + + `pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} ` + + `(${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); + } + const reply = this.packet_reply(packet, {ack: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + } + else if(packet.tcp.ack && packet.tcp_data.length > 0) { + this.ack += packet.tcp_data.length; + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.on_data(packet.tcp_data); + } - this.on_data(packet.tcp_data); this.pump(); }; @@ -1049,37 +1226,83 @@ TCPConnection.prototype.process = function(packet) { * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { - if(this.send_buffer.length > 0) { - // TODO: Pretty inefficient - let concat = new Uint8Array(this.send_buffer.byteLength + data.byteLength); - concat.set(this.send_buffer, 0); - concat.set(data, this.send_buffer.byteLength); - this.send_buffer = concat; - } else { - this.send_buffer = data; + if(!this.in_active_close) { + this.send_buffer.write(data); + } + this.pump(); +}; + +/** + * @param {!Array |