From ff20099855171a2f02766fa8f96ba5a2ca2c3f80 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Thu, 13 Apr 2023 23:16:10 +0900 Subject: [PATCH] Initial support for networking Signed-off-by: Kohei Tokunaga --- .github/workflows/release.yml | 18 +- .github/workflows/tests.yml | 12 +- Dockerfile | 30 +- Dockerfile.test | 14 + Makefile | 21 +- README.md | 83 ++- cmd/c2w-net/main.go | 137 +++++ cmd/create-spec/main.go | 61 +- cmd/init/main.go | 61 +- cmd/init/types/types.go | 2 + .../alpine-emscripten-host-networking.png | Bin 0 -> 57622 bytes ...alpine-wasi-on-browser-host-networking.png | Bin 0 -> 58193 bytes ...rl-wasi-on-browser-frontend-networking.png | Bin 0 -> 74943 bytes ...ix-wasi-on-browser-frontend-networking.png | Bin 0 -> 54144 bytes examples/README.md | 1 + examples/emscripten/README.md | 4 +- examples/emscripten/htdocs/index.html | 2 +- examples/emscripten/htdocs/module.js | 29 + examples/emscripten/htdocs/worker.js | 1 + examples/networking/README.md | 7 + examples/networking/fetch/README.md | 129 +++++ examples/networking/wasi/README.md | 148 +++++ examples/networking/websocket/README.md | 97 ++++ examples/wasi-browser/README.md | 2 + examples/wasi-browser/htdocs/index.html | 34 +- examples/wasi-browser/htdocs/stack-worker.js | 245 ++++++++ examples/wasi-browser/htdocs/stack.js | 248 ++++++++ examples/wasi-browser/htdocs/wasi-util.js | 127 +++++ examples/wasi-browser/htdocs/worker-util.js | 266 +++++++++ examples/wasi-browser/htdocs/worker.js | 250 ++++---- examples/wasi-browser/htdocs/ws-delegate.js | 105 ++++ extras/c2w-net-proxy/README.md | 19 + extras/c2w-net-proxy/go.mod | 35 ++ extras/c2w-net-proxy/go.sum | 130 +++++ extras/c2w-net-proxy/main.go | 533 ++++++++++++++++++ go.mod | 18 +- go.sum | 87 ++- patches/bochs/Bochs/bochs/iodev/devices.cc | 1 + patches/bochs/Bochs/bochs/main.cc | 103 +++- patches/bochs/Bochs/bochs/plugin.cc | 1 + patches/bochs/Bochs/bochs/plugin.h | 2 + patches/bochs/Bochs/bochs/wasm.cc | 451 ++++++++++++++- patches/bochs/Bochs/bochs/wasm.h | 59 +- patches/bochs/grub.cfg.template | 2 +- patches/bochs/vfs/vfs.c | 20 + patches/tinyemu/tinyemu.config.template | 2 +- patches/tinyemu/tinyemu/temu.c | 407 ++++++++++++- patches/tinyemu/tinyemu/virtio.h | 1 + patches/tinyemu/tinyemu/wasi.c | 20 + tests/c2w-net-proxy-test/go.mod | 5 + tests/c2w-net-proxy-test/go.sum | 2 + tests/c2w-net-proxy-test/main.go | 398 +++++++++++++ tests/httphello/main.go | 16 + tests/integration/utils/utils.go | 113 +++- tests/integration/wasmtime_test.go | 140 +++++ tests/integration/wazero_test.go | 180 ++++++ tests/wazero/go.mod | 25 +- tests/wazero/go.sum | 119 ++++ tests/wazero/main.go | 100 +++- 59 files changed, 4826 insertions(+), 297 deletions(-) create mode 100644 cmd/c2w-net/main.go create mode 100644 docs/images/alpine-emscripten-host-networking.png create mode 100644 docs/images/alpine-wasi-on-browser-host-networking.png create mode 100644 docs/images/debian-curl-wasi-on-browser-frontend-networking.png create mode 100644 docs/images/nix-wasi-on-browser-frontend-networking.png create mode 100644 examples/emscripten/htdocs/module.js create mode 100644 examples/networking/README.md create mode 100644 examples/networking/fetch/README.md create mode 100644 examples/networking/wasi/README.md create mode 100644 examples/networking/websocket/README.md create mode 100644 examples/wasi-browser/htdocs/stack-worker.js create mode 100644 examples/wasi-browser/htdocs/stack.js create mode 100644 examples/wasi-browser/htdocs/wasi-util.js create mode 100644 examples/wasi-browser/htdocs/worker-util.js create mode 100644 examples/wasi-browser/htdocs/ws-delegate.js create mode 100644 extras/c2w-net-proxy/README.md create mode 100644 extras/c2w-net-proxy/go.mod create mode 100644 extras/c2w-net-proxy/go.sum create mode 100644 extras/c2w-net-proxy/main.go create mode 100644 patches/bochs/vfs/vfs.c create mode 100644 tests/c2w-net-proxy-test/go.mod create mode 100644 tests/c2w-net-proxy-test/go.sum create mode 100644 tests/c2w-net-proxy-test/main.go create mode 100644 tests/httphello/main.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 712e0ee..ad42a8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,17 +19,25 @@ jobs: env: OUTPUT_DIR: ${{ github.workspace }}/builds steps: + - uses: actions/setup-go@v4 + with: + go-version: 1.21.x - uses: actions/checkout@v4 - - name: Build + - name: Build binaries run: | PREFIX=${OUTPUT_DIR} make artifacts + - name: Build wasm image + run: | + PREFIX=${OUTPUT_DIR} make c2w-net-proxy.wasm + - name: sha256sum + run: | ( cd ${OUTPUT_DIR}; sha256sum * ) > "${GITHUB_WORKSPACE}/SHA256SUMS" mv "${GITHUB_WORKSPACE}/SHA256SUMS" "${OUTPUT_DIR}/SHA256SUMS" - name: Create Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - SHA256SUM_OF_SHA256SUM=$(sha256sum ${OUTPUT_DIR}/SHA256SUMS | cut -d ' ' -f 1) + SHA256SUM_OF_SHA256SUMS=$(sha256sum ${OUTPUT_DIR}/SHA256SUMS | cut -d ' ' -f 1) RELEASE_TAG="${GITHUB_REF##*/}" MINIMAL_TAR=$(ls -1 ${OUTPUT_DIR} | grep container2wasm-v | head -1) MINIMAL_TAR_LIST=$(tar --list -vvf ${OUTPUT_DIR}/${MINIMAL_TAR}) @@ -50,9 +58,13 @@ jobs: + ## About \`c2w-net-proxy.wasm\` + + Please refer to [the document about networking for container on browser](https://github.com/ktock/container2wasm/tree/${RELEASE_TAG}/examples/networking/fetch/) for details and usage. + --- - The sha256sum of SHA256SUM is \`${SHA256SUM_OF_SHA256SUM}\` + The sha256sum of SHA256SUMS is \`${SHA256SUM_OF_SHA256SUMS}\` EOF ASSET_FLAGS=() diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1f7602..4184a25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,8 @@ jobs: make ls -al ./out/c2w if ldd ./out/c2w ; then echo "must be static binary" ; exit 1 ; fi + ls -al ./out/c2w-net + if ldd ./out/c2w-net ; then echo "must be static binary" ; exit 1 ; fi test: runs-on: ubuntu-22.04 @@ -30,16 +32,12 @@ jobs: fail-fast: false matrix: target: ["TestWasmtime", "TestWamr", "TestWasmer", "TestWazero", "TestWasmedge"] + arch: ["x86_64", "riscv64", "aarch64"] steps: - uses: actions/checkout@v4 - - name: x86_64 + - name: test env: - GO_TEST_FLAGS: -run ${{ matrix.target }}/.*arch=x86_64.* - run: | - make test - - name: riscv64 - env: - GO_TEST_FLAGS: -run ${{ matrix.target }}/.*arch=(riscv64|aarch64).* + GO_TEST_FLAGS: -run ${{ matrix.target }}/.*arch=${{ matrix.arch }}.* run: | make test diff --git a/Dockerfile b/Dockerfile index 694cf30..efdb650 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,8 +53,8 @@ COPY --link --from=oci-image-src / /oci # /oci/rootfs : rootfs dir this Dockerfile creates container's rootfs and used by the container. # /oci/image.json : container image config file used by init # /oci/spec.json : container runtime spec file used by init -# /etc/initconfig.json : configuration file for init -RUN mkdir -p /out/oci/rootfs /out/oci/bundle /out/etc && \ +# /oci/initconfig.json : configuration file for init +RUN mkdir -p /out/oci/rootfs /out/oci/bundle && \ IS_WIZER=false && \ if test "${OPTIMIZATION_MODE}" = "wizer" ; then IS_WIZER=true ; fi && \ NO_VMTOUCH_F=false && \ @@ -65,7 +65,7 @@ RUN mkdir -p /out/oci/rootfs /out/oci/bundle /out/etc && \ --runtime-config-path=/oci/spec.json \ --rootfs-path=/oci/rootfs \ /oci "${TARGETPLATFORM}" /out/oci/rootfs && \ - mv image.json spec.json /out/oci/ && mv initconfig.json /out/etc/ + mv image.json spec.json /out/oci/ && mv initconfig.json /out/oci/ FROM ubuntu:22.04 AS gcc-riscv64-linux-gnu-base RUN apt-get update && apt-get install -y gcc-riscv64-linux-gnu libc-dev-riscv64-cross git make @@ -162,10 +162,11 @@ RUN git clone -b $BUSYBOX_VERSION --depth 1 https://git.busybox.net/busybox WORKDIR /work/busybox RUN make CROSS_COMPILE=riscv64-linux-gnu- LDFLAGS=--static defconfig RUN make CROSS_COMPILE=riscv64-linux-gnu- LDFLAGS=--static -j$(nproc) -RUN mkdir -p /out && mv busybox /out/busybox +RUN mkdir -p /out/bin && mv busybox /out/bin/busybox RUN make LDFLAGS=--static defconfig RUN make LDFLAGS=--static -j$(nproc) -RUN for i in $(./busybox --list) ; do ln -s busybox /out/$i ; done +RUN for i in $(./busybox --list) ; do ln -s busybox /out/bin/$i ; done +RUN mkdir -p /out/usr/share/udhcpc/ && cp ./examples/udhcp/simple.script /out/usr/share/udhcpc/default.script FROM gcc-riscv64-linux-gnu-base AS tini-riscv64-dev # https://github.com/krallin/tini#building-tini @@ -179,15 +180,14 @@ RUN cmake . && make && mkdir /out/ && mv tini /out/ FROM ubuntu:22.04 AS rootfs-riscv64-dev RUN apt-get update -y && apt-get install -y mkisofs -COPY --link --from=busybox-riscv64-dev /out/ /rootfs/bin/ +COPY --link --from=busybox-riscv64-dev /out/ /rootfs/ COPY --link --from=binfmt-dev / /rootfs/ COPY --link --from=runc-riscv64-dev /out/runc /rootfs/sbin/runc COPY --link --from=bundle-dev /out/ /rootfs/ COPY --link --from=init-riscv64-dev /out/init /rootfs/sbin/init COPY --link --from=vmtouch-riscv64-dev /out/vmtouch /rootfs/bin/ COPY --link --from=tini-riscv64-dev /out/tini /rootfs/sbin/tini -RUN mkdir -p /rootfs/proc /rootfs/sys /rootfs/mnt /rootfs/run /rootfs/tmp /rootfs/dev /rootfs/var && mknod /rootfs/dev/null c 1 3 && chmod 666 /rootfs/dev/null -RUN touch /rootfs/etc/resolv.conf /rootfs/etc/hosts +RUN mkdir -p /rootfs/proc /rootfs/sys /rootfs/mnt /rootfs/run /rootfs/tmp /rootfs/dev /rootfs/var /rootfs/etc && mknod /rootfs/dev/null c 1 3 && chmod 666 /rootfs/dev/null RUN mkdir /out/ && mkisofs -l -J -R -o /out/rootfs.bin /rootfs/ # RUN isoinfo -i /out/rootfs.bin -l @@ -320,10 +320,11 @@ RUN git clone -b $BUSYBOX_VERSION --depth 1 https://git.busybox.net/busybox WORKDIR /work/busybox RUN make CROSS_COMPILE=x86_64-linux-gnu- LDFLAGS=--static defconfig RUN make CROSS_COMPILE=x86_64-linux-gnu- LDFLAGS=--static -j$(nproc) -RUN mkdir -p /out && mv busybox /out/busybox +RUN mkdir -p /out/bin && mv busybox /out/bin/busybox RUN make LDFLAGS=--static defconfig RUN make LDFLAGS=--static -j$(nproc) -RUN for i in $(./busybox --list) ; do ln -s busybox /out/$i ; done +RUN for i in $(./busybox --list) ; do ln -s busybox /out/bin/$i ; done +RUN mkdir -p /out/usr/share/udhcpc/ && cp ./examples/udhcp/simple.script /out/usr/share/udhcpc/default.script FROM golang-base AS runc-amd64-dev ARG RUNC_VERSION @@ -385,14 +386,13 @@ RUN git clone https://github.com/hoytech/vmtouch.git && \ FROM ubuntu:22.04 AS rootfs-amd64-dev RUN apt-get update -y && apt-get install -y mkisofs -COPY --link --from=busybox-amd64-dev /out/ /rootfs/bin/ +COPY --link --from=busybox-amd64-dev /out/ /rootfs/ COPY --link --from=runc-amd64-dev /out/runc /rootfs/sbin/runc COPY --link --from=bundle-dev /out/ /rootfs/ COPY --link --from=init-amd64-dev /out/init /rootfs/sbin/init COPY --link --from=vmtouch-amd64-dev /out/vmtouch /rootfs/bin/ COPY --link --from=tini-amd64-dev /out/tini /rootfs/sbin/tini RUN mkdir -p /rootfs/proc /rootfs/sys /rootfs/mnt /rootfs/run /rootfs/tmp /rootfs/dev /rootfs/var /rootfs/etc && mknod /rootfs/dev/null c 1 3 && chmod 666 /rootfs/dev/null -RUN touch /rootfs/etc/resolv.conf /rootfs/etc/hosts RUN mkdir /out/ && mkisofs -l -J -R -o /out/rootfs.bin /rootfs/ # RUN isoinfo -i /out/rootfs.bin -l @@ -450,6 +450,10 @@ RUN ${WASI_SDK_PATH}/bin/clang --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -O2 RUN ${WASI_SDK_PATH}/bin/clang --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -O2 --target=wasm32-unknown-wasi -Wl,--export=wasm_setjmp -c jmp.S -o jmp_wrapper.o RUN ${WASI_SDK_PATH}/bin/wasm-ld jmp.o jmp_wrapper.o --export=wasm_setjmp --export=wasm_longjmp --export=handle_jmp --no-entry -r -o jmp +COPY --link --from=assets ./patches/bochs/vfs /vfs +WORKDIR /vfs +RUN ${WASI_SDK_PATH}/bin/clang --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -O2 --target=wasm32-unknown-wasi -c vfs.c -I . -o vfs.o + COPY --link --from=assets /patches/bochs/Bochs /Bochs WORKDIR /Bochs/bochs ARG INIT_DEBUG @@ -461,7 +465,7 @@ RUN LOGGING_FLAG=--disable-logging && \ ./configure --host wasm32-unknown-wasi --enable-x86-64 --with-nogui --enable-usb --enable-usb-ehci \ --disable-large-ramfile --disable-show-ips --disable-stats ${LOGGING_FLAG} \ --enable-repeat-speedups --enable-fast-function-calls --disable-trace-linking --enable-handlers-chaining # TODO: --enable-trace-linking causes "out of bounds memory access" -RUN make -j$(nproc) bochs EMU_DEPS="/tools/wasi-vfs/libwasi_vfs.a /jmp/jmp -lrt" +RUN make -j$(nproc) bochs EMU_DEPS="/tools/wasi-vfs/libwasi_vfs.a /jmp/jmp /vfs/vfs.o -lrt" RUN /binaryen/binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt bochs --asyncify -O2 -o bochs.async --pass-arg=asyncify-ignore-imports RUN mv bochs.async bochs diff --git a/Dockerfile.test b/Dockerfile.test index 8db36c9..6a634ec 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -16,6 +16,16 @@ COPY ./tests/wazero /wazero WORKDIR /wazero RUN go build -o /out/wazero-test main.go +FROM golang:1.21 AS httphello-dev +COPY ./tests/httphello /httphello +WORKDIR /httphello +RUN go build -o /out/httphello main.go + +FROM golang:1.21 AS c2w-net-proxy-test-dev +COPY ./tests/c2w-net-proxy-test /c2w-net-proxy-test +WORKDIR /c2w-net-proxy-test +RUN go build -o /out/c2w-net-proxy-test main.go + FROM ubuntu:22.04 ARG BUILDX_VERSION ARG DOCKER_VERSION @@ -58,6 +68,8 @@ RUN wget https://github.com/WasmEdge/WasmEdge/releases/download/${WASMEDGE_VERSI # install wazero COPY --from=wazero-test-dev /out/wazero-test /usr/local/bin/ +COPY --from=httphello-dev /out/httphello /usr/local/bin/ +COPY --from=c2w-net-proxy-test-dev /out/c2w-net-proxy-test /usr/local/bin/ # install golang RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz @@ -69,6 +81,8 @@ ENV PATH=$PATH:/usr/local/go/bin COPY . /test/ WORKDIR /test/ RUN go build -o /usr/local/bin/ ./cmd/c2w +RUN go build -o /usr/local/bin/ ./cmd/c2w-net +RUN cd extras/c2w-net-proxy/ ; GOOS=wasip1 GOARCH=wasm go build -o /opt/c2w-net-proxy.wasm . ENTRYPOINT ["dockerd-entrypoint.sh"] CMD [] diff --git a/Makefile b/Makefile index 4db3b00..1057804 100644 --- a/Makefile +++ b/Makefile @@ -8,24 +8,31 @@ GO_EXTRA_LDFLAGS=-extldflags '-static' GO_LD_FLAGS=-ldflags '-s -w -X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revision=$(REVISION) $(GO_EXTRA_LDFLAGS)' GO_BUILDTAGS=-tags "osusergo netgo static_build" -all: c2w +all: c2w c2w-net -build: c2w +build: c2w c2w-net c2w: CGO_ENABLED=0 go build -o $(PREFIX)/c2w $(GO_LD_FLAGS) $(GO_BUILDTAGS) -v ./cmd/c2w +c2w-net: + CGO_ENABLED=0 go build -o $(PREFIX)/c2w-net $(GO_LD_FLAGS) $(GO_BUILDTAGS) -v ./cmd/c2w-net + +c2w-net-proxy.wasm: + cd extras/c2w-net-proxy/ ; GOOS=wasip1 GOARCH=wasm go build -o $(PREFIX)/c2w-net-proxy.wasm . + install: install -D -m 755 $(PREFIX)/c2w $(CMD_DESTDIR)/bin + install -D -m 755 $(PREFIX)/c2w-net $(CMD_DESTDIR)/bin artifacts: clean - GOOS=linux GOARCH=amd64 make c2w - tar -C $(PREFIX) --owner=0 --group=0 -zcvf $(PREFIX)/container2wasm-$(VERSION)-linux-amd64.tar.gz c2w + GOOS=linux GOARCH=amd64 make c2w c2w-net + tar -C $(PREFIX) --owner=0 --group=0 -zcvf $(PREFIX)/container2wasm-$(VERSION)-linux-amd64.tar.gz c2w c2w-net - GOOS=linux GOARCH=arm64 make c2w - tar -C $(PREFIX) --owner=0 --group=0 -zcvf $(PREFIX)/container2wasm-$(VERSION)-linux-arm64.tar.gz c2w + GOOS=linux GOARCH=arm64 make c2w c2w-net + tar -C $(PREFIX) --owner=0 --group=0 -zcvf $(PREFIX)/container2wasm-$(VERSION)-linux-arm64.tar.gz c2w c2w-net - rm -f $(PREFIX)/c2w + rm -f $(PREFIX)/c2w $(PREFIX)/c2w-net test: ./tests/test.sh diff --git a/README.md b/README.md index 17fb862..4e06220 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ $ wasmtime --mapdir /mnt/share::/tmp/share out.wasm cat /mnt/share/from-host hi ``` +> Please refer to [`./examples/networking/wasi/`](./examples/networking/wasi/) for enabling networking + ### Container on Browser ![Container on browser](./docs/images/ubuntu-wasi-on-browser.png) @@ -55,7 +57,9 @@ hi You can run the container on browser as well. There are two methods for running the container on browser. -> NOTE: Please also refer to [`./examples/wasi-browser`](./examples/wasi-browser/) (WASI-on-browser example) and [`./examples/emscripten`](./examples/emscripten/) (emscripten example). +> Please also refer to [`./examples/wasi-browser`](./examples/wasi-browser/) (WASI-on-browser example) and [`./examples/emscripten`](./examples/emscripten/) (emscripten example). + +> Please refer to [`./examples/networking/`](./examples/networking/) for details about enabling networking. #### WASI on browser @@ -80,6 +84,41 @@ $ docker run --rm -p 8080:80 \ You can run the container on browser via `localhost:8080`. +##### WASI on browser with networking + +![Debian container on browser with browser networking](./docs/images/debian-curl-wasi-on-browser-frontend-networking.png) + +Container can also perform networking. +This section is the demo of using curl command in the container. + +> Tested only on Chrome. The example might not work on other browsers. + +``` +$ cat <> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +You can run the container on browser with several types of configurations: + +- `localhost:8080/?net=browser`: Container with networking. [Network stack](./extras/c2w-net-proxy/) based on [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) runs on browser and forwards HTTP/HTTPS packets using the browser's Fetch API. The set of accesible sites is restricted by the browser configuration (e.g. CORS restriction). See also [`./examples/networking/fetch`](./examples/networking/fetch/) for detalis. +- `localhost:8080/?net=delegate=ws://localhost:8888`: Container with networking. You need to run [user-space network stack](./cmd/c2w-net/) based on [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) on the host (outside of browser). It forwards all packets received from the browser over WebSocket. See also [`./examples/networking/websocket`](./examples/networking/websocket/) for detalis and configuration. +- `localhost:8080`: Container without networking. + #### emscripten on browser This example uses emscripten for converting the container to WASM. @@ -108,20 +147,25 @@ You can run the container on browser via `localhost:8080`. > NOTE: It can take some time to load and start the container. +Networking can also be enabled using the [user-space network stack](./cmd/c2w-net/) based on [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) serving over WebSocket on the host (outside of browser). +See also [`./examples/networking/websocket`](./examples/networking/websocket/) for detalis. + ## Getting Started - requirements - Docker 18.09+ (w/ `DOCKER_BUILDKIT=1`) - [Docker Buildx](https://docs.docker.com/build/install-buildx/) v0.8+ (recommended) or `docker build` (w/ `DOCKER_BUILDKIT=1`) -You can install the converter command `c2w` using one of the following methods: +You can install the converter command `c2w` using one of the following methods. + +> NOTE: The output binary also contains [`c2w-net`](./cmd/c2w-net/) which a command usable for controlling networking feature (please see also [./examples/networking](./examples/networking/) for details). -### Release binary +### Release binaries Binaries are available from https://github.com/ktock/container2wasm/releases Extract the tarball and put the binary somewhere under `$PATH`. -### Building binary using make +### Building binaries using make Go 1.19+ is needed. @@ -218,23 +262,23 @@ The following shows the techniqual details: ### x86_64 containers -|runtime |stdio|mapdir|note| -|---|---|---|---| -|wasmtime|:heavy_check_mark:|:heavy_check_mark:|| -|wamr(wasm-micro-runtime)|:heavy_check_mark:|:heavy_check_mark:|| -|wazero|:heavy_check_mark:|:heavy_check_mark:|| -|wasmer|:construction: (stdin unsupported)|:heavy_check_mark:|non-blocking stdin doesn't seem to work| -|wasmedge|:construction: (stdin unsupported)|:heavy_check_mark:|non-blocking stdin doesn't seem to work| +|runtime|stdio|mapdir|networking|note| +|---|---|---|---|---| +|wasmtime|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: (w/ [host-side network stack](./examples/networking/wasi/))|| +|wamr(wasm-micro-runtime)|:heavy_check_mark:|:heavy_check_mark:|:construction:|| +|wazero|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: (w/ [host-side network stack](./examples/networking/wasi/)|| +|wasmer|:construction: (stdin unsupported)|:heavy_check_mark:|:construction:|non-blocking stdin doesn't seem to work| +|wasmedge|:construction: (stdin unsupported)|:heavy_check_mark:|:construction:|non-blocking stdin doesn't seem to work| ### risc-v and other architecutre's containers -|runtime |stdio|mapdir|note| -|---|---|---|---| -|wasmtime|:heavy_check_mark:|:heavy_check_mark:|| -|wamr(wasm-micro-runtime)|:heavy_check_mark:|:heavy_check_mark:|| -|wazero|:heavy_check_mark:|:heavy_check_mark:|| -|wasmer|:construction: (stdin unsupported)|:heavy_check_mark:|non-blocking stdin doesn't seem to work| -|wasmedge|:construction: (stdin unsupported)|:heavy_check_mark:|non-blocking stdin doesn't seem to work| +|runtime |stdio|mapdir|networking|note| +|---|---|---|---|---| +|wasmtime|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: (w/ [host-side network stack](./examples/networking/wasi/))|| +|wamr(wasm-micro-runtime)|:heavy_check_mark:|:heavy_check_mark:|:construction:|| +|wazero|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: (w/ [host-side network stack](./examples/networking/wasi/))|| +|wasmer|:construction: (stdin unsupported)|:heavy_check_mark:|:construction:|non-blocking stdin doesn't seem to work| +|wasmedge|:construction: (stdin unsupported)|:heavy_check_mark:|:construction:|non-blocking stdin doesn't seem to work| ## Similar projects @@ -282,6 +326,7 @@ Re-compilation (and possibe re-implementation) of the application is needed. - vmtouch ([license](https://github.com/hoytech/vmtouch/blob/master/LICENSE)): https://github.com/hoytech/vmtouch - BusyBox ([GNU General Public License version 2](https://www.busybox.net/license.html)): https://git.busybox.net/busybox -- On-browser example relies on xterm-pty and `browser_wasi_shim`(for WASI-on-browser). +- On-browser example relies on the following softwares. - xterm-pty ([MIT License](https://github.com/mame/xterm-pty/blob/main/LICENSE.txt)): https://github.com/mame/xterm-pty - `browser_wasi_shim` (either of [MIT License](https://github.com/bjorn3/browser_wasi_shim/blob/main/LICENSE-MIT) and [Apache License 2.0](https://github.com/bjorn3/browser_wasi_shim/blob/main/LICENSE-APACHE)): https://github.com/bjorn3/browser_wasi_shim + - `gvisor-tap-vsock` ([Apache License 2.0](https://github.com/containers/gvisor-tap-vsock/blob/main/LICENSE)): https://github.com/containers/gvisor-tap-vsock diff --git a/cmd/c2w-net/main.go b/cmd/c2w-net/main.go new file mode 100644 index 0000000..864d649 --- /dev/null +++ b/cmd/c2w-net/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "strings" + "time" + + gvntypes "github.com/containers/gvisor-tap-vsock/pkg/types" + gvnvirtualnetwork "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "golang.org/x/net/websocket" +) + +const ( + gatewayIP = "192.168.127.1" + vmIP = "192.168.127.3" + vmMAC = "02:00:00:00:00:01" +) + +func main() { + var portFlags sliceFlags + flag.Var(&portFlags, "p", "map port between host and guest (host:guest). -mac must be set correctly.") + var ( + debug = flag.Bool("debug", false, "enable debug print") + listenWS = flag.Bool("listen-ws", false, "listen on a websocket port specified as argument") + invoke = flag.Bool("invoke", false, "invoke the container with NW support") + mac = flag.String("mac", vmMAC, "mac address assigned to the container") + wasiAddr = flag.String("wasi-addr", "127.0.0.1:1234", "IP address used to communicate between wasi and network stack (valid only with invoke flag)") // TODO: automatically use empty random port or unix socket + ) + flag.Parse() + args := flag.Args() + if len(args) < 1 { + panic("specify args") + } + qemuAddr := args[0] + forwards := make(map[string]string) + for _, p := range portFlags { + parts := strings.Split(p, ":") + switch len(parts) { + case 3: + // IP:PORT1:PORT2 + forwards[strings.Join(parts[0:2], ":")] = strings.Join([]string{vmIP, parts[2]}, ":") + case 2: + // PORT1:PORT2 + forwards["0.0.0.0:"+parts[0]] = vmIP + ":" + parts[1] + } + } + if *debug { + fmt.Fprintf(os.Stderr, "port mapping: %+v\n", forwards) + } + config := &gvntypes.Configuration{ + Debug: *debug, + MTU: 1500, + Subnet: "192.168.127.0/24", + GatewayIP: gatewayIP, + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + DHCPStaticLeases: map[string]string{ + vmIP: *mac, + }, + Forwards: forwards, + NAT: map[string]string{ + "192.168.127.254": "127.0.0.1", + }, + GatewayVirtualIPs: []string{"192.168.127.254"}, + Protocol: gvntypes.QemuProtocol, + } + vn, err := gvnvirtualnetwork.New(config) + if err != nil { + panic(err) + } + if *invoke { + go func() { + var conn net.Conn + for i := 0; i < 5; i++ { + time.Sleep(1 * time.Second) + fmt.Fprintf(os.Stderr, "connecting to NW...\n") + conn, err = net.Dial("tcp", *wasiAddr) + if err == nil { + break + } + fmt.Fprintf(os.Stderr, "failed connecting to NW: %v\n", err) + } + if conn == nil { + panic("failed to connect to vm") + } + // We register our VM network as a qemu "-netdev socket". + if err := vn.AcceptQemu(context.TODO(), conn); err != nil { + fmt.Fprintf(os.Stderr, "failed AcceptQemu: %v\n", err) + } + }() + cmd := exec.Command("wasmtime", append([]string{"run", "--tcplisten=" + *wasiAddr, "--env='LISTEN_FDS=1'", "--"}, args...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(err) + } + return + } + if *listenWS { + http.Handle("/", websocket.Handler(func(ws *websocket.Conn) { + ws.PayloadType = websocket.BinaryFrame + if err := vn.AcceptQemu(context.TODO(), ws); err != nil { + fmt.Fprintf(os.Stderr, "forwarding finished: %v\n", err) + } + })) + if err := http.ListenAndServe(qemuAddr, nil); err != nil { + panic(err) + } + return + } + conn, err := net.Dial("tcp", qemuAddr) + if err != nil { + panic(err) + } + // We register our VM network as a qemu "-netdev socket". + if err := vn.AcceptQemu(context.TODO(), conn); err != nil { + panic(err) + } +} + +type sliceFlags []string + +func (f *sliceFlags) String() string { + var s []string = *f + return fmt.Sprintf("%v", s) +} + +func (f *sliceFlags) Set(value string) error { + *f = append(*f, value) + return nil +} diff --git a/cmd/create-spec/main.go b/cmd/create-spec/main.go index e387ea8..022cf60 100644 --- a/cmd/create-spec/main.go +++ b/cmd/create-spec/main.go @@ -9,7 +9,6 @@ import ( "io" "os" "path/filepath" - "syscall" "github.com/containerd/containerd/archive" "github.com/containerd/containerd/archive/compression" @@ -244,7 +243,9 @@ func generateSpec(config spec.Image, rootfs string) (_ *specs.Spec, err error) { if config.Architecture == "amd64" { p = "linux/amd64" } - s, err := ctdoci.GenerateSpecWithPlatform(ctdCtx, nil, p, &ctdcontainers.Container{}) + s, err := ctdoci.GenerateSpecWithPlatform(ctdCtx, nil, p, &ctdcontainers.Container{}, + ctdoci.WithHostNamespace(specs.NetworkNamespace), + ) if err != nil { return nil, fmt.Errorf("failed to generate spec: %w", err) } @@ -365,35 +366,19 @@ func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPat }, }, { - // FSType: "bind", - Src: "/run/etc/hosts", - Dst: "/etc/hosts", - Data: "bind", - Flags: syscall.MS_BIND, - Dir: []inittype.DirInfo{ - { - Path: "/run/etc", - Mode: 0666, // TODO: better mode - }, - }, - File: []inittype.FileInfo{ + // make etc writable (e.g. by udhcpc) + FSType: "tmpfs", + Src: "tmpfs", + Dst: "/etc", + PostFile: []inittype.FileInfo{ { - Path: "/run/etc/hosts", - Mode: 0666, + Path: "/etc/hosts", + Mode: 0644, Contents: "127.0.0.1 localhost\n", }, - }, - }, - { - // FSType: "bind", - Src: "/run/etc/resolv.conf", - Dst: "/etc/resolv.conf", - Data: "bind", - Flags: syscall.MS_BIND, - File: []inittype.FileInfo{ { - Path: "/run/etc/resolv.conf", - Mode: 0666, + Path: "/etc/resolv.conf", + Mode: 0644, Contents: "", }, }, @@ -428,6 +413,28 @@ func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPat Mode: 0755, }, }, + PostDir: []inittype.DirInfo{ + { + Path: "/run/rootfs/etc/", + Mode: 0644, + }, + { + Path: "/run/rootfs/etc/", + Mode: 0644, + }, + }, + PostFile: []inittype.FileInfo{ + { + Path: "/run/rootfs/etc/hosts", + Mode: 0644, + Contents: "127.0.0.1 localhost\n", + }, + { + Path: "/run/rootfs/etc/resolv.conf", + Mode: 0644, + Contents: "", + }, + }, }, }, } diff --git a/cmd/init/main.go b/cmd/init/main.go index 21cf44f..c2d5576 100644 --- a/cmd/init/main.go +++ b/cmd/init/main.go @@ -20,7 +20,7 @@ import ( const ( // initConfigPath is path to the config file used by this init process. - initConfigPath = "/etc/initconfig.json" + initConfigPath = "/oci/initconfig.json" // wasi0: wasi root directory rootFSTag = "wasi0" @@ -157,7 +157,9 @@ func doInit() error { return err } log.Printf("INFO:\n%s\n", string(infoD)) - s = patchSpec(s, infoD, imageConfig) + var withNet bool + var mac string + s, withNet, mac = patchSpec(s, infoD, imageConfig) log.Printf("Running: %+v\n", s.Process.Args) sd, err := json.Marshal(s) if err != nil { @@ -167,6 +169,31 @@ func doInit() error { return err } + if withNet { + if mac != "" { + if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "down").CombinedOutput(); err != nil { + return fmt.Errorf("failed eth0 down: %v: %w", string(o), err) + } + if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "address", mac).CombinedOutput(); err != nil { + return fmt.Errorf("failed change mac address of eth0: %v: %w", string(o), err) + } + } + if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "up").CombinedOutput(); err != nil { + return fmt.Errorf("failed eth0 up: %v: %w", string(o), err) + } + if o, err := exec.Command("udhcpc", "-i", "eth0").CombinedOutput(); err != nil { + return fmt.Errorf("failed udhcpc: %w", err) + } else if cfg.Debug { + o2, _ := exec.Command("ip", "a").CombinedOutput() + log.Printf("finished udhcpc: %s\n %s\n", string(o), string(o2)) + } + for _, f := range []string{"/etc/hosts", "/etc/resolv.conf"} { + if err := syscall.Mount(f, filepath.Join("/run/rootfs", f), "", syscall.MS_BIND, ""); err != nil { + return fmt.Errorf("cannot mount %q: %w", f, err) + } + } + } + var lastErr error for _, cmd := range cfg.Cmd { log.Printf("executing: %+v\n", cmd) @@ -219,6 +246,23 @@ func mount(m inittype.MountInfo) error { return fmt.Errorf("failed to run command %+v: %w", m.Cmd, err) } } + for _, d := range m.PostDir { + if err := os.MkdirAll(d.Path, os.FileMode(d.Mode)); err != nil { + return fmt.Errorf("failed to create %q: %w", d.Path, err) + } + } + for _, f := range m.PostFile { + cf, err := os.Create(f.Path) + if err != nil { + return fmt.Errorf("failed to create %q: %w", f.Path, err) + } + if _, err := cf.Write([]byte(f.Contents)); err != nil { + return fmt.Errorf("failed to write contents to %q: %w", f.Path, err) + } + if err := cf.Close(); err != nil { + return fmt.Errorf("failed to close %q: %w", f.Path, err) + } + } return nil } @@ -227,7 +271,7 @@ var ( delimArgs = regexp.MustCompile(`[^\\] `) ) -func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) runtimespec.Spec { +func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) (_ runtimespec.Spec, withNet bool, mac string) { var options []string lmchs := delimLines.FindAllIndex(infoD, -1) prev := 0 @@ -275,6 +319,15 @@ func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) ru entrypoint = []string{o} case "env": s.Process.Env = append(s.Process.Env, o) + case "n": + withNet = true // TODO: check mode (e.g. dhcp, ...) + mac = o + case "t": + if o != "" { + if err := exec.Command("date", "+%s", "-s", "@"+o).Run(); err != nil { + log.Printf("failed setting date: %v", err) // TODO: return error + } + } default: log.Printf("unsupported prefix: %q", inst) } @@ -286,5 +339,5 @@ func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) ru args = imageConfig.Config.Cmd } s.Process.Args = append(entrypoint, args...) - return s + return s, withNet, mac } diff --git a/cmd/init/types/types.go b/cmd/init/types/types.go index d1e9714..f675913 100644 --- a/cmd/init/types/types.go +++ b/cmd/init/types/types.go @@ -23,6 +23,8 @@ type MountInfo struct { Data string `json:"data,omitempty"` Dir []DirInfo `json:"dir,omitempty"` File []FileInfo `json:"file,omitempty"` + PostDir []DirInfo `json:"post_dir,omitempty"` + PostFile []FileInfo `json:"post_file,omitempty"` Cmd []string `json:"cmd,omitempty"` Async bool `json:"async,omitempty"` Optional bool `json:"optional,omitempty"` diff --git a/docs/images/alpine-emscripten-host-networking.png b/docs/images/alpine-emscripten-host-networking.png new file mode 100644 index 0000000000000000000000000000000000000000..778ca56133e5659f67cd24fe4667e6d4c4ca74aa GIT binary patch literal 57622 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%0D) z$D9WeB-yZ(6Hq1=8Y!jCbWcLUsWJl*@D&4+wAKN7`l+UnVfJl z7m?1K`MRE=jx2;BULBN6kzVtab?@}eM{*>t?_y%VHjmh#C?$$x)T*Xqbb#XuNGzO9 zY(X3mojq`LBrC;Nh%jtFX#1&1{7f`(DrP|Z)S2~)n4`W;`9#Nb9t3u z{`9h-hMR3@35!fD<$-~ktM7F85mSh3Cn>_ulc&CUh^1*U@kA4aLXwMf*4-9Ep15%W zNx)@IP+`5xej_Ry!VL}U5*Yz>s-5D&n}tux8Ey@z^e_2JSeQm|HW~;v?*oTVyE&{hS{vzr@Escy zQFNs_6`m1?`J_G2S58>-2EGcVU8i!y$i>;McyA|Ox?f4A(=BYr0ommf(K;0Qwk#z7tPxQTr|jEBGC`wy@)P zufQ311IvcUb-R1V(91Ez@+3z2-F^I{LHQ9&X2R(%OP{3QR4@EUHs}Q(#K1e1j6~jk&`~m@ga0*VFIuXB z|CkYkREgSnn_vg?5`8dxPnUOHQVb|iyD$llSJ=X>LuU6IqryHiR~=eEE8Or8Tt>&h zU2pufNouQ&j$r|pYXUznQSAE{Y@psDdtB=?Vsy<-?rGc#Ljz0(xc)-jp^w(^L4Jf1 zSjPS~{NN5vQ4B~5mA%+iFyT z^MQ~t3`66V+;(nf73@qG$wy$9)ZfN>ae;nmtC*Ktw*b$}F8)Z6^y^}^B@K9A_ zs{e@lqsHlqm3{g6GI(Tf&=IDUTW1~D!b6G?SN7t}WBqh`;Q@c5)m}xU>6DNCO8_j2 zL7A7-y7A&lD<$)y!QHuC7as4+m5gudr`RytXJyR5O@+jN1?H z)z*Zr$X8aX_NWbPrtI9@T@t3Y=mNkA8!ps{zEj3WHv^a8?fW=7Gy7<7AL}!i>OZ|8 zSE~aa2j}6cu<;HpaFKI=pN_gkBf}QXKH%r2Z_Jk3*00V|Y%+___$2RJ7kFzK4)DZ^NG~u@?Jb8uVeJd_c>L-=C5%3!r`5;G^&Og`BQ6 zeN@vv+N?I>yLXZ^G@~NYE9I_U3%I>~Bn?jQ^>jIeKi?UDeY_V%*RP!V8FKZ9x*bfv zaP)h_KB)`H}wlDnU9q6e-lYbsYxZg5PNsJ5&^v?Rzne2R@ zrTjbnOBVdVVVpr7f8Ntp?#sts z&=LyO4m&)YJe(v#0bj|m0{3~*7EE>bOzt!(pb`v7eS8w}U|DD)*JJ$#TRFPNooL)0 z3L-%?A+bJ|@qHYF&0|{Ayo+g-H0$4foRLxzeo%aOMRu;+eMg1SA8+62!y>j)vF8M8RH?_3FCK*0F*a*z$`I;`C)*VKX&7>7}NQCCBt_+kqv@F0B4 zKxH1=pIu=$T7Kf1c=P+(Es49V(hFic%}N|>j4}_{vHK5Ti>!hJffSk{KwsXxbzxHF zr`zXdk8UrmsfrcJY)>!A!)+$I7Ic|xgD7Sc{b{UYCv6-CA-2` zkq@PcC57&mLtl~tN_b%WNqxCu72)JhAb=(unvaSQvZn7Geqag$H2Jf%g&e9dJoI&P zMo1T@EfHfcf#WB$hr4yq+(kznM#g0FeMFh3i5UiC*7VFw&AfV3_ydct+HP|Q zYxmHp6x>iAMwvTSo*N?!9dtd-z^QXTaPRShK(et|H9UG(X}&9$u-=wW!SfIHUsvNi zPyX^o*dy#Pk#gKz{cBNNOuvgr`Z}{B9lt?dGK4Iq90K{?(kqU_ca179sNs58Rp|N( zZw)(FHkdSU(Ojzw)!Ieo_e4n1gzyAbybQPDUS970@CCvMs-IX_Z8 zTGjdm+dVB}A_EPBFZOn^<5!btsG@_ueOu9ni!nXAX6-V4#Z4MHWqip2pXTbtuZwDx zmX7JDp?$da3{!2Pl{-kV@Wsh<^EKztpRuAc3XV1;j$KGqUs4(;N6;^V=^ydZ5$~&+5@s@ex)@w)MsSuBc!3f;}@%qpm+dgHGD=O z&?4hxWnM(!F#+#&_okw%>fmhWfbTcc`?SD|-cJ6f)5qsf&P>>mq?JBkW3Zm{T?#6c zm8_J5+0N(A;MM>8G^+pl`}Lkc`!@j_oPbxznu}*`{R>_kgph``kjRX z@09PCo0~dc`4D@1fS(?da(c2pKU;u43<=*41vGG3fEfUFE(jq5ZXr;4Fu@}L=dfR_ z3#SijjPOfpe%4X9OVrnp+ip3t=IyPA@zchb7@m*guCW2P^s6V&o-04dkb#{}Lz5cB zI4Tu+ng_-*P6k}WIlAFkcXNjbU4%o;L0R69qdehlPuT72XYZXF4SV)KF1KUH9^*Um zQNHBSUGe45!#@WB^Vb>e{``&GVJ8MCqAEqq0bhVVQYajz2I#d1Z>AJqqkgX$=}M;b z71D|u4%g>`?tq+6(BkWcE=Y_YRlhkvd&c8{3R8Ghc0rUWvEMPMVM}cps-*)Lo`uX9f_t0ttTX3xbh3 zkvzO#xxZgsn%Y@CeqmMpN?R2?YREAYBz*9wUbFRz?0rp+zkAyAM*4BBdl_qri=j(& zX4ktDna9(1Kc^X!pnb|?U64AqedOx+XldR5=d~5Ly~;Qd^=sBo=te`6(=p8;LZO1I z^A*Jf+RkZWF@?kHifZWEI;)d%1TM( zk4tCaL9o8MH{DfvPA)qQLRozETSW11xntDrh9vUWP*5RT@}z*PCa0!5m8ppn71Ee4 zSR#VmNe~BeKjvspgkd0 zPZs|HE%`{qgpMGPiGS4k<0x-aEME2@4kcW%yDVP6R4JlW#YiSzHu>;}F4@Y`PEWUI z3|yD6xm$HbW^uBO9Q|bjZo!tZ&tx=B%sUYOb{+(tKxX=IFmHKLTR}jwOm;Vya(%7m zuB*{`Tow2DM8xn9d4~1e=yLi@tFPo+Q3(?bXj5$FdU%G?0VdwhXm-Kd#$LGSX=^uw z83$dvN$BtpROam>0SDt_`>_-Xtvp_9v&t6A=uSN{$t6)bWS28XKJT_sABW^JuWmv? z^HF|o5=U<=>z=3D@LZK=(?l`q(;Bqma|#g<**Q(>c;37CbBQ(VH?qXgRL zaGu8)= zs@Q^$;)5?r(wzU|G^xCV;gT$suWgfn<4#Lp#)QX!oq9awM3+CE2q>+ z6O#cNW9&vE(R>$_(625N3dw&Z516V=iFle+RyKFHjB_AE-UL#fhDHqZFhs74t3g2| zFq8w}LUiP*t;6H;A0`0kBRvkGO`6ofXLjI;rDBd5y;)DYH4JP>0ss{)fobdeer2|Ig%8*m0ZtR3h z^XX=0L$ApI2ydQ;Sq%0^A{A(shWT^wn-wS`+xzvFWuebI^UD=zhZ`77vQoaJ8yE(7 zgR}y?JD}kXdWYsjgWR-u&NDZq4E$8Lt=1;>F{(I{R-ut*AvX8;>oWM{6s{9|?-+3#OY5ppaQ6rg-$W*TL|DK9Q6>n#~x?e>@^D zV}-%z$kz6L?GgSJ! zt!-48zV*z|^-E=ojmSEfxU9U+vA#bodz~@a09TNpic={VHMT-2Sey-mI5nBKOkbPD zr25t{j;oWi@o-Wp6gTc$jsFXqrBrBzM|}+kQQZJ=Gt&dY5sXQZJl(!x-Xi3r76R5P zDG9^AKA!7pSP1s}h?L5C?wU94sy37j&Zt_G^fF^#f3brm<_lnxNFhS2>Dj4o_gAE#|XG40YX3PZF{#V3Ax-;)n*lS{0dJu=G$%b8nwC?Dn-v` zEBeX>Lwr}Rx80j6*ByJ#ZQe6_TAZ(VFFSS~+xlIR1F$vNnD-&6w4QGV_pqIuH~VUT zy8y0k;X@6SC3`%D>Li6yZV&&w0}`iSJ}JVAmdE+8v}!l~p#+$#v%*sNf?1bCTgK#+P(M8Xv#u^k~`+ zwGZ?W9{;p}Q%ljO62O!na;n;A>@=ziF5!rgSq)Q83H)jcB7QudiEKXTWBT!7hH2qv z51?imsbVZ_enIZ*hc15=Gy<_3^#M78 z_R{q*dWn${4iiFuzQ({uOL&(cWC8rp@gijBVtH-|IDCnn#5u{v1htFqLqvnCN+HQle$2ILXH@<7CBeTuf`8jRL zrDtT#X3$)5t1^56{W8VDQ6=YIr^u{-|(G+HZu6Xe7YAwX)C*wMmvafxtY;sVRv zF4Yn4-w>hN9tN~Lx|&Sje_dW_sC`9fTj6lU#yP!n8Q8No-~j)HS96Zg#9f>kP44p0 zb?yXcj}l}2i=DdQrxQVDcpo@)-6Bu%In#kx8R=&C_krVMOV4|Zob7G1(B2(F1n2KJ zNd2RQotWs%uJ-PJSSGFPUVc(-h?f$Oi{4zGi0Ya>7hw?`;rGxFiqNBWI|N@KWWq~p zCW&j4Zj)1c4HnkG_MjQkA{o@5PVCtG7mtH8nqt+ei6QKj;0kiadM&1nIxgr2QC{sh z`YEC(H`*iZv0h~$9}Me?{04rr8#Wht)Blno=XpCCadqp)St>pk4CH+VHYhWc8W3Jw z=w3%W+3N0>cZz(Mg3t**L#V;9iDbmlo?yv}oa5yjSQFsrQA<{rQH{HGj)OLxy+4rC zUfZ`|TMe7(FsH|(1^_8T)*!Kt)J4Ck$hXaQ`Mm zAsipyoX<7v)387lRVi(W#fun^)wWSR9AG`AySPXsQD9W z=`0xHM?zO|N;~~a43KRvXr!oGmg4&8 zAQJ3w?3JvYLbWb$9eRH0F@*ctzRb|?A7tK3!p%b*5ICxzG~Uhgf-UnAq$20zi069h zJyDw$K5#8QK4k@G_wL)6F$@OT z){WiKwK@HMD**u~vzgp0PL9mp>|D^9k&qNYV5}QOm?(afGE5P&;BQbyGL!9%a@zj_ zBR6*c9Y!ko{2Pq4B*SL)p^dVbj_{@nz}V{KBU=e!QyuzQ2dOBvx?Rx`Ju`X3#j|I_p`S-A9xew(=xSc$f1r;6Qv%jW zYa)291EYm#p+G{qFd>k5kuoPoGeCSog{*@|Br&-Ya0vdtMSJ&pjo3T$IxwjV*3a{C zI*yO=KdvyI!d@-kZf1HmpIAniW-2509-y+-5GaSh`4f@GaSgV@;P2hSj~9VUMquTU z2%tb-&tM`ptS1bohM6)W8MA@1dwGO_{BeY@8Gxj4?b=tSMl_GQmo6A?VL<{bWC91g zv3`~Ys4!iMWMc&nAmeN@^wR+$5ePeoxuzUm4#^=eMS|W7LDWol3s5nD0&r$7{=icAm(3NrxNyk;CHhZRZ9%JpXQ`>YKYQ1YV@zY3}^H33O&jG5DMQ#IDC zLUFRMFrAZ7U|;VCr}ppW$I1#vO8MTiJ7xFm(6%GgjV3ZA3{^R zM31ia9#R&?S(Cr&Oe{a$w?$vjn$HsPsWuEomN7j`r!x0wBv&<MMPUirQf^Ie_&~%R!A=@31s5SpQyS5er(ssU_ zvm17w3Z+c<($I{*!0~~v@_xsxIRC~j3 zU?_vw8;2~q7IbO0J9yY8VVE!Oj$LsZQ?%b&$f-LTMh$1!n$O|LMyMAT;Fuhv0F*60 zTxc#_A~;W;x}%-ICgB3k8Dlc2<+QFLc2NZP7uhY{zI{;Xtilv6*Aj3OmAHh;WRgIeWrCQk+UD33sZoW4N1%#GnbU3yOT0s2MikCmgaS3IniiY;rZyA>8i)pLW z-wxY5Y1jKCam7naJdOF;CBHWegHdNpC!mcfI^@z?`B=HAR;+6u4)6Ye*qCad%eQ=C z01>u0cxbfZ6tW&ACNC>OHIF({c-Pu@$LRP!kjd1)$>eFy%rEJthI-Jf{pxJ=?s2pn z^`Ee?ZvTH+;jXof}J1D-8eHlK(4P35Av@aTkmuZn_XR9 zEWUWYKA=~Z)Vw~NpP#nCZzBRPGeM0UsUUg?IDbIETLff*FK{uBbJBrv?%(~H;{~Z0 zggNKlm+2UO+o>UeAz$#pEM?-q7c6@49#ODpUfFs`yZjsx#s9q9{Jmqh$@bJ60Dz5~ zj14OxVqYpK&|gy*aZ{s1%rFeax*FSt=tFL+1VoBAM&f)Toxk}FzHz@lrsa6Q%?U>I z?sTqPsaE}sD>FrV4SkkQxVM(V|DczDLQF33zfS+xQK!TI`w^b=HX)T5oBJ#rD1iDY zhZ3nT=rzVOAckTfSz{99V~baS5ptDY{dtu?FO~#};|eABLmBOnoZ_P68uyVWI<{Yb z6<(?&6$Hq{^?A&twxz`-sk00DCXw`#7fej(d_>+q2w1*hZ~4Lt1w$1iXH zl)1i;l=%DnAq}8Bu4cfgNC6bM7(16_NKbc(JqAPnm?I{ z%_WSge@Vk_|JQul;o&2;4j24eY6)b8UNZ~|JzDt|MT7fl`nqIr)fjBtpLM-?1=K zb36y8j(-oGT{a97*L6|{pGX;}U-!&VKoM@@74r+=G3#^{tNZ&Y!kLbC+dg-szj|4S zx%Tm~$V&L{he-$TF6h0 zi&payYBu0ICn505If}ghmiPS|+`a14$K@vJ%vXGeqB#wUJ(-_Pntd%dy*xdZ8$%=- zLPz^v9dWj}&PLscZrM;Y_oomsb3V&<)EoG~ms&L)KP^ArD;}oIh^DbWA@|?$U7?ur zi^w3-frr)9r=Xs)bP298@dg(*|7gJZvW_h^s%W(!M(7kX`#QG%LYXV0i7O%?lzu#>@v0`e8&Ll z0d<&r{siU&y21tegzt3$<^sL)0kH#p>H+eE;)A&21$yHJLIe7w_vIyo1!5ra6Ch23 zs}g=L6{sgkK%tZqC7-7vL`kjIC*w~P3p*%doE>EdElnb^zt{PaBxWf$PYz&IlH!Vr z#0rx9*pKb9X&4D&ap4l}`bcG@Ffc1QApmr}nI|wP8c(7~u|cpjTZ)dHW~{%Ql?Ugw zhKDumYcZCq&NGK{@0iceUl%8ut0zuckV2kA5#JgMrr!~HUCb{+Y%x=u?7pFry}q`Q zZL3B?C?h?PYoeuA;nAoc+h^xRs6Z@TBX1PZP2+|~_WcO)^F?>`$V+Ah*JaFb#}JU7 zYUj^tJjHYBh??mMxFcxyfi?CPJ+|o{&~VAlr?shgeck+A^OL^(4t-6AC6cgvsr_n5 z-{tlCusntU^Z{m)qvN+W@`=yz`i9?+1NfLrG<0?!4)GgXsv!jM0p>1fs$ux2e^A|U z0QWUg0sK*6ejyEF33w4$k|Rq#5fB?r0V*0ZrRU2H+M(^V{mTi`&;yzcAd*_nfJRml z70kprH`I{XuI1KdK5-hjyw{tr&Ub%6?}fuCF;?nObba$?Py5y(n^uENER6QpF{&Ig z@f-+bppO{5kLLYy0%yCKxp$uejg!r87t7&W6|$V~?^pmi#h`aYr4d9C|IK>m8%{El zW8Z03zUDN+^)7aUr;dr;VsYgDIxaV5lv25JoTl<<2!mJ%AwgN-}pQJi0U5bXJ z6>Mf=yd-%eEHQ2-PcH@F zv^7iiS(+T9((S{5Vl!FynNaRJ z#7&UK(S)hq)HQ-TpZw+F^1gh`xOWB01+fU0m?#hdQZgG1r3sI5xoBp3a0kxDO|Ld* zO-cxFVIv;bgP^Xps#OPBx>X{`_By!+iC;P9^r8ZUEz+Nb1&d5n+PuguEk&jC#ftNvWEH1>{dMVq>(+zzxrQ&?e6ZqgNN%3cLqj4@CM++8&UW~()-9&j8OJ9s z1Rl-D0Le^eria@N?JCP=r@3Oqpo&YSqiMtIkMCATp=!sZ6qY^ITcH`OXHFPyYLIw9 z;@1%7?h*5GTuzP-`Nt|Bis~1UkdoB(hvPQGdDAg3?)i|?il7cm{54gW073Kl8I zF`QeNDM3TG)V^w;tc3Sm`$6934xE+n)~Qzc{zK0^xRqPs2hkVB*uPne>O3D{0P$l$ zY2)DvA!i?>;?%*R{m*))Ce5#=cy+$gd|Fz8Wh%mTj<7spw8z&`BOU#B_ifltsUbNW zaJdUvJxtWQ2=B(24j^>=HjPnY*KKFey&Wi3TZE~XBZQRfjCC48=r{#zaxC&_L&&E) zM`z-Z11=2L>p5nw*DK<;yNVTdZqA{KKeRB>t6>EwSfFjc!{A9lxDo1>t3Z1-Yr!SQ zqjm%@rNNIMl*~s%1Tp_b#nl$?rcMDTx2PH^A5R$aCJIWr%y!GDVqNuEj5s>3+`Xk8N4(5%uf%%dE(;@~=N#>)0!)gU3?t><+{T}4<*&pS z0PHaN&zxG@t;;gRc}w|y_ZL25N5zsl)>1X-juIT5SZL7$qjieBj|21R`X5%IS5(c% zE-DTVJTg1iU>}`jHj)V0{!nddU&I3JB*VbaF|5(}dr)*Gck=|-KjGff-8Ydzg6MGl zJ7z2^>j1zOx&;5-g+0s0^_L8nM>ZdZjULyq8KK;kIv%Gn1t84nC=D4IJqLWXMAb5K zI^Hx4>*XD}ScdHvcER)sJ{rzag;?vl82B&Ohcm2fHzu~-Z$U9KVCuKrc8Z8qz%E7xh8%suX2_Ka3+nw z1h*Rkn$us#+6+el0?qxqteESi4OkDhfA(EjH?MoXC`yeaaK3InwBI(qk1*MA&40wa zyS9#3pHI74aa}pL{~Mj0d4rK(bOi0H?7XWQ+YfyhCep_W&(~Y{UGgQm!UvsKa~;I0 zI|V+?)OOm=-Q7KDdUF-__P_PVWxjvWAAz8*gd0Xjyn5GRwK}u3Y1#2zWYJAvXnv0 zp9LI9(WGUifuI2cc&1-f%loN|uO)Nfw5{8J%g69wL4h9Kt@vQF0U)oYSH%;l|6V{| zy=3i)sbzUYcjNMQt$~M#ap4i`9tSviTf0+i^w;|(yn`b%U7@&~NHVy;Xa=kq`>r9M zo^#%jBYWP#KTdMk!YqdwT$E9Hb+uXm3uD1LaCEjOz@JYR^*IRKu;}QYMf@@yw$sPXi)5H*8dW$v3Esv}uX>ab`00PqYta zA8l#|3@Eip-c}Y#>0t$tTrq!6ih-Hnld5AzRu3!Zfz)1@o%v!-6S((QeLQ7U9_MqcA6T3 zcI)bHwM#DW^98qC<5h5V*G3Vcq6bqVVDf(u zPA~H6fMcV`Yp^S@2SN~}-xT2?zC)218 z_{pmvT-_gt6*`*wjeyO@kejaCwgcBe#JZW&VbMg8Fi&`j2FvUuscdkyLP>IQ?&*Pl zMil2N6wH)u9YogGxo3Ye`8=Z{!E&2e{$RHniTK2Cu!=1g$%cXlQDMplNsbQo-=X}a zt|da0vO-4*s#FWCuFxYiwF$@BrWyp`tof!q1LrGX+bYNHx+j+1n+_tP)o&&*ld97I z4v?5C=O3UEV<3!w{12(K7#9$20MyI)$se5p+*1UCD`Mx16lG#)Ri#FlUz_TAoVWDi zAn9QCu@@H{nL;Oj&;V-AHCD&;9yMQvO>_;}M*{S(slkHgZ|ljJ#8x1bGA{cEl1;nS zYLj~CECoyWi6#4nemH|u0(t>_+Gs7&Yu+zDd%YxC%+~OFbhl}#-7vhYI-yW19nCd) zR4DyyE$eSL+ToSDS*rhtUmlPM+JxzV2tS}W+RQO6f!+6*$7q% z{${P>D^3dk1zTB|G{hYSfK(GL()qjIcB{pSf+Rqh7QYH>ojrh%g#XR}%zk^pU&<%8 z=H%bXC+$y=;r3gS_mFQklQo}aFT8Mnh?|EIE&E*ebS~v6yNg+~DIrWXPLUgd49HSi-o0=7UW zbVHd(%(BCVe~Yc{<^IUEvZxs_)~r zGT=+${7}ldU#To^MJE+VLX`BvAM8^QIRF&sP!q%!mK|r>dIuB{f%#7=8FGd3nmUU% zaKj@qgsDKkbr_}hU5XtCO10$noKUlt<0xZb$J1lS8u)OIfNU<^)`P1m_?fli68KG- z`fmt#h}^5&wEGVhg#-KVD6-CvI#JXiw~t}4j-Bgo7XV5K0r}_Q>LnH`GKcv}F?|o{@~A`3 zna1NY=XzJCE!$1i!2cju#=TzU1UXR(fib0r^5zXlZhrcO5D;rv802CR5Mj25%aB6v zH+K;*hR*x)opLDrtNYksdF7F~mU2n!h*`7nQ=(4#5toQcT#19qF@P^PSckT&vg1$E zG%?~^?JOrwm(xE{xPmaTreFeutodPEk1w~6pYh>h-*&(}+-hsMrVpg3{t z*bPf%ZM^Q5_l#EBW!EW8nAkb+Hr_QdYKd>XMsLwvbx)=RS5KSUdoJH|)=%zJ>uz$i z8|`g%w+px-aS$Lg!xGl*jEln z{@H3|*AvN=6DFPins^6@$$l%$FD`~ID$EXdU8o-XGXm)h3OXWERC;=*5g&Ol#F2>h14kkRsE#m5Se0f;AD!r^!z0HX0 za0o`XnE@8(&t2e{mdRU&F@B$?l}`bYO9%IhR!O{u$zADyNRhkZu0W_I#{iCFO`L7F z(?;R!S1H8&*A}@0m&A($&Pu04%AdP`O#|-g9dxmFi~T7Ax91ro6bK^A6#)IruVntD zrXW=iFA-59;ma3m7J5F~4a)|6V2JZL2{o^!(5-iV31MlT@HTJ>`-^P8Rft%PuuZ1F zc-Gy`ShePY*Eo$_p-Hw_XPx4xBknK>Rie$ESVj)|29*${`a*K`;RNC&@IX8qEGxQz z(wy18l2%;)Z9ZLUM_?Th^e=J^IZ_(WjyBff=(-X*-rCKY8~@Az1;X>OBS>0 zOf7^kqM*}-6nO0-6;he*Z+_mdl+&ai7%VBXI#vGZE{qw04)JR-0Hx#!(uBWLo~cimE}_vV$odT(&cc*=EvN;sbmSVkmJbPDyf^HF&FUM<)1r+$$C zyZl&h2k%$t7y<_<@U}0qO8uug>YpQD^1|D>PRWKdOrD=631~;<%i)$+=$rjQGCrOz zzs>i2V0k_HZE&qLi@gf@|91iP&V=*mtcOzg%P46$31q`!`iOj-dUntcnU~2+alopj zW;G6lZho@^)YuMP^!_p(L{e5Vv>uqrPjQmcYg(cEt$&Q^M^I8$WVskA3FiB|5sUcG z8tTWGp+y&Fbo??_<)o=sbHSzlxmNgE zaQSH!owm$4{u43iznD$B%QF1QZ!Z!G@UNJl|>l|Bu)$jgd1=R1>&(Qqp zn51)9mb8{9#3LFikt5MA@YA2wJKvk@00@psn3Y_+S*k?sm5aAq@Lqt;R3e%>8L38o zbobNG_=tS?EHP_2&kr$G7?ZLxybK zW;7*D`<<3@JFm!N&tutHz1RAE?PkClR}`XyK4l;{x&XkD82hTbE{G6MdqD{(HwmO| z}`fX#gUlA zceQV>SE>X9Q|yEJRapQ;5%0h+B+IpR&S%nS2_gM_#eXAc`@jz_`=ECZ7$fDsjkuKW zZ3rk(ob41TGd91i@q>|}0^>&ScfND=iL0z#BhV802carGhKz#!vOD{}jbOy^t7f+l zt$oPAiaHG(p!3c5n^&RKmo0$nc;3Oy@Vr6szMp)PnUT3ZnE*rCNA3hv**4Re5XStK zJS3j1RyqT?c{&fj|6PikNdmx}wD3H3nA~4Ey$obW81H8IF2GWU5v6BGO;wp)SFN~L z_zx*}e_M^oY2fI8y_)}3ADp2&q5UU@b8REzhYQcx>C6-P`?W8Jf{HFSG91TQSCJe|<<^!3dl`3(r-oYfyd|A#`|9%1rq;RCFn zZ2-#Gs|<7aQw`(roKdic_YXsET_tJ_&_(?pb-UMgtwjHex3`RntLe5z8-hClg1aR^ zaCd@x&;)mPZ`|D_!QBD`cXt|s2X}XOhuh?NUODeM=YHR~Kkoju$AI2aRlC-lbInyX z+H3K6nfroLIDs6!5Na}tvD<|l0m89rSu<|^>5=thib&JO2TcrOgc>PU;Nz*0{-+cc6fb|Wy5Sm34uLoR!fsGD#kd-)_6J<)Py;BH{& z_U;7rlq2(rkUbLX>p7jnLczXPZvfbL*mL|e)^*HdXdwe6w*5J=f*cJB-NUqXZ>i&% zyiU^R&RCqgshdeJJQDbBw-I~oeF*BliK)Fh$Y}VH`!!Qn;F}K;Hj1L&YFr7jZu3l#MhciAtUkemYiU8Ajh8(DhsoRSRkU!O6dQ>g?>Z8e{& zZ|7i8mmC@JnMapj3!^V-(pxKWw*yJ4BB`Xtdp(DtZ{KKh_K`YumLjIEKfnBA?L#X4ui zs;4IVm6M8&z@Nvqrn$-crk9CqlCjGmz*xlaiT`vkgjJ4r)ZrO-XNPO_q5*wCbQMQ6 zwtQ6ka9W`(dQ}Rm5<5Y!UVZJ7N8L$+^s$dw6U zdCqA49zQuI@88?sws3w6wX*pPCw}Vr0O5zb?1;cWakwLc!RjFDXbF#rsMoJboA%7N zX~2H@tJln(%I9N%2z~U=6_gScG0sf!o=Fk*hp#UI!~Jf!^CQ9i*cyz`RsFC>F(e&y z!3sZJ#(v!u)<;Ly%)yZ#7_H*4R1JX@a=3~{7g7kcL!nJYYhAi7!@1*~ENt9JF`ofn zANds%xw$u^T{=SKgB|O_WK_bCBwPj((3UupgS#>SLBICN?yL-7&`;u4zbWW?*`6OW zJQwhbq9^b-%MWNPl^#LF&s!!t%fo7oLwSMb4gih!kx7<8Bx-B2g%uWET;A?r*Q#ie zC5nU}KnN`&(?6cNe}|_cpsN4=CS3I>gk|8@vK(t#&KzMk~*sC~ZuStjJY_*>xRrBaW+ z)7Rfdo4h6(9?vTU)cuJ(6;M?Y?n7uK9fy+S_!U{-MmNSz-P2>+phAoAJT@l|6D2B9uP2S%#rb{K7cxS@x~rZ`V2<@SynsZ5=rdtpvydGoQtGxCX%3;TNS7yBDp?nHc@@^7Fd zU!%h{$XcvOR+a2$q0pSFZ+?ONW+|T{H_zqFcZe;-f+n<56C8Vmo{{`7yU%wEz5UDpq4Uqi zO>!XN@V>(C_nAjOKE}F~%NF|6XAml0tS90AikA>T#g3l(cBE#ISq3cEIN{{ASwcQ^ z5jp`jFLDz~Mz&LH&Th-rfe!Oq#dL_fs#o&GNTtU8_*qMFNs|1P6G7yyaS2rhP#nDkm>eM%3Yz z=qZ_Pd^9vIDI!dh6^YuYO$K@vjFro@XFj=Qh7hen^6>CrKP4?8828}6$qOd^y6jow zYcf7pi`Mlc@?3Bzz@_Yp)CHpR7o4aPW#;AgEMR%9*@$8^ANY%4jrKtMIuq*7WckDYj?8> zn~JcUMvga0FCO%v$DXgjo-o2RLfZ@5UlXn}SSM!u?z#1tGEYQ{$$nXnut!eUp=ORW_+O^vQ-b*4TXg9s*P#M4~Wh`-M3 zAgPJ|I;||-=-H5#6l^FEDuEh}(&6{4t4O!AZpUS)F4GV>P0TV!?insgG@{jXj7Z*~ z9vy#+jzVAC2@OSySEGKr-Uwh<#D2YavzEiUUJ!?xkcL8bER2d-ZrtVSrF<2JyOy8g z`RJoUJ(;FL)(5tLqrqP-2(&H9c=KT}WPSyXq^q?U9O$)r@D8 zVMb864^I=}F^#*&LfV$!H#@x&1tWLk;LF%87e(k}1?#vQJaD%;@uc9NRjQe-a>``u zf?M6_x%;$vedIAoQ$D?qo=%rDmu0A?2fzMdP}6izck<7i6eqo;aNU{Oa0zR;FLhDo z=PmLbl)&ra{u7iKM5xm}t~ZdHMSSr1Zf3*;?6A%lvPqvr6ROW8*9{`yDai7SNj|Jr zHLgm#to~9}K*=nSrcF#HAX{VXG)#dH$DbR4Yjwj)H{9OGA%M1I(z0r(r{ z2-(DZU27A**hg2T3)eWqI& zAs**YqZtC-Yu!*)!{`LZTC|@B%x{aR?>Tq!)w zy4#Rfy}dEtrNOH)Q;Y06BqW7+4hyqmg9`r;uz%ao!p?l{wEvqJT1iR)>IQ%J8tX@K zm>2n*lSK@g`}BPOHiSsHlqO*CudnW^sr)Ib(qx*ZSqn$P3RsR7l0airwgG=&QDj@O zuY~`+fZ&IHO(2wsN@^nGgTlVWZlYgBnNlJJxwe@w*YX#4ArWbWnnoTIV<5{!b5sh9 z>|RbCc7SJDmde9Y7M9g_Kdc{E|*_E3n% zQN_IHsBjP-Qf&h^uLui4YJwShO(vDTOgc+fmi+g_@QahTkcw+~X!chulM->^U<}W=t)d=JDw@w+#<@(Oib!KV^JVNk%6_!5MzvP*!5yKOUeS#F;;_APC;6Q#)1EYx-y$Eo~!2x_=Tc&e)b7 zNibRn`k5(JECy?Lac`B8k(zf^RJlXhOc65|pBKdKZi!PgINP@RdfeKBz*~Cd8xR88 z5T8=CgS;YNoOWj(Z?bj(?{>L5wRX9>zGUwpLCO^_0+HRUnNlq@<=07phhzvG2(x;_VIN^SJgI3*Tuei2P*Sp8~^4gbG@Z_1EnlSx}EM9kPw(iDC+r-&?lc zb}bQD#T0z+Z>JkzR-U=kx3*4|z$`N>#2)qwzQ&$GBq`yL8qbp19;`9xy%uP_ozjWF zLUun-AxR!E&yeKIf6pe7tNt}fZDq=eUy}EyTo?zYBEDoC_Iz7a*+!n{HJO*XK)R){ z>)v&}$$Z}M&&2VkHX7A>H4@i$`MiXFZx0k{q2eDROUdkI^q|=@kEWr!0iRg*1CW?+ zOL(ZB_sftMPd&Z=gP%Y6*cc?N=R0e_%2iKx_N=J+WlHL!$>ee0Fp!gL!pY0qQ%&oF zThZ0NBEqNliLK_bl0E`v_mUeYg8qG9Kl3)4A}L179YKb6?VR+SZZ)8L89;5pw3p4H z*`8<8OBdFyh3PKrh-&`cFz0lkerCy&_b}V#ULXPl5K+kNRdVdmgMdjVGQJtP0A;Sm z2M5podTjJ3ax26~_wPij_^Qk0HLxvQJS(E3kX=kODTLBo-QF&jDvAhsM`M2s@jI4y zZHTm?n7$KcF!^a&sD$I8BjXKwINXW%=?ApZ&4QacvtmHK7T_o}_KiNaiMhqwpGv4%Ys9}_%<^IVpIC*v0rnaJWeLIDF5 z>$_|24KLfCEjOod;8gND82tCIGPC^jg>xSN)Y$S(ddoqxs_-gY9Qe|P%&voUw~h8r z2X9Lb_S5{W^OB3hMx9*V(c!yNi^S9@M<&W!mrfUO5J5|v)5q+ zN2B`LtEIDDwsG7r+dB^yLM_T$59<*Bcl-VmqT<~UL4XW_2$DGB&Tk*dXc;8=D0mFj z`tk(=5)BRao$@}a1T3DccCJfs#=lD;?5I7+ko2H9yw?;g51sB=Wv(uVT@)+~yPt+} z#?$A7qC;6QbBBUmtQ$P zKOV(DX|4|(I+djN@F`)YB;wRIpzbps(;SjeRadH<-b_HFg`E)EC8t*cJXzkxOjNym zQLqr-n`kv}(Tb+kx=@@ewYu7SX;rWp$(FWGT4K`&@aK0ZblG%O zUOW^+ep49Y{F!3?Oxh)UL24cnJ%$nh3KOIvA7pN7y6N;UAp z&9!<5F$*ctiyalYy*D;Xd%#@700zC43ny-3wUBVHK{LB92R&BI{KZGgZd3+#e?p;j zn_Flhck**+f|PAp_wYBd+P9C&ilb(Snjmmi4`^049JmpiSTVv(0;g9DHpwfMEYDrG zlol#dHi(Jq3rdWnogg zR)Q@XN*?OOv0}x)^A%^gpORyn3exvX#B|h`JC|jaWdi5Jr0~$qz4^gGFyJ*IVHCy6kSP9v9@MJWUklt9+^|woHy{@+rP3;lba%NEFg=pBKCB5FM##t zadqDO3067yGE7FkoM@-YdFRVNOh8bAgGH9DEO~8Q8W%g}zUs&V|IUn&sgCXI;Zd|v z=Rz-lJ8y0dYl0EG0koph&irsmJ8@#tzD%71+t4vGG9oHqdLxKRO^atYy(qU1}RI9CJ4dk`42CE*;oQXS9mR&rgmJd?B|c_yp~djdp1l2muEm!;ed2*n>~%xL|4>4_PW)`tYELDv7_ub> zx9I?f<$Ayf8viVHs*`_;jk<~<8d8AvEB95OThq|P<{sQMSx^`4{}cDjx#kcF)V3t0pT z45llIgB*_BIFzDTCbZp-Eh&Am4&jbx#`vtOUpWDS>ssX6{t5hR`|&tOV!-(Ud^cO^ z&7QgjHchKZB~;`5af=;O*ED5> zs{VH9%){kRsrW-aKPw)5t~P8Tb6+p#`TX{SLp*^tDU`rBWvjcqpaVLG_%4Ec7_$S} zriLS{+uzrFy*r64b5ybiiXZk#o2TULZ8PU-N<9y43J5&c1xwRO3n^k*F}kRttaPnE zNu79QvgQjVXZ@_7YZr&5iB&w!u)4rn!QlLJu%p*|LJF-mVMOmmtSv=c9KvL1ckHpG zxL>IyexW3a63EYd@kV$$s+ajf14`a|`{7ITDF*MuWFh~N`#T2U)m+p!#RR_x@qHp(U z(qGRmzmSn0IsD>(KtF(4UL(n+ebP!ID*bE!Nnl3K)A%<3{(%BfY@x;2oqjQhg(Z=A z^nDu#k3(apr#TQ`)7Gm~rn3uz?Yae#rKdy$Rp>B=FD|%=(EPb z?+nYr)n|yS!qiL0X=mEI_=g1e`JH`KRn{DTPPN2@#?Tuml-DDw1d5vIiwrN6jW6c@ z?~E3YH;63QhMm8P13Eu>pL>7E^SUl{Jh>`Kc=L9XZ$JK1Ad95`S;PA?xM^0hQu%K` zlx%|8WUxeE0&am^A2i8TiWC+<>}}2YEAJLhhKy>`{fneP!ut_bJGovzh)6}|$c2JE z{PtBs#nS~54wB8~9w38xm;vd)wmR0cudT=Tg_n8k?>|UGbxJL_!dy+wA&UDy2%TQU)s3#Rt50p$&O3Bo+fkuPD48c*CQ14T8gt3j_R9bMay-E{?cICl#QrxQ z`_gjTF2m)fSX^tN%(WYzQ=E6XpgeG!NCk-AJi`Z*UBFGjy594U8db6s>19Nj=Ul;o=7n3AP7!Lr@wt7k2HK zt-65`4(?n(HC9=cl_04bi43l+z+aGdDJI>cc`^Y}azF-bGNYepp z8>v62Wqt%f-mae7hmK7Ba`ly8ZVPX|S?;^x_l)Q+3S30^L#W&}&4v*B-vIuiYU-R+X#g}>$eG;E~< zyfBST_ERwG*~#-@fx5A8FH}U(Omd$@OddI}_cxzC!4q$ zksfyJSK--hqZd3?gQmuvqo)CtFjBD3fF9S=rkZW%(bpTH68z~5Nz_&dU5Dl!eg*8G z&R-Lk`ZG;X&)C@&`$kT%XUE|!mYKjnLBqJj*$!PluNI6el3CTlt5=H z@)9I$d8{P?fO1D`Ou)wp-{ECu?gN7JJPv|yq2T-~3?I_kBWxdE;>G)V5PxyW!(L4T z*mV%l3!&hQffFBYWm21k*)5z>XRJnD7k1F@^brdL-`!;qzJ*HKVhp=NQRjMDY+=R* zPjQ+aUXrIfza-{TBm#b$b|O`)p-OT$x95E(Dpf_i8%s{c=1>~R3(npZqx&M(S6*IE zohHttFI1Q$?hx-1K;@KhK;EDzdI)VaTdK<0Dv60<9g(8Knz$+Z#Z~?lG9?|~2T!Ps z;C3m;n6-Or!x!^2`0u4&PPB<-dON;6DgYu9s!w?miMbuP!zSMrl4Hw?Pp9Ccn+=$1 z7^70s-ZM6Uy7!0@)+rc9KqUQ`wHuH zkPtitGoCHLeMN7+e`9tgE$#`WSZ$nHdwDub#OWEc9Ks35kCI&jdg4qSqKoq_S5B=z z!SWz#fxmcaAgM`-hch*V0A#^(b}y5ylk6q%aK(?#*8%P9994ova^f_GRD)&<&MasE zM?9_Kumk0=j`!&`S9mn^m7`;P zd<&G;;;<97X5`}RybhQLctNZtBI>j!dA&gU=nVIio9Hu{fy-)%Y9XmOhYCJp3HKMI zTc9L2B13K}zay=Ta{SJ~SQfteF{}JaA%#EA@Tw5qA$t60U71nlV$C&<%fv|eLdXin zD~XbATox}a(7VR|QC??ul0v=nme+l9`d;~5htZb^Bn`*n%2GQ4(oLYBV%BDMaHq%O z*m`V=>^y-dcs)2MZxu>t1;^;k1q#t|l21GvEq!8eaH_1PwWr8? zUp*$~yRA#{?YzK0H5mF~A;X(Cd+;{odw{y%3zg2dlnvAOF!P*$quZ7U)yk5%zo2vP zz0wspf%*zmRanu`S!<*HKVhDK(u*SyUCTmMA&)7@(hAE|GiGDq9YC1(#KJTC{9u)^ zo-tVKj@21;>SfTRjW_uWfiNx|6=QU{$mBQ50Dgi))y8WcqFi$%`rtQFbGFo<+dB)FUGqjSOtwH?cH3f?GaTL)tBTfDQ+Pj-eQln@SMH@`H z`fSXfF~zkX%ydls`6OXeq`qYtR`9uLQ9mSnKUC@l^@X8K(C*M;Q0xapA?hce^?6g!JTL-32PrZ4C`$9t`P5zApW41~f8 zR)C7KX_OH)-~n@BU7dyvK|?d6Xzp=_hhiOzt6<7}!BbZJP@IUfd>kHvLi;M8 zBn^_52jV|P`B%Jqag@ex@)WoXv@T%np!+XO-DCSqO{#BMYW#qv|B*>9Y@GV!?BBLXUtJhW-f zzQPHSnb=07OeP<_q3uOw-9HKbz%9%9&C6Xfu7xa*af&t4T+i&->BBsUY{_Ta^r`?7 z73Ih#SlG!@%7GKs_&H0;W+(N!)xtQbNx2bf<#VshA390LO1VzBoGz-hpw+wD`$Z;a zFI-m}rs=<1xdiqP$r#~wC)Gp#BB6kfd1JCGsFl<&9b@J_&>PG*!QVQ|*GO_bq=lx@ zst^XVwO3dtCU6Ct49=fSzPwg6t4I0WDc2F;`eo_$||=wWu*`aEtUZ$`{SRM(XkhU{f!2? zxa21v59*R%)pq|41iJ-{Xz-TH3elZM_MJ{|tpn^^8;vr&@q~Vw3zwz%6cGp=j)x~_ za_d%y*yeLP5Kg*(z!nKdW(S}V+J5yBDh!@0BOfL9_LC*tJ^J0n z>^dn*tPjyz?z2BV_m-7Y%pV4&qxE$Us9m2LnzL1`u9U$mapEX2?FV)%H zyK_QqTHs;Z>=}~BQ<1;_gL*6eUIbveg?6_}ijv6(&QRW<52ETK_O3R|7put2 zyGk`Y3h(K4@;-w*S*a&II3r!X=-iRcO_RtbTIj9k_vF_qh|uJUbfbfOC=_il;Ljow zavdm}8xlPzHZbrro`-dLc9iYw9tqnxGLOm-=v})gF?N8=$gZY~4k3(;AI0bg7cbc}UU;(hEc|5TFbQSH+H;y`n|r|D2jt{#-^Y$V zYpwUj|1f}rAqKRA0$#g!R0MXavTufgzj^Go`m_c$D`Rjhwa&3<->6Iha4Ct+tBUp$ z3Qv3!W#VE<*vSiC22?aIIb?1ksPg;-5%)#A?;=B_4l>n8Ol68t9S1wrHEr;q004-V z1sA`gJ{FS4lLiTQY*wVzf$_L!GN~`1W8G(CQvlKu1(6YFNQbjb(*-`xFab;WNHp!O zv|G&Ck+%Z^fp6tAb=J;5R|$VNn#G27eCzkV2yb5UVR0~bqxmZt;BxE`Nzd+R~JeS@s-+W^*;DB|s|^ahGj}->Ln+3DFiNzCR$nV;r%CGX>y>PnwC` ztxDM1+dFD;k%n)3jHBMy;g0wUJCMM9OBnN`?d(y-Xdu@1o{rl`VxV(bOM%a(vh+pOP*7*T%HU?>P|#Fyiv zC7Y=siIeZ18X$LRu&rz^<^x-_{wL>DL&Fp(h{=ycD2_rR4n7Ai1Wga@g?o3Ci1{mb z%AAx~{fB{~cq6|btxAvf=b$C?z=>25rnSBwYN8hsDxsFj;=&`02of9{UNpT#ymAD9?0qWGE_* zBG#o{TD@K5IbN;?4AggmwXF_=2e>$R4~8m|MD8n+9uRZklUr+Mb`OLXuA3*{i@Z9w zX43H4m190=AH)@-qnqW?Xwk@utNs>mP^;7y17^|5Ts1iNchJGeF!#Hvdw z*4PSqs*uYn{m=-Frz>qdex)TxX1Mogd9e9`_62`3!&0@U`X)C1N50=4lP9v>Rq|!O zMYt4v6pN0;BJFnrt>!BiL+$YV9(CU4*Z3aa1(yD3@X*HxuFvRddiO^^mzE469kh5T zmmnVaiP*P@Scy(VcRoX4MTtvjP!|yXXdVp8<5TOCTkASwti=0p^p=uukmpQiaT^QV zQrEBcrwpE;5r#J>AI0iOVP6jr%=-V8{w|%2Zi5s<6r}5VGal)cv504t6^}=<2}F&y zu@|0842X5uxlYpoe;{pXQBm{)X2<_$6(0Ze2NqA}KZwJ&-am+g^`U6&zqy&x94jpJ zHWwQscmDW;Qtj65{AGy>(FU(pe>pMcFuC1nA=#7m5NXcB?@cIhlJ$2}2?|3Wx~w{hqGJd?KCObU=QUPZZ@ z9?1dyu*n&H0D0}#w1RBLO=t_$s5aK!5`?`kfPZN>OA@l>jwZ~P$(|o^8Q2XaP7-aG z6=FV8-50R|0Iy3b3nAiM*iI>qRuRdh&AxTJiA%%>^a&0g?Qfih>h~ryizBaXY;ZW; z(AFb-N)~HtpeajwnJdoe;T9q;fIzB3*qtuX(Y%Q;NkpGZz>-Y>zv|yzO@1Uj4&G@Y za@UBTyS`(qLT{2T=k^k=1BwCV;(c9TMWZHTsfsG<@H9@_SHN2+AQ?nRIu#6yG+c(~ z<=$mfpKG*0B#0a-gRi^VME-13520}zjw%)LJDYlS6FtOQ3QRtxsBOEiwT^Fb@h%7R z!IBpG<9ighA0{qLBjPdUQI&Kv_Ed_e$QL0kFCV}V15{0&xvGJJaoqZ17sG*OSV)ujru$Sc{CQrP+AX8^XMbqeVOib2V;*%l5SiWik< z>mYPB$!Wz!0P~SWC$fq>S+D&0qT@s}Lu$HtoX3A$!tb(x|6>*rqQR0q? zq?*27f3>JgvNvxG0|wW$`;?|n>Jq;Vpb+i z^>0|s&Fn}Td<}(?QNL|+za!IWel2H*Avx|tGwoY-yCCvfKo=g26+PP=2?fa4g)APU z&1oTujKk=7A?cKbT*@48$*3yG&y7hQbr~~vV!em-B`bD4`3>qJajp2mpkJTp!*Gr^ ziabLpD2E-pKuB6|(TV?E%Me}om&y1^+;B}H9=ZYwm#$kklm-cEK-drCt#}GCgdGJW z2-i$#o0c;enPW_tV*>k1Spo0<*Osi~3cqk38kryk0N-?cfmnpF#qMZ_)P*=Lhgf{0 z3w!DWuz`qycqH{X`HkRLFNp9#`4qun<8h?@IIopHcK*j_VN$Z%PkI;0cE0k$Rq=F# zhKclFx4SCh+Q%r5^1k6YAqrKMDFv~~!K$Y+%Ozho(M%y=DKyxk{AW+@#<2JVj(JMNIvpOjmwgzIMTM zNStE?_5Y`c-q_pRXc@2(cNqHX*L@ju4F5*GMz8iV^+6GZbO(646_0#2!NQcP@~v zOJw^GJ09nrv39V%`+#P2VzDV+^*(F!G^S#zRp-W4`^Hriiq9PRV~2;`&4E#~8+ta| za=&Wi!`^>dwB;iM)Yyl3oT3GTchEasdkEQrtq9aVEq(YgmKD4_aG{2Q1S8y`&*h0Z z{9lbe4v}_$%t6bG2HJnqospHjw;$m)OZppOCB(e~SmYl{i=`_%^&x^^?D zdgJCZP6GCjZU`i27cQ`XWoQ2$u7ZoM!}f>ECP(wCJ)qPBFFNz!%c0+(ll=?;z<8X^ z0cA-K(qy){qK*xH*ohCf!~=w3rDtYe*Fnbw$Z8YXChr(IMipT;QXK@<47c_LS&!62>rtgkdx7nO!36l zS_YreNXA_U6&A>7Sb`<@LUhiA@3ysmzcBaT%Q_;0aH0UI?$~buokSw^TQGfOGT-;Q zzk-4P|4^HO(@m)&hcXe8=Vog(*iS&+A-4bNzvU3BZ>&&a8R~Qq3}aE}NrimWP;$SX zWPQ7kk9Yi=cMx6F;J5x=LZUHm{HHBbSJk-_-;i|3WEg@?D`FyeMNe;Ndihw=V++&m zq@VBPK-cftl*zzK%J3P^0d@B*$_F%g zvCHEvmBbVe>gn5s>4W&m6P^V{lcf{`EYw^^j0yJ{SR>`Nj&9R@O%%*+j2(g(FyYfW*{f6!D|h*6|HlqvyI9r~vcKER$H z+;f;Aw+;z&|y516tel z&euzTGk{I_iNM8|@X_om?=YL^>8sk#!{MxHl`JzTGUOCRA)uW(+J`V0^m*JJO4`s| zA*hr-p+{~LGHkS;rek;tn)b)NN?;F}SSin+%emuPtFE8%tn>x1v<8np?+4!od#hcz z=y+pzKfgRvKkl9EzF@69d7s|3zvNy~6Jqm9?0~KV4OHy;0mVpV}pP58D_J;YCQnH#AuPt>oSKd*h zAb@VCMidkOOJVF8WB?<0`>-I_!%revy!@`1A@>h4g)SISqx|{c_l8g{soQ-3y57__9ntU?K%3I$esEK+1&irU+{4s)Uo#Z zsBW3r9959>igu@&PlwomD(>t9ZtB=p&94OzVDMzcL2*{GD$jqm9;s~W@ za9cj`Iq&z^Bvx;?L7S40C<+0l=h>WJzmvkpOo(nei}xR?p9UFWtM$a3tNEM(OGj_h z2WY^gd@JTn0f$Tl^ts@V5OdWauP46F+!*LO5yTAzEYgXZ)*O6d&2{65-6&FH8nVrb zj_pB%O*zuc?=z;O+ng-mLj8BFEmzGpYb%eLR^3K_>MuY= zonK#GSdG7Pt696gT@&oz>8MB%JwAm?;stFe<6WUoAJ~xfhxBUhVkzB@$73hy`e6s7 z#=O7QMHE@(Z@bTsVI+!%h+raOusFdBQpsEP4k=^+n^+~^^}c#JgxkkB<}+l`ctZj; zaa5Hg_`IUvsesR%k;<-cpn`+HY-kLFiU*Cvjb6KlHErWV^uz7K%?tKpg_AoEQ~ld_9vhwEiAr-oCzq4N zd7?if?)q25G7r%@$LSOCqlW3rn($-ui#>Sj<%jAO(8%2jWN`EiGlb6OzlCi8vx~=#{K$S&CvZe$5h~c4QHGqK-?lA)3NNt z>?b3x5%vRAgR^(bLbN-X_=Nvqoh5oGsD{`$Y6P|-(XoJq!^St#B;hrnS5B9~S_Ofq ze#c9XxAkhU0MEdhJS=DU0B!$gxM>?mG(Zc)Y6;*N(15tA15p9EodFy}RJ^(6_NfQ- z1D?@3T+E=8+9U;zEH3ZFZ{Rzyx6gg|@g`aeu6ne>k^W0?Tc*ld8(U zB^XL7iKz-}X6cjHYw3&dz~0$E-qof)H$=>zbagiO>f}m3&gZU z$z>-bROXcjfCC=+27!N)8Q^rrb>IJ0MaCt;y3;OKuZ=F%4CnLqEXoKK8R%?YriEW= z_e;Ubs8_4J2&%1bdU|@e*JBvdzkH;pu-AXO+)_&%96FzucwCNgw4F^%s8O!8>RDhW!&;}OeKLRJke+16H*~GWhL>i2)X`kq3{Ap)8L;7~N zT$uU;(cJrUh5qzfB}1VFvE0qnC1kV!5Z1=acIoIZ1L>jhD$l=_*8TCNg6CeRG~lb+ z34YAFsDIb3{t;!l<#_-66~{mW6bk!iS~IXb_?Ij7)!F*jjluPL{~s=TbMM5lO6Fb* zqqz=^7{LR4NIz+F)>o)1BToBk4RriA!^Hp>Isz+=5*P^d3+8_uwVIz1T3?GvL+X0RO}m zhm@=T(^DTCQxIJv&8ve081jdm!NY1#904xi;q@V4EIEX*tg)Gqb0uduct zgVRvpv`kdG8Qt>>r2~0Fn(p__bw-kjRwFu@WAHu zVfO)#)2*%h<}ZQX57$VKL5IDu|BU_{QzBPyrLga zfx|TiGSJ$X_jo9AUo3aIj$e9r#_k1rIlL`sdoykD4{EbLo2Ev z6C;mc>@%&&Kc|k49bsRSFhpmyX*gWDW9NT7=JUAzBG4o7I2bgjZIpU&#eDYHPn#>X zi@?MfPmdM6EM|&jY1B?H=D9HOZ!=VO7ah@m!&((%D_@HaW=GswW&Y7ZRbOb%I!wXV zMjY#dC|H0@23cxZ7p4?>T~VPq!Ad9Sq}8|mVHac?&7lmY-<8@77&gMMEmnztG$q9w zL^xR@>ln09y1lD_Fc*bbR73pxn{@TRLz@-jzM6UCnFFJBt3S$-^F*t9C}`ZkQGo|ZjogUx#cZu$8i4Y0f~{4=v&dV^A2%u@fot8dIHi5SK=8N(fy z0wmZW4~!69pNQHn#_R2FP6Ya*U(T8`P*dt-uiTP>_FIx*AH+`pe4lQ_!r+GCaY{k|8TbjAH^2V?uvU(mm)a!r^4EB4IL*Q@H3D7Gx|g3hXR5j;6Vr@Dp>UxwGtKJS+-p zr!kCj-Hh~2SH|9|R+k@X5qCdiL`d62^o3{U;VeSZZ906U0-*u2Ea0M{8wpOLBYWK~ zTzYzLQk#Dm>n@lc9tvQ=X7FI8+xdQ0J;=0R;tB!)mV|+HlRE;TvE(}&zLk~0_b07A zo~Z^W8Iep^zf1{QV;mhzEw=u?aLFYmpHKekhDosgRkdu3?!5>vUj_FZa6W%;v^I%A z7~oS@Mc1{FU%wLR)at~Z#D|Y+J>P1&VV0PG-}o-x01i;W`CSxvxO}NQE-ay6P2!N}V4U~%Kn?T03PyXH-#biSK63;@7vY^?tB?U2o4 zdULr>nT8JoJs$ncNW?>>vW@xXR!T&3m~Mtyx=}wAjwA6601z$+1ONa7PGZ2tD-Rj% zl!%>^s!TcN%Ul*av;C=A4-G_y;|>ELXrsJ$GJgQL_dMOU;5_xj(7Tymblbb(Lg&G# z_3ZrykfoN6PUM)uQxDojg;}3OgkA5+3@4JoUpxa6a96PvpdN+9-Z0eHgb$uX_e2R# zRBrWW-&*6(zX9lSp%W>ObRE|y&0)YUhntQjnkQr4QD5sFw8~$g*oKF zcsW$KJTtnpw0T``3jz?6dYbjg+r#F?cJXPv6UWn{X}7bcw-~mr=Z)vi`sP{mLRGs1 zNWSmx8CHMLB>@&7sNLm{f4L0aZ6MkUUHpbHs)$xJx`6K<7B<)W+9Qo`A>&Dl-<^GA z_!_j@)OLM{2GGStBl0LUA#pxlA*yd@BZ3w9iZJt4Yo@)9LH}M~({h1`p|5u@r0w~+ z?g$;ATXgWj4d58nM%`y{yDYx}I`CfZi@#j|m>vKL*ahGGz-Hlf(+M}Ud_wc8S?%S5gZAM|Gi8TL@A;oZ8U1oAeUSk6(Wq;U13)Q{M-{tkF?N}RFT66=dk*=c|BO#- zSNDS$_t6@;zOOO)Qq7gnD}OCyY%*H@sND0!Fp-TC>W-madV)Etv8MZ@i?HcHrOb?) zTuni~S6@e=x!>_%2>SFHPSz8>Yagp+@J-d&X(Vc4K@S2DzEhA9m^@r*LJS`+Ojb7A zFoqHfU5QL2M_)4c)mgfHDxay-m$Q6+i7E7ILJP@g5!lEnp`rxBS&Cb8qPwtXn8K=l zJ60{2p?I1buzY#*3Vt4ruIgP3E#hoQ?sXs-n=E?P#H^%9llq*ki<0-N@0j~YXSGfr zqdODn0=1;W(+g`M$b-#hzm&B72ZEVdl*%6n=JXcvsp(_oh`?>c8OoZgk;~NI6syj0 zAC?+UUJI0C+L4Qv&(&r=?r+8rTRQ#VX44D{BnaF}vpIOwRq;ZN(AkG(COs=ZMD z^^jd81Pvak%G7dBoMF}WGHGURz2azB-49uDn43%6t=v7OIVR&BIli|Qp;x3R?6a>D z9ew{#;mF+xCAq*_)Vt1Sb?z-Cdqsj)ITJD;72_J%79zPqD8K;rcVR1Z;5-Lt` zvPJ*VuWZ-rCdohyb8HODnoy}W96Id1I}8Euco}vmQHK+jN(%I8-NH$3pKeW>1V2+a zHJX|tg9xE71~+${(`QlbF=Br-Xe0HfE?&wBM^`?t)7@cUebl03Pf10fz%K}8ub1Rn zAg;|i&cpyuRxvOz*jX9W@RJd}Was_E9E)lXi*OZrSBK{mHmYL1yrV%qjeWepNwcb< zAA(+Keh-JBeg0Ko= zaj(ArX8XOvS(eJHAV?PlGw##>N0Z=cfn04_)T+L3*u2HH{c`Zg{3xL}CirYbo0u$B z^jx^=K2?16N3dh6>EQ7^eoLt*!MnoO1W}IQRn^DzrU4HePcOj07{QyKqD>X3qcCj< zYP!+*P^jFY(${{Hl_Hj_^;kn|DifqBC!4}Naax*leQB>-O(wYkWmvGgmygYH$hp2S zaZI0p{9P@E$#d2P4i1;jYaFdLmf||c z8Wd@Wvv<$5nyU$Hh4H%TPA{#e{CRW9Zp~2fieAxBdW{A=H7^6vq!YYK`!i#PhG7SYtq z|8DKPO*mrj43A#YsbG#uuYHK2T?FUz>V~2*u$8}S#5*=~qteUFmhlp%aOl?Xdo||F zcQwY+v4A6pkh`XUUAF(8+PfJ3^bX$?TQvh91s0oqq@LnJ#n}jath3%3$m;`!TU9$M z4)3^3rdU+-*#0}I_jWvp9gHH-mO#)0Xb?|RB3vd3WR$2yGf`W^0GM4 znENe{(t7xGE^B1bHJ)h?%r-<}`7*J9_Jfx~IAfR7#l~iR+zYHHpr!{mi_^pdtH|YM zhQk1mPRjksxtr}qtI2q?tt?V-Npl!j7La5$@Rim$GmO~Bx6Yp!X=(nl1bX%E*lNtdtb3WZo4%*-}r@wAEdSyLIPKEi?6q-L1{%DI&h5S8Z;cHNXQ-Z&vVX=W=Ysq{VVDkgY2!a z2z2f!Mcq}3JBTp(5{-@O1}B8Gmpz!7{yO_nYxYLibj8V%JqIqWzLUn**nV_khcusm z&&EDvUQ)cHZ@9az@rog%#5@4tG>Pv#cXlTd-#|3H92r8JsTk^-aK<}$eaz&Omo*aZ zk`nPrj{UQU9Wua-Njydep6IgI%WfAMkD>&Uv2Uyk@X=V2)dZdjpETIWTk>IMc<)55tpQq6|K>jke@xKH-XeEtWP@c8F@6#7QCFnT0NH(3ouJttMv|C8}}& z`BRKmUk~lEQ`tmIGe*7Fr#!UIFEW0_iLt0A3GG6(LPWAtIGkxUc;9_l@bSFkG!C6> zd64Pz#H}*${F}_S^A0>cYOaXn*jk#baZq$T{})2Vo_2uzX!FQ(EOTX!sNBx?Lpc9J zZRVa$_|B{}Qlhv0J*_U?m{y>V3lyYr%D^eSi<^T_x{*;B8Jqt#HXD_Bo3w@tV#MnZ z!tK$N;t!!VdOX`HwfQsW4}J(OZ|eSjGjN;o(~jF5(2WY4xZQsI8SgJql_$gP3%pPT z#f@Sjz!^=$ZC0&eNf>86K9Ler7MQJf7f!!!+PYhnsW&EGq@j_5Mk}k~rd7dUY<9$r zbmP?%wU&Is^Yt(Ydf^+AxryLxI47IOkn#t$DSLTzav#@5BY$2$(#%qh7lB6xj;u?4=xRyoOoN-56HEJc~S!%!7brla; q?6-#kDwjuFWhst!sDK)cq;gg{lx@}tk*~o7=#ZtYMZtk{H~t2hU|zKV literal 0 HcmV?d00001 diff --git a/docs/images/alpine-wasi-on-browser-host-networking.png b/docs/images/alpine-wasi-on-browser-host-networking.png new file mode 100644 index 0000000000000000000000000000000000000000..47a0d55c56ce38aed9b2c16859ce51b1373bcbc3 GIT binary patch literal 58193 zcmaI7by!v17d46^f*_!P(jbC>ba!`mr*wA<9J;$Z1ZfT_-7U?byZg}He4E$bcmKNg z`Wzl)pSAX0G1r`9jxl$LoQxSo_ zS3&^(c_0`D1Ak*X3adFP+88^!=-C^=m{{9b8BseJ*c%yHJDA!y9>ccr0F4;Ul++vr z?Tz#t&1|f{D4AIq0T*Fl80Z)nw;KM?v2Ujb)3IzP<$a-N-A?gkV%rK1q-WVFhz+p0oV9jun5~ zRg=P;v5SREm@_3T8MmF)F+%8 zqw2Q5?CnxG%@AJ(h(KiNfPuh<$dnF8baYUMEz6Qt_36hf631^+q5npMFlr$_90(pg zdP6d@4NGmoW5m58-@Zl;V}}u3pUV619^LF^Z{Q+*k=%LlJ)OTjer9&EXCAXPN@;m) zNEQ9pLoE1LJAY6~R>oAY*l^Ili5xk6e#3^uZ~%uN>HAT2 zyhd(89r78P-KFlWM>%WD3{&|;^>0?T5hc=vK(mM)c1gjl&PH#jPf%{YJUFG|bWVli z-_j$@%p!3ez+sh1P2Vfd-C8Dr+q&OA+M{<8yqEpGo>J}k5EFRaN_=+)jG zP)Un+h*tmj{Y@IY_>Se^(J}7|mSpzd@H+7$Iii6H3{57q471c$V*0DW6QZcY6I_{d zY9VtpTYA*i4C#E(r<8VgL;rW{S+CK+mh7%Z<1Q)fxn{eQUsaVuI`b5k@I=xem%83A zoiSi$70h#peCfHp8+sowoz%!)eqq512wjTR!KnPnT6}JMm%0~{=5R?fj7{oW z++{wOFKw0ipW|R1Oy<@NnuRx-I9sRldCI{yjZM%g9(nzAxF%g z*Lbh2Y!Vtt;CZ@}<+Y562PZWfg)}r=80FTurUNJ?$@my zE2FZyI`Tx!c>m_=s1TYXCok{QmbH?%rl*(dLoiE7J}{jvM`MTKsy5?YkLJiU>FlpH zSupx&GLq>;M*F`tLdhma1TN%_24LpHw6Lj`xqQ2^HRB_v#ig7@WUII{gD&Am_(j3A z%bIiiTY+613}Jpa5Z{+8KNRRkGqAy%(rrziruoT%rBy+bGH}ALlK(XnFdsJlbw zoLoCumPe--D#5`n)jQxM=HQb%`SF_-Qckdf)_!cTrfJHetgJxVbyoFgLn}Wy&K?}E z$uO4OKV6Vnw{4&#&0q@r0egGH=x?kCM}!fZjgw55>I_%qNpf;%zRca!;L^y|{gJ~G zj&O|YrUKi)e!ijk^!Uo03x~&jVRpKVb>*j|FA5PyLsP!&&hXYCCkO6-{RksgW=yn~ z>pnTE*;vF;dU9!SG$trI>CUcv}h?@Lo+E$~vx$4Zec~PQW zk?C+>rm-Ew%4ZnQkD9clbPyoeL#f

G+&CPxr^19F)K_x{m`{5mm407)qOBOs%L} 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 literal 0 HcmV?d00001 diff --git a/docs/images/debian-curl-wasi-on-browser-frontend-networking.png b/docs/images/debian-curl-wasi-on-browser-frontend-networking.png new file mode 100644 index 0000000000000000000000000000000000000000..1d5cf1d1086875acbc89136c704a1e26fe075bef GIT binary patch literal 74943 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 z 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%0Dyt3EsB+D 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∾LaV#A(vM^|vXhEL*=MY1iSn9_yH7)w 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 literal 0 HcmV?d00001 diff --git a/examples/README.md b/examples/README.md index 16e0851..1364792 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,3 +11,4 @@ Please refer to [README](./../README.md) for basic examples (e.g. Ubuntu on WASM - [`./php-x86_64`](./php-x86_64/): Running x86_64 PHP container on the wasm runtime and browser. - [`./php-riscv64`](./php-riscv64/): Running RISC-V PHP container on the wasm runtime and browser. - [`./target-aarch64`](./target-aarch64/): Running aarch64 container on the wasm runtime and browser. +- [`./networking`](./networking/): Running container on WASM with networking support. diff --git a/examples/emscripten/README.md b/examples/emscripten/README.md index 7fdc9e5..75244f3 100644 --- a/examples/emscripten/README.md +++ b/examples/emscripten/README.md @@ -9,4 +9,6 @@ This example relies on [xterm-pty](https://github.com/mame/xterm-pty). Please refer to [Emscripten integration](https://github.com/mame/xterm-pty#emscripten-integration) section on the xterm-pty's README for detail. Instead of emscripten WASI image, you can also run [WASI](https://github.com/WebAssembly/WASI)-compiled image on browser, with leveraging WASI-specific performance optimization including pre-initialization by [Wizer](https://github.com/bytecodealliance/wizer/). -Please see [wasi-browser example](../wasi-browser) for details. +Please see [wasi-browser example](../wasi-browser) for details about WASI-on-browser. + +Examples of enabling networking can be found at [`./../networking`](./../networking/). diff --git a/examples/emscripten/htdocs/index.html b/examples/emscripten/htdocs/index.html index 4e2adbe..2a1d5a0 100644 --- a/examples/emscripten/htdocs/index.html +++ b/examples/emscripten/htdocs/index.html @@ -21,7 +21,7 @@ //termios.cflag |= CS8; slave.ioctl("TCSETS", new Termios(termios.iflag, termios.oflag, termios.cflag, termios.lflag, termios.cc)); xterm.loadAddon(master); - const worker = new Worker("./worker.js"); + const worker = new Worker("./worker.js"+location.search); new TtyServer(slave).start(worker); diff --git a/examples/emscripten/htdocs/module.js b/examples/emscripten/htdocs/module.js new file mode 100644 index 0000000..f9fd23e --- /dev/null +++ b/examples/emscripten/htdocs/module.js @@ -0,0 +1,29 @@ +var Module = {}; + +var netParam = getNetParam(); +if (netParam && (netParam.mode == 'delegate')) { + Module['arguments'] = ['--net', 'qemu', '--mac', genmac()]; + Module['websocket'] = { + 'url': netParam.param + }; +} + +function getNetParam() { + var vars = location.search.substring(1).split('&'); + for (var i = 0; i < vars.length; i++) { + var kv = vars[i].split('='); + if (decodeURIComponent(kv[0]) == 'net') { + return { + mode: kv[1], + param: kv[2], + }; + } + } + return null; +} + +function genmac(){ + return "02:XX:XX:XX:XX:XX".replace(/X/g, function() { + return "0123456789ABCDEF".charAt(Math.floor(Math.random() * 16)) + }); +} diff --git a/examples/emscripten/htdocs/worker.js b/examples/emscripten/htdocs/worker.js index ccd4775..33577dc 100644 --- a/examples/emscripten/htdocs/worker.js +++ b/examples/emscripten/htdocs/worker.js @@ -1,6 +1,7 @@ importScripts("https://cdn.jsdelivr.net/npm/xterm-pty@0.9.4/workerTools.js"); onmessage = (msg) => { + importScripts(location.origin + "/module.js"+location.search); importScripts(location.origin + "/out.js"); var c = new TtyClient(msg.data); diff --git a/examples/networking/README.md b/examples/networking/README.md new file mode 100644 index 0000000..ae62489 --- /dev/null +++ b/examples/networking/README.md @@ -0,0 +1,7 @@ +# Networking examples + +Examples of container-on-wasm with enabling networking. + +- [`./fetch`](./fetch/): Running container on browser with on-browser network stack based on Fetch API. +- [`./websocket`](./websocket/): Running container on browser with the network stack running on the host and accessible over WebSocket. +- [`./wasi`](./wasi/): Running container on WASI runtimes, with network stack running on the host. diff --git a/examples/networking/fetch/README.md b/examples/networking/fetch/README.md new file mode 100644 index 0000000..36de554 --- /dev/null +++ b/examples/networking/fetch/README.md @@ -0,0 +1,129 @@ +# Running container on browser with on-browser network stack based on Fetch API + +This is an example of running a container on browser with networking support. + +Please refer to [`../../wasi-browser`](../../wasi-browser/) for the basics of WASI-on-browser. + +This example runs the container with network stack running on browser. +The entire network stack runs on browser so this doesn't require network stack daemon outside of browser. + +- pros: No need to run network stack daemon on the host. Networking is done based on browser's Fetch API and follows its security configuration including CORS restriction. +- cons: Container can send only HTTP/HTTPS packets to outside of the browser. And the set of accesible HTTP/HTTPS sites is limited by the browser's security rule (e.g. limited CORS). + +We use [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) as the network stack written in Go. +We provide the customized version of the network stack [`c2w-net-proxy`](../../../extras/c2w-net-proxy) for container-on-browser use-case. +This is compiled to WASM and runs on browser. + +`c2w-net-proxy` running on browser provides HTTP/HTTPS proxy for the container. +The proxy runs on top of `gvisor-tap-vsock`'s network stack that receives packets from the container. +`c2w-net-proxy` forwards HTTP/HTTPS requests using the browser's `fetch` API so it doesn't require network stack daemon outside of the browser. + +For HTTPS, the proxy teminates the TLS connection from the contaienr with its own certificate and re-encrypt the connection to the destination using the Fetch API. +So the proxy's certificate needs to be trusted by the processes in the container (`SSL_CERT_FILE` envvar is pre-configured). +In [our example JS wrapper for container](../../wasi-browser/), by defualt, the following well-known proxy-related envvars are configured and the proxy's certificate for HTTPS proxy is provided to `/.wasmenv/proxy.crt`. + +- `SSL_CERT_FILE=/.wasmenv/proxy.crt` +- `https_proxy=http://192.168.127.253:80` +- `http_proxy=http://192.168.127.253:80` +- `HTTPS_PROXY=http://192.168.127.253:80` +- `HTTP_PROXY=http://192.168.127.253:80` + +## Current limitations + +- Containers can't access to sites not allowing CORS access. For example, we haven't find apt mirrors accessible from browser so the container can't run `apt-get`. We expect more sites will allow CORS access. +- The proxy supports only HTTP/HTTPS and the implementation isn't mature. So it's possible that some HTTP networking fails on some cases. We'll work on support for more features. +- Only chrome is our tested browser. The set of accesible sites might be different among browsers and the configurations. +- WASI-on-browser container is only supported. Emscripten support is the future work. + +## Example1: curl + +> Tested only on Chrome (116.0.5845.179). The example might not work on other browsers. + +![Debian container on browser with browser networking](../../../docs/images/debian-curl-wasi-on-browser-frontend-networking.png) + +First, prepare a WASI image named `out.wasm`. + +``` +$ cat < Alternatively, you can also build it as the following: +> +> NOTE: Run this at the project repo root directory. Go >= 1.21 is needed on your machine. +> +> ``` +> $ PREFIX=/tmp/out-js2/htdocs/ make c2w-net-proxy.wasm +> ``` + +Finally, serve them to browser. + +> Run this at the project repo root directory. + +``` +$ cp -R ./examples/wasi-browser/* /tmp/out-js2/ && chmod 755 /tmp/out-js2/htdocs +$ docker run --rm -p 8080:80 \ + -v "/tmp/out-js2/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js2/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +You can run the container on browser via `localhost:8080/?net=browser`. + +The proxy's certificate for HTTPS connection is available at `/.wasmenv/proxy.crt`. +This needs to be trusted to perform HTTPS proxy. +`SSL_CERT_FILE` is configured in the container by default. + +The container can access to the sites allowed by the browser (e.g. CORS-enabled sites). +The example accesses to a site published via GitHub Pages (`curl https://ktock.github.io/container2wasm-demo/`). + +## Example2: nix + +> Tested only on Chrome (116.0.5845.179). The example might not work on other browsers. + +![Nix container on browser with browser networking](../../../docs/images/nix-wasi-on-browser-frontend-networking.png) + +Nix's binary cache (`https://cache.nixos.org/`) seems accessbile from browser. +So binaries available there can be installed and run on browser. + +First, prepare a WASI image named `out.wasm` that contains `nixos/nix` image. + +``` +$ c2w nixos/nix /tmp/out-js2/htdocs/out.wasm +``` + +Then, put the WASM-compiled network stack (`c2w-net-proxy.wasm`) to `/tmp/out-js2/htdocs/`. +The WASM binary can be found at the [release page](https://github.com/ktock/container2wasm/releases). + +> Alternatively, you can also build it as the following: +> +> NOTE: Run this at the project repo root directory. Go >= 1.21 is needed on your machine. +> +> ``` +> $ PREFIX=/tmp/out-js2/htdocs/ make c2w-net-proxy.wasm +> ``` + +Finally, serve them to browser. + +> Run this at the project repo root directory. + +``` +$ cp -R ./examples/wasi-browser/* /tmp/out-js2/ && chmod 755 /tmp/out-js2/htdocs +$ docker run --rm -p 8080:80 \ + -v "/tmp/out-js2/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js2/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +You can run the container on browser via `localhost:8080/?net=browser`. +The proxy's certificate needs to be trusted by nix using `export NIX_SSL_CERT_FILE=/.wasmenv/proxy.crt`. + +The example installs and runs `hello` package in the container (`nix-env -iA nixpkgs.hello`). + +> It might takes several minutes to complete `nix-env`. diff --git a/examples/networking/wasi/README.md b/examples/networking/wasi/README.md new file mode 100644 index 0000000..faa1ee5 --- /dev/null +++ b/examples/networking/wasi/README.md @@ -0,0 +1,148 @@ +# Running container on WASI runtimes with host networking + +Networking on WASI runtimes is possible by relying on the network stack running on the host (outside of the WASI runtime). + +We use [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) as the user-space network stack running on the host. +We provide a wrapper command [`c2w-net`](../../../cmd/c2w-net/) for container-on-WASI-runtime use-case. + +The WASM image converted from the container can be configured to expose packets sent from the container via WASI's socket (`sock_*` API). +WASI runtime binds the socket on a TCP port (e.g. using wasmtime's `--tcplisten` flag and wazero's [`WithTCPListener`](https://github.com/tetratelabs/wazero/blob/405a5c9daca906cc8f52ee13e16511f44ae79557/experimental/sock/sock.go#L31) option). +`c2w-net` running on the host connects to that port and forwards packets sent to/from the container. + +The WASM image converted from the container has `--net` flag that enables this networking feature. +`--net=qemu` is the only supported mode which sends packets to `gvisor-tap-vsock` (wrapped by `c2w-net`) using [QEMU's forwarding protocol](https://github.com/containers/gvisor-tap-vsock#run-with-qemu-linux-or-macos). + +> NOTE: By default, the WASM image tries to establish connection with `c2w-net` via WASI's fd=3. +> However WASI runtimes might use larger fd when directory sharing is enabled. +> In that case, `--net=qemu=listenfd=` flag can be used for configuring the WASM image to use the correct socket fd. + +## Example + +This doc shows examples for wasmtime and wazero. + +`c2w` and `c2w-net` are available on the [release page](https://github.com/ktock/container2wasm/releases). +You can also build them using `make` command: + +``` +$ make +``` + +### wasmtime + +- Requirement + - wasmtime needs to be installed. + +`c2w-net` command with `--invoke` flag runs containers on wasmtime with automatically configuring it. + +First, prepare a WASI image named `out.wasm`. + +``` +$ c2w alpine:3.18 /tmp/out/out.wasm +``` + +Then launch it on wasmtime with enabling networking. + +```console +$ c2w-net --invoke /tmp/out/out.wasm --net=qemu sh +connecting to NW... +INFO[0001] new connection from 127.0.0.1:1234 to 127.0.0.1:50470 +/ # apk update && apk add --no-progress figlet +apk update && apk add --no-progress figlet +apk update && apk add --no-progress figlet +fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/main/x86_64/APKINDEX.tar.gz +fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/community/x86_64/APKINDEX.tar.gz +v3.18.3-149-g8225da85c11 [https://dl-cdn.alpinelinux.org/alpine/v3.18/main] +v3.18.3-151-g6953e6f988a [https://dl-cdn.alpinelinux.org/alpine/v3.18/community] +OK: 20071 distinct packages available +(1/1) Installing figlet (2.2.5-r3) +Executing busybox-1.36.0-r9.trigger +OK: 8 MiB in 16 packages +/ # figlet hello +figlet hello +figlet hello + _ _ _ +| |__ ___| | | ___ +| '_ \ / _ \ | |/ _ \ +| | | | __/ | | (_) | +|_| |_|\___|_|_|\___/ + +``` + +> It might takes several minutes to complete `apk add`. + +Port mapping is also supported. +The following example launches httpd server listening on `localhost:8000`. + +``` +$ c2w httpd /tmp/out/httpd.wasm +$ c2w-net --invoke -p localhost:8000:80 /tmp/out/httpd.wasm --net=qemu +``` + +> It might takes several seconds to the server becoming up-and-running. + +The server is accessible via `localhost:8000`. + +``` +$ curl localhost:8000 +

It works!

+``` + +### wazero + +Wazero doesn't require `c2w-net` but the network stack can be directly implemented on the Go code that imports Wazero runtime. +[`../../../tests/wazero/`](../../../tests/wazero/) is an example command for wazero with enabling networking of the container. +This is used in our integration test CI for wazero. + +First, prepare a WASI image named `out.wasm`. + +``` +$ c2w alpine:3.18 /tmp/out/out.wasm +``` + +Then, it can run on wazero with networking support: + +> Run this at the project repo root directory. + +``` +$ ( cd ./tests/wazero && go build -o ../../out/wazero-test . ) +$ ./out/wazero-test -net /tmp/out/out.wasm --net=qemu sh +connecting to NW... +INFO[0001] new connection from 127.0.0.1:1234 to 127.0.0.1:53666 +/ # apk update && apk add --no-progress figlet +apk update && apk add --no-progress figlet +fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/main/x86_64/APKINDEX.tar.gz +fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/community/x86_64/APKINDEX.tar.gz +v3.18.3-149-g8225da85c11 [https://dl-cdn.alpinelinux.org/alpine/v3.18/main] +v3.18.3-151-g6953e6f988a [https://dl-cdn.alpinelinux.org/alpine/v3.18/community] +OK: 20071 distinct packages available +(1/1) Installing figlet (2.2.5-r3) +Executing busybox-1.36.0-r9.trigger +OK: 8 MiB in 16 packages +/ # figlet hello +figlet hello + _ _ _ +| |__ ___| | | ___ +| '_ \ / _ \ | |/ _ \ +| | | | __/ | | (_) | +|_| |_|\___|_|_|\___/ + +``` + +> It might takes several minutes to complete `apk add`. + +Port mapping is also supported. +The following example launches httpd server listening on `localhost:8000`. + +``` +$ c2w httpd /tmp/out/httpd.wasm +$ ./out/wazero-test -net -p localhost:8000:80 /tmp/out/httpd.wasm --net=qemu +``` + +> It might takes several seconds to the server becoming up-and-running. + +The server is accessible via `localhost:8000`. + +``` +$ curl localhost:8000 +

It works!

+``` diff --git a/examples/networking/websocket/README.md b/examples/networking/websocket/README.md new file mode 100644 index 0000000..78ca23b --- /dev/null +++ b/examples/networking/websocket/README.md @@ -0,0 +1,97 @@ +# Running container on browser with the network stack on the host accessible over WebSocket + +This is an example of container on browser with networking support relying on the network stack on the host. + +Please refer to [`../../wasi-browser`](../../wasi-browser/) for the basics of WASI-on-browser and [`../../emscripten`](../../emscripten/) for the basics of containers on emscripten. + +This example relies on the network stack running on the host (outside of the browser) accessible via WebSocket. + +> NOTE: Please see also [`../fetch`](../fetch/) for networking using on-browser network stack and Fetch API, without host-side daemon. + +- pros: Container can access to anywhere accesible from the network stack daemon running on the host. +- cons: The network stack daemon needs to run on the machine and forward packets received over WebSocket. + +We use [`gvisor-tap-vsock`](https://github.com/containers/gvisor-tap-vsock) as the user-space network stack running on the host. +We provide a wrapper command [`c2w-net`](../../../cmd/c2w-net/) for container-on-wasm use-case. + +`c2w-net` exposes an WebSocket port and it forwards all packets received from that WebSocket. +The container running on browser can connect to that WebSocket and pass all packets there and let `c2w-net` forward them on the host. + +## Getting `c2w` and `c2w-net` + +`c2w` and `c2w-net` are available on the [release page](https://github.com/ktock/container2wasm/releases). +You can also build them using `make` command: + +``` +$ make +``` + +## Example1: WASI-on-browser + +![Alpine container on browser with host networking](../../../docs/images/alpine-wasi-on-browser-host-networking.png) + +The following builds and starts the network stack listening on the WebSocket `localhost:8888`. + +``` +$ c2w-net --listen-ws localhost:8888 +``` + +Prepare a WASI image named `out.wasm`. + +``` +$ c2w alpine:3.18 /tmp/out-js2/htdocs/out.wasm +``` + +Then, serve it via browser. + +> Run this at the project repo root directory. + +``` +$ cp -R ./examples/wasi-browser/* /tmp/out-js2/ && chmod 755 /tmp/out-js2/htdocs +$ docker run --rm -p 8080:80 \ + -v "/tmp/out-js2/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js2/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +You can run the container on browser via `localhost:8080/?net=delegate=ws://localhost:8888`. +The parameter `net=delegate` tells the [container's Javascript wrapper](../../wasi-browser/) to forward packets via the specified WebSocket address listened by `c2w-net`. + +This example installs and runs `figlet` command in the container (`apk update && apk add figlet`). + +> It might takes several minutes to complete `apk add`. + +## Example2: emscripten + +![Alpine container on browser with host networking](../../../docs/images/alpine-emscripten-host-networking.png) + +The following builds and starts the network stack listening on the WebSocket `localhost:8888`. + +``` +$ c2w-net --listen-ws localhost:8888 +``` + +Prepare a WASI image named `out.wasm`. + +``` +$ c2w --to-js alpine:3.18 /tmp/out-js/htdocs/ +``` + +Then, serve it via browser. + +> Run this at the project repo root directory. + +``` +$ cp -R ./examples/emscripten/* /tmp/out-js/ && chmod 755 /tmp/out-js/htdocs +$ docker run --rm -p 8080:80 \ + -v "/tmp/out-js/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +You can run the container on browser via `localhost:8080/?net=delegate=ws://localhost:8888`. +The parameter `net=delegate` tells the [container's Javascript wrapper](../../wasi-browser/) to forward packets via the specified WebSocket address listened by `c2w-net`. + +This example installs and runs `figlet` command in the container (`apk update && apk add figlet`). + +> It might takes several minutes to complete `apk add`. diff --git a/examples/wasi-browser/README.md b/examples/wasi-browser/README.md index c391f76..3433bb9 100644 --- a/examples/wasi-browser/README.md +++ b/examples/wasi-browser/README.md @@ -9,6 +9,8 @@ The difference between this and [emscripten example](../emscripten) is that this This example leverages polyfill library [browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim) provides WASI APIs to the WASM binary on browser. We integrated that WASI polyfill's IO to xterm-pty for allowing the user connecting to that container via the terminal. +Examples of enabling networking can be found at [`./../networking`](./../networking/). + ## Example ![Ubuntu container on browser](../../docs/images/ubuntu-wasi-on-browser.png) diff --git a/examples/wasi-browser/htdocs/index.html b/examples/wasi-browser/htdocs/index.html index 84669ff..27b1b70 100644 --- a/examples/wasi-browser/htdocs/index.html +++ b/examples/wasi-browser/htdocs/index.html @@ -7,6 +7,8 @@
+ + diff --git a/examples/wasi-browser/htdocs/stack-worker.js b/examples/wasi-browser/htdocs/stack-worker.js new file mode 100644 index 0000000..de3411a --- /dev/null +++ b/examples/wasi-browser/htdocs/stack-worker.js @@ -0,0 +1,245 @@ +importScripts(location.origin + "/browser_wasi_shim/index.js"); +importScripts(location.origin + "/browser_wasi_shim/wasi_defs.js"); +importScripts(location.origin + "/worker-util.js"); +importScripts(location.origin + "/wasi-util.js"); + +onmessage = (msg) => { + serveIfInitMsg(msg); + var fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + undefined, // 3: receive certificates + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + var certfd = 3; + var listenfd = 4; + var args = ['arg0', '--certfd='+certfd, '--net-listenfd='+listenfd, '--debug']; + var env = []; + var wasi = new WASI(args, env, fds); + wasiHack(wasi, certfd, 5); + wasiHackSocket(wasi, listenfd, 5); + fetch(getImagename(), { credentials: 'same-origin' }).then((resp) => { + resp['arrayBuffer']().then((wasm) => { + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + "env": envHack(wasi), + }).then((inst) => { + wasi.start(inst.instance); + }); + }) + }); +}; + +// definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt +const ERRNO_INVAL = 28; +const ERRNO_AGAIN= 6; + +function wasiHack(wasi, certfd, connfd) { + var certbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == certfd) { + sendCert(certbuf); + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if (fd == certfd) { + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + wasi.wasiImport.fd_fdstat_set_flags = (fd, fdflags) => { + // TODO + return 0; + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if ((fd == 1) || (fd == 2) || (fd == certfd)) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + console.log(new TextDecoder().decode(buf)); + if (fd == certfd) { + certbuf = appendData(certbuf, buf); + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + wasi.wasiImport.poll_oneoff = (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => { + if (nsubscriptions == 0) { + return ERRNO_INVAL; + } + let buffer = new DataView(wasi.inst.exports.memory.buffer); + let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); + let isReadPollStdin = false; + let isReadPollConn = false; + let isClockPoll = false; + let pollSubStdin; + let pollSubConn; + let clockSub; + let timeout = Number.MAX_VALUE; + for (let sub of in_) { + if (sub.u.tag.variant == "fd_read") { + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; + } + } else if (sub.u.tag.variant == "clock") { + if (sub.u.data.timeout < timeout) { + timeout = sub.u.data.timeout + isClockPoll = true; + clockSub = sub; + } + } else { + return ERRNO_INVAL; // FIXME + } + } + let events = []; + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var sockreadable = sockWaitForReadable(timeout / 1000000000); + if (isReadPollConn) { + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } + if (isClockPoll) { + let event = new Event(); + event.userdata = clockSub.userdata; + event.error = 0; + event.type = new EventType("clock"); + events.push(event); + } + } + var len = events.length; + Event.write_bytes_array(buffer, out_ptr, events); + buffer.setUint32(nevents_ptr, len, true); + return 0; + } +} + +function envHack(wasi){ + return { + http_send: function(addressP, addresslen, reqP, reqlen, idP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var address = new Uint8Array(wasi.inst.exports.memory.buffer, addressP, addresslen); + var req = new Uint8Array(wasi.inst.exports.memory.buffer, reqP, reqlen); + streamCtrl[0] = 0; + postMessage({ + type: "http_send", + address: address, + req: req, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var id = streamStatus[0]; + buffer.setUint32(idP, id, true); + return 0; + }, + http_writebody: function(id, bodyP, bodylen, nwrittenP, isEOF){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var body = new Uint8Array(wasi.inst.exports.memory.buffer, bodyP, bodylen); + streamCtrl[0] = 0; + postMessage({ + type: "http_writebody", + id: id, + body: body, + isEOF: isEOF, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + buffer.setUint32(nwrittenP, bodylen, true); + return 0; + }, + http_isreadable: function(id, isOKP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + streamCtrl[0] = 0; + postMessage({type: "http_isreadable", id: id}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var readable = 0; + if (streamData[0] == 1) { + readable = 1; + } + buffer.setUint32(isOKP, readable, true); + return 0; + }, + http_recv: function(id, respP, bufsize, respsizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_recv", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var resp = streamData.slice(0, ddlen); + buffer8.set(resp, respP); + buffer.setUint32(respsizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + }, + http_readbody: function(id, bodyP, bufsize, bodysizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_readbody", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var body = streamData.slice(0, ddlen); + buffer8.set(body, bodyP); + buffer.setUint32(bodysizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + } + }; +} diff --git a/examples/wasi-browser/htdocs/stack.js b/examples/wasi-browser/htdocs/stack.js new file mode 100644 index 0000000..126becc --- /dev/null +++ b/examples/wasi-browser/htdocs/stack.js @@ -0,0 +1,248 @@ +function newStack(worker, workerImageName, stackWorker, stackImageName) { + let p2vbuf = { + buf: new Uint8Array(0) // proxy => vm + }; + let v2pbuf = { + buf: new Uint8Array(0) // vm => proxy + }; + var proxyConn = { + sendbuf: p2vbuf, + recvbuf: v2pbuf + }; + var vmConn = { + sendbuf: v2pbuf, + recvbuf: p2vbuf + } + var proxyShared = new SharedArrayBuffer(12 + 4096); + var certbuf = { + buf: new Uint8Array(0), + done: false + } + stackWorker.onmessage = connect("proxy", proxyShared, proxyConn, certbuf); + stackWorker.postMessage({type: "init", buf: proxyShared, imagename: stackImageName}); + + var vmShared = new SharedArrayBuffer(12 + 4096); + worker.postMessage({type: "init", buf: vmShared, imagename: workerImageName}); + return connect("vm", vmShared, vmConn, certbuf); +} + +function connect(name, shared, conn, certbuf) { + var streamCtrl = new Int32Array(shared, 0, 1); + var streamStatus = new Int32Array(shared, 4, 1); + var streamLen = new Int32Array(shared, 8, 1); + var streamData = new Uint8Array(shared, 12); + var sendbuf = conn.sendbuf; + var recvbuf = conn.recvbuf; + let accepted = false; + var httpConnections = {}; + var curID = 0; + var maxID = 0x7FFFFFFF; // storable in streamStatus(signed 32bits) + function getID() { + var startID = curID; + while (true) { + if (httpConnections[curID] == undefined) { + return curID; + } + if (curID >= maxID) { + curID = 0; + } else { + curID++; + } + if (curID == startID) { + return -1; // exhausted + } + } + return curID; + } + function serveData(data, len) { + var length = len; + if (length > streamData.byteLength) + length = streamData.byteLength; + if (length > data.byteLength) + length = data.byteLength + var buf = data.slice(0, length); + var remain = data.slice(length, data.byteLength); + streamLen[0] = buf.byteLength; + streamData.set(buf, 0); + return remain; + } + return function(msg){ + const req_ = msg.data; + if (typeof req_ == "object" && req_.type) { + switch (req_.type) { + case "accept": + accepted = true; + streamData[0] = 1; // opened + streamStatus[0] = 0; + break; + case "send": + if (!accepted) { + console.log(name + ":" + "cannot send to unaccepted socket"); + streamStatus[0] = -1; + break; + } + sendbuf.buf = appendData(sendbuf.buf, req_.buf); + streamStatus[0] = 0; + break; + case "recv": + if (!accepted) { + console.log(name + ":" + "cannot recv from unaccepted socket"); + streamStatus[0] = -1; + break; + } + recvbuf.buf = serveData(recvbuf.buf, req_.len); + streamStatus[0] = 0; + break; + case "recv-is-readable": + var recvbufP = recvbuf.buf; + if (recvbufP.byteLength > 0) { + streamData[0] = 1; // ready for reading + } else { + if ((req_.timeout != undefined) && (req_.timeout > 0)) { + setTimeout(() => { + if (recvbuf.buf.byteLength > 0) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + }, req_.timeout * 1000); + return; + } + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + break; + case "http_send": + var reqObj = JSON.parse(new TextDecoder().decode(req_.req)); + reqObj.mode = "cors"; + reqObj.credentials = "omit"; + var reqID = getID(); + if (reqID < 0) { + console.log(name + ":" + "failed to get id"); + streamStatus[0] = -1; + break; + } + var connObj = { + address: new TextDecoder().decode(req_.address), + request: reqObj, + requestSent: false, + reqBodybuf: new Uint8Array(0), + reqBodyEOF: false, + }; + httpConnections[reqID] = connObj; + streamStatus[0] = reqID; + break; + case "http_writebody": + httpConnections[req_.id].reqBodybuf = appendData(httpConnections[req_.id].reqBodybuf, req_.body) + httpConnections[req_.id].reqBodyEOF = req_.isEOF; + streamStatus[0] = 0; + if (req_.isEOF && !httpConnections[req_.id].requestSent) { + httpConnections[req_.id].requestSent = true; + var connObj = httpConnections[req_.id]; + if ((connObj.request.method != "HEAD") && (connObj.request.method != "GET")) { + connObj.request.body = connObj.reqBodybuf; + } + fetch(connObj.address, connObj.request).then((resp) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + bodyUsed: resp.bodyUsed, + headers: resp.headers, + redirected: resp.redirected, + status: resp.status, + statusText: resp.statusText, + type: resp.type, + url: resp.url + })), + connObj.done = false; + connObj.respBodybuf = new Uint8Array(0); + if (resp.ok) { + resp.arrayBuffer().then((data) => { + connObj.respBodybuf = new Uint8Array(data); + connObj.done = true; + }).catch((error) => { + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + console.log("failed to fetch body: " + error); + }); + } else { + connObj.done = true; + } + }).catch((error) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + status: 503, + statusText: "Service Unavailable", + })) + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + }); + } + break; + case "http_isreadable": + if ((httpConnections[req_.id] != undefined) && (httpConnections[req_.id].response != undefined)) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // nothing to read + } + streamStatus[0] = 0; + break; + case "http_recv": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == undefined)) { + console.log(name + ":" + "response is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].response = serveData(httpConnections[req_.id].response, req_.len); + streamStatus[0] = 0; + if (httpConnections[req_.id].response.byteLength == 0) { + streamStatus[0] = 1; // isEOF + } + break; + case "http_readbody": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == undefined)) { + console.log(name + ":" + "response body is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].respBodybuf = serveData(httpConnections[req_.id].respBodybuf, req_.len); + streamStatus[0] = 0; + if ((httpConnections[req_.id].done) && (httpConnections[req_.id].respBodybuf.byteLength == 0)) { + streamStatus[0] = 1; + delete httpConnections[req_.id]; // connection done + } + break; + case "send_cert": + certbuf.buf = appendData(certbuf.buf, req_.buf); + certbuf.done = true; + streamStatus[0] = 0; + break; + case "recv_cert": + if (!certbuf.done) { + streamStatus[0] = -1; + break; + } + certbuf.buf = serveData(certbuf.buf, req_.len); + streamStatus[0] = 0; + if (certbuf.buf.byteLength == 0) { + streamStatus[0] = 1; // isEOF + } + break; + default: + console.log(name + ":" + "unknown request: " + req_.type) + return; + } + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } else { + console.log("UNKNOWN MSG " + msg); + } + } +} + +function appendData(data1, data2) { + buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} diff --git a/examples/wasi-browser/htdocs/wasi-util.js b/examples/wasi-browser/htdocs/wasi-util.js new file mode 100644 index 0000000..7714298 --- /dev/null +++ b/examples/wasi-browser/htdocs/wasi-util.js @@ -0,0 +1,127 @@ +//////////////////////////////////////////////////////////// +// +// event-related classes adopted from the on-going discussion +// towards poll_oneoff support in browser_wasi_sim project. +// Ref: https://github.com/bjorn3/browser_wasi_shim/issues/14#issuecomment-1450351935 +// +//////////////////////////////////////////////////////////// + +class EventType { + /*:: variant: "clock" | "fd_read" | "fd_write"*/ + + constructor(variant/*: "clock" | "fd_read" | "fd_write"*/) { + this.variant = variant; + } + + static from_u8(data/*: number*/)/*: EventType*/ { + switch (data) { + case EVENTTYPE_CLOCK: + return new EventType("clock"); + case EVENTTYPE_FD_READ: + return new EventType("fd_read"); + case EVENTTYPE_FD_WRITE: + return new EventType("fd_write"); + default: + throw "Invalid event type " + String(data); + } + } + + to_u8()/*: number*/ { + switch (this.variant) { + case "clock": + return EVENTTYPE_CLOCK; + case "fd_read": + return EVENTTYPE_FD_READ; + case "fd_write": + return EVENTTYPE_FD_WRITE; + default: + throw "unreachable"; + } + } +} + +class Event { + /*:: userdata: UserData*/ + /*:: error: number*/ + /*:: type: EventType*/ + /*:: fd_readwrite: EventFdReadWrite | null*/ + + write_bytes(view/*: DataView*/, ptr/*: number*/) { + view.setBigUint64(ptr, this.userdata, true); + view.setUint8(ptr + 8, this.error); + view.setUint8(ptr + 9, 0); + view.setUint8(ptr + 10, this.type.to_u8()); + // if (this.fd_readwrite) { + // this.fd_readwrite.write_bytes(view, ptr + 16); + // } + } + + static write_bytes_array(view/*: DataView*/, ptr/*: number*/, events/*: Array*/) { + for (let i = 0; i < events.length; i++) { + events[i].write_bytes(view, ptr + 32 * i); + } + } +} + +class SubscriptionClock { + /*:: timeout: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionClock(); + self.timeout = Number(view.getBigUint64(ptr + 8, true)); + return self; + } +} + +class SubscriptionFdReadWrite { + /*:: fd: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionFdReadWrite(); + self.fd = view.getUint32(ptr, true); + return self; + } +} + +class SubscriptionU { + /*:: tag: EventType */ + /*:: data: SubscriptionClock | SubscriptionFdReadWrite */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionU*/ { + let self = new SubscriptionU(); + self.tag = EventType.from_u8(view.getUint8(ptr)); + switch (self.tag.variant) { + case "clock": + self.data = SubscriptionClock.read_bytes(view, ptr + 8); + break; + case "fd_read": + case "fd_write": + self.data = SubscriptionFdReadWrite.read_bytes(view, ptr + 8); + break; + default: + throw "unreachable"; + } + return self; + } +} + +class Subscription { + /*:: userdata: UserData */ + /*:: u: SubscriptionU */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: Subscription*/ { + let subscription = new Subscription(); + subscription.userdata = view.getBigUint64(ptr, true); + subscription.u = SubscriptionU.read_bytes(view, ptr + 8); + return subscription; + } + + static read_bytes_array(view/*: DataView*/, ptr/*: number*/, len/*: number*/)/*: Array*/ { + let subscriptions = []; + for (let i = 0; i < len; i++) { + subscriptions.push(Subscription.read_bytes(view, ptr + 48 * i)); + } + return subscriptions; + } +} + diff --git a/examples/wasi-browser/htdocs/worker-util.js b/examples/wasi-browser/htdocs/worker-util.js new file mode 100644 index 0000000..82b4627 --- /dev/null +++ b/examples/wasi-browser/htdocs/worker-util.js @@ -0,0 +1,266 @@ +var streamCtrl; +var streamStatus; +var streamLen; +var streamData; +function registerSocketBuffer(shared){ + streamCtrl = new Int32Array(shared, 0, 1); + streamStatus = new Int32Array(shared, 4, 1); + streamLen = new Int32Array(shared, 8, 1); + streamData = new Uint8Array(shared, 12); +} + +var imagename; +function serveIfInitMsg(msg) { + const req_ = msg.data; + if (typeof req_ == "object"){ + if (req_.type == "init") { + if (req_.buf) + var shared = req_.buf; + registerSocketBuffer(shared); + if (req_.imagename) + imagename = req_.imagename; + return true; + } + } + + return false; +} + +function getImagename() { + return imagename; +} + +const errStatus = { + val: 0, +}; + +function sockAccept(){ + streamCtrl[0] = 0; + postMessage({type: "accept"}); + Atomics.wait(streamCtrl, 0, 0); + return streamData[0] == 1; +} +function sockSend(data){ + streamCtrl[0] = 0; + postMessage({type: "send", buf: data}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } +} +function sockRecv(len){ + streamCtrl[0] = 0; + postMessage({type: "recv", len: len}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } + let ddlen = streamLen[0]; + var res = streamData.slice(0, ddlen); + return res; +} + +function sockWaitForReadable(timeout){ + streamCtrl[0] = 0; + postMessage({type: "recv-is-readable", timeout: timeout}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } + return streamData[0] == 1; +} + +function sendCert(data){ + streamCtrl[0] = 0; + postMessage({type: "send_cert", buf: data}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } +} + +function recvCert(){ + var buf = new Uint8Array(0); + return new Promise((resolve, reject) => { + function getCert(){ + streamCtrl[0] = 0; + postMessage({type: "recv_cert"}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + setTimeout(getCert, 100); + return; + } + var ddlen = streamLen[0]; + buf = appendData(buf, streamData.slice(0, ddlen)); + if (streamStatus[0] == 0) { + resolve(buf); // EOF + } else { + setTimeout(getCert, 0); + return; + } + } + getCert(); + }); +} + +function appendData(data1, data2) { + buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} + +function getCertDir(cert) { + var certDir = new PreopenDirectory("/.wasmenv", { + "proxy.crt": new File(cert) + }); + var _path_open = certDir.path_open; + certDir.path_open = (e, r, s, n, a, d) => { + var ret = _path_open.apply(certDir, [e, r, s, n, a, d]); + if (ret.fd_obj != null) { + var o = ret.fd_obj; + ret.fd_obj.fd_pread = (view8, iovs, offset) => { + var old_offset = o.file_pos; + var r = o.fd_seek(offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + var read_ret = o.fd_read(view8, iovs); + r = o.fd_seek(old_offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + return read_ret; + } + } + return ret; + } + certDir.dir.contents["."] = certDir.dir; + return certDir; +} + +function wasiHackSocket(wasi, listenfd, connfd) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var connfdUsed = false; + var connbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == connfd) { + connfdUsed = false; + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_read = wasi.wasiImport.fd_read; + wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_recv(fd, iovs_ptr, iovs_len, 0, nread_ptr, 0); + } + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_send(fd, iovs_ptr, iovs_len, 0, nwritten_ptr); + } + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if ((fd == listenfd) || (fd == connfd) && connfdUsed){ + let buffer = new DataView(wasi.inst.exports.memory.buffer); + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fdstat-struct + buffer.setUint8(fdstat_ptr, 6); // filetype = 6 (socket_stream) + buffer.setUint8(fdstat_ptr + 1, 2); // fdflags = 2 (nonblock) + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + wasi.wasiImport.sock_accept = (fd, flags, fd_ptr) => { + if (fd != listenfd) { + console.log("sock_accept: unknown fd " + fd); + return ERRNO_INVAL; + } + if (connfdUsed) { + console.log("sock_accept: multi-connection is unsupported"); + return ERRNO_INVAL; + } + if (!sockAccept()) { + return ERRNO_AGAIN; + } + connfdUsed = true; + var buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint32(fd_ptr, connfd, true); + return 0; + } + wasi.wasiImport.sock_send = (fd, iovs_ptr, iovs_len, si_flags/*not defined*/, nwritten_ptr) => { + if (fd != connfd) { + console.log("sock_send: unknown fd " + fd); + return ERRNO_INVAL; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + var ret = sockSend(buf.buffer.slice(0, iovec.buf_len)); + if (ret == errStatus) { + return ERRNO_INVAL; + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + wasi.wasiImport.sock_recv = (fd, iovs_ptr, iovs_len, ri_flags, nread_ptr, ro_flags_ptr) => { + if (ri_flags != 0) { + console.log("ri_flags are unsupported"); // TODO + } + if (fd != connfd) { + console.log("sock_recv: unknown fd " + fd); + return ERRNO_INVAL; + } + var sockreadable = sockWaitForReadable(); + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == false) { + return ERRNO_AGAIN; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var nread = 0; + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var data = sockRecv(iovec.buf_len); + if (data == errStatus) { + return ERRNO_INVAL; + } + buffer8.set(data, iovec.buf); + nread += data.length; + } + buffer.setUint32(nread_ptr, nread, true); + // TODO: support ro_flags_ptr + return 0; + } + wasi.wasiImport.sock_shutdown = (fd, sdflags) => { + if (fd == connfd) { + connfdUsed = false; + } + return 0; + } +} diff --git a/examples/wasi-browser/htdocs/worker.js b/examples/wasi-browser/htdocs/worker.js index ca0820c..6794788 100644 --- a/examples/wasi-browser/htdocs/worker.js +++ b/examples/wasi-browser/htdocs/worker.js @@ -1,28 +1,72 @@ importScripts("https://cdn.jsdelivr.net/npm/xterm-pty@0.9.4/workerTools.js"); importScripts(location.origin + "/browser_wasi_shim/index.js"); importScripts(location.origin + "/browser_wasi_shim/wasi_defs.js"); +importScripts(location.origin + "/worker-util.js"); +importScripts(location.origin + "/wasi-util.js"); onmessage = (msg) => { + if (serveIfInitMsg(msg)) { + return; + } var ttyClient = new TtyClient(msg.data); var args = []; var env = []; var fds = []; - var wasi = new WASI(args, env, fds); - wasiHack(ttyClient, wasi); - fetch(location.origin + "/out.wasm", { credentials: 'same-origin' }).then((resp) => { + var netParam = getNetParam(); + var listenfd = 3; + fetch(getImagename(), { credentials: 'same-origin' }).then((resp) => { resp['arrayBuffer']().then((wasm) => { - WebAssembly.instantiate(wasm, { - "wasi_snapshot_preview1": wasi.wasiImport, - }).then((inst) => { - wasi.start(inst.instance); - }); + if (netParam) { + if (netParam.mode == 'delegate') { + args = ['arg0', '--net=qemu', '--mac', genmac()]; + } else if (netParam.mode == 'browser') { + recvCert().then((cert) => { + var certDir = getCertDir(cert); + fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + certDir, // 3: certificates dir + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + args = ['arg0', '--net=qemu=listenfd=4', '--mac', genmac()]; + env = [ + "SSL_CERT_FILE=/.wasmenv/proxy.crt", + "https_proxy=http://192.168.127.253:80", + "http_proxy=http://192.168.127.253:80", + "HTTPS_PROXY=http://192.168.127.253:80", + "HTTP_PROXY=http://192.168.127.253:80" + ]; + listenfd = 4; + startWasi(wasm, ttyClient, args, env, fds, listenfd, 5); + }); + return; + } + } + startWasi(wasm, ttyClient, args, env, fds, listenfd, 5); }) - }) + }); }; +function startWasi(wasm, ttyClient, args, env, fds, listenfd, connfd) { + var wasi = new WASI(args, env, fds); + wasiHack(wasi, ttyClient, connfd); + wasiHackSocket(wasi, listenfd, connfd); + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + }).then((inst) => { + wasi.start(inst.instance); + }); +} + // wasiHack patches wasi object for integrating it to xterm-pty. -function wasiHack(ttyClient, wasi) { +function wasiHack(wasi, ttyClient, connfd) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var _fd_read = wasi.wasiImport.fd_read; wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { if (fd == 0) { var buffer = new DataView(wasi.inst.exports.memory.buffer); @@ -40,9 +84,13 @@ function wasiHack(ttyClient, wasi) { } buffer.setUint32(nread_ptr, nread, true); return 0; + } else { + console.log("fd_read: unknown fd " + fd); + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); } return ERRNO_INVAL; } + var _fd_write = wasi.wasiImport.fd_write; wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { if ((fd == 1) || (fd == 2)) { var buffer = new DataView(wasi.inst.exports.memory.buffer); @@ -60,6 +108,9 @@ function wasiHack(ttyClient, wasi) { } buffer.setUint32(nwritten_ptr, wtotal, true); return 0; + } else { + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); } return ERRNO_INVAL; } @@ -69,18 +120,26 @@ function wasiHack(ttyClient, wasi) { } let buffer = new DataView(wasi.inst.exports.memory.buffer); let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); - let isReadPoll = false; + let isReadPollStdin = false; + let isReadPollConn = false; let isClockPoll = false; - let pollSub; + let pollSubStdin; + let pollSubConn; let clockSub; let timeout = Number.MAX_VALUE; for (let sub of in_) { if (sub.u.tag.variant == "fd_read") { - if (sub.u.data.fd != 0) { - return ERRNO_INVAL; // only fd=0 is supported as of now (FIXME) + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + console.log("poll_oneoff: unknown fd " + sub.u.data.fd); + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; } - isReadPoll = true; - pollSub = sub; } else if (sub.u.tag.variant == "clock") { if (sub.u.data.timeout < timeout) { timeout = sub.u.data.timeout @@ -88,19 +147,35 @@ function wasiHack(ttyClient, wasi) { clockSub = sub; } } else { + console.log("poll_oneoff: unknown variant " + sub.u.tag.variant); return ERRNO_INVAL; // FIXME } } let events = []; - if (isReadPoll || isClockPoll) { - var readable = ttyClient.onWaitForReadable(timeout / 1000000000); - if (readable && isReadPoll) { + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var readable = false; + if (isReadPollStdin || (isClockPoll && timeout > 0)) { + readable = ttyClient.onWaitForReadable(timeout / 1000000000); + } + if (readable && isReadPollStdin) { let event = new Event(); - event.userdata = pollSub.userdata; + event.userdata = pollSubStdin.userdata; event.error = 0; event.type = new EventType("fd_read"); events.push(event); } + if (isReadPollConn) { + var sockreadable = sockWaitForReadable(); + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } if (isClockPoll) { let event = new Event(); event.userdata = clockSub.userdata; @@ -116,129 +191,22 @@ function wasiHack(ttyClient, wasi) { } } -//////////////////////////////////////////////////////////// -// -// event-related classes adopted from the on-going discussion -// towards poll_oneoff support in browser_wasi_sim project. -// Ref: https://github.com/bjorn3/browser_wasi_shim/issues/14#issuecomment-1450351935 -// -//////////////////////////////////////////////////////////// - -class EventType { - /*:: variant: "clock" | "fd_read" | "fd_write"*/ - - constructor(variant/*: "clock" | "fd_read" | "fd_write"*/) { - this.variant = variant; - } - - static from_u8(data/*: number*/)/*: EventType*/ { - switch (data) { - case EVENTTYPE_CLOCK: - return new EventType("clock"); - case EVENTTYPE_FD_READ: - return new EventType("fd_read"); - case EVENTTYPE_FD_WRITE: - return new EventType("fd_write"); - default: - throw "Invalid event type " + String(data); - } - } - - to_u8()/*: number*/ { - switch (this.variant) { - case "clock": - return EVENTTYPE_CLOCK; - case "fd_read": - return EVENTTYPE_FD_READ; - case "fd_write": - return EVENTTYPE_FD_WRITE; - default: - throw "unreachable"; +function getNetParam() { + var vars = location.search.substring(1).split('&'); + for (var i = 0; i < vars.length; i++) { + var kv = vars[i].split('='); + if (decodeURIComponent(kv[0]) == 'net') { + return { + mode: kv[1], + param: kv[2], + }; } } + return null; } -class Event { - /*:: userdata: UserData*/ - /*:: error: number*/ - /*:: type: EventType*/ - /*:: fd_readwrite: EventFdReadWrite | null*/ - - write_bytes(view/*: DataView*/, ptr/*: number*/) { - view.setBigUint64(ptr, this.userdata, true); - view.setUint8(ptr + 8, this.error); - view.setUint8(ptr + 9, 0); - view.setUint8(ptr + 10, this.type.to_u8()); - // if (this.fd_readwrite) { - // this.fd_readwrite.write_bytes(view, ptr + 16); - // } - } - - static write_bytes_array(view/*: DataView*/, ptr/*: number*/, events/*: Array*/) { - for (let i = 0; i < events.length; i++) { - events[i].write_bytes(view, ptr + 32 * i); - } - } -} - -class SubscriptionClock { - /*:: timeout: number*/ - - static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { - let self = new SubscriptionClock(); - self.timeout = Number(view.getBigUint64(ptr + 8, true)); - return self; - } -} - -class SubscriptionFdReadWrite { - /*:: fd: number*/ - - static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { - let self = new SubscriptionFdReadWrite(); - self.fd = view.getUint32(ptr, true); - return self; - } -} - -class SubscriptionU { - /*:: tag: EventType */ - /*:: data: SubscriptionClock | SubscriptionFdReadWrite */ - - static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionU*/ { - let self = new SubscriptionU(); - self.tag = EventType.from_u8(view.getUint8(ptr)); - switch (self.tag.variant) { - case "clock": - self.data = SubscriptionClock.read_bytes(view, ptr + 8); - break; - case "fd_read": - case "fd_write": - self.data = SubscriptionFdReadWrite.read_bytes(view, ptr + 8); - break; - default: - throw "unreachable"; - } - return self; - } -} - -class Subscription { - /*:: userdata: UserData */ - /*:: u: SubscriptionU */ - - static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: Subscription*/ { - let subscription = new Subscription(); - subscription.userdata = view.getBigUint64(ptr, true); - subscription.u = SubscriptionU.read_bytes(view, ptr + 8); - return subscription; - } - - static read_bytes_array(view/*: DataView*/, ptr/*: number*/, len/*: number*/)/*: Array*/ { - let subscriptions = []; - for (let i = 0; i < len; i++) { - subscriptions.push(Subscription.read_bytes(view, ptr + 48 * i)); - } - return subscriptions; - } +function genmac(){ + return "02:XX:XX:XX:XX:XX".replace(/X/g, function() { + return "0123456789ABCDEF".charAt(Math.floor(Math.random() * 16)) + }); } diff --git a/examples/wasi-browser/htdocs/ws-delegate.js b/examples/wasi-browser/htdocs/ws-delegate.js new file mode 100644 index 0000000..199a873 --- /dev/null +++ b/examples/wasi-browser/htdocs/ws-delegate.js @@ -0,0 +1,105 @@ +function delegate(worker, workerImageName, address) { + var shared = new SharedArrayBuffer(8 + 4096); + var streamCtrl = new Int32Array(shared, 0, 1); + var streamStatus = new Int32Array(shared, 4, 1); + var streamLen = new Int32Array(shared, 8, 1); + var streamData = new Uint8Array(shared, 12); + worker.postMessage({type: "init", buf: shared, imagename: workerImageName}); + + var opts = 'binary'; + var ongoing = false; + var opened = false; + var accepted = false; + var wsconn; + var connbuf = new Uint8Array(0); + return function(msg) { + const req_ = msg.data; + if (typeof req_ == "object" && req_.type) { + switch (req_.type) { + case "accept": + if (opened) { + streamData[0] = 1; // opened + accepted = true; + } else { + streamData[0] = 0; // not opened + if (!ongoing) { + ongoing = true; + wsconn = new WebSocket(address, opts); + wsconn.binaryType = 'arraybuffer'; + wsconn.onmessage = function(event) { + buf2 = new Uint8Array(connbuf.length + event.data.byteLength); + var o = connbuf.length; + buf2.set(connbuf, 0); + buf2.set(new Uint8Array(event.data), o); + connbuf = buf2; + }; + wsconn.onclose = function(event) { + console.log("websocket closed" + event.code + " " + event.reason + " " + event.wasClean); + opened = false; + accepted = false; + ongoing = false; + }; + wsconn.onopen = function(event) { + opened = true; + accepted = false; + ongoing = false; + }; + wsconn.onerror = function(error) { + console.log("websocket error: "+error.data); + opened = false; + accepted = false; + ongoing = false; + }; + } + } + streamStatus[0] = 0; + break; + case "send": + if (!accepted) { + console.log("ERROR: cannot send to unaccepted websocket"); + streamStatus[0] = -1; + break; + } + wsconn.send(req_.buf); + streamStatus[0] = 0; + break; + case "recv": + if (!accepted) { + console.log("ERROR: cannot receive from unaccepted websocket"); + streamStatus[0] = -1; + break; + } + var length = req_.len; + if (length > streamData.length) + length = streamData.length; + if (length > connbuf.length) + length = connbuf.length + var buf = connbuf.slice(0, length); + var remain = connbuf.slice(length, connbuf.length); + connbuf = remain; + streamLen[0] = buf.length; + streamData.set(buf, 0); + streamStatus[0] = 0; + break; + case "recv-is-readable": + if (!accepted) { + console.log("ERROR: cannot poll unaccepted websocket"); + streamStatus[0] = -1; + break; + } + if (connbuf.length > 0) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + break; + default: + console.log("unknown request: " + req_.type) + return; + } + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } + } +} diff --git a/extras/c2w-net-proxy/README.md b/extras/c2w-net-proxy/README.md new file mode 100644 index 0000000..dd13879 --- /dev/null +++ b/extras/c2w-net-proxy/README.md @@ -0,0 +1,19 @@ +# Document is at [`../../examples/networking/fetch/`](../../examples/networking/fetch/). + +## how to build + +- Requirement: Go >= 1.21 + +The following builds the binary at `/tmp/out/c2w-net-proxy.wasm`. + +Makefile is avaliable at the root dir of this repo: + +``` +$ PREFIX=/tmp/out/ make c2w-net-proxy.wasm +``` + +Or you can manually build using go command: + +``` +$ GOOS=wasip1 GOARCH=wasm go build -o /tmp/out/c2w-net-proxy.wasm . +``` diff --git a/extras/c2w-net-proxy/go.mod b/extras/c2w-net-proxy/go.mod new file mode 100644 index 0000000..3850b5a --- /dev/null +++ b/extras/c2w-net-proxy/go.mod @@ -0,0 +1,35 @@ +module c2w-net-proxy + +go 1.21.0 + +require ( + github.com/containers/gvisor-tap-vsock v0.7.0 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect + github.com/miekg/dns v1.1.55 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.9.3 // indirect + gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db // indirect + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect +) + +replace github.com/sirupsen/logrus => github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5 + +// Patched for enabling to compile it to wasi +replace github.com/insomniacslk/dhcp => github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1 + +// Patched for enabling to compile it to wasi +replace github.com/u-root/uio => github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29 diff --git a/extras/c2w-net-proxy/go.sum b/extras/c2w-net-proxy/go.sum new file mode 100644 index 0000000..d816afc --- /dev/null +++ b/extras/c2w-net-proxy/go.sum @@ -0,0 +1,130 @@ +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/containers/gvisor-tap-vsock v0.7.0 h1:lL5UpfXhl+tuTwK43CXhKWwfvAiZtde6G5alFeoO+Z8= +github.com/containers/gvisor-tap-vsock v0.7.0/go.mod h1:edQTwl8ar+ACuQOkazpQkgd/ZMF6TJ2Xr3fv+MKUaw8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1 h1:KD92SLhTiV7HRgw3BicNh7mPT1+OpRpaWvyIQLT9by8= +github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29 h1:BNd7VYl9yNjxsA4Bt9YKAyKJKIymo1v9f7M2cWhh9iU= +github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5 h1:4gcU4XfYM+65xu4TiRFTE0fVJ854zjKHq0tcMwszt2g= +github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db h1:WZSmkyu/hep9YhWIlBZefwGVBrnGE5yW8JPD56YRsXs= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 h1:PqdHrvQRVK1zapJkd0qf6+tevvSIcWdfenVqJd3PHWU= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= diff --git a/extras/c2w-net-proxy/main.go b/extras/c2w-net-proxy/main.go new file mode 100644 index 0000000..bf0a53e --- /dev/null +++ b/extras/c2w-net-proxy/main.go @@ -0,0 +1,533 @@ +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "flag" + "fmt" + "io" + "log" + "math/big" + "net" + "net/http" + "os" + "strings" + "syscall" + "time" + "unsafe" + + gvntypes "github.com/containers/gvisor-tap-vsock/pkg/types" + gvnvirtualnetwork "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "github.com/sirupsen/logrus" +) + +var proxyKey crypto.PrivateKey +var proxyCert *x509.Certificate + +const proxyIP = "192.168.127.253" + +func handleTunneling(w http.ResponseWriter, r *http.Request) { + serverURL := r.URL + cert, err := generateCert(serverURL.Hostname()) + if err != nil { + log.Printf("failed to generate cert: %v\n", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Hijacking not supported", http.StatusInternalServerError) + return + } + client_conn, _, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + go func() { + defer client_conn.Close() + l := newListener(&stringAddr{"tcp", proxyIP + ":80"}, client_conn) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Scheme == "" { + r.URL.Scheme = "https" + } + if r.URL.Host == "" { + r.URL.Host = serverURL.Host + } + handleHTTP(w, r) + }), + // Disable HTTP/2 (FIXME) + // https://pkg.go.dev/net/http#hdr-HTTP_2 + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{*cert}, + } + log.Printf("serving server for %s...\n", serverURL.Host) + server.ServeTLS(l, "", "") + }() +} + +func generateCert(host string) (*tls.Certificate, error) { + ser, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 60)) + if err != nil { + return nil, err + } + cert := &x509.Certificate{ + SerialNumber: ser, + Subject: pkix.Name{ + CommonName: host, + }, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{host}, + } + if ip := net.ParseIP(host); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + certD, err := x509.CreateCertificate(rand.Reader, cert, proxyCert, key.Public(), proxyKey) + if err != nil { + return nil, err + } + return &tls.Certificate{ + Certificate: [][]byte{certD}, + PrivateKey: key, + }, nil +} + +type listener struct { + ch net.Conn + addr net.Addr + closeCh chan struct{} +} + +func newListener(addr net.Addr, ch net.Conn) *listener { + return &listener{ + ch: ch, + addr: addr, + closeCh: make(chan struct{}), + } +} + +func (l *listener) Accept() (net.Conn, error) { + if l.ch == nil { + <-l.closeCh + return nil, fmt.Errorf("closed") + } + c := l.ch + l.ch = nil + return c, nil +} + +func (l *listener) Close() error { close(l.closeCh); return nil } + +func (l *listener) Addr() net.Addr { return l.addr } + +type stringAddr struct { + network string + address string +} + +func (a *stringAddr) Network() string { return a.network } +func (a *stringAddr) String() string { return a.address } + +type FetchParameters struct { + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +type FetchResponse struct { + Headers map[string]string `json:"headers,omitempty"` + Status int `json:"status,omitempty"` + StatusText string `json:"statusText,omitempty"` +} + +func encodeHeader(h http.Header) map[string]string { + res := make(map[string]string) + for k, vs := range h { + res[k] = strings.Join(vs, ", ") + } + return res +} + +func decodeHeader(h map[string]string) http.Header { + res := make(map[string][]string) + for k, v := range h { + res[k] = []string{v} // TODO: separate fields + } + return http.Header(res) +} + +func httpRequestToFetchParameters(req *http.Request) *FetchParameters { + return &FetchParameters{ + Method: req.Method, + Headers: encodeHeader(req.Header), + } +} + +func fetchResponseToHTTPResponse(resp *FetchResponse) *http.Response { + return &http.Response{ + Status: resp.StatusText, + StatusCode: resp.Status, + Header: decodeHeader(resp.Headers), + } +} + +//go:wasmimport env http_send +func http_send(addressP uint32, addresslen uint32, reqP, reqlen uint32, idP uint32) uint32 + +//go:wasmimport env http_writebody +func http_writebody(id uint32, chunkP, len uint32, nwrittenP uint32, isEOF uint32) uint32 + +//go:wasmimport env http_isreadable +func http_isreadable(id uint32, isOKP uint32) uint32 + +//go:wasmimport env http_recv +func http_recv(id uint32, respP uint32, bufsize uint32, respsizeP uint32, isEOFP uint32) uint32 + +//go:wasmimport env http_readbody +func http_readbody(id uint32, bodyP uint32, bufsize uint32, bodysizeP uint32, isEOFP uint32) uint32 + +func doHttpRoundTrip(req *http.Request) (*http.Response, error) { + defer req.Body.Close() + + address := req.URL.String() + if address == "" { + return nil, fmt.Errorf("specify destination address") + } + + fetchReqD, err := json.Marshal(httpRequestToFetchParameters(req)) + if err != nil { + return nil, err + } + if len(fetchReqD) == 0 { + return nil, fmt.Errorf("empty request") + } + + var id uint32 + res := http_send( + uint32(uintptr(unsafe.Pointer(&[]byte(address)[0]))), + uint32(len(address)), + uint32(uintptr(unsafe.Pointer(&[]byte(fetchReqD)[0]))), + uint32(len(fetchReqD)), + uint32(uintptr(unsafe.Pointer(&id))), + ) + if res != 0 { + return nil, fmt.Errorf("failed to send request") + } + + var isEOF uint32 + var reqBodyD []byte = make([]byte, 4096) + var nwritten uint32 = 0 + idx := 0 + chunksize := 0 + for { + if idx >= chunksize { + // chunk is fully written. full another one. + chunksize, err = req.Body.Read(reqBodyD) + if err != nil && err != io.EOF { + return nil, err + } + if err == io.EOF { + isEOF = 1 + } + idx = 0 + } + res := http_writebody( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(reqBodyD[idx:])[0]))), + uint32(chunksize), + uint32(uintptr(unsafe.Pointer(&nwritten))), + isEOF, + ) + if res != 0 { + return nil, fmt.Errorf("failed to write request body") + } + idx += int(nwritten) + if idx < chunksize { + // not fully written. retry for the remaining. + continue + } + if isEOF == 1 { + break + } + } + + var isOK uint32 = 0 + for { + res := http_isreadable(id, uint32(uintptr(unsafe.Pointer(&isOK)))) + if res != 0 { + return nil, fmt.Errorf("response body is not readable") + } + if isOK == 1 { + break + } + time.Sleep(10 * time.Millisecond) + } + + var respD []byte = make([]byte, 4096) + var respsize uint32 + var respFull []byte + isEOF = 0 + for { + res := http_recv( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(respD)[0]))), + 4096, + uint32(uintptr(unsafe.Pointer(&respsize))), + uint32(uintptr(unsafe.Pointer(&isEOF))), + ) + if res != 0 { + return nil, fmt.Errorf("failed to receive response") + } + respFull = append(respFull, respD[:int(respsize)]...) + if isEOF == 1 { + break + } + } + var resp FetchResponse + if err := json.Unmarshal(respFull, &resp); err != nil { + return nil, err + } + + isEOF = 0 + pr, pw := io.Pipe() + go func() { + var body []byte = make([]byte, 4096) + var bodysize uint32 + for { + res := http_readbody( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(body)[0]))), + 4096, + uint32(uintptr(unsafe.Pointer(&bodysize))), + uint32(uintptr(unsafe.Pointer(&isEOF))), + ) + if res != 0 { + pw.CloseWithError(fmt.Errorf("failed to read response body")) + return + } + if bodysize > 0 { + if _, err := pw.Write(body[:int(bodysize)]); err != nil { + pw.CloseWithError(err) + return + } + } + if isEOF == 1 { + break + } + } + pw.Close() + }() + r := fetchResponseToHTTPResponse(&resp) + r.Body = pr + return r, nil +} + +func handleHTTP(w http.ResponseWriter, req *http.Request) { + resp, err := doHttpRoundTrip(req) + if err != nil { + log.Printf("failed to proxy request: %v\n", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + defer resp.Body.Close() + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +const ( + gatewayIP = "192.168.127.1" + vmIP = "192.168.127.3" +) + +func main() { + var listenFd int + flag.IntVar(&listenFd, "net-listenfd", 0, "fd to listen for the connection") + var certFd int + flag.IntVar(&certFd, "certfd", 0, "fd to output cert") + var certFile string + flag.StringVar(&certFile, "certfile", "", "file to output cert") + var debug bool + flag.BoolVar(&debug, "debug", false, "debug log") + flag.Parse() + + if debug { + log.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) + } else { + log.SetOutput(io.Discard) + logrus.SetLevel(logrus.FatalLevel) + } + + ser, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 60)) + if err != nil { + panic(err) + } + cert := &x509.Certificate{ + SerialNumber: ser, + Subject: pkix.Name{ + CommonName: proxyIP, + OrganizationalUnit: []string{"proxy"}, + Organization: []string{"proxy"}, + Country: []string{"US"}, + }, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{proxyIP}, + IPAddresses: []net.IP{net.ParseIP(proxyIP)}, + IsCA: true, + BasicConstraintsValid: true, + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + certD, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) + if err != nil { + panic(err) + } + var f io.WriteCloser + if certFd != 0 { + f = os.NewFile(uintptr(certFd), "") + } else if certFile != "" { + var err error + f, err = os.OpenFile(certFile, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + panic(err) + } + } else { + panic("specify cert destination") + } + if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certD}); err != nil { + panic(err) + } + if err := f.Close(); err != nil { + panic(err) + } + tlsCert := &tls.Certificate{ + Certificate: [][]byte{certD}, + PrivateKey: key, + } + proxyCert, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + panic(err) + } + proxyKey = key + + config := &gvntypes.Configuration{ + Debug: debug, + MTU: 1500, + Subnet: "192.168.127.0/24", + GatewayIP: gatewayIP, + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + GatewayVirtualIPs: []string{proxyIP}, + Protocol: gvntypes.QemuProtocol, + } + vn, err := gvnvirtualnetwork.New(config) + if err != nil { + panic(err) + } + + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + handleTunneling(w, r) + } else { + handleHTTP(w, r) + } + }), + // Disable HTTP/2 (FIXME) + // https://pkg.go.dev/net/http#hdr-HTTP_2 + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + + go func() { + s := server + s.Addr = "0.0.0.0:80" + l, err := vn.Listen("tcp", proxyIP+":80") + if err != nil { + panic(err) + } + log.Println("serving proxy with http") + log.Fatal(server.Serve(l)) + }() + go func() { + s := server + s.Addr = "0.0.0.0:443" + l, err := vn.Listen("tcp", proxyIP+":443") + if err != nil { + panic(err) + } + s.TLSConfig = &tls.Config{ + Certificates: make([]tls.Certificate, 1), + } + s.TLSConfig.Certificates[0] = *tlsCert + log.Println("serving proxy with https") + log.Fatal(server.ServeTLS(l, "", "")) + }() + ql, err := findListener(listenFd) + if err != nil { + panic(err) + } + if ql == nil { + panic("socket fd not found") + } + qconn, err := ql.Accept() + if err != nil { + panic(err) + } + if err := vn.AcceptQemu(context.TODO(), qconn); err != nil { + panic(err) + } +} + +func findListener(listenFd int) (net.Listener, error) { + if listenFd == 0 { + for preopenFd := 3; ; preopenFd++ { + var stat syscall.Stat_t + if err := syscall.Fstat(preopenFd, &stat); err != nil { + var se syscall.Errno + if errors.As(err, &se) && se == syscall.EBADF { + err = nil + } + log.Printf("findListner failed (fd=%d): %v\n", preopenFd, err) + return nil, err + } else if stat.Filetype == syscall.FILETYPE_SOCKET_STREAM { + listenFd = preopenFd + break + } + } + } + syscall.SetNonblock(listenFd, true) + f := os.NewFile(uintptr(listenFd), "") + defer f.Close() + log.Printf("using socket at fd=%d\n", listenFd) + return net.FileListener(f) +} diff --git a/go.mod b/go.mod index c5eacfb..7252594 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,19 @@ go 1.19 require ( github.com/containerd/containerd v1.7.5 + github.com/containers/gvisor-tap-vsock v0.7.0 github.com/opencontainers/image-spec v1.1.0-rc4 github.com/opencontainers/runc v1.1.9 github.com/opencontainers/runtime-spec v1.1.0 github.com/urfave/cli v1.22.14 + golang.org/x/net v0.12.0 gotest.tools/v3 v3.5.0 ) require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.10.0-rc.8 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.4.2 // indirect github.com/containerd/ttrpc v1.2.2 // indirect @@ -22,20 +25,29 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect github.com/klauspost/compress v1.16.0 // indirect + github.com/miekg/dns v1.1.55 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.10.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.9.3 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.29.1 // indirect + gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db // indirect + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect ) diff --git a/go.sum b/go.sum index a55468e..9c73eb7 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,9 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/hcsshim v0.10.0-rc.8 h1:YSZVvlIIDD1UxQpJp0h+dnpLUw+TrY0cx8obKsp3bek= github.com/Microsoft/hcsshim v0.10.0-rc.8/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -18,6 +21,8 @@ github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtO github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containers/gvisor-tap-vsock v0.7.0 h1:lL5UpfXhl+tuTwK43CXhKWwfvAiZtde6G5alFeoO+Z8= +github.com/containers/gvisor-tap-vsock v0.7.0/go.mod h1:edQTwl8ar+ACuQOkazpQkgd/ZMF6TJ2Xr3fv+MKUaw8= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,6 +32,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -47,25 +54,53 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f h1:l1QCwn715k8nYkj4Ql50rzEog3WnMdrd4YYMMwemxEo= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= @@ -84,18 +119,23 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -105,24 +145,35 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -134,28 +185,43 @@ golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -191,11 +257,16 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db h1:WZSmkyu/hep9YhWIlBZefwGVBrnGE5yW8JPD56YRsXs= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 h1:PqdHrvQRVK1zapJkd0qf6+tevvSIcWdfenVqJd3PHWU= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= diff --git a/patches/bochs/Bochs/bochs/iodev/devices.cc b/patches/bochs/Bochs/bochs/iodev/devices.cc index 4cc9782..f82480f 100644 --- a/patches/bochs/Bochs/bochs/iodev/devices.cc +++ b/patches/bochs/Bochs/bochs/iodev/devices.cc @@ -363,6 +363,7 @@ void bx_devices_c::init(BX_MEM_C *newmem) PLUG_load_plugin(mapdirVirtio9p, PLUGTYPE_STANDARD); PLUG_load_plugin(packVirtio9p, PLUGTYPE_STANDARD); PLUG_load_plugin(stdioVirtioConsole, PLUGTYPE_STANDARD); + PLUG_load_plugin(qemuVirtioNet, PLUGTYPE_STANDARD); #endif bx_init_plugins(); diff --git a/patches/bochs/Bochs/bochs/main.cc b/patches/bochs/Bochs/bochs/main.cc index 1869575..fd58442 100644 --- a/patches/bochs/Bochs/bochs/main.cc +++ b/patches/bochs/Bochs/bochs/main.cc @@ -411,10 +411,52 @@ int write_env(FSVirtFile *f, int pos1, const char *env) return pos - pos1; } +int write_net(FSVirtFile *f, int pos1, const char *mac) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "n: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(mac); j++) { + if (putchar_info(f, pos++, mac[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + +int write_time(FSVirtFile *f, int pos1, const char *timestr) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "t: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(timestr); j++) { + if (putchar_info(f, pos++, timestr[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + static struct option options[] = { { "help", no_argument, NULL, 'h' }, { "no-stdin", no_argument }, { "entrypoint", required_argument }, + { "net", required_argument }, + { "mac", required_argument }, { NULL }, }; @@ -426,20 +468,24 @@ void print_usage(void) "OPTIONS:\n" " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" " -no-stdin : disable stdin. (default: false)\n" + " -net : enable networking with the specified mode (default: disabled. supported mode: \"qemu\")\n" + " -mac : use a custom mac address for the VM\n" "\n" "This tool is based on Bochs emulator.\n" ); exit(0); } -int init_wasi_info(int argc, char **argv, FSVirtFile *info) +int CDECL init_func(void); + +int init_vm(int argc, char **argv, FSVirtFile *info) { info->contents = (char *)calloc(1024, sizeof(char)); info->len = 0; info->lim = 1024; /* const char *cmdline, *build_preload_file; */ - char *entrypoint = NULL; + char *entrypoint = NULL, *net = NULL, *mac = NULL; bool enable_stdin = true; int pos, c, option_index, i; for(;;) { @@ -458,6 +504,12 @@ int init_wasi_info(int argc, char **argv, FSVirtFile *info) case 2: /* entrypoint */ entrypoint = optarg; break; + case 3: /* net */ + net = optarg; + break; + case 4: /* mac */ + mac = optarg; + break; default: fprintf(stderr, "unknown option index: %d\n", option_index); exit(1); @@ -471,6 +523,14 @@ int init_wasi_info(int argc, char **argv, FSVirtFile *info) } } + if (!vm_init_done) { + int ret = init_func(); + if (ret != CI_INIT_DONE) { + printf("initialization failed\n"); + return -1; + } + } + if (!enable_stdin) { SIM->get_param_bool(BXPN_WASM_NOSTDIN)->set(1); } @@ -484,7 +544,7 @@ int init_wasi_info(int argc, char **argv, FSVirtFile *info) } pos += p; } - + if (optind < argc) { int p = write_args(info, argc, argv, optind, pos); if (p < 0) { @@ -507,17 +567,41 @@ int init_wasi_info(int argc, char **argv, FSVirtFile *info) } #endif + if (net != NULL) { + if (!strncmp(net, "qemu", 4)) { + if (start_qemu_net(net) != 0) { + fprintf(stderr, "failed to wait qemu net"); + exit(1); + } + } + int p = write_net(info, pos, mac); + if (p < 0) { + printf("failed to prepare net info\n"); + exit(1); + } + pos += p; + } + + char buf[32]; + snprintf(buf, sizeof(buf), "%d", (unsigned)time(NULL)); + int ps = write_time(info, pos, buf); + if (ps < 0) { + printf("failed to prepare time info\n"); + exit(1); + } + pos += ps; + info->len = pos; #ifdef WASI info->len += write_preopen_info(info, pos); #endif + return 0; } int bxmain(void) { if (vm_init_done) { - init_wasi_info(bx_startup_flags.argc, bx_startup_flags.argv, get_vm_info()); bx_param_enum_c *ci_param = SIM->get_param_enum(BXPN_SEL_CONFIG_INTERFACE); const char *ci_name = ci_param->get_selected(); int status = SIM->configuration_interface(ci_name, CI_START); @@ -801,15 +885,10 @@ int CDECL main(int argc, char *argv[]) return -1; } #endif - if (!vm_init_done) { - int ret = init_func(); - if (ret != CI_INIT_DONE) { - printf("initialization failed\n"); - return -1; - } + if (init_vm(argc, argv, get_vm_info()) < 0) { + fprintf(stderr, "failed to init vm"); + return -1; } - bx_startup_flags.argc = argc; - bx_startup_flags.argv = argv; return start_vm(); } #endif diff --git a/patches/bochs/Bochs/bochs/plugin.cc b/patches/bochs/Bochs/bochs/plugin.cc index 27d0fb4..1a07a61 100644 --- a/patches/bochs/Bochs/bochs/plugin.cc +++ b/patches/bochs/Bochs/bochs/plugin.cc @@ -1065,6 +1065,7 @@ plugin_t bx_builtin_plugins[] = { BUILTIN_OPTPCI_PLUGIN_ENTRY(mapdirVirtio9p), BUILTIN_OPTPCI_PLUGIN_ENTRY(packVirtio9p), BUILTIN_OPTPCI_PLUGIN_ENTRY(stdioVirtioConsole), + BUILTIN_OPTPCI_PLUGIN_ENTRY(qemuVirtioNet), #if BX_SUPPORT_SOUNDLOW BUILTIN_SND_PLUGIN_ENTRY(dummy), BUILTIN_SND_PLUGIN_ENTRY(file), diff --git a/patches/bochs/Bochs/bochs/plugin.h b/patches/bochs/Bochs/bochs/plugin.h index 0ce9926..f497fa6 100644 --- a/patches/bochs/Bochs/bochs/plugin.h +++ b/patches/bochs/Bochs/bochs/plugin.h @@ -79,6 +79,7 @@ extern "C" { #define BX_PLUGIN_MAPDIR_VIRTIO_9P "mapdirVirtio9p" #define BX_PLUGIN_PACK_VIRTIO_9P "packVirtio9p" #define BX_PLUGIN_STDIO_VIRTIO_CONSOLE "stdioVirtioConsole" +#define BX_PLUGIN_QEMU_VIRTIO_NET "qemuVirtioNet" #define BX_REGISTER_DEVICE_DEVMODEL(a,b,c,d) pluginRegisterDeviceDevmodel(a,b,c,d) #define BX_UNREGISTER_DEVICE_DEVMODEL(a,b) pluginUnregisterDeviceDevmodel(a,b) @@ -495,6 +496,7 @@ PLUGIN_ENTRY_FOR_IMG_MODULE(vvfat); PLUGIN_ENTRY_FOR_MODULE(mapdirVirtio9p); PLUGIN_ENTRY_FOR_MODULE(packVirtio9p); PLUGIN_ENTRY_FOR_MODULE(stdioVirtioConsole); +PLUGIN_ENTRY_FOR_MODULE(qemuVirtioNet); #endif #ifdef __cplusplus diff --git a/patches/bochs/Bochs/bochs/wasm.cc b/patches/bochs/Bochs/bochs/wasm.cc index 08b3ecd..968308d 100644 --- a/patches/bochs/Bochs/bochs/wasm.cc +++ b/patches/bochs/Bochs/bochs/wasm.cc @@ -57,15 +57,31 @@ #include "iodev/pci.h" #include "wasm.h" +#ifdef EMSCRIPTEN +#include +#endif + +#include +#include +// not defined in wasi-libc +#ifdef WASI +#define PF_INET 1 +#define PF_INET6 2 +#endif + +#define LOG_THIS genlog-> + ///////////////////////////////////////////////////////////////////////// // Register mapdir device via viritio-9p ///////////////////////////////////////////////////////////////////////// bx_virtio_9p_ctrl_c *wasi0; bx_virtio_9p_ctrl_c *wasi1; -FSVirtFile *info; +FSVirtFile *info = NULL; FSVirtFile *get_vm_info() { + if (info == NULL) + info = (FSVirtFile *)malloc(sizeof(FSVirtFile)); return info; } @@ -91,8 +107,7 @@ PLUGIN_ENTRY_FOR_MODULE(mapdirVirtio9p) PLUGIN_ENTRY_FOR_MODULE(packVirtio9p) { if (mode == PLUGIN_INIT) { - info = (FSVirtFile *)malloc(sizeof(FSVirtFile)); - FSDevice *wasi1fs = fs_disk_init_with_info("pack", "info", info); + FSDevice *wasi1fs = fs_disk_init_with_info("pack", "info", get_vm_info()); wasi1 = new bx_virtio_9p_ctrl_c(BX_PLUGIN_PACK_VIRTIO_9P, "wasi1", wasi1fs); BX_REGISTER_DEVICE_DEVMODEL(plugin, type, wasi1, BX_PLUGIN_PACK_VIRTIO_9P); } else if (mode == PLUGIN_FINI) { @@ -246,6 +261,27 @@ PLUGIN_ENTRY_FOR_MODULE(stdioVirtioConsole) return 0; // Success } +bx_virtio_net_ctrl_c *qemu0; +EthernetDevice *qemu_net; +static EthernetDevice *qemu_net_init(); + +PLUGIN_ENTRY_FOR_MODULE(qemuVirtioNet) +{ + if (mode == PLUGIN_INIT) { + qemu_net = qemu_net_init(); + qemu0 = new bx_virtio_net_ctrl_c(BX_PLUGIN_QEMU_VIRTIO_NET, qemu_net); + BX_REGISTER_DEVICE_DEVMODEL(plugin, type, qemu0, BX_PLUGIN_QEMU_VIRTIO_NET); + } else if (mode == PLUGIN_FINI) { + delete qemu0; + } else if (mode == PLUGIN_PROBE) { + return (int)PLUGTYPE_STANDARD; + } else if (mode == PLUGIN_FLAGS) { + return PLUGFLAG_PCI; + } + return 0; // Success +} + + ///////////////////////////////////////////////////////////////////////// // WASI preopens ///////////////////////////////////////////////////////////////////////// @@ -400,6 +436,14 @@ void *mallocz(size_t size) return ptr; } +static inline int max_int(int a, int b) +{ + if (a > b) + return a; + else + return b; +} + static inline int min_int(int a, int b) { if (a < b) @@ -1268,7 +1312,7 @@ bx_virtio_ctrl_c::bx_virtio_ctrl_c() bx_virtio_ctrl_c::~bx_virtio_ctrl_c() { SIM->get_bochs_root()->remove("virtio"); - // BX_DEBUG(("Exit")); + BX_DEBUG(("Exit")); } int bx_virtio_ctrl_c::pci_add_capability(Bit8u *buf, int size) @@ -1822,7 +1866,7 @@ bx_virtio_9p_ctrl_c::bx_virtio_9p_ctrl_c(char *plugin_name, char *mount_tag, FSD bx_virtio_9p_ctrl_c::~bx_virtio_9p_ctrl_c() { SIM->get_bochs_root()->remove("virtio_9p"); - // BX_DEBUG(("Exit")); + BX_DEBUG(("Exit")); } void bx_virtio_9p_ctrl_c::init() @@ -2727,7 +2771,7 @@ bx_virtio_console_ctrl_c::bx_virtio_console_ctrl_c(char *plugin_name, CharacterD bx_virtio_console_ctrl_c::~bx_virtio_console_ctrl_c() { SIM->get_bochs_root()->remove("virtio_console"); - // BX_DEBUG(("Exit")); + BX_DEBUG(("Exit")); } void bx_virtio_console_ctrl_c::init() @@ -2886,4 +2930,399 @@ void bx_virtio_console_ctrl_c::rx_timer_handler(void *this_ptr) } } +///////////////////////////////////////////////////////////////////////// +// Virtio-net +///////////////////////////////////////////////////////////////////////// +bx_virtio_net_ctrl_c::bx_virtio_net_ctrl_c(char *plugin_name, EthernetDevice *es) +{ + put("VIRTIO NET"); + BX_VIRTIO_NET_THIS plugin_name = plugin_name; + BX_VIRTIO_NET_THIS es = es; // TODO +} + +bx_virtio_net_ctrl_c::~bx_virtio_net_ctrl_c() +{ + SIM->get_bochs_root()->remove("virtio_net"); + BX_DEBUG(("Exit")); +} + +bool bx_virtio_net_ctrl_c::virtio_net_can_write_packet(EthernetDevice *es) +{ + QueueState *qs = &BX_VIRTIO_THIS s.queue[0]; + uint16_t avail_idx; + + if (!qs->ready) + return false; + avail_idx = virtio_read16(qs->avail_addr + 2); + return qs->last_avail_idx != avail_idx; +} + +void bx_virtio_net_ctrl_c::virtio_net_write_packet(EthernetDevice *es, const uint8_t *buf, int buf_len) +{ + VIRTIONetDevice *s1 = &BX_VIRTIO_NET_THIS net; + int queue_idx = 0; + QueueState *qs = &BX_VIRTIO_THIS s.queue[0]; + int desc_idx; + VIRTIONetHeader h; + int len, read_size, write_size; + uint16_t avail_idx; + + if (!qs->ready) + return; + avail_idx = virtio_read16(qs->avail_addr + 2); + if (qs->last_avail_idx == avail_idx) + return; + desc_idx = virtio_read16(qs->avail_addr + 4 + + (qs->last_avail_idx & (qs->num - 1)) * 2); + if (get_desc_rw_size(&read_size, &write_size, queue_idx, desc_idx)) + return; + len = s1->header_size + buf_len; + if (len > write_size) + return; + memset(&h, 0, s1->header_size); + memcpy_to_queue(queue_idx, desc_idx, 0, &h, s1->header_size); + memcpy_to_queue(queue_idx, desc_idx, s1->header_size, (void*)buf, buf_len); + virtio_consume_desc(queue_idx, desc_idx, len); + qs->last_avail_idx++; +} + +void bx_virtio_net_ctrl_c::init() +{ + bx_virtio_ctrl_c::init(BX_VIRTIO_NET_THIS plugin_name, 0x1000, 0x0200, 0x1, 1 << 5, "Virtio-net"); // initialize as virtio-net + + BX_VIRTIO_NET_THIS config_space_size = 6 + 2; + Bit8u *cfg; + cfg = BX_VIRTIO_NET_THIS config_space; + memcpy(cfg, BX_VIRTIO_NET_THIS es->mac_addr, 6); + /* status */ + cfg[6] = 0; + cfg[7] = 0; + QueueState *qs = &BX_VIRTIO_NET_THIS s.queue[0]; + qs->manual_recv = true; + BX_VIRTIO_NET_THIS net.header_size = sizeof(VIRTIONetHeader); + BX_VIRTIO_NET_THIS es->virtio_device = this; + BX_VIRTIO_NET_THIS timer_id = DEV_register_timer(this, rx_timer_handler, 0, false, false, "virtio-net.rx");; + bx_pc_system.activate_timer(BX_VIRTIO_NET_THIS timer_id, 100, false); /* not continuous */ +} + +void bx_virtio_net_ctrl_c::rx_timer_handler(void *this_ptr) +{ + bx_virtio_net_ctrl_c *class_ptr = (bx_virtio_net_ctrl_c *)this_ptr; + EthernetDevice *es = class_ptr->es; + int n_fd_max = -1, delay = 0, n_ret = 0; + fd_set n_rfds, n_wfds, n_efds; + struct timeval tv; + + FD_ZERO(&n_rfds); + FD_ZERO(&n_wfds); + FD_ZERO(&n_efds); + es->select_fill(es, &n_fd_max, &n_rfds, &n_wfds, &n_efds, &delay); + if (n_fd_max >= 0) { + tv.tv_sec = 0; + tv.tv_usec = 0; + n_ret = select(n_fd_max + 1, &n_rfds, NULL, NULL, &tv); + es->select_poll(es, &n_rfds, &n_wfds, &n_efds, n_ret); + } + int watch_res = es->watch(es); + int duration = 100; + if (watch_res < 0) { + duration = 1000000; + } else if (n_ret <= 0) { + duration = 100000; + } + bx_pc_system.activate_timer(class_ptr->timer_id, duration, false); /* not continuous */ +} + +int bx_virtio_net_ctrl_c::device_recv(int queue_idx, int desc_idx, int read_size, int write_size) +{ + VIRTIONetDevice *s1 = &BX_VIRTIO_NET_THIS net; + EthernetDevice *es = BX_VIRTIO_NET_THIS es; + VIRTIONetHeader h; + uint8_t *buf; + int len; + + if (queue_idx == 1) { + /* send to network */ + if (memcpy_from_queue(&h, queue_idx, desc_idx, 0, s1->header_size) < 0) + return 0; + len = read_size - s1->header_size; + buf = (uint8_t *)malloc(len); + memcpy_from_queue(buf, queue_idx, desc_idx, s1->header_size, len); + es->write_packet(es, buf, len); + free(buf); + virtio_consume_desc(queue_idx, desc_idx, 0); + } + return 0; +} + +typedef struct { + int fd; + bool select_filled; + char *raw_flag; + + int tmpfd; + bool enabled; + int retrynum; + + uint32_t sizebuf; + int sizeoff; + int pktsize; + int pktoff; + uint8_t *pktbuf; +} QemuSocketState; + +static void qemu_write_packet(EthernetDevice *net, + const uint8_t *buf, int len) +{ + QemuSocketState *s = (QemuSocketState *)net->opaque; + uint32_t size = htonl(len); + int ret; + + if (s->fd < 0) { + return; + } + + ret = send(s->fd, &size, 4, 0); // TODO: check error + if (ret < 0) { + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } + ret = send(s->fd, buf, len, 0); // TODO: check error + if (ret < 0) { + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } +} + +static int try_get_fd(QemuSocketState *s); + +static int qemu_watch1(EthernetDevice *net) +{ + QemuSocketState *s = (QemuSocketState *)net->opaque; + + if (!s->enabled) + return -1; + + if ((s->fd >= 0) || (try_get_fd((QemuSocketState *)net->opaque) >= 0)) + return 0; + + return -1; +} + +static void qemu_select_fill1(EthernetDevice *net, int *pfd_max, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int *pdelay) +{ + QemuSocketState *s = (QemuSocketState *)net->opaque; + int fd = s->fd; + + if (fd < 0) { + return; + } + + s->select_filled = net->virtio_device->virtio_net_can_write_packet(net); + if (s->select_filled) { + FD_SET(fd, rfds); + *pfd_max = max_int(*pfd_max, fd); + } +} + +static void qemu_select_poll1(EthernetDevice *net, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int select_ret) +{ + QemuSocketState *s = (QemuSocketState *)net->opaque; + int fd = s->fd; + uint32_t size = 0; + int ret; + + if (fd < 0) { + return; + } + + if (select_ret <= 0) { + if (select_ret < 0) { + fflush(stdout); + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + } + return; + } + + if (s->select_filled && FD_ISSET(fd, rfds)) { + if (s->pktsize <= 0) { + while (s->sizeoff < 4) { + ret = recv(fd, &s->sizebuf + s->sizeoff, 4 - s->sizeoff, 0); + if (ret <= 0) { + if (errno == EAGAIN) + return; // try later + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } + s->sizeoff += ret; + } + s->pktsize = ntohl(s->sizebuf); + s->sizeoff = 0; + s->sizebuf = 0; + } + + if (s->pktsize > 0) { + size = s->pktsize; + if (s->pktbuf == NULL) { + s->pktoff = 0; + s->pktbuf = (uint8_t *)mallocz(size); // TODO: make limit + } + while (s->pktoff < size) { + ret = recv(fd, s->pktbuf + s->pktoff, size - s->pktoff, 0); + if (ret <= 0) { + if (errno == EAGAIN) + return; // try later + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } + s->pktoff += ret; + } + net->virtio_device->virtio_net_write_packet(net, s->pktbuf, size); + free(s->pktbuf); + s->pktbuf = NULL; + s->pktsize = 0; + s->pktoff = 0; + } + } +} + +static EthernetDevice *qemu_net_init() +{ + EthernetDevice *net; + QemuSocketState *s; + + net = (EthernetDevice *)mallocz(sizeof(*net)); + net->mac_addr[0] = 0x02; + net->mac_addr[1] = 0x00; + net->mac_addr[2] = 0x00; + net->mac_addr[3] = 0x00; + net->mac_addr[4] = 0x00; + net->mac_addr[5] = 0x01; + net->opaque = NULL; + s = (QemuSocketState *)mallocz(sizeof(*s)); + s->fd = -1; + s->tmpfd = -1; + s->enabled = false; + net->opaque = s; + net->write_packet = qemu_write_packet; + net->select_fill = qemu_select_fill1; + net->select_poll = qemu_select_poll1; + net->watch = qemu_watch1; + + return net; +} + +static void reset_qemu_socket_state(QemuSocketState *s) +{ + s->sizebuf = 0; + s->sizeoff = 0; + s->pktsize = 0; + s->pktoff = 0; + if (s->pktbuf != NULL) { + free(s->pktbuf); + } + s->pktbuf = NULL; +} + +#ifdef EMSCRIPTEN +static int try_get_fd(QemuSocketState *s) +{ + int sock= s->fd; + fd_set wfds; + struct sockaddr_in qemuAddr; + + if (s->tmpfd < 0) { + if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { + BX_INFO(("failed to prepare socket: %s", strerror(errno))); + return -1; + } + // connect to the proxy + memset(&qemuAddr, 0, sizeof(qemuAddr)); + qemuAddr.sin_family = AF_INET; + int cret = connect(sock, (struct sockaddr *) &qemuAddr, sizeof(qemuAddr)); + bool connected = false; + if ((cret == 0) || (errno == EISCONN)) { + BX_INFO(("socket connected")); + s->fd = sock; + s->tmpfd = -1; + reset_qemu_socket_state(s); + return 0; + } else if (errno != EINPROGRESS) { + BX_INFO(("failed to connect: %s", strerror(errno))); + s->fd = -1; + s->tmpfd = -1; + return -1; + } + s->retrynum = 0; + s->tmpfd = sock; + } + + BX_INFO(("waiting for connection...")); + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&wfds); + FD_SET(s->tmpfd, &wfds); + int sret = select(s->tmpfd + 1, NULL, &wfds, NULL, &tv); + if (sret > 0) { + s->fd = s->tmpfd; + s->tmpfd = -1; + reset_qemu_socket_state(s); + return 0; + } else if (sret < 0) { + s->fd = -1; + s->tmpfd = -1; + } // else, still wait for the connection. + BX_INFO(("select result: %d", sret)); + + s->retrynum++; + if (s->retrynum > 5) { // TODO: make max retry number configurable + close(s->tmpfd); + s->tmpfd = -1; // too many errors. retry connection. + } + + return -1; +} +#elif defined(WASI) +static int try_get_fd(QemuSocketState *s) +{ + int sock = 3; + if ((s->raw_flag != NULL) && (!strncmp(s->raw_flag, "qemu=", 5))) { + // TODO: allow specifying more options + if (!strncmp(s->raw_flag + 5, "listenfd=", 9)) { + sock = atoi(s->raw_flag + 5 + 9); + } + } + int sock_a = 0; + // wait for connection + BX_INFO(("accept trying...(sockfd=%d)", sock)); + sock_a = accept4(sock, NULL, NULL, SOCK_NONBLOCK); + if (sock_a > 0) { + BX_INFO(("accepted fd=%d", sock_a)); + s->fd = sock_a; + reset_qemu_socket_state(s); + return 0; + } + BX_INFO(("failed to accept socket: %s", strerror(errno))); + return -1; +} +#endif + +int start_qemu_net(char *flag) +{ + if (qemu_net != NULL) { + ((QemuSocketState *)qemu_net->opaque)->enabled = true; + ((QemuSocketState *)qemu_net->opaque)->raw_flag = flag; + } + return 0; +} #endif /* BX_SUPPORT_PCI */ diff --git a/patches/bochs/Bochs/bochs/wasm.h b/patches/bochs/Bochs/bochs/wasm.h index 5b9fd7a..e2dbe35 100644 --- a/patches/bochs/Bochs/bochs/wasm.h +++ b/patches/bochs/Bochs/bochs/wasm.h @@ -371,10 +371,8 @@ class bx_virtio_ctrl_c : public bx_pci_device_c { Bit32u device_features; Bit32u next_cap_offset; } s; - int get_desc_rw_size(int *pread_size, int *pwrite_size, int queue_idx, int desc_idx); - private: bool mem_read(bx_phy_address addr, unsigned len, void *data); bool mem_write(bx_phy_address addr, unsigned len, void *data); @@ -534,7 +532,6 @@ class bx_virtio_console_ctrl_c : public bx_virtio_ctrl_c { int virtio_console_get_write_len(); int virtio_console_write_data(const uint8_t *buf, int buf_len); void virtio_console_resize_event(int width, int height); - static void rx_timer_handler(void *this_ptr); private: @@ -543,4 +540,60 @@ class bx_virtio_console_ctrl_c : public bx_virtio_ctrl_c { VIRTIOConsoleDevice dev; }; +///////////////////////////////////////////////////////////////////////// +// Virtio-net +///////////////////////////////////////////////////////////////////////// +#define BX_VIRTIO_NET_THIS this-> + +typedef struct bx_virtio_net_ctrl_c bx_virtio_net_ctrl_c; + +struct EthernetDevice { + uint8_t mac_addr[6]; /* mac address of the interface */ + void (*write_packet)(EthernetDevice *net, + const uint8_t *buf, int len); + void *opaque; + bx_virtio_net_ctrl_c *virtio_device; + void (*select_fill)(EthernetDevice *net, int *pfd_max, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int *pdelay); + void (*select_poll)(EthernetDevice *net, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int select_ret); + int (*watch)(EthernetDevice *net); +}; + +typedef struct VIRTIONetDevice { + int header_size; +} VIRTIONetDevice; + +typedef struct { + uint8_t flags; + uint8_t gso_type; + uint16_t hdr_len; + uint16_t gso_size; + uint16_t csum_start; + uint16_t csum_offset; + uint16_t num_buffers; +} VIRTIONetHeader; + +class bx_virtio_net_ctrl_c : public bx_virtio_ctrl_c { +public: + bx_virtio_net_ctrl_c(char *plugin_name, EthernetDevice *es); + virtual ~bx_virtio_net_ctrl_c(); + virtual void init(void); + virtual int device_recv(int queue_idx, int desc_idx, int read_size, int write_size); + + bool virtio_net_can_write_packet(EthernetDevice *es); + void virtio_net_write_packet(EthernetDevice *es, const uint8_t *buf, int buf_len); + static void rx_timer_handler(void *this_ptr); + +private: + char *plugin_name; + int timer_id; + VIRTIONetDevice net; + EthernetDevice *es; +}; + +int start_qemu_net(char *flag); + #endif diff --git a/patches/bochs/grub.cfg.template b/patches/bochs/grub.cfg.template index 7dbd50e..0a7f7b9 100644 --- a/patches/bochs/grub.cfg.template +++ b/patches/bochs/grub.cfg.template @@ -2,5 +2,5 @@ set default=0 set timeout=0 menuentry 'linux' { - linux /boot/grub/bzImage console=hvc0 root=/dev/sr0 rootwait ro quiet loglevel=${LOGLEVEL} init=/sbin/tini -- /sbin/init + linux /boot/grub/bzImage console=hvc0 root=/dev/sr0 rootwait ro virtio_net.napi_tx=false quiet loglevel=${LOGLEVEL} init=/sbin/tini -- /sbin/init } \ No newline at end of file diff --git a/patches/bochs/vfs/vfs.c b/patches/bochs/vfs/vfs.c new file mode 100644 index 0000000..eff0418 --- /dev/null +++ b/patches/bochs/vfs/vfs.c @@ -0,0 +1,20 @@ +#include + +// allow directly controlling fd for sockets, etc. +int32_t poll_oneoff(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("poll_oneoff") +)); + +int32_t __imported_wasi_snapshot_preview1_poll_oneoff(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) { + return poll_oneoff(arg0, arg1, arg2, arg3); +} + +int32_t fd_close(int32_t arg0) __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("fd_close") +)); + +int32_t __imported_wasi_snapshot_preview1_fd_close(int32_t arg0) { + return fd_close(arg0); +} diff --git a/patches/tinyemu/tinyemu.config.template b/patches/tinyemu/tinyemu.config.template index 7e2222e..e0c86a8 100644 --- a/patches/tinyemu/tinyemu.config.template +++ b/patches/tinyemu/tinyemu.config.template @@ -4,6 +4,6 @@ memory_size: ${MEMORY_SIZE}, bios: "/pack/bbl.bin", kernel: "/pack/Image", - cmdline: "console=hvc0 root=/dev/vda ro quiet loglevel=${LOGLEVEL} init=/sbin/tini -- /sbin/init", + cmdline: "console=hvc0 root=/dev/vda ro quiet virtio_net.napi_tx=false loglevel=${LOGLEVEL} init=/sbin/tini -- /sbin/init", drive0: { file: "/pack/rootfs.bin" }, } diff --git a/patches/tinyemu/tinyemu/temu.c b/patches/tinyemu/tinyemu/temu.c index 57d966b..85a2344 100644 --- a/patches/tinyemu/tinyemu/temu.c +++ b/patches/tinyemu/tinyemu/temu.c @@ -64,6 +64,14 @@ extern char **environ; #include #endif +#include +#include +#ifndef ON_BROWSER +// not defined in wasi-libc +#define PF_INET 1 +#define PF_INET6 2 +#endif + #ifndef _WIN32 typedef struct { @@ -583,6 +591,276 @@ static EthernetDevice *slirp_open(void) #endif /* CONFIG_SLIRP */ +/*******************************************************/ +/* qemu */ + +typedef struct { + int fd; + BOOL select_filled; + char *raw_flag; + + int tmpfd; + BOOL enabled; + int next_watch; + int retrynum; + + uint32_t sizebuf; + int sizeoff; + int pktsize; + int pktoff; + uint8_t *pktbuf; +} QemuSocketState; + +static void qemu_write_packet(EthernetDevice *net, + const uint8_t *buf, int len) +{ + QemuSocketState *s = net->opaque; + uint32_t size = htonl(len); + int ret; + + if (s->fd < 0) { + return; + } + + ret = send(s->fd, &size, 4, 0); // TODO: check error + if (ret < 0) { + close(s->fd); + s->fd = -1; + return; + } + ret = send(s->fd, buf, len, 0); // TODO: check error + if (ret < 0) { + close(s->fd); + s->fd = -1; + return; + } +} + +static int try_get_fd(QemuSocketState *s); + +static int qemu_watch1(EthernetDevice *net) +{ + QemuSocketState *s = (QemuSocketState *)net->opaque; + + if (!s->enabled) + return -1; + + if (s->fd >= 0) + return 0; + + s->next_watch--; + if (s->next_watch > 0) + return -1; + if (try_get_fd(s) >= 0) + return 0; + if (s->next_watch <= 0) { + s->next_watch = 500; // TODO: make it configable or should be time interval + } + + return -1; +} + +static void qemu_select_fill1(EthernetDevice *net, int *pfd_max, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int *pdelay) +{ + QemuSocketState *s = net->opaque; + int fd = s->fd; + + if (s->fd < 0) { + return; + } + + s->select_filled = net->device_can_write_packet(net); + if (s->select_filled) { + FD_SET(fd, rfds); + *pfd_max = max_int(*pfd_max, fd); + } +} + +static void qemu_select_poll1(EthernetDevice *net, + fd_set *rfds, fd_set *wfds, fd_set *efds, + int select_ret) +{ + QemuSocketState *s = net->opaque; + int fd = s->fd; + uint32_t size = 0; + int ret; + + if (s->fd < 0) { + return; + } + + if (select_ret <= 0) + return; + + if (s->select_filled && FD_ISSET(fd, rfds)) { + if (s->pktsize <= 0) { + while (s->sizeoff < 4) { + ret = recv(fd, &s->sizebuf + s->sizeoff, 4 - s->sizeoff, 0); + if (ret <= 0) { + if (errno == EAGAIN) + return; // try later + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } + s->sizeoff += ret; + } + s->pktsize = ntohl(s->sizebuf); + s->sizeoff = 0; + s->sizebuf = 0; + } + + if (s->pktsize > 0) { + size = s->pktsize; + if (s->pktbuf == NULL) { + s->pktoff = 0; + s->pktbuf = (uint8_t *)mallocz(size); // TODO: make limit + } + while (s->pktoff < size) { + ret = recv(fd, s->pktbuf + s->pktoff, size - s->pktoff, 0); + if (ret <= 0) { + if (errno == EAGAIN) + return; // try later + close(s->fd); + s->fd = -1; // invalid fd. hopefully the watch loop will recover this. + return; + } + s->pktoff += ret; + } + net->device_write_packet(net, s->pktbuf, size); + free(s->pktbuf); + s->pktbuf = NULL; + s->pktsize = 0; + s->pktoff = 0; + } + } +} + +static EthernetDevice *qemu_net_init() +{ + EthernetDevice *net; + QemuSocketState *s; + + net = mallocz(sizeof(*net)); + net->mac_addr[0] = 0x02; + net->mac_addr[1] = 0x00; + net->mac_addr[2] = 0x00; + net->mac_addr[3] = 0x00; + net->mac_addr[4] = 0x00; + net->mac_addr[5] = 0x01; + net->opaque = NULL; + s = mallocz(sizeof(*s)); + s->fd = -1; + s->tmpfd = -1; + s->enabled = FALSE; + s->next_watch = 0; + net->opaque = s; + net->write_packet = qemu_write_packet; + net->select_fill = qemu_select_fill1; + net->select_poll = qemu_select_poll1; + net->watch = qemu_watch1; + + return net; +} + +static void reset_qemu_socket_state(QemuSocketState *s) +{ + s->sizebuf = 0; + s->sizeoff = 0; + s->pktsize = 0; + s->pktoff = 0; + if (s->pktbuf != NULL) { + free(s->pktbuf); + } + s->pktbuf = NULL; +} + +#ifdef ON_BROWSER +static int try_get_fd(QemuSocketState *s) +{ + int sock= s->fd; + fd_set wfds; + struct sockaddr_in qemuAddr; + + if (s->tmpfd < 0) { + if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { + printf("failed to prepare socket: %s\n", strerror(errno)); + return -1; + } + // connect to the network stack + memset(&qemuAddr, 0, sizeof(qemuAddr)); + qemuAddr.sin_family = AF_INET; + int cret = connect(sock, (struct sockaddr *) &qemuAddr, sizeof(qemuAddr)); + BOOL connected = FALSE; + if ((cret == 0) || (errno == EISCONN)) { + /* printf("socket connected\n"); */ + s->fd = sock; + s->tmpfd = -1; + reset_qemu_socket_state(s); + return 0; + } else if (errno != EINPROGRESS) { + printf("failed to connect: %s\n", strerror(errno)); + s->fd = -1; + s->tmpfd = -1; + return -1; + } + s->retrynum = 0; + s->tmpfd = sock; + } + + /* printf("waiting for connection...\n"); */ + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&wfds); + FD_SET(s->tmpfd, &wfds); + int sret = select(s->tmpfd + 1, NULL, &wfds, NULL, &tv); + if (sret > 0) { + s->fd = s->tmpfd; + s->tmpfd = -1; + reset_qemu_socket_state(s); + return 0; + } else if (sret < 0) { + s->fd = -1; + s->tmpfd = -1; + } // else, still wait for the connection. + /* printf("select result: %d\n", sret); */ + + s->retrynum++; + if (s->retrynum > 5) { // TODO: make max retry number configurable + close(s->tmpfd); + s->tmpfd = -1; // too many errors. retry connection. + } + + return -1; +} +#elif defined(WASI) +static int try_get_fd(QemuSocketState *s) +{ + int sock = 3; + if ((s->raw_flag != NULL) && (!strncmp(s->raw_flag, "qemu=", 5))) { + // TODO: allow specifying more options + if (!strncmp(s->raw_flag + 5, "listenfd=", 9)) { + sock = atoi(s->raw_flag + 5 + 9); + } + } + int sock_a = 0; + // wait for connection + /* printf("accept trying...(sockfd=%d)\n", sock); */ + sock_a = accept4(sock, NULL, NULL, SOCK_NONBLOCK); + if (sock_a > 0) { + /* printf("accepted fd=%d\n", sock_a); */ + s->fd = sock_a; + reset_qemu_socket_state(s); + return 0; + } + /* printf("failed to accept socket: %s\n", strerror(errno)); */ + return -1; +} +#endif + #define MAX_EXEC_CYCLE 500000 #define MAX_SLEEP_TIME 10 /* in ms */ @@ -617,12 +895,16 @@ void virt_machine_run(VirtMachine *m, int enable_stdin) } } #endif + fd_set n_rfds, n_wfds, n_efds; + FD_ZERO(&n_rfds); + FD_ZERO(&n_wfds); + FD_ZERO(&n_efds); + int n_fd_max = -1; if (m->net) { - // TODO: do this when enable net - // m->net->select_fill(m->net, &fd_max, &rfds, &wfds, &efds, &delay); + m->net->select_fill(m->net, &n_fd_max, &n_rfds, &n_wfds, &n_efds, &delay); } #ifdef CONFIG_FS_NET - fs_net_set_fdset(&fd_max, &rfds, &wfds, &efds, &delay); + fs_net_set_fdset(&n_fd_max, &n_rfds, &n_wfds, &n_efds, &delay); #endif tv.tv_sec = delay / 1000; tv.tv_usec = (delay % 1000) * 1000; @@ -634,8 +916,12 @@ void virt_machine_run(VirtMachine *m, int enable_stdin) if (enable_stdin && init_done) ret = select(fd_max + 1, &rfds, &wfds, NULL, &tv); if (m->net) { - // TODO: do this when enable net - // m->net->select_poll(m->net, &rfds, &wfds, &efds, ret); + if (n_fd_max >= 0) { + // TODO: select should be unified (stdin + net), but it doesn't work as of now. + int n_ret = select(n_fd_max + 1, &n_rfds, &n_wfds, NULL, &tv); + m->net->select_poll(m->net, &n_rfds, &n_wfds, &n_efds, n_ret); + } + m->net->watch(m->net); } if ((ret > 0) || (init_start && !init_done)) { #ifndef _WIN32 @@ -646,7 +932,7 @@ void virt_machine_run(VirtMachine *m, int enable_stdin) len = min_int(len, sizeof(buf)); ret = m->console->read_data(m->console->opaque, buf, len); if (ret > 0) { - virtio_console_write_data(m->console_dev, buf, ret); + virtio_console_write_data(m->console_dev, buf, ret); } } #endif @@ -668,6 +954,8 @@ static struct option options[] = { { "help", no_argument, NULL, 'h' }, { "no-stdin", no_argument }, { "entrypoint", required_argument }, + { "net", required_argument }, + { "mac", required_argument }, // The following flags are unsupported as of now: // { "ctrlc", no_argument }, // { "rw", no_argument }, @@ -686,6 +974,8 @@ void help(void) "OPTIONS:\n" " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" " -no-stdin : disable stdin. (default: false)\n" + " -net : enable networking with the specified mode (default: disabled. supported mode: \"qemu\")\n" + " -mac : use a custom mac address for the VM\n" "\n" "This tool is based on:\n" "temu version 2019-12-21, Copyright (c) 2016-2018 Fabrice Bellard\n" @@ -713,7 +1003,7 @@ int initialized = FALSE; VirtMachine *s; VirtMachineParams p_s, *p = &p_s; -void init_func() +void init_func_args(char *net) { const char *path, *cmdline = NULL; int i = 0; @@ -794,6 +1084,7 @@ void init_func() } p->fs_count++; + // networking for(i = 0; i < p->eth_count; i++) { #ifdef CONFIG_SLIRP if (!strcmp(p->tab_eth[i].driver, "user")) { @@ -816,6 +1107,15 @@ void init_func() exit(1); } } + + if ((p->eth_count == 0) && (net != NULL)) { + if (!strncmp(net, "qemu", 4)) { + p->tab_eth[0].net = qemu_net_init(); + p->eth_count++; + if (!p->tab_eth[0].net) + exit(1); + } + } #ifdef CONFIG_SDL if (p->display_device) { @@ -853,6 +1153,11 @@ void init_func() initialized = TRUE; } +void init_func() +{ + init_func_args("qemu"); +} + #ifdef WASI WIZER_INIT(init_func); #endif @@ -948,6 +1253,46 @@ int write_env(FSVirtFile *f, int pos1, const char *env) return pos - pos1; } +int write_net(FSVirtFile *f, int pos1, const char *mac) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "n: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(mac); j++) { + if (putchar_info(f, pos++, mac[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + +int write_time(FSVirtFile *f, int pos1, const char *timestr) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "t: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(timestr); j++) { + if (putchar_info(f, pos++, timestr[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + int main(int argc, char **argv) { #ifdef WASI @@ -957,14 +1302,8 @@ int main(int argc, char **argv) } #endif - if (!initialized) { - init_func(); - } else { - virt_machine_resume(s); - } - /* const char *cmdline, *build_preload_file; */ - char *entrypoint = NULL; + char *entrypoint = NULL, *net = NULL, *mac = NULL; int pos, c, option_index, i, enable_stdin = TRUE; for(;;) { c = getopt_long_only(argc, argv, "+h", options, &option_index); @@ -982,6 +1321,12 @@ int main(int argc, char **argv) case 2: /* entrypoint */ entrypoint = optarg; break; + case 3: /* net */ + net = optarg; + break; + case 4: /* mac */ + mac = optarg; + break; default: fprintf(stderr, "unknown option index: %d\n", option_index); exit(1); @@ -995,6 +1340,12 @@ int main(int argc, char **argv) } } + if (!initialized) { + init_func_args(net); + } else { + virt_machine_resume(s); + } + pos = info->len; if (entrypoint) { int p = write_entrypoint(info, pos, entrypoint); @@ -1027,6 +1378,34 @@ int main(int argc, char **argv) } #endif + if (net) { + if (!strncmp(net, "qemu", 4)) { + EthernetDevice *netd; + for(i = 0; i < p->eth_count; i++) { + netd = p->tab_eth[i].net; + if (netd != NULL) { + ((QemuSocketState *)netd->opaque)->enabled = TRUE; + ((QemuSocketState *)netd->opaque)->raw_flag = net; + } + } + } + int p = write_net(info, pos, mac); + if (p < 0) { + printf("failed to prepare net info\n"); + exit(1); + } + pos += p; + } + + char buf[32]; + snprintf(buf, sizeof(buf), "%d", (unsigned)time(NULL)); + int ps = write_time(info, pos, buf); + if (ps < 0) { + printf("failed to prepare time info\n"); + exit(1); + } + pos += ps; + info->len = pos; #ifdef WASI info->len += write_preopen_info(info, pos); diff --git a/patches/tinyemu/tinyemu/virtio.h b/patches/tinyemu/tinyemu/virtio.h index d53c8c4..0664b95 100644 --- a/patches/tinyemu/tinyemu/virtio.h +++ b/patches/tinyemu/tinyemu/virtio.h @@ -95,6 +95,7 @@ struct EthernetDevice { fd_set *rfds, fd_set *wfds, fd_set *efds, int select_ret); #endif + int (*watch)(EthernetDevice *net); /* the following is set by the device */ void *device_opaque; BOOL (*device_can_write_packet)(EthernetDevice *net); diff --git a/patches/tinyemu/tinyemu/wasi.c b/patches/tinyemu/tinyemu/wasi.c index b601021..b5e599c 100644 --- a/patches/tinyemu/tinyemu/wasi.c +++ b/patches/tinyemu/tinyemu/wasi.c @@ -159,6 +159,26 @@ int write_preopen_info(FSVirtFile *f, int pos1) extern void __wasi_vfs_rt_init(void); +// allow directly controlling fd for sockets, etc. + +int32_t poll_oneoff(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("poll_oneoff") +)); + +int32_t __imported_wasi_snapshot_preview1_poll_oneoff(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) { + return poll_oneoff(arg0, arg1, arg2, arg3); +} + +int32_t fd_close(int32_t arg0) __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("fd_close") +)); + +int32_t __imported_wasi_snapshot_preview1_fd_close(int32_t arg0) { + return fd_close(arg0); +} + int init_wasi() { __wasilibc_ensure_environ(); diff --git a/tests/c2w-net-proxy-test/go.mod b/tests/c2w-net-proxy-test/go.mod new file mode 100644 index 0000000..ec744ed --- /dev/null +++ b/tests/c2w-net-proxy-test/go.mod @@ -0,0 +1,5 @@ +module c2w-net-proxy-test + +go 1.19 + +require github.com/tetratelabs/wazero v1.5.0 diff --git a/tests/c2w-net-proxy-test/go.sum b/tests/c2w-net-proxy-test/go.sum new file mode 100644 index 0000000..68da83b --- /dev/null +++ b/tests/c2w-net-proxy-test/go.sum @@ -0,0 +1,2 @@ +github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= +github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= diff --git a/tests/c2w-net-proxy-test/main.go b/tests/c2w-net-proxy-test/main.go new file mode 100644 index 0000000..259ea6b --- /dev/null +++ b/tests/c2w-net-proxy-test/main.go @@ -0,0 +1,398 @@ +package main + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental/sock" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" +) + +func main() { + var ( + stack = flag.String("stack", "", "path to the stack wasm path") + stackPort = flag.Int("stack-port", 8999, "listen port of network stack") + vmPort = flag.Int("vm-port", 1234, "listen port of vm") + debug = flag.Bool("debug", false, "enable debug log") + ) + var envs envFlags + flag.Var(&envs, "env", "environment variables") + flag.Parse() + if *debug { + log.SetOutput(os.Stdout) + } else { + log.SetOutput(io.Discard) + } + args := flag.Args() + + if stack == nil || *stack == "" { + panic("specify network stack wasm") + } + + crtDir, err := os.MkdirTemp("", "test") + if err != nil { + panic(err) + } + defer func() { + os.Remove(filepath.Join(crtDir, "ca.crt")) + os.Remove(crtDir) + }() + + go func() { + ctx := context.Background() + sockCfg := sock.NewConfig().WithTCPListener("127.0.0.1", *stackPort) + ctx = sock.WithConfig(ctx, sockCfg) + c, err := os.ReadFile(*stack) + if err != nil { + panic(err) + } + r := wazero.NewRuntime(ctx) + defer func() { + r.Close(ctx) + }() + wasi_snapshot_preview1.MustInstantiate(ctx, r) + b := r.NewHostModuleBuilder("env"). + NewFunctionBuilder().WithFunc(http_send).Export("http_send"). + NewFunctionBuilder().WithFunc(http_writebody).Export("http_writebody"). + NewFunctionBuilder().WithFunc(http_isreadable).Export("http_isreadable"). + NewFunctionBuilder().WithFunc(http_recv).Export("http_recv"). + NewFunctionBuilder().WithFunc(http_readbody).Export("http_readbody") + if _, err := b.Instantiate(ctx); err != nil { + panic(err) + } + compiled, err := r.CompileModule(ctx, c) + if err != nil { + panic(err) + } + stackFSConfig := wazero.NewFSConfig() + stackFSConfig = stackFSConfig.WithDirMount(crtDir, "/test") + flagargs := []string{"--certfile=/test/proxy.crt"} + if *debug { + flagargs = append(flagargs, "--debug") + } + conf := wazero.NewModuleConfig().WithSysWalltime().WithSysNanotime().WithSysNanosleep().WithRandSource(crand.Reader).WithStdout(os.Stdout).WithStderr(os.Stderr).WithFSConfig(stackFSConfig).WithArgs(append([]string{"arg0"}, flagargs...)...) + _, err = r.InstantiateModule(ctx, compiled, conf) + if err != nil { + panic(err) + } + }() + go func() { + var conn1 net.Conn + var conn2 net.Conn + var err error + for { + conn1, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", *stackPort)) + if err == nil { + break + } + log.Println("conn1 retry...") + time.Sleep(10 * time.Millisecond) + } + for { + conn2, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", *vmPort)) + if err == nil { + break + } + log.Println("conn2 retry...") + time.Sleep(10 * time.Millisecond) + } + done1 := make(chan struct{}) + done2 := make(chan struct{}) + go func() { + if _, err := io.Copy(conn1, conn2); err != nil { + panic(err) + } + close(done1) + }() + go func() { + if _, err := io.Copy(conn2, conn1); err != nil { + panic(err) + } + close(done2) + }() + <-done1 + <-done2 + }() + + fsConfig := wazero.NewFSConfig() + crtFile := filepath.Join(crtDir, "proxy.crt") + for { + if _, err := os.Stat(crtFile); err == nil { + break + } + log.Println("waiting for cert", crtFile) + time.Sleep(time.Second) + } + + fsConfig = fsConfig.WithDirMount(crtDir, "/.wasmenv") + ctx := context.Background() + sockCfg := sock.NewConfig().WithTCPListener("127.0.0.1", *vmPort) + ctx = sock.WithConfig(ctx, sockCfg) + c, err := os.ReadFile(args[0]) + if err != nil { + panic(err) + } + r := wazero.NewRuntime(ctx) + defer func() { + r.Close(ctx) + }() + wasi_snapshot_preview1.MustInstantiate(ctx, r) + compiled, err := r.CompileModule(ctx, c) + if err != nil { + panic(err) + } + conf := wazero.NewModuleConfig().WithSysWalltime().WithSysNanotime().WithSysNanosleep().WithRandSource(crand.Reader).WithStdout(os.Stdout).WithStderr(os.Stderr).WithStdin(os.Stdin).WithFSConfig(fsConfig).WithArgs(append([]string{"arg0"}, args[1:]...)...) + envs = append(envs, []string{ + "SSL_CERT_FILE=/.wasmenv/proxy.crt", + "https_proxy=http://192.168.127.253:80", + "http_proxy=http://192.168.127.253:80", + "HTTPS_PROXY=http://192.168.127.253:80", + "HTTP_PROXY=http://192.168.127.253:80", + }...) + for _, v := range envs { + es := strings.SplitN(v, "=", 2) + if len(es) == 2 { + conf = conf.WithEnv(es[0], es[1]) + } else { + panic("env must be a key value pair") + } + } + _, err = r.InstantiateModule(ctx, compiled, conf) + if err != nil { + panic(err) + } +} + +var respMap = make(map[uint32]*http.Response) +var respMapMu sync.Mutex + +var respRMap = make(map[uint32]io.Reader) +var respRMapMu sync.Mutex + +type fetchParameters struct { + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +var reqMap = make(map[uint32]*io.PipeWriter) +var reqMapMu sync.Mutex +var reqMapId uint32 + +const ERRNO_INVAL = 28 + +func http_send(ctx context.Context, m api.Module, addressP uint32, addresslen uint32, reqP, reqlen uint32, idP uint32) uint32 { + mem := m.Memory() + + addressB, ok := mem.Read(addressP, uint32(addresslen)) + if !ok { + log.Println("failed to get address") + return ERRNO_INVAL + } + address := string(addressB) + + reqB, ok := mem.Read(reqP, uint32(reqlen)) + if !ok { + log.Println("failed to get req") + return ERRNO_INVAL + } + var fetchReq fetchParameters + if err := json.Unmarshal(reqB, &fetchReq); err != nil { + log.Println("failed to marshal req:", err) + return ERRNO_INVAL + } + + pr, pw := io.Pipe() + req, err := http.NewRequest(fetchReq.Method, address, pr) + if err != nil { + log.Println("failed to create req:", err) + return ERRNO_INVAL + } + if req.Header == nil { + req.Header = make(map[string][]string) + } + for k, v := range fetchReq.Headers { + req.Header[k] = append(req.Header[k], v) // TODO: separate fields + } + + reqMapMu.Lock() + id := reqMapId + reqMapId++ + reqMapMu.Unlock() + reqMap[id] = pw + go func() { + c := &http.Client{} + resp, err := c.Do(req) + if err != nil { + log.Println("failed to do request:", err) + return + } + respMapMu.Lock() + respMap[id] = resp + respMapMu.Unlock() + }() + if !mem.WriteUint32Le(idP, id) { + log.Println("failed to pass id") + return ERRNO_INVAL + } + return 0 +} + +func http_writebody(ctx context.Context, m api.Module, id uint32, chunkP, len uint32, nwrittenP uint32, isEOF uint32) uint32 { + mem := m.Memory() + + chunkB, ok := mem.Read(chunkP, uint32(len)) + if !ok { + log.Println("failed to get chunk") + return ERRNO_INVAL + } + reqMapMu.Lock() + w := reqMap[id] + reqMapMu.Unlock() + if _, err := w.Write(chunkB); err != nil { + w.CloseWithError(err) + log.Println("failed to write req:", err) + return ERRNO_INVAL + } + if isEOF == 1 { + w.Close() + } + if !mem.WriteUint32Le(nwrittenP, len) { + log.Println("failed to pass written number") + return ERRNO_INVAL + } + return 0 +} + +func http_isreadable(ctx context.Context, m api.Module, id uint32, isOKP uint32) uint32 { + mem := m.Memory() + respMapMu.Lock() + _, ok := respMap[id] + respMapMu.Unlock() + v := uint32(0) + if ok { + v = 1 + } + if !mem.WriteUint32Le(isOKP, v) { + log.Println("failed to pass status") + return ERRNO_INVAL + } + return 0 +} + +func http_recv(ctx context.Context, m api.Module, id uint32, respP uint32, bufsize uint32, respsizeP uint32, isEOFP uint32) uint32 { + mem := m.Memory() + respRMapMu.Lock() + respR, ok := respRMap[id] + respRMapMu.Unlock() + if !ok { + respMapMu.Lock() + resp, ok := respMap[id] + respMapMu.Unlock() + if !ok { + log.Println("failed to get resp") + return ERRNO_INVAL + } + var fetchResp struct { + Headers map[string]string `json:"headers,omitempty"` + Status int `json:"status,omitempty"` + StatusText string `json:"statusText,omitempty"` + } + fetchResp.Headers = make(map[string]string) + for k, v := range resp.Header { + fetchResp.Headers[k] = strings.Join(v, ", ") + } + fetchResp.Status = resp.StatusCode + fetchResp.StatusText = resp.Status + respD, err := json.Marshal(fetchResp) + if err != nil { + log.Println("failed to marshal resp:", err) + return ERRNO_INVAL + } + respR = bytes.NewReader(respD) + respRMapMu.Lock() + respRMap[id] = respR + respRMapMu.Unlock() + } + + isEOF := uint32(0) + buf := make([]byte, bufsize) + n, err := respR.Read(buf) + if err != nil && err != io.EOF { + log.Println("failed to read resp:", err) + return ERRNO_INVAL + } else if err == io.EOF { + isEOF = 1 + } + if !mem.Write(respP, buf[:n]) { + log.Println("failed to write resp") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(respsizeP, uint32(n)) { + log.Println("failed to write resp size") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(isEOFP, isEOF) { + log.Println("failed to write EOF status") + return ERRNO_INVAL + } + return 0 +} + +func http_readbody(ctx context.Context, m api.Module, id uint32, bodyP uint32, bufsize uint32, bodysizeP uint32, isEOFP uint32) uint32 { + mem := m.Memory() + respMapMu.Lock() + resp, ok := respMap[id] + respMapMu.Unlock() + if !ok { + log.Println("failed to get resp") + return ERRNO_INVAL + } + + isEOF := uint32(0) + buf := make([]byte, bufsize) + n, err := resp.Body.Read(buf) + if err != nil && err != io.EOF { + log.Println("failed to read resp body:", err) + return ERRNO_INVAL + } else if err == io.EOF { + isEOF = 1 + } + if !mem.Write(bodyP, buf[:n]) { + log.Println("failed to write resp body") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(bodysizeP, uint32(n)) { + log.Println("failed to write resp body size") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(isEOFP, isEOF) { + log.Println("failed to write resp body EOF status") + return ERRNO_INVAL + } + return 0 +} + +type envFlags []string + +func (i *envFlags) String() string { + return fmt.Sprintf("%v", []string(*i)) +} +func (i *envFlags) Set(value string) error { + *i = append(*i, value) + return nil +} diff --git a/tests/httphello/main.go b/tests/httphello/main.go new file mode 100644 index 0000000..b641c5d --- /dev/null +++ b/tests/httphello/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "net/http" + "os" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "hello") + }) + if err := http.ListenAndServe(os.Args[1], nil); err != nil { + fmt.Println(err) + } +} diff --git a/tests/integration/utils/utils.go b/tests/integration/utils/utils.go index 1fe9031..28446e8 100644 --- a/tests/integration/utils/utils.go +++ b/tests/integration/utils/utils.go @@ -4,18 +4,24 @@ import ( "context" "fmt" "io" + "net/http" "os" "os/exec" "path/filepath" + "strconv" "strings" + "sync" "testing" + "time" "gotest.tools/v3/assert" ) // Defined in Dockerfile.test. // TODO: Make it a flag -const assetPath = "/test/" +const AssetPath = "/test/" +const C2wBin = "c2w" +const C2wNetProxyBin = "/opt/c2w-net-proxy.wasm" type Architecture int @@ -43,23 +49,24 @@ type Input struct { Image string ConvertOpts []string Architecture Architecture + Dockerfile string } type TestSpec struct { - Name string - Inputs []Input - Prepare func(t *testing.T, workdir string) - Finalize func(t *testing.T, workdir string) - ImageName string // default: test.wasm - Runtime string - RuntimeOpts func(t *testing.T, workdir string) []string - Args func(t *testing.T, workdir string) []string - Want func(t *testing.T, workdir string, in io.Writer, out io.Reader) - NoParallel bool + Name string + Inputs []Input + Prepare func(t *testing.T, workdir string) + Finalize func(t *testing.T, workdir string) + ImageName string // default: test.wasm + Runtime string + RuntimeOpts func(t *testing.T, workdir string) []string + Args func(t *testing.T, workdir string) []string + Want func(t *testing.T, workdir string, in io.Writer, out io.Reader) + NoParallel bool + IgnoreExitCode bool } func RunTestRuntimes(t *testing.T, tests ...TestSpec) { - c2wBin := "c2w" for _, tt := range tests { tt := tt for _, in := range tt.Inputs { @@ -72,10 +79,21 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { tmpdir, err := os.MkdirTemp("", "testc2w") assert.NilError(t, err) t.Logf("test root: %v", tmpdir) - defer assert.NilError(t, os.RemoveAll(tmpdir)) + defer func() { + assert.NilError(t, os.RemoveAll(tmpdir)) + }() + + if in.Dockerfile != "" { + df := filepath.Join(tmpdir, "Dockerfile-integrationtest") + assert.NilError(t, os.WriteFile(df, []byte(in.Dockerfile), 0755)) + dcmd := exec.Command("docker", "build", "--progress=plain", "-t", in.Image, "-f", df, AssetPath) + dcmd.Stdout = os.Stdout + dcmd.Stderr = os.Stderr + assert.NilError(t, dcmd.Run()) + } testWasm := filepath.Join(tmpdir, "test.wasm") - c2wCmd := exec.Command(c2wBin, append(in.ConvertOpts, "--assets="+assetPath, in.Image, testWasm)...) + c2wCmd := exec.Command(C2wBin, append(in.ConvertOpts, "--assets="+AssetPath, in.Image, testWasm)...) c2wCmd.Stdout = os.Stdout c2wCmd.Stderr = os.Stderr assert.NilError(t, c2wCmd.Run()) @@ -106,13 +124,20 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { inW, err := testCmd.StdinPipe() assert.NilError(t, err) defer inW.Close() + testCmd.Stderr = os.Stderr assert.NilError(t, testCmd.Start()) tt.Want(t, tmpdir, inW, io.TeeReader(outR, os.Stdout)) inW.Close() - assert.NilError(t, testCmd.Wait()) + if !tt.IgnoreExitCode { + assert.NilError(t, testCmd.Wait()) + } else { + if err := testCmd.Wait(); err != nil { + t.Logf("command test error: %v", err) + } + } // cleanup cache assert.NilError(t, exec.Command("docker", "buildx", "prune", "-f", "--keep-storage=10GB").Run()) @@ -186,3 +211,61 @@ func readUntilPrompt(ctx context.Context, prompt string, outR io.Reader) (out [] func StringFlags(opts ...string) func(t *testing.T, workdir string) []string { return func(t *testing.T, workdir string) []string { return opts } } + +var usedPorts = make(map[int]struct{}) +var usedPortsMu sync.Mutex + +func GetPort(t *testing.T) int { + usedPortsMu.Lock() + defer usedPortsMu.Unlock() + for i := 8001; i < 9000; i++ { + if _, ok := usedPorts[i]; !ok { + usedPorts[i] = struct{}{} + return i + } + } + t.Fatalf("ports exhausted") + return -1 +} + +func DonePort(i int) { + usedPortsMu.Lock() + defer usedPortsMu.Unlock() + delete(usedPorts, i) +} + +func ReadInt(t *testing.T, p string) int { + d, err := os.ReadFile(p) + assert.NilError(t, err) + i, err := strconv.Atoi(string(d)) + assert.NilError(t, err) + return i +} + +func ReadString(t *testing.T, p string) string { + d, err := os.ReadFile(p) + assert.NilError(t, err) + return string(d) +} + +func StartHelloServer(t *testing.T) (pid int, port int) { + port = GetPort(t) + t.Logf("launching server on %d", port) + cmd := exec.Command("httphello", fmt.Sprintf("localhost:%d", port)) + assert.NilError(t, cmd.Start()) + go func() { + if err := cmd.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + DonePort(port) + }() + for { + if cmd.Process != nil { + if _, err := http.Get(fmt.Sprintf("http://localhost:%d/", port)); err == nil { + break + } + } + time.Sleep(1 * time.Millisecond) + } + return cmd.Process.Pid, port +} diff --git a/tests/integration/wasmtime_test.go b/tests/integration/wasmtime_test.go index d1251d6..c0498da 100644 --- a/tests/integration/wasmtime_test.go +++ b/tests/integration/wasmtime_test.go @@ -1,7 +1,12 @@ package integration import ( + "crypto/rand" + "fmt" + "io" + "math/big" "os" + "os/exec" "path/filepath" "testing" @@ -10,6 +15,8 @@ import ( "github.com/ktock/container2wasm/tests/integration/utils" ) +const hostVirtIP = "192.168.127.254" + func TestWasmtime(t *testing.T) { utils.RunTestRuntimes(t, []utils.TestSpec{ { @@ -115,6 +122,139 @@ func TestWasmtime(t *testing.T) { Args: utils.StringFlags("/bin/sh", "-c", "echo -n $AAA $BBB"), Want: utils.WantString("hello world"), }, + { + Name: "wasmtime-net", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Architecture: utils.X86_64}, + {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartHelloServer(t) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "c2w-net", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + }, + Args: func(t *testing.T, workdir string) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) + return []string{"--net=qemu", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + }, + Want: utils.WantString("hello"), + }, + { + Name: "wasmtime-net-port", + Inputs: []utils.Input{ + {Image: "httphello-alpine-x86-64", Architecture: utils.X86_64, Dockerfile: ` +FROM golang:1.21-bullseye AS dev +COPY ./tests/httphello /httphello +WORKDIR /httphello +RUN GOARCH=amd64 go build -ldflags "-s -w -extldflags '-static'" -tags "osusergo netgo static_build" -o /out/httphello main.go + +FROM alpine:3.17 +COPY --from=dev /out/httphello / +ENTRYPOINT ["/httphello", "0.0.0.0:80"] +`}, + {Image: "httphello-alpine-rv64", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64, Dockerfile: ` +FROM golang:1.21-bullseye AS dev +COPY ./tests/httphello /httphello +WORKDIR /httphello +RUN GOARCH=riscv64 go build -ldflags "-s -w -extldflags '-static'" -tags "osusergo netgo static_build" -o /out/httphello main.go + +FROM riscv64/alpine:20221110 +COPY --from=dev /out/httphello / +ENTRYPOINT ["/httphello", "0.0.0.0:80"] +`}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "c2w-net", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + wasiPort := utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")) + port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + t.Logf("port is %s", fmt.Sprintf("localhost:%d:80", port)) + return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", wasiPort), "-p", fmt.Sprintf("localhost:%d:80", port)} + }, + Args: func(t *testing.T, workdir string) []string { + return []string{"--net=qemu"} + }, + Want: func(t *testing.T, workdir string, in io.Writer, out io.Reader) { + port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + cmd := exec.Command("wget", "-q", "-O", "-", fmt.Sprintf("localhost:%d", port)) + cmd.Stderr = os.Stderr + d, err := cmd.Output() + assert.NilError(t, err) + assert.Equal(t, string(d), "hello") + t.Logf("GOT %s", string(d)) + }, + IgnoreExitCode: true, + }, + { + Name: "wasmtime-net-mac", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Architecture: utils.X86_64}, + {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartHelloServer(t) + var v [5]int64 + for i := 0; i < 5; i++ { + n, err := rand.Int(rand.Reader, big.NewInt(256)) + assert.NilError(t, err) + v[i] = n.Int64() + } + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "c2w-net", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + }, + Args: func(t *testing.T, workdir string) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) + t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(workdir, "mac"))) + return []string{"--net=qemu", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(workdir, "mac"))), "sh"} + }, + Want: utils.WantPromptWithWorkdir("/ # ", + func(workdir string) [][2]string { + return [][2]string{ + [2]string{fmt.Sprintf("wget -q -O - http://%s:%d/\n", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))), "hello"}, + [2]string{`/bin/sh -c 'ip a show eth0 | grep ether | sed -E "s/ +/ /g" | cut -f 3 -d " " | tr -d "\n"'` + "\n", utils.ReadString(t, filepath.Join(workdir, "mac"))}, + } + }, + ), + }, // Other architectures { Name: "wasmtime-hello-arch-aarch64", diff --git a/tests/integration/wazero_test.go b/tests/integration/wazero_test.go index 3e7693a..aaa2c08 100644 --- a/tests/integration/wazero_test.go +++ b/tests/integration/wazero_test.go @@ -1,7 +1,12 @@ package integration import ( + "crypto/rand" + "fmt" + "io" + "math/big" "os" + "os/exec" "path/filepath" "testing" @@ -117,5 +122,180 @@ func TestWazero(t *testing.T) { Args: utils.StringFlags("/bin/sh", "-c", "echo -n $AAA $BBB"), Want: utils.WantString("hello world"), }, + { + Name: "wazero-net-proxy", + Runtime: "c2w-net-proxy-test", + Inputs: []utils.Input{ + {Image: "debian-wget-x86-64", Architecture: utils.X86_64, Dockerfile: ` +FROM debian:sid-slim +RUN apt-get update && apt-get install -y wget +`}, + {Image: "debian-wget-rv64", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64, Dockerfile: ` +FROM riscv64/debian:sid-slim +RUN apt-get update && apt-get install -y wget +`}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-vm-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-stack-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartHelloServer(t) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-vm-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-stack-port"))) + }, + RuntimeOpts: func(t *testing.T, workdir string) []string { + return []string{ + "--stack=" + utils.C2wNetProxyBin, + fmt.Sprintf("--stack-port=%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-stack-port"))), + fmt.Sprintf("--vm-port=%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-vm-port"))), + } + }, + Args: func(t *testing.T, workdir string) []string { + return []string{"--net=qemu=listenfd=4", "sh", "-c", fmt.Sprintf("for I in $(seq 1 50) ; do if wget -O - http://127.0.0.1:%d/ 2>/dev/null ; then break ; fi ; sleep 1 ; done", utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + }, + Want: utils.WantString("hello"), + }, + { + Name: "wazero-net", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Architecture: utils.X86_64}, + {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartHelloServer(t) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "wazero-test", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + }, + Args: func(t *testing.T, workdir string) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) + return []string{"--net=qemu", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + }, + Want: utils.WantString("hello"), + }, + { + Name: "wazero-net-port", + Inputs: []utils.Input{ + {Image: "httphello-alpine-x86-64", Architecture: utils.X86_64, Dockerfile: ` +FROM golang:1.21-bullseye AS dev +COPY ./tests/httphello /httphello +WORKDIR /httphello +RUN GOARCH=amd64 go build -ldflags "-s -w -extldflags '-static'" -tags "osusergo netgo static_build" -o /out/httphello main.go + +FROM alpine:3.17 +COPY --from=dev /out/httphello / +ENTRYPOINT ["/httphello", "0.0.0.0:80"] +`}, + {Image: "httphello-alpine-rv64", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64, Dockerfile: ` +FROM golang:1.21-bullseye AS dev +COPY ./tests/httphello /httphello +WORKDIR /httphello +RUN GOARCH=riscv64 go build -ldflags "-s -w -extldflags '-static'" -tags "osusergo netgo static_build" -o /out/httphello main.go + +FROM riscv64/alpine:20221110 +COPY --from=dev /out/httphello / +ENTRYPOINT ["/httphello", "0.0.0.0:80"] +`}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "wazero-test", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + wasiPort := utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")) + port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + t.Logf("port is %s", fmt.Sprintf("localhost:%d:80", port)) + return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", wasiPort), "-p", fmt.Sprintf("localhost:%d:80", port)} + }, + Args: func(t *testing.T, workdir string) []string { + return []string{"--net=qemu"} + }, + Want: func(t *testing.T, workdir string, in io.Writer, out io.Reader) { + port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + cmd := exec.Command("wget", "-q", "-O", "-", fmt.Sprintf("localhost:%d", port)) + cmd.Stderr = os.Stderr + d, err := cmd.Output() + assert.NilError(t, err) + assert.Equal(t, string(d), "hello") + t.Logf("GOT %s", string(d)) + }, + IgnoreExitCode: true, + }, + { + Name: "wazero-net-mac", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Architecture: utils.X86_64}, + {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, + }, + Prepare: func(t *testing.T, workdir string) { + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartHelloServer(t) + var v [5]int64 + for i := 0; i < 5; i++ { + n, err := rand.Int(rand.Reader, big.NewInt(256)) + assert.NilError(t, err) + v[i] = n.Int64() + } + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, workdir string) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("hello server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + }, + Runtime: "wazero-test", + RuntimeOpts: func(t *testing.T, workdir string) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) + return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + }, + Args: func(t *testing.T, workdir string) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) + t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(workdir, "mac"))) + return []string{"--net=qemu", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(workdir, "mac"))), "sh"} + }, + Want: utils.WantPromptWithWorkdir("/ # ", + func(workdir string) [][2]string { + return [][2]string{ + [2]string{fmt.Sprintf("wget -q -O - http://%s:%d/\n", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))), "hello"}, + [2]string{`/bin/sh -c 'ip a show eth0 | grep ether | sed -E "s/ +/ /g" | cut -f 3 -d " " | tr -d "\n"'` + "\n", utils.ReadString(t, filepath.Join(workdir, "mac"))}, + } + }, + ), + }, }...) } diff --git a/tests/wazero/go.mod b/tests/wazero/go.mod index 28e7095..85ec487 100644 --- a/tests/wazero/go.mod +++ b/tests/wazero/go.mod @@ -2,4 +2,27 @@ module github.com/ktock/container2wasm/examples/wazero go 1.19 -require github.com/tetratelabs/wazero v1.5.0 +require ( + github.com/containers/gvisor-tap-vsock v0.7.0 + github.com/tetratelabs/wazero v1.5.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect + github.com/miekg/dns v1.1.55 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.9.3 // indirect + gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db // indirect + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect +) diff --git a/tests/wazero/go.sum b/tests/wazero/go.sum index 68da83b..3a02cf6 100644 --- a/tests/wazero/go.sum +++ b/tests/wazero/go.sum @@ -1,2 +1,121 @@ +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/containers/gvisor-tap-vsock v0.7.0 h1:lL5UpfXhl+tuTwK43CXhKWwfvAiZtde6G5alFeoO+Z8= +github.com/containers/gvisor-tap-vsock v0.7.0/go.mod h1:edQTwl8ar+ACuQOkazpQkgd/ZMF6TJ2Xr3fv+MKUaw8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f h1:l1QCwn715k8nYkj4Ql50rzEog3WnMdrd4YYMMwemxEo= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db h1:WZSmkyu/hep9YhWIlBZefwGVBrnGE5yW8JPD56YRsXs= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 h1:PqdHrvQRVK1zapJkd0qf6+tevvSIcWdfenVqJd3PHWU= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= diff --git a/tests/wazero/main.go b/tests/wazero/main.go index 360cf54..1f78da2 100644 --- a/tests/wazero/main.go +++ b/tests/wazero/main.go @@ -5,18 +5,38 @@ import ( crand "crypto/rand" "flag" "fmt" + "net" + "net/url" "os" + "strconv" "strings" + "time" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + + gvntypes "github.com/containers/gvisor-tap-vsock/pkg/types" + gvnvirtualnetwork "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "github.com/tetratelabs/wazero/experimental/sock" +) + +const ( + gatewayIP = "192.168.127.1" + vmIP = "192.168.127.3" + vmMAC = "02:00:00:00:00:01" ) func main() { var ( - mapDir = flag.String("mapdir", "", "directory mapping to the image") + mapDir = flag.String("mapdir", "", "directory mapping to the image") + debug = flag.Bool("debug", false, "debug log") + mac = flag.String("mac", vmMAC, "mac address assigned to the container") + wasiAddr = flag.String("wasi-addr", "127.0.0.1:1234", "IP address used to communicate between wasi and network stack") // TODO: automatically use empty random port or unix socket + enableNet = flag.Bool("net", false, "enable network") ) - var envs envFlags + var portFlags sliceFlags + flag.Var(&portFlags, "p", "map port between host and guest (host:guest). -mac must be set correctly.") + var envs sliceFlags flag.Var(&envs, "env", "environment variables") flag.Parse() @@ -31,6 +51,69 @@ func main() { } ctx := context.Background() + if *enableNet { + forwards := make(map[string]string) + for _, p := range portFlags { + parts := strings.Split(p, ":") + switch len(parts) { + case 3: + // IP:PORT1:PORT2 + forwards[strings.Join(parts[0:2], ":")] = strings.Join([]string{vmIP, parts[2]}, ":") + case 2: + // PORT1:PORT2 + forwards["0.0.0.0:"+parts[0]] = vmIP + ":" + parts[1] + } + } + config := &gvntypes.Configuration{ + Debug: *debug, + MTU: 1500, + Subnet: "192.168.127.0/24", + GatewayIP: gatewayIP, + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + DHCPStaticLeases: map[string]string{ + vmIP: *mac, + }, + Forwards: forwards, + NAT: map[string]string{ + "192.168.127.254": "127.0.0.1", + }, + GatewayVirtualIPs: []string{"192.168.127.254"}, + Protocol: gvntypes.QemuProtocol, + } + vn, err := gvnvirtualnetwork.New(config) + if err != nil { + panic(err) + } + go func() { + var conn net.Conn + for i := 0; i < 100; i++ { + time.Sleep(1 * time.Second) + fmt.Fprintf(os.Stderr, "connecting to NW...\n") + conn, err = net.Dial("tcp", *wasiAddr) + if err == nil { + break + } + fmt.Fprintf(os.Stderr, "failed connecting to NW: %v; retrying...\n", err) + } + if conn == nil { + panic("failed to connect to vm") + } + // We register our VM network as a qemu "-netdev socket". + if err := vn.AcceptQemu(context.TODO(), conn); err != nil { + fmt.Fprintf(os.Stderr, "failed AcceptQemu: %v\n", err) + } + }() + u, err := url.Parse("dummy://" + *wasiAddr) + if err != nil { + panic(err) + } + p, err := strconv.Atoi(u.Port()) + if err != nil { + panic(err) + } + sockCfg := sock.NewConfig().WithTCPListener(u.Hostname(), p) + ctx = sock.WithConfig(ctx, sockCfg) + } c, err := os.ReadFile(args[0]) if err != nil { panic(err) @@ -57,15 +140,16 @@ func main() { if err != nil { panic(err) } - return } -type envFlags []string +type sliceFlags []string -func (i *envFlags) String() string { - return fmt.Sprintf("%v", []string(*i)) +func (f *sliceFlags) String() string { + var s []string = *f + return fmt.Sprintf("%v", s) } -func (i *envFlags) Set(value string) error { - *i = append(*i, value) + +func (f *sliceFlags) Set(value string) error { + *f = append(*f, value) return nil }