mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-03 13:11:31 +00:00
Compare commits
239 Commits
v0.0.7
...
release-pl
Author | SHA1 | Date | |
---|---|---|---|
b3748bf50f | |||
b9559525c6 | |||
cc63412a7f | |||
aa473b1c8b | |||
7e34766bdc | |||
83fd7607dd | |||
ab0d66c611 | |||
67652a5e1b | |||
a320efad6b | |||
a61e2fb30a | |||
3cb0e214e9 | |||
0e0c5264eb | |||
19f35ef20a | |||
79e27256e6 | |||
b6c726e7aa | |||
0d2b7a3ae3 | |||
f1e3d59b6a | |||
0106b85de9 | |||
96ccbd50bb | |||
41aa1aa707 | |||
ec74bc8d2b | |||
694de5d1fd | |||
f2db826ba6 | |||
7f5609a846 | |||
adb7b29354 | |||
bd448ee8d9 | |||
1647a07226 | |||
151b43eeec | |||
1123a1a50a | |||
6a6b5b6e0b | |||
274136825a | |||
2ab2cda937 | |||
2519d76479 | |||
dbeb8bf43b | |||
6093627bdd | |||
1d75dfb88a | |||
18bf370f74 | |||
506d2ccf46 | |||
6096dee2fe | |||
bf3b73bf24 | |||
87530edf70 | |||
1dca770091 | |||
01a94ad23e | |||
2a107a370f | |||
313d3f72a5 | |||
5ec3d9d5c1 | |||
1cf03a460e | |||
ffc9dcc0ea | |||
0358c9c775 | |||
dcffaf110e | |||
b81ae5d01a | |||
1756bc6647 | |||
6bf3741ec9 | |||
b7d41ee9f4 | |||
53059e8cca | |||
11bb99b1e4 | |||
eaa84089ce | |||
680244fc5e | |||
d469da4d9b | |||
99091df3cf | |||
08b30c2eaa | |||
224fdbe227 | |||
62569f6c59 | |||
0b991f454e | |||
75aba8a1e3 | |||
8216ab3602 | |||
902fffe207 | |||
45cfc6bb27 | |||
146bda0810 | |||
45e7d7515b | |||
f161b5afd6 | |||
7fe3e2c7cb | |||
3a5be71db4 | |||
d1b910f5c4 | |||
8806a79161 | |||
c8795fa08d | |||
d792eb5439 | |||
398e555bd3 | |||
75901233b1 | |||
04665ce690 | |||
481a5884d9 | |||
5ee1035896 | |||
9bd8d1bb1d | |||
3bada811b2 | |||
e08d25ebde | |||
2c884a6882 | |||
d756fa82f4 | |||
6e051f52b9 | |||
b2fba6400e | |||
b26469be28 | |||
28d63d7d70 | |||
6b91f0be94 | |||
9e91ffe065 | |||
b57d95c610 | |||
de6bfe38fe | |||
f6dffd6e17 | |||
07cceed0c8 | |||
4ef466ceb6 | |||
8c9b3a6ceb | |||
a970cddacf | |||
a878d16c3c | |||
1126f1ffc9 | |||
d1b2cb3683 | |||
8e1e197113 | |||
ffb7de7d68 | |||
bd464d9f03 | |||
31d04c2f43 | |||
04401c1d07 | |||
b2dd4af09b | |||
783dd51f05 | |||
2f866ad47b | |||
94e45c1c8c | |||
3a398810b6 | |||
5da214fa48 | |||
8840bf34a4 | |||
f953c87b90 | |||
ff571630b9 | |||
e45a9d82d2 | |||
98ca623828 | |||
deeaa20a4a | |||
fe8e1d5521 | |||
367d31b11f | |||
71301ee689 | |||
350e02c553 | |||
f0914fb39f | |||
0e64d4ea79 | |||
35d585e3b1 | |||
a79320b4fc | |||
39ded9c7f4 | |||
b42b730b77 | |||
0f49d0cec4 | |||
dc4b14b5d1 | |||
f5b4c66ec7 | |||
9062d78e51 | |||
6161bea7bf | |||
8363ed0085 | |||
8ddc190018 | |||
c687561541 | |||
4c83902729 | |||
6f50167798 | |||
88a62441b1 | |||
93aae83b3f | |||
6e1e4e3806 | |||
9e532345f0 | |||
89b7f40520 | |||
4175e1e3fe | |||
1bdf3bda87 | |||
9a45d754bf | |||
6c3fc54688 | |||
af6a1a3ad2 | |||
7bef74fadf | |||
ec1b6d4370 | |||
b2d146713b | |||
b730b08d6e | |||
87758d7ae9 | |||
349664abf1 | |||
ef068e790c | |||
6f39f115b7 | |||
23c7302c04 | |||
e219f3adf1 | |||
2c7210d85e | |||
ade37e92f3 | |||
ef3bc83069 | |||
14084f13d8 | |||
fbc953cf46 | |||
fd7974fc98 | |||
d17769d69f | |||
7ba04f26a3 | |||
11235b6837 | |||
e8849048db | |||
cd15337ad8 | |||
037261991a | |||
67fb5891e4 | |||
d1f6d1e742 | |||
18fc2c3a7e | |||
54486b119b | |||
04a633d501 | |||
612203f014 | |||
e9ba336f68 | |||
94790ce7dc | |||
023063327f | |||
d46aa878af | |||
2462a99fdc | |||
fc18bc6a18 | |||
b0f0934fa4 | |||
f6721d5e2c | |||
0d43a8be54 | |||
0193921053 | |||
485f6e8319 | |||
09ee251c9e | |||
75011ef8cb | |||
69c7af5220 | |||
a364abe887 | |||
95accc6d3f | |||
04fb6cce8e | |||
5420214bdd | |||
b4f26787d4 | |||
51dff0361d | |||
3187830ff5 | |||
338322619c | |||
511f83bfd9 | |||
b0f5c38fb0 | |||
520018a86d | |||
39c2e58fbc | |||
d1d6eb5c8b | |||
84920a88ab | |||
bece7f33c7 | |||
95fbc62486 | |||
284ed8f17b | |||
82576df7b7 | |||
1b90eedbcd | |||
aa941c6e87 | |||
d0bf3c4c77 | |||
38e892e249 | |||
1a90372037 | |||
4754cdd128 | |||
f843abcabf | |||
e8d89d4d5b | |||
4e9738b959 | |||
8135307283 | |||
e450ebd2a2 | |||
79f7742caa | |||
c3c18271b4 | |||
218f848170 | |||
9d8c516a29 | |||
89055ef77c | |||
24c71e9725 | |||
0a6a112133 | |||
1627cbcdd7 | |||
f8247f13e4 | |||
6d07112e3d | |||
6cef03bffa | |||
73fd95dbe2 | |||
f41a1e2168 | |||
346cf4a7fa | |||
5e16f3149f | |||
ec9060d872 | |||
6050e99aa7 | |||
7cfdb27d23 |
@ -1,2 +0,0 @@
|
||||
/target
|
||||
/kernel/linux-*
|
34
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "bug: "
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- edera-dev/engineering
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version / Commit
|
||||
description: What version of our software are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@ -4,7 +4,21 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
actions-updates:
|
||||
dependency-type: "production"
|
||||
applies-to: "version-updates"
|
||||
actions-dev-updates:
|
||||
dependency-type: "development"
|
||||
applies-to: "version-updates"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
dep-updates:
|
||||
dependency-type: "production"
|
||||
applies-to: "version-updates"
|
||||
dev-updates:
|
||||
dependency-type: "development"
|
||||
applies-to: "version-updates"
|
||||
|
119
.github/workflows/check.yml
vendored
119
.github/workflows/check.yml
vendored
@ -7,23 +7,122 @@ on:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
fmt:
|
||||
name: fmt
|
||||
rustfmt:
|
||||
name: rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh fmt --all -- --check
|
||||
- name: install stable rust toolchain with rustfmt
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup component add rustfmt
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo fmt
|
||||
run: ./hack/build/cargo.sh fmt --all -- --check
|
||||
shellcheck:
|
||||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: ./hack/code/shellcheck.sh
|
||||
- name: shellcheck
|
||||
run: ./hack/code/shellcheck.sh
|
||||
full-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full build linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo build
|
||||
run: ./hack/build/cargo.sh build
|
||||
full-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full test linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo test
|
||||
run: ./hack/build/cargo.sh test
|
||||
full-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: full clippy linux-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: install stable rust toolchain with clippy
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
rustup component add clippy
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: cargo clippy
|
||||
run: ./hack/build/cargo.sh clippy
|
||||
|
44
.github/workflows/client.yml
vendored
44
.github/workflows/client.yml
vendored
@ -1,44 +0,0 @@
|
||||
name: client
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: client build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --bin kratactl
|
32
.github/workflows/kernel.yml
vendored
32
.github/workflows/kernel.yml
vendored
@ -1,32 +0,0 @@
|
||||
name: kernel
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "kernel/**"
|
||||
- "hack/ci/**"
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: kernel build ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/kernel/build.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_JOBS: "5"
|
102
.github/workflows/nightly.yml
vendored
102
.github/workflows/nightly.yml
vendored
@ -1,102 +0,0 @@
|
||||
name: nightly
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 10 * * *"
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: nightly server ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/dist/bundle.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_JOBS: "5"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: krata-bundle-systemd-${{ matrix.arch }}
|
||||
path: "target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
|
||||
compression-level: 0
|
||||
- run: ./hack/dist/deb.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: krata-debian-${{ matrix.arch }}
|
||||
path: "target/dist/*.deb"
|
||||
compression-level: 0
|
||||
- run: ./hack/dist/apk.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: krata-alpine-${{ matrix.arch }}
|
||||
path: "target/dist/*_${{ matrix.arch }}.apk"
|
||||
compression-level: 0
|
||||
- run: ./hack/os/build.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: krata-os-${{ matrix.arch }}
|
||||
path: "target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
compression-level: 0
|
||||
client:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: nightly client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false && git config --global core.eol lf
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
path: "target/*/release/kratactl"
|
||||
if: ${{ matrix.platform.os != 'windows' }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kratactl-${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
path: "target/*/release/kratactl.exe"
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
40
.github/workflows/os.yml
vendored
40
.github/workflows/os.yml
vendored
@ -1,40 +0,0 @@
|
||||
name: os
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "os/**"
|
||||
- "hack/os/**"
|
||||
- "hack/ci/**"
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: os build ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/os/build.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_JOBS: "5"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: krata-os-${{ matrix.arch }}
|
||||
path: "target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
compression-level: 0
|
94
.github/workflows/release-binaries.yml
vendored
94
.github/workflows/release-binaries.yml
vendored
@ -1,94 +0,0 @@
|
||||
name: release-binaries
|
||||
permissions:
|
||||
contents: write
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: release-binaries server ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/dist/bundle.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_JOBS: "5"
|
||||
- run: "./hack/ci/assemble-release-assets.sh bundle-systemd ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/bundle-systemd-${{ matrix.arch }}.tgz"
|
||||
- run: ./hack/dist/deb.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- run: "./hack/ci/assemble-release-assets.sh debian ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*.deb"
|
||||
- run: ./hack/dist/apk.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- run: "./hack/ci/assemble-release-assets.sh alpine ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/dist/*_${{ matrix.arch }}.apk"
|
||||
- run: ./hack/os/build.sh
|
||||
env:
|
||||
KRATA_KERNEL_BUILD_SKIP: "1"
|
||||
- run: "./hack/ci/assemble-release-assets.sh os ${{ github.event.release.tag_name }} ${{ matrix.arch }} target/os/krata-${{ matrix.arch }}.qcow2"
|
||||
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
client:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- { os: linux, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: linux, arch: aarch64, on: ubuntu-latest, deps: linux }
|
||||
- { os: darwin, arch: x86_64, on: macos-14, deps: darwin }
|
||||
- { os: darwin, arch: aarch64, on: macos-14, deps: darwin }
|
||||
- { os: freebsd, arch: x86_64, on: ubuntu-latest, deps: linux }
|
||||
- { os: windows, arch: x86_64, on: windows-latest, deps: windows }
|
||||
env:
|
||||
TARGET_OS: "${{ matrix.platform.os }}"
|
||||
TARGET_ARCH: "${{ matrix.platform.arch }}"
|
||||
runs-on: "${{ matrix.platform.on }}"
|
||||
name: release-binaries client ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
if: ${{ matrix.platform.os != 'darwin' }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.platform.arch }}-apple-darwin"
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- uses: homebrew/actions/setup-homebrew@master
|
||||
if: ${{ matrix.platform.os == 'darwin' }}
|
||||
- run: ./hack/ci/install-${{ matrix.platform.deps }}-deps.sh
|
||||
- run: ./hack/build/cargo.sh build --release --bin kratactl
|
||||
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl"
|
||||
if: ${{ matrix.platform.os != 'windows' }}
|
||||
- run: "./hack/ci/assemble-release-assets.sh kratactl ${{ github.event.release.tag_name }} ${{ matrix.platform.os }}-${{ matrix.platform.arch }} target/*/release/kratactl.exe"
|
||||
if: ${{ matrix.platform.os == 'windows' }}
|
||||
- run: "./hack/ci/upload-release-assets.sh ${{ github.event.release.tag_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
50
.github/workflows/release-plz.yml
vendored
50
.github/workflows/release-plz.yml
vendored
@ -1,7 +1,4 @@
|
||||
name: release-plz
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -13,21 +10,34 @@ jobs:
|
||||
release-plz:
|
||||
name: release-plz
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
|
||||
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
token: "${{ steps.generate-token.outputs.token }}"
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- name: release-plz
|
||||
uses: MarcoIeni/release-plz-action@v0.5
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
|
||||
CARGO_REGISTRY_TOKEN: "${{ secrets.KRATA_RELEASE_CARGO_TOKEN }}"
|
||||
- name: harden runner
|
||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: generate cultivator token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
|
||||
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
token: "${{ steps.generate-token.outputs.token }}"
|
||||
- name: install stable rust toolchain
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default stable
|
||||
- name: install linux dependencies
|
||||
run: ./hack/ci/install-linux-deps.sh
|
||||
- name: release-plz
|
||||
uses: MarcoIeni/release-plz-action@e28810957ef1fedfa89b5e9692e750ce45f62a67 # v0.5.65
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
|
||||
CARGO_REGISTRY_TOKEN: "${{ secrets.KRATA_RELEASE_CARGO_TOKEN }}"
|
||||
|
82
.github/workflows/server.yml
vendored
82
.github/workflows/server.yml
vendored
@ -1,82 +0,0 @@
|
||||
name: server
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server build ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh build
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server test ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh test
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server clippy ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/build/cargo.sh clippy
|
||||
initrd:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- aarch64
|
||||
env:
|
||||
TARGET_ARCH: "${{ matrix.arch }}"
|
||||
name: server initrd ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "${{ matrix.arch }}-unknown-linux-gnu,${{ matrix.arch }}-unknown-linux-musl"
|
||||
- run: ./hack/ci/install-linux-deps.sh
|
||||
- run: ./hack/initrd/build.sh
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "vendor"]
|
||||
path = vendor
|
||||
url = https://github.com/edera-dev/krata-vendor.git
|
36
CHANGELOG.md
36
CHANGELOG.md
@ -1,4 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
@ -6,40 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.7](https://github.com/edera-dev/krata/compare/v0.0.6...v0.0.7) - 2024-04-09
|
||||
## [0.0.23](https://github.com/edera-dev/krata/compare/v0.0.22...v0.0.23) - 2024-09-17
|
||||
|
||||
### Other
|
||||
|
||||
- update Cargo.toml dependencies
|
||||
- update Cargo.lock dependencies
|
||||
|
||||
## [0.0.6](https://github.com/edera-dev/krata/compare/v0.0.5...v0.0.6) - 2024-04-09
|
||||
|
||||
### Fixed
|
||||
- increase channel acquisition timeout to support lower performance hosts ([#36](https://github.com/edera-dev/krata/pull/36))
|
||||
## [0.0.22](https://github.com/edera-dev/krata/compare/v0.0.21...v0.0.22) - 2024-09-16
|
||||
|
||||
### Other
|
||||
|
||||
- update Cargo.toml dependencies
|
||||
- update Cargo.lock dependencies
|
||||
|
||||
## [0.0.5](https://github.com/edera-dev/krata/compare/v0.0.4...v0.0.5) - 2024-04-09
|
||||
|
||||
### Added
|
||||
- *(ctl)* add help and about to commands and arguments ([#25](https://github.com/edera-dev/krata/pull/25))
|
||||
|
||||
### Other
|
||||
- update Cargo.toml dependencies
|
||||
- update Cargo.lock dependencies
|
||||
|
||||
## [0.0.4](https://github.com/edera-dev/krata/releases/tag/v${version}) - 2024-04-03
|
||||
|
||||
### Other
|
||||
- implement automatic releases
|
||||
- reimplement console to utilize channels, and provide logs support
|
||||
- set hostname from launch config
|
||||
- implement event stream retries
|
||||
- work on parallel reconciliation
|
||||
- implement parallel guest reconciliation
|
||||
- log when a guest start failures occurs
|
||||
- remove device restriction
|
||||
- setup loopback interface
|
||||
- place running tasks in cgroup
|
||||
- preparations for xen control-plane
|
||||
|
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Contributing to Krata
|
||||
|
||||
Welcome! We're very glad you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies.
|
||||
|
||||
## Before contributing
|
||||
|
||||
1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md), which applies to all interactions in/with all Edera projects and venues.
|
||||
2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions.
|
||||
3. For security or otherwise sensitive topics, please read our [Security Policy].
|
||||
4. Ask questions! If you want to ask something, chances are someone else wants to ask it as well.
|
||||
|
||||
## Contributing Code
|
||||
|
||||
To get started with technical contributions, please read out [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issue].
|
||||
|
||||
## Reporting bugs and other issues
|
||||
|
||||
While it's totally fine to simply bring it up on our Discord, we encourage opening an issue on GitHub using the Bug Report template.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
|
||||
- Else skip to step 3.
|
||||
2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
|
||||
3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft.
|
||||
4. Please include a detailed list of changes that the PR makes.
|
||||
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
|
||||
6. After the review cycle concludes and we know you are ready for merging, a team member will submit the PR to the merge queue.
|
||||
|
||||
|
||||
[Code of Conduct]: ./CODE_OF_CONDUCT.md
|
||||
[Security Policy]: ./SECURITY.md
|
||||
[good-first-issues]: https://github.com/edera-dev/krata/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
|
2968
Cargo.lock
generated
2968
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
96
Cargo.toml
96
Cargo.toml
@ -1,111 +1,49 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/krata",
|
||||
"crates/oci",
|
||||
"crates/guest",
|
||||
"crates/runtime",
|
||||
"crates/daemon",
|
||||
"crates/network",
|
||||
"crates/ctl",
|
||||
"crates/xen/xencall",
|
||||
"crates/xen/xenclient",
|
||||
"crates/xen/xenevtchn",
|
||||
"crates/xen/xengnt",
|
||||
"crates/xen/xenplatform",
|
||||
"crates/xen/xenstore",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.7"
|
||||
version = "0.0.23"
|
||||
homepage = "https://krata.dev"
|
||||
license = "Apache-2.0"
|
||||
license = "GPL-2.0-or-later"
|
||||
repository = "https://github.com/edera-dev/krata"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
arrayvec = "0.7.4"
|
||||
async-compression = "0.4.8"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.77"
|
||||
backhand = "0.15.0"
|
||||
async-trait = "0.1.82"
|
||||
byteorder = "1"
|
||||
bytes = "1.5.0"
|
||||
cgroups-rs = "0.3.4"
|
||||
circular-buffer = "0.1.7"
|
||||
comfy-table = "7.1.1"
|
||||
crossterm = "0.27.0"
|
||||
ctrlc = "3.4.4"
|
||||
c2rust-bitfields = "0.18.0"
|
||||
elf = "0.7.4"
|
||||
env_logger = "0.11.0"
|
||||
etherparse = "0.14.3"
|
||||
env_logger = "0.11.5"
|
||||
flate2 = "1.0"
|
||||
futures = "0.3.30"
|
||||
ipnetwork = "0.20.0"
|
||||
indexmap = "2.5.0"
|
||||
libc = "0.2"
|
||||
log = "0.4.20"
|
||||
loopdev-3 = "0.5.1"
|
||||
krata-advmac = "1.1.0"
|
||||
krata-tokio-tar = "0.4.0"
|
||||
log = "0.4.22"
|
||||
memchr = "2"
|
||||
nix = "0.28.0"
|
||||
oci-spec = "0.6.4"
|
||||
once_cell = "1.19.0"
|
||||
path-absolutize = "3.1.1"
|
||||
path-clean = "1.0.1"
|
||||
prost = "0.12.4"
|
||||
prost-build = "0.12.4"
|
||||
prost-reflect-build = "0.13.0"
|
||||
rand = "0.8.5"
|
||||
redb = "2.0.0"
|
||||
rtnetlink = "0.14.1"
|
||||
serde_json = "1.0.113"
|
||||
serde_yaml = "0.9"
|
||||
sha256 = "1.5.0"
|
||||
signal-hook = "0.3.17"
|
||||
nix = "0.29.0"
|
||||
regex = "1.10.6"
|
||||
slice-copy = "0.3.0"
|
||||
smoltcp = "0.11.0"
|
||||
thiserror = "1.0"
|
||||
tokio-tun = "0.11.4"
|
||||
tonic-build = "0.11.0"
|
||||
tower = "0.4.13"
|
||||
udp-stream = "0.0.11"
|
||||
url = "2.5.0"
|
||||
walkdir = "2"
|
||||
xz2 = "0.1"
|
||||
|
||||
[workspace.dependencies.clap]
|
||||
version = "4.4.18"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.prost-reflect]
|
||||
version = "0.13.1"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.12.3"
|
||||
default-features = false
|
||||
features = ["rustls-tls"]
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
version = "1.0.196"
|
||||
version = "1.0.209"
|
||||
features = ["derive"]
|
||||
|
||||
[workspace.dependencies.sys-mount]
|
||||
version = "3.0.0"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.tokio]
|
||||
version = "1.35.1"
|
||||
version = "1.40.0"
|
||||
features = ["full"]
|
||||
|
||||
[workspace.dependencies.tokio-stream]
|
||||
version = "0.1"
|
||||
features = ["io-util", "net"]
|
||||
|
||||
[workspace.dependencies.tonic]
|
||||
version = "0.11.0"
|
||||
features = ["tls"]
|
||||
|
||||
[workspace.dependencies.uuid]
|
||||
version = "1.6.1"
|
||||
version = "1.10.0"
|
||||
features = ["v4"]
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
strip = "symbols"
|
||||
|
34
Cross.toml
34
Cross.toml
@ -1,34 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.riscv64gc-unknown-linux-gnu]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.x86_64-unknown-freebsd]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
||||
|
||||
[target.x86_64-unknown-netbsd]
|
||||
pre-build = [
|
||||
"apt-get update && apt-get --assume-yes install protobuf-compiler"
|
||||
]
|
79
DEV.md
79
DEV.md
@ -1,79 +0,0 @@
|
||||
# Development Guide
|
||||
|
||||
## Structure
|
||||
|
||||
krata is composed of four major executables:
|
||||
|
||||
| Executable | Runs On | User Interaction | Dev Runner | Code Path |
|
||||
| ---------- | ------- | ---------------- | ------------------------ | ----------------- |
|
||||
| kratad | host | backend daemon | ./hack/debug/kratad.sh | crates/daemon |
|
||||
| kratanet | host | backend daemon | ./hack/debug/kratanet.sh | crates/network |
|
||||
| kratactl | host | CLI tool | ./hack/debug/kratactl.sh | crates/ctl |
|
||||
| krataguest | guest | none, guest init | N/A | crates/guest |
|
||||
|
||||
You will find the code to each executable available in the bin/ and src/ directories inside
|
||||
it's corresponding code path from the above table.
|
||||
|
||||
## Environment
|
||||
|
||||
| Component | Specification | Notes |
|
||||
| ------------- | ------------- | --------------------------------------------------------------------------------- |
|
||||
| Architecture | x86_64 | aarch64 support is still in development |
|
||||
| Memory | At least 6GB | dom0 will need to be configured will lower memory limit to give krata guests room |
|
||||
| Xen | 4.17 | Temporary due to hardcoded interface version constants |
|
||||
| Debian | stable / sid | Debian is recommended due to the ease of Xen setup |
|
||||
| rustup | any | Install Rustup from https://rustup.rs |
|
||||
|
||||
## Setup Guide
|
||||
|
||||
1. Install the specified Debian version on a x86_64 host _capable_ of KVM (NOTE: KVM is not used, Xen is a type-1 hypervisor).
|
||||
|
||||
2. Install required packages: `apt install git xen-system-amd64 flex bison libelf-dev libssl-dev bc`
|
||||
|
||||
3. Install [rustup](https://rustup.rs) for managing a Rust environment.
|
||||
|
||||
4. Configure `/etc/default/grub.d/xen.cfg` to give krata guests some room:
|
||||
|
||||
```sh
|
||||
# Configure dom0_mem to be 4GB, but leave the rest of the RAM for krata guests.
|
||||
GRUB_CMDLINE_XEN_DEFAULT="dom0_mem=4G,max:4G"
|
||||
```
|
||||
|
||||
After changing the grub config, update grub: `update-grub`
|
||||
|
||||
Then reboot to boot the system as a Xen dom0.
|
||||
|
||||
You can validate that Xen is setup by running `xl info` and ensuring it returns useful information about the Xen hypervisor.
|
||||
|
||||
5. Clone the krata source code:
|
||||
```sh
|
||||
$ git clone https://github.com/edera-dev/krata.git krata
|
||||
$ cd krata
|
||||
```
|
||||
|
||||
6. Build a guest kernel image:
|
||||
|
||||
```sh
|
||||
$ ./hack/kernel/build.sh
|
||||
```
|
||||
|
||||
7. Copy the guest kernel image at `target/kernel/kernel-x86_64` to `/var/lib/krata/guest/kernel` to have it automatically detected by kratad.
|
||||
8. Launch `./hack/debug/kratanet.sh` and keep it running in the foreground.
|
||||
9. Launch `./hack/debug/kratad.sh` and keep it running in the foreground.
|
||||
10. Run kratactl to launch a guest:
|
||||
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh launch --attach alpine:latest
|
||||
```
|
||||
|
||||
To detach from the guest console, use `Ctrl + ]` on your keyboard.
|
||||
|
||||
To list the running guests, run:
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh list
|
||||
```
|
||||
|
||||
To destroy a running guest, copy it's UUID from either the launch command or the guest list and run:
|
||||
```sh
|
||||
$ ./hack/debug/kratactl.sh destroy GUEST_UUID
|
||||
```
|
14
FAQ.md
14
FAQ.md
@ -1,19 +1,9 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## How does krata currently work?
|
||||
|
||||
The krata hypervisor makes it possible to launch OCI containers on a Xen hypervisor without utilizing the Xen userspace tooling. krata contains just enough of the userspace of Xen (reimplemented in Rust) to start an x86_64 Xen Linux PV guest, and implements a Linux init process that can boot an OCI container. It does so by converting an OCI image into a squashfs file and packaging basic startup data in a bundle which the init container can read.
|
||||
|
||||
In addition, due to the desire to reduce dependence on the dom0 network, krata contains a networking daemon called kratanet. kratanet listens for krata guests to startup and launches a userspace networking environment. krata guests can access the dom0 networking stack via the proxynat layer that makes it possible to communicate over UDP, TCP, and ICMP (echo only) to the outside world. In addition, each krata guest is provided a "gateway" IP (both in IPv4 and IPv6) which utilizes smoltcp to provide a virtual host. That virtual host in the future could dial connections into the container to access container networking resources.
|
||||
|
||||
## Why utilize Xen instead of KVM?
|
||||
|
||||
Xen is a very interesting technology, and Edera believes that type-1 hypervisors are ideal for security. Most OCI isolation techniques use KVM, which is not a type-1 hypervisor, and thus is subject to the security limitations of the OS kernel. A type-1 hypervisor on the otherhand provides a minimal amount of attack surface upon which less-trusted guests can be launched on top of.
|
||||
Xen is a very interesting technology, and Edera believes that type-1 hypervisors are ideal for security. Most OCI isolation techniques use KVM, which is not a type-1 hypervisor, and thus is subject to the security limitations of the OS kernel. A type-1 hypervisor on the other hand provides a minimal attack surface upon which less-trusted guests can be launched on top of.
|
||||
|
||||
## Why not utilize pvcalls to provide access to the host network?
|
||||
|
||||
pvcalls is extremely interesting, and although it is certainly possible to utilize pvcalls to get the job done, we chose to utilize userspace networking technology in order to enhance security. Our goal is to drop the use of all xen networking backend drivers within the kernel and have the guest talk directly to a userspace daemon, bypassing the vif (xen-netback) driver. Currently, in order to develop the networking layer, we utilize xen-netback and then use raw sockets to provide the userspace networking layer on the host.
|
||||
|
||||
## What are the future plans?
|
||||
|
||||
Edera is building a company to compete in the hypervisor space with open-source technology. More information to come soon on official channels.
|
||||
pvcalls is fascinating, and although it is certainly possible to utilize pvcalls to get the job done, we chose to utilize userspace networking technology in order to enhance security. Our goal is to drop the use of all xen networking backend drivers within the kernel and have the guest talk directly to a userspace daemon, bypassing the vif (xen-netback) driver. Currently, in order to develop the networking layer, we utilize xen-netback and then use raw sockets to provide the userspace networking layer on the host.
|
||||
|
476
LICENSE
476
LICENSE
@ -1,201 +1,339 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
Preamble
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
Copyright 2024 Edera Inc.
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
17
README.md
17
README.md
@ -1,28 +1,25 @@
|
||||
# krata
|
||||
|
||||
The Edera Hypervisor
|
||||
krata is an implementation of a Xen control-plane in Rust.
|
||||
|
||||

|
||||

|
||||
[](https://github.com/edera-dev/krata/actions/workflows/check.yml)
|
||||
[](https://github.com/edera-dev/krata/actions/workflows/nightly.yml)
|
||||
|
||||
---
|
||||
|
||||
- [Frequently Asked Questions](FAQ.md)
|
||||
- [Development Guide](DEV.md)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [Security Policy](SECURITY.md)
|
||||
|
||||
## Introduction
|
||||
|
||||
krata is a single-host hypervisor service built for OCI-compliant containers. It isolates containers using a type-1 hypervisor, providing workload isolation that can exceed the security level of KVM-based OCI-compliant runtimes.
|
||||
|
||||
krata utilizes the core of the Xen hypervisor, with a fully memory-safe Rust control plane to bring Xen tooling into a new secure era.
|
||||
krata is a component of [Edera Protect](https://edera.dev/protect-kubernetes), for secure-by-design infrastructure.
|
||||
It provides the base layer upon which Edera Protect zones are built on: a securely booted virtualization guest on the Xen hypervisor.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Architecture | Completion Level | Virtualization Technology |
|
||||
| ------------ | ---------------- | ------------------------- |
|
||||
| x86_64 | 100% Completed | Intel VT-x, AMD-V |
|
||||
| aarch64 | 30% Completed | AArch64 virtualization |
|
||||
| Architecture | Completion Level | Hardware Virtualization |
|
||||
|--------------|------------------|-------------------------|
|
||||
| x86_64 | 100% Completed | None, Intel VT-x, AMD-V |
|
||||
| aarch64 | 10% Completed | AArch64 virtualization |
|
||||
|
@ -1,34 +0,0 @@
|
||||
[package]
|
||||
name = "krata-ctl"
|
||||
description = "Command-line tool to control the krata hypervisor"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
comfy-table = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
ctrlc = { workspace = true, features = ["termination"] }
|
||||
env_logger = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.7" }
|
||||
log = { workspace = true }
|
||||
prost-reflect = { workspace = true, features = ["serde"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratactl"
|
||||
|
||||
[[bin]]
|
||||
name = "kratactl"
|
||||
path = "bin/control.rs"
|
@ -1,11 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
|
||||
use kratactl::cli::ControlCommand;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
ControlCommand::parse().run().await
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||
|
||||
use tokio::select;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use super::resolve_guest;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Attach to the guest console")]
|
||||
pub struct AttachCommand {
|
||||
#[arg(help = "Guest to attach to, either the name or the uuid")]
|
||||
guest: String,
|
||||
}
|
||||
|
||||
impl AttachCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let input = StdioConsoleStream::stdin_stream(guest_id.clone()).await;
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::GuestStatus,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
DestroyGuestRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::cli::resolve_guest;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Destroy a guest")]
|
||||
pub struct DestroyCommand {
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the destruction of the guest to complete"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Guest to destroy, either the name or the uuid")]
|
||||
guest: String,
|
||||
}
|
||||
|
||||
impl DestroyCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let _ = client
|
||||
.destroy_guest(Request::new(DestroyGuestRequest {
|
||||
guest_id: guest_id.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if self.wait {
|
||||
wait_guest_destroyed(&guest_id, events).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_guest_destroyed(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::GuestChanged(changed) => {
|
||||
let Some(guest) = changed.guest else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == GuestStatus::Failed {
|
||||
error!("destroy failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("guest error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Destroyed {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{
|
||||
guest_image_spec::Image, GuestImageSpec, GuestOciImageSpec, GuestSpec, GuestStatus,
|
||||
GuestTaskSpec, GuestTaskSpecEnvVar,
|
||||
},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
CreateGuestRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::error;
|
||||
use tokio::select;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Launch a new guest")]
|
||||
pub struct LauchCommand {
|
||||
#[arg(short, long, help = "Name of the guest")]
|
||||
name: Option<String>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = 1,
|
||||
help = "vCPUs available to the guest"
|
||||
)]
|
||||
cpus: u32,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = 512,
|
||||
help = "Memory available to the guest, in megabytes"
|
||||
)]
|
||||
mem: u64,
|
||||
#[arg[short, long, help = "Environment variables set in the guest"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Attach to the guest after guest starts, implies --wait"
|
||||
)]
|
||||
attach: bool,
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the guest to start, implied by --attach"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Container image for guest to use")]
|
||||
oci: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the guest"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl LauchCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let request = CreateGuestRequest {
|
||||
spec: Some(GuestSpec {
|
||||
name: self.name.unwrap_or_default(),
|
||||
image: Some(GuestImageSpec {
|
||||
image: Some(Image::Oci(GuestOciImageSpec { image: self.oci })),
|
||||
}),
|
||||
vcpus: self.cpus,
|
||||
mem: self.mem,
|
||||
task: Some(GuestTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| GuestTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
}),
|
||||
annotations: vec![],
|
||||
}),
|
||||
};
|
||||
let response = client
|
||||
.create_guest(Request::new(request))
|
||||
.await?
|
||||
.into_inner();
|
||||
let id = response.guest_id;
|
||||
|
||||
if self.wait || self.attach {
|
||||
wait_guest_started(&id, events.clone()).await?;
|
||||
}
|
||||
|
||||
let code = if self.attach {
|
||||
let input = StdioConsoleStream::stdin_stream(id.clone()).await;
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(id.clone(), events).await?;
|
||||
select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
}
|
||||
} else {
|
||||
println!("{}", id);
|
||||
None
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::GuestChanged(changed) => {
|
||||
let Some(guest) = changed.guest else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == GuestStatus::Failed {
|
||||
error!("launch failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("guest error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Destroyed {
|
||||
error!("guest destroyed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if state.status() == GuestStatus::Started {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{Guest, GuestStatus},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, ListGuestsRequest, ResolveGuestRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::format::{guest_simple_line, guest_status_text, kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List the guests on the hypervisor")]
|
||||
pub struct ListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ListFormat,
|
||||
#[arg(help = "Limit to a single guest, either the name or the uuid")]
|
||||
guest: Option<String>,
|
||||
}
|
||||
|
||||
impl ListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut guests = if let Some(ref guest) = self.guest {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
name: guest.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(guest) = reply.guest {
|
||||
vec![guest]
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve guest '{}'", guest));
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.list_guests(Request::new(ListGuestsRequest {}))
|
||||
.await?
|
||||
.into_inner()
|
||||
.guests
|
||||
};
|
||||
|
||||
guests.sort_by(|a, b| {
|
||||
a.spec
|
||||
.as_ref()
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("")
|
||||
.cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
|
||||
});
|
||||
|
||||
match self.format {
|
||||
ListFormat::Table => {
|
||||
self.print_guest_table(guests)?;
|
||||
}
|
||||
|
||||
ListFormat::Simple => {
|
||||
for guest in guests {
|
||||
println!("{}", guest_simple_line(&guest));
|
||||
}
|
||||
}
|
||||
|
||||
ListFormat::Json | ListFormat::JsonPretty | ListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for guest in guests {
|
||||
let message = proto2dynamic(guest)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ListFormat::Jsonl => {
|
||||
for guest in guests {
|
||||
let message = proto2dynamic(guest)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ListFormat::KeyValue => {
|
||||
self.print_key_value(guests)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_guest_table(&self, guests: Vec<Guest>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "uuid", "status", "ipv4", "ipv6"]);
|
||||
for guest in guests {
|
||||
let ipv4 = guest
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.guest_ipv4.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let ipv6 = guest
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.guest_ipv6.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let Some(spec) = guest.spec else {
|
||||
continue;
|
||||
};
|
||||
let status = guest.state.as_ref().cloned().unwrap_or_default().status();
|
||||
let status_text = guest_status_text(status);
|
||||
|
||||
let status_color = match status {
|
||||
GuestStatus::Destroyed | GuestStatus::Failed => Color::Red,
|
||||
GuestStatus::Destroying | GuestStatus::Exited | GuestStatus::Starting => {
|
||||
Color::Yellow
|
||||
}
|
||||
GuestStatus::Started => Color::Green,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(spec.name),
|
||||
Cell::new(guest.id),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(ipv4.to_string()),
|
||||
Cell::new(ipv6.to_string()),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
if self.guest.is_none() {
|
||||
println!("no guests have been launched");
|
||||
}
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, guests: Vec<Guest>) -> Result<()> {
|
||||
for guest in guests {
|
||||
let kvs = proto2kv(guest)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ConsoleDataRequest},
|
||||
};
|
||||
|
||||
use tokio::select;
|
||||
use tokio_stream::{pending, StreamExt};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use super::resolve_guest;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "View the logs of a guest")]
|
||||
pub struct LogsCommand {
|
||||
#[arg(short, long, help = "Follow output from the guest")]
|
||||
follow: bool,
|
||||
#[arg(help = "Guest to show logs for, either the name or the uuid")]
|
||||
guest: String,
|
||||
}
|
||||
|
||||
impl LogsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
|
||||
let guest_id_stream = guest_id.clone();
|
||||
let follow = self.follow;
|
||||
let input = stream! {
|
||||
yield ConsoleDataRequest { guest_id: guest_id_stream, data: Vec::new() };
|
||||
if follow {
|
||||
let mut pending = pending::<ConsoleDataRequest>();
|
||||
while let Some(x) = pending.next().await {
|
||||
yield x;
|
||||
}
|
||||
}
|
||||
};
|
||||
let output = client.console_data(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
pub mod attach;
|
||||
pub mod destroy;
|
||||
pub mod launch;
|
||||
pub mod list;
|
||||
pub mod logs;
|
||||
pub mod resolve;
|
||||
pub mod watch;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use krata::{
|
||||
client::ControlClientProvider,
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ResolveGuestRequest},
|
||||
};
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use self::{
|
||||
attach::AttachCommand, destroy::DestroyCommand, launch::LauchCommand, list::ListCommand,
|
||||
logs::LogsCommand, resolve::ResolveCommand, watch::WatchCommand,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
about = "Control the krata hypervisor, a secure platform for running containers"
|
||||
)]
|
||||
pub struct ControlCommand {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "The connection URL to the krata hypervisor",
|
||||
default_value = "unix:///var/lib/krata/daemon.socket"
|
||||
)]
|
||||
connection: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Launch(LauchCommand),
|
||||
Destroy(DestroyCommand),
|
||||
List(ListCommand),
|
||||
Attach(AttachCommand),
|
||||
Logs(LogsCommand),
|
||||
Watch(WatchCommand),
|
||||
Resolve(ResolveCommand),
|
||||
}
|
||||
|
||||
impl ControlCommand {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let client = ControlClientProvider::dial(self.connection.parse()?).await?;
|
||||
let events = EventStream::open(client.clone()).await?;
|
||||
|
||||
match self.command {
|
||||
Commands::Launch(launch) => {
|
||||
launch.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Destroy(destroy) => {
|
||||
destroy.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Attach(attach) => {
|
||||
attach.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Logs(logs) => {
|
||||
logs.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::List(list) => {
|
||||
list.run(client, events).await?;
|
||||
}
|
||||
|
||||
Commands::Watch(watch) => {
|
||||
watch.run(events).await?;
|
||||
}
|
||||
|
||||
Commands::Resolve(resolve) => {
|
||||
resolve.run(client).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_guest(
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
name: name.to_string(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
if let Some(guest) = reply.guest {
|
||||
Ok(guest.id)
|
||||
} else {
|
||||
Err(anyhow!("unable to resolve guest '{}'", name))
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, ResolveGuestRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Resolve a guest name to a uuid")]
|
||||
pub struct ResolveCommand {
|
||||
#[arg(help = "Guest name")]
|
||||
guest: String,
|
||||
}
|
||||
|
||||
impl ResolveCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let reply = client
|
||||
.resolve_guest(Request::new(ResolveGuestRequest {
|
||||
name: self.guest.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(guest) = reply.guest {
|
||||
println!("{}", guest.id);
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{common::Guest, control::watch_events_reply::Event},
|
||||
};
|
||||
use prost_reflect::ReflectMessage;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{guest_simple_line, kv2line, proto2dynamic, proto2kv};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum WatchFormat {
|
||||
Simple,
|
||||
Json,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Watch for guest changes")]
|
||||
pub struct WatchCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: WatchFormat,
|
||||
}
|
||||
|
||||
impl WatchCommand {
|
||||
pub async fn run(self, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
loop {
|
||||
let event = stream.recv().await?;
|
||||
match event {
|
||||
Event::GuestChanged(changed) => {
|
||||
let guest = changed.guest.clone();
|
||||
self.print_event("guest.changed", changed, guest)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_event(
|
||||
&self,
|
||||
typ: &str,
|
||||
event: impl ReflectMessage,
|
||||
guest: Option<Guest>,
|
||||
) -> Result<()> {
|
||||
match self.format {
|
||||
WatchFormat::Simple => {
|
||||
if let Some(guest) = guest {
|
||||
println!("{}", guest_simple_line(&guest));
|
||||
}
|
||||
}
|
||||
|
||||
WatchFormat::Json => {
|
||||
let message = proto2dynamic(event)?;
|
||||
let mut value = serde_json::to_value(&message)?;
|
||||
if let Value::Object(ref mut map) = value {
|
||||
map.insert("event.type".to_string(), Value::String(typ.to_string()));
|
||||
}
|
||||
println!("{}", serde_json::to_string(&value)?);
|
||||
}
|
||||
|
||||
WatchFormat::KeyValue => {
|
||||
let mut map = proto2kv(event)?;
|
||||
map.insert("event.type".to_string(), typ.to_string());
|
||||
println!("{}", kv2line(map),);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
|
||||
tty::IsTty,
|
||||
};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::GuestStatus,
|
||||
control::{watch_events_reply::Event, ConsoleDataReply, ConsoleDataRequest},
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
use tokio::{
|
||||
io::{stdin, stdout, AsyncReadExt, AsyncWriteExt},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tonic::Streaming;
|
||||
|
||||
pub struct StdioConsoleStream;
|
||||
|
||||
impl StdioConsoleStream {
|
||||
pub async fn stdin_stream(guest: String) -> impl Stream<Item = ConsoleDataRequest> {
|
||||
let mut stdin = stdin();
|
||||
stream! {
|
||||
yield ConsoleDataRequest { guest_id: guest, data: vec![] };
|
||||
|
||||
let mut buffer = vec![0u8; 60];
|
||||
loop {
|
||||
let size = match stdin.read(&mut buffer).await {
|
||||
Ok(size) => size,
|
||||
Err(error) => {
|
||||
debug!("failed to read stdin: {}", error);
|
||||
break;
|
||||
}
|
||||
};
|
||||
let data = buffer[0..size].to_vec();
|
||||
if size == 1 && buffer[0] == 0x1d {
|
||||
break;
|
||||
}
|
||||
yield ConsoleDataRequest { guest_id: String::default(), data };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stdout(mut stream: Streaming<ConsoleDataReply>) -> Result<()> {
|
||||
if stdin().is_tty() {
|
||||
enable_raw_mode()?;
|
||||
StdioConsoleStream::register_terminal_restore_hook()?;
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
while let Some(reply) = stream.next().await {
|
||||
let reply = reply?;
|
||||
if reply.data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
stdout.write_all(&reply.data).await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn guest_exit_hook(
|
||||
id: String,
|
||||
events: EventStream,
|
||||
) -> Result<JoinHandle<Option<i32>>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::GuestChanged(changed) => {
|
||||
let Some(guest) = changed.guest else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(state) = guest.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if guest.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(exit_info) = state.exit_info {
|
||||
return Some(exit_info.code);
|
||||
}
|
||||
|
||||
let status = state.status();
|
||||
if status == GuestStatus::Destroying || status == GuestStatus::Destroyed {
|
||||
return Some(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}))
|
||||
}
|
||||
|
||||
fn register_terminal_restore_hook() -> Result<()> {
|
||||
if stdin().is_tty() {
|
||||
ctrlc::set_handler(move || {
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_terminal_mode() {
|
||||
if is_raw_mode_enabled().unwrap_or(false) {
|
||||
let _ = disable_raw_mode();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::{Guest, GuestStatus};
|
||||
use prost_reflect::{DynamicMessage, ReflectMessage, Value};
|
||||
|
||||
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
|
||||
Ok(DynamicMessage::decode(
|
||||
proto.descriptor(),
|
||||
proto.encode_to_vec().as_slice(),
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
|
||||
let message = proto2dynamic(proto)?;
|
||||
let mut map = HashMap::new();
|
||||
|
||||
fn crawl(prefix: &str, map: &mut HashMap<String, String>, message: &DynamicMessage) {
|
||||
for (field, value) in message.fields() {
|
||||
let path = if prefix.is_empty() {
|
||||
field.name().to_string()
|
||||
} else {
|
||||
format!("{}.{}", prefix, field.name())
|
||||
};
|
||||
match value {
|
||||
Value::Message(child) => {
|
||||
crawl(&path, map, child);
|
||||
}
|
||||
|
||||
Value::EnumNumber(number) => {
|
||||
if let Some(e) = field.kind().as_enum() {
|
||||
if let Some(value) = e.get_value(*number) {
|
||||
map.insert(path, value.name().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Value::String(value) => {
|
||||
map.insert(path, value.clone());
|
||||
}
|
||||
|
||||
_ => {
|
||||
map.insert(path, value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crawl("", &mut map, &message);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn kv2line(map: HashMap<String, String>) -> String {
|
||||
map.iter()
|
||||
.map(|(k, v)| format!("{}=\"{}\"", k, v.replace('"', "\\\"")))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn guest_status_text(status: GuestStatus) -> String {
|
||||
match status {
|
||||
GuestStatus::Starting => "starting",
|
||||
GuestStatus::Started => "started",
|
||||
GuestStatus::Destroying => "destroying",
|
||||
GuestStatus::Destroyed => "destroyed",
|
||||
GuestStatus::Exited => "exited",
|
||||
GuestStatus::Failed => "failed",
|
||||
_ => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn guest_simple_line(guest: &Guest) -> String {
|
||||
let state = guest_status_text(
|
||||
guest
|
||||
.state
|
||||
.as_ref()
|
||||
.map(|x| x.status())
|
||||
.unwrap_or(GuestStatus::Unknown),
|
||||
);
|
||||
let name = guest.spec.as_ref().map(|x| x.name.as_str()).unwrap_or("");
|
||||
let network = guest.state.as_ref().and_then(|x| x.network.as_ref());
|
||||
let ipv4 = network.map(|x| x.guest_ipv4.as_str()).unwrap_or("");
|
||||
let ipv6 = network.map(|x| x.guest_ipv6.as_str()).unwrap_or("");
|
||||
format!("{}\t{}\t{}\t{}\t{}", guest.id, state, name, ipv4, ipv6)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
pub mod cli;
|
||||
pub mod console;
|
||||
pub mod format;
|
@ -1,36 +0,0 @@
|
||||
[package]
|
||||
name = "krata-daemon"
|
||||
description = "Daemon for the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
circular-buffer = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.7" }
|
||||
krata-runtime = { path = "../runtime", version = "^0.0.7" }
|
||||
log = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
redb = { workspace = true }
|
||||
signal-hook = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true, features = ["tls"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratad"
|
||||
|
||||
[[bin]]
|
||||
name = "kratad"
|
||||
path = "bin/daemon.rs"
|
@ -1,40 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use krata::dial::ControlDialAddress;
|
||||
use kratad::Daemon;
|
||||
use kratart::Runtime;
|
||||
use log::LevelFilter;
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct DaemonCommand {
|
||||
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
|
||||
listen: String,
|
||||
#[arg(short, long, default_value = "/var/lib/krata")]
|
||||
store: String,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
|
||||
.filter(Some("backhand::filesystem::writer"), LevelFilter::Warn)
|
||||
.init();
|
||||
mask_sighup()?;
|
||||
|
||||
let args = DaemonCommand::parse();
|
||||
let addr = ControlDialAddress::from_str(&args.listen)?;
|
||||
let runtime = Runtime::new(args.store.clone()).await?;
|
||||
let mut daemon = Daemon::new(args.store.clone(), runtime).await?;
|
||||
daemon.listen(addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mask_sighup() -> Result<()> {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
signal_hook::flag::register(signal_hook::consts::SIGHUP, flag)?;
|
||||
Ok(())
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use circular_buffer::CircularBuffer;
|
||||
use kratart::channel::ChannelService;
|
||||
use log::error;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{error::TrySendError, Receiver, Sender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
const CONSOLE_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
type RawConsoleBuffer = CircularBuffer<CONSOLE_BUFFER_SIZE, u8>;
|
||||
type ConsoleBuffer = Box<RawConsoleBuffer>;
|
||||
|
||||
type ListenerMap = Arc<Mutex<HashMap<u32, Vec<Sender<Vec<u8>>>>>>;
|
||||
type BufferMap = Arc<Mutex<HashMap<u32, ConsoleBuffer>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonConsoleHandle {
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonConsoleAttachHandle {
|
||||
pub initial: Vec<u8>,
|
||||
listeners: ListenerMap,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
domid: u32,
|
||||
}
|
||||
|
||||
impl DaemonConsoleAttachHandle {
|
||||
pub async fn unsubscribe(&self) -> Result<()> {
|
||||
let mut guard = self.listeners.lock().await;
|
||||
let _ = guard.remove(&self.domid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(&self, data: Vec<u8>) -> Result<()> {
|
||||
Ok(self.sender.send((self.domid, data)).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl DaemonConsoleHandle {
|
||||
pub async fn attach(
|
||||
&self,
|
||||
domid: u32,
|
||||
sender: Sender<Vec<u8>>,
|
||||
) -> Result<DaemonConsoleAttachHandle> {
|
||||
let buffers = self.buffers.lock().await;
|
||||
let buffer = buffers.get(&domid).map(|x| x.to_vec()).unwrap_or_default();
|
||||
drop(buffers);
|
||||
let mut listeners = self.listeners.lock().await;
|
||||
let senders = listeners.entry(domid).or_default();
|
||||
senders.push(sender);
|
||||
Ok(DaemonConsoleAttachHandle {
|
||||
initial: buffer,
|
||||
sender: self.sender.clone(),
|
||||
listeners: self.listeners.clone(),
|
||||
domid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonConsoleHandle {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonConsole {
|
||||
listeners: ListenerMap,
|
||||
buffers: BufferMap,
|
||||
receiver: Receiver<(u32, Vec<u8>)>,
|
||||
sender: Sender<(u32, Vec<u8>)>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DaemonConsole {
|
||||
pub async fn new() -> Result<DaemonConsole> {
|
||||
let (service, sender, receiver) =
|
||||
ChannelService::new("krata-console".to_string(), Some(0)).await?;
|
||||
let task = service.launch().await?;
|
||||
let listeners = Arc::new(Mutex::new(HashMap::new()));
|
||||
let buffers = Arc::new(Mutex::new(HashMap::new()));
|
||||
Ok(DaemonConsole {
|
||||
listeners,
|
||||
buffers,
|
||||
receiver,
|
||||
sender,
|
||||
task,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonConsoleHandle> {
|
||||
let listeners = self.listeners.clone();
|
||||
let buffers = self.buffers.clone();
|
||||
let sender = self.sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = self.process().await {
|
||||
error!("failed to process console: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(DaemonConsoleHandle {
|
||||
listeners,
|
||||
buffers,
|
||||
sender,
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(&mut self) -> Result<()> {
|
||||
loop {
|
||||
let Some((domid, data)) = self.receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
let mut buffers = self.buffers.lock().await;
|
||||
let buffer = buffers
|
||||
.entry(domid)
|
||||
.or_insert_with_key(|_| RawConsoleBuffer::boxed());
|
||||
buffer.extend_from_slice(&data);
|
||||
drop(buffers);
|
||||
let mut listeners = self.listeners.lock().await;
|
||||
if let Some(senders) = listeners.get_mut(&domid) {
|
||||
senders.retain(|sender| {
|
||||
!matches!(sender.try_send(data.to_vec()), Err(TrySendError::Closed(_)))
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonConsole {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
|
||||
use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use krata::v1::{
|
||||
common::{Guest, GuestState, GuestStatus},
|
||||
control::{
|
||||
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
|
||||
CreateGuestReply, CreateGuestRequest, DestroyGuestReply, DestroyGuestRequest,
|
||||
ListGuestsReply, ListGuestsRequest, ResolveGuestReply, ResolveGuestRequest,
|
||||
WatchEventsReply, WatchEventsRequest,
|
||||
},
|
||||
};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{channel, Sender},
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{console::DaemonConsoleHandle, db::GuestStore, event::DaemonEventContext};
|
||||
|
||||
pub struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ApiError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Status {
|
||||
fn from(value: ApiError) -> Self {
|
||||
Status::unknown(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeControlService {
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
}
|
||||
|
||||
impl RuntimeControlService {
|
||||
pub fn new(
|
||||
events: DaemonEventContext,
|
||||
console: DaemonConsoleHandle,
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
events,
|
||||
console,
|
||||
guests,
|
||||
guest_reconciler_notify,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConsoleDataSelect {
|
||||
Read(Option<Vec<u8>>),
|
||||
Write(Option<Result<ConsoleDataRequest, tonic::Status>>),
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl ControlService for RuntimeControlService {
|
||||
type ConsoleDataStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ConsoleDataReply, Status>> + Send + 'static>>;
|
||||
|
||||
type WatchEventsStream =
|
||||
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn create_guest(
|
||||
&self,
|
||||
request: Request<CreateGuestRequest>,
|
||||
) -> Result<Response<CreateGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let Some(spec) = request.spec else {
|
||||
return Err(ApiError {
|
||||
message: "guest spec not provided".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let uuid = Uuid::new_v4();
|
||||
self.guests
|
||||
.update(
|
||||
uuid,
|
||||
Guest {
|
||||
id: uuid.to_string(),
|
||||
state: Some(GuestState {
|
||||
status: GuestStatus::Starting.into(),
|
||||
network: None,
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
domid: u32::MAX,
|
||||
}),
|
||||
spec: Some(spec),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
self.guest_reconciler_notify
|
||||
.send(uuid)
|
||||
.await
|
||||
.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
Ok(Response::new(CreateGuestReply {
|
||||
guest_id: uuid.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn destroy_guest(
|
||||
&self,
|
||||
request: Request<DestroyGuestRequest>,
|
||||
) -> Result<Response<DestroyGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let Some(mut guest) = self.guests.read(uuid).await.map_err(ApiError::from)? else {
|
||||
return Err(ApiError {
|
||||
message: "guest not found".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
|
||||
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
|
||||
|
||||
if guest.state.as_ref().unwrap().status() == GuestStatus::Destroyed {
|
||||
return Err(ApiError {
|
||||
message: "guest already destroyed".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
guest.state.as_mut().unwrap().status = GuestStatus::Destroying.into();
|
||||
self.guests
|
||||
.update(uuid, guest)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
self.guest_reconciler_notify
|
||||
.send(uuid)
|
||||
.await
|
||||
.map_err(|x| ApiError {
|
||||
message: x.to_string(),
|
||||
})?;
|
||||
Ok(Response::new(DestroyGuestReply {}))
|
||||
}
|
||||
|
||||
async fn list_guests(
|
||||
&self,
|
||||
request: Request<ListGuestsRequest>,
|
||||
) -> Result<Response<ListGuestsReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let guests = self.guests.list().await.map_err(ApiError::from)?;
|
||||
let guests = guests.into_values().collect::<Vec<Guest>>();
|
||||
Ok(Response::new(ListGuestsReply { guests }))
|
||||
}
|
||||
|
||||
async fn resolve_guest(
|
||||
&self,
|
||||
request: Request<ResolveGuestRequest>,
|
||||
) -> Result<Response<ResolveGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let guests = self.guests.list().await.map_err(ApiError::from)?;
|
||||
let guests = guests
|
||||
.into_values()
|
||||
.filter(|x| {
|
||||
let comparison_spec = x.spec.as_ref().cloned().unwrap_or_default();
|
||||
(!request.name.is_empty() && comparison_spec.name == request.name)
|
||||
|| x.id == request.name
|
||||
})
|
||||
.collect::<Vec<Guest>>();
|
||||
Ok(Response::new(ResolveGuestReply {
|
||||
guest: guests.first().cloned(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn console_data(
|
||||
&self,
|
||||
request: Request<Streaming<ConsoleDataRequest>>,
|
||||
) -> Result<Response<Self::ConsoleDataStream>, Status> {
|
||||
let mut input = request.into_inner();
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(ApiError {
|
||||
message: "expected to have at least one request".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let request = request?;
|
||||
let uuid = Uuid::from_str(&request.guest_id).map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let guest = self
|
||||
.guests
|
||||
.read(uuid)
|
||||
.await
|
||||
.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| ApiError {
|
||||
message: "guest did not exist in the database".to_string(),
|
||||
})?;
|
||||
|
||||
let Some(ref state) = guest.state else {
|
||||
return Err(ApiError {
|
||||
message: "guest did not have state".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
|
||||
let domid = state.domid;
|
||||
if domid == 0 {
|
||||
return Err(ApiError {
|
||||
message: "invalid domid on the guest".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let (sender, mut receiver) = channel(100);
|
||||
let console = self
|
||||
.console
|
||||
.attach(domid, sender)
|
||||
.await
|
||||
.map_err(|error| ApiError {
|
||||
message: format!("failed to attach to console: {}", error),
|
||||
})?;
|
||||
|
||||
let output = try_stream! {
|
||||
yield ConsoleDataReply { data: console.initial.clone(), };
|
||||
loop {
|
||||
let what = select! {
|
||||
x = receiver.recv() => ConsoleDataSelect::Read(x),
|
||||
x = input.next() => ConsoleDataSelect::Write(x),
|
||||
};
|
||||
|
||||
match what {
|
||||
ConsoleDataSelect::Read(Some(data)) => {
|
||||
yield ConsoleDataReply { data, };
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Read(None) => {
|
||||
break;
|
||||
}
|
||||
|
||||
ConsoleDataSelect::Write(Some(request)) => {
|
||||
let request = request?;
|
||||
if !request.data.is_empty() {
|
||||
console.send(request.data).await.map_err(|error| ApiError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
}
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::new(Box::pin(output) as Self::ConsoleDataStream))
|
||||
}
|
||||
|
||||
async fn watch_events(
|
||||
&self,
|
||||
request: Request<WatchEventsRequest>,
|
||||
) -> Result<Response<Self::WatchEventsStream>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let mut events = self.events.subscribe();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = events.recv().await {
|
||||
yield WatchEventsReply { event: Some(event), };
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Box::pin(output) as Self::WatchEventsStream))
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::v1::common::Guest;
|
||||
use log::error;
|
||||
use prost::Message;
|
||||
use redb::{Database, ReadableTable, TableDefinition};
|
||||
use uuid::Uuid;
|
||||
|
||||
const GUESTS: TableDefinition<u128, &[u8]> = TableDefinition::new("guests");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuestStore {
|
||||
database: Arc<Database>,
|
||||
}
|
||||
|
||||
impl GuestStore {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let database = Database::create(path)?;
|
||||
let write = database.begin_write()?;
|
||||
let _ = write.open_table(GUESTS);
|
||||
write.commit()?;
|
||||
Ok(GuestStore {
|
||||
database: Arc::new(database),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn read(&self, id: Uuid) -> Result<Option<Guest>> {
|
||||
let read = self.database.begin_read()?;
|
||||
let table = read.open_table(GUESTS)?;
|
||||
let Some(entry) = table.get(id.to_u128_le())? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = entry.value();
|
||||
Ok(Some(Guest::decode(bytes)?))
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<Uuid, Guest>> {
|
||||
let mut guests: HashMap<Uuid, Guest> = HashMap::new();
|
||||
let read = self.database.begin_read()?;
|
||||
let table = read.open_table(GUESTS)?;
|
||||
for result in table.iter()? {
|
||||
let (key, value) = result?;
|
||||
let uuid = Uuid::from_u128_le(key.value());
|
||||
let state = match Guest::decode(value.value()) {
|
||||
Ok(state) => state,
|
||||
Err(error) => {
|
||||
error!(
|
||||
"found invalid guest state in database for uuid {}: {}",
|
||||
uuid, error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
guests.insert(uuid, state);
|
||||
}
|
||||
Ok(guests)
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: Uuid, entry: Guest) -> Result<()> {
|
||||
let write = self.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(GUESTS)?;
|
||||
let bytes = entry.encode_to_vec();
|
||||
table.insert(id.to_u128_le(), bytes.as_slice())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: Uuid) -> Result<()> {
|
||||
let write = self.database.begin_write()?;
|
||||
{
|
||||
let mut table = write.open_table(GUESTS)?;
|
||||
table.remove(id.to_u128_le())?;
|
||||
}
|
||||
write.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::{
|
||||
idm::protocol::{idm_event::Event, IdmPacket},
|
||||
v1::common::{GuestExitInfo, GuestState, GuestStatus},
|
||||
};
|
||||
use log::error;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
broadcast,
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
},
|
||||
task::JoinHandle,
|
||||
time,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
db::GuestStore,
|
||||
idm::{DaemonIdmHandle, DaemonIdmSubscribeHandle},
|
||||
};
|
||||
|
||||
pub type DaemonEvent = krata::v1::control::watch_events_reply::Event;
|
||||
|
||||
const EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
const IDM_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonEventContext {
|
||||
sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventContext {
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DaemonEvent> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
|
||||
pub fn send(&self, event: DaemonEvent) -> Result<()> {
|
||||
let _ = self.sender.send(event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonEventGenerator {
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
feed: broadcast::Receiver<DaemonEvent>,
|
||||
idm: DaemonIdmHandle,
|
||||
idms: HashMap<u32, (Uuid, DaemonIdmSubscribeHandle)>,
|
||||
idm_sender: Sender<(u32, IdmPacket)>,
|
||||
idm_receiver: Receiver<(u32, IdmPacket)>,
|
||||
_event_sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventGenerator {
|
||||
pub async fn new(
|
||||
guests: GuestStore,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
idm: DaemonIdmHandle,
|
||||
) -> Result<(DaemonEventContext, DaemonEventGenerator)> {
|
||||
let (sender, _) = broadcast::channel(EVENT_CHANNEL_QUEUE_LEN);
|
||||
let (idm_sender, idm_receiver) = channel(IDM_CHANNEL_QUEUE_LEN);
|
||||
let generator = DaemonEventGenerator {
|
||||
guests,
|
||||
guest_reconciler_notify,
|
||||
feed: sender.subscribe(),
|
||||
idm,
|
||||
idms: HashMap::new(),
|
||||
idm_sender,
|
||||
idm_receiver,
|
||||
_event_sender: sender.clone(),
|
||||
};
|
||||
let context = DaemonEventContext { sender };
|
||||
Ok((context, generator))
|
||||
}
|
||||
|
||||
async fn handle_feed_event(&mut self, event: &DaemonEvent) -> Result<()> {
|
||||
match event {
|
||||
DaemonEvent::GuestChanged(changed) => {
|
||||
let Some(ref guest) = changed.guest else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(ref state) = guest.state else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let status = state.status();
|
||||
let id = Uuid::from_str(&guest.id)?;
|
||||
let domid = state.domid;
|
||||
match status {
|
||||
GuestStatus::Started => {
|
||||
if let Entry::Vacant(e) = self.idms.entry(domid) {
|
||||
let subscribe =
|
||||
self.idm.subscribe(domid, self.idm_sender.clone()).await?;
|
||||
e.insert((id, subscribe));
|
||||
}
|
||||
}
|
||||
|
||||
GuestStatus::Destroyed => {
|
||||
if let Some((_, handle)) = self.idms.remove(&domid) {
|
||||
handle.unsubscribe().await?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_idm_packet(&mut self, id: Uuid, packet: IdmPacket) -> Result<()> {
|
||||
if let Some(Event::Exit(exit)) = packet.event.and_then(|x| x.event) {
|
||||
self.handle_exit_code(id, exit.code).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_exit_code(&mut self, id: Uuid, code: i32) -> Result<()> {
|
||||
if let Some(mut guest) = self.guests.read(id).await? {
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Exited.into(),
|
||||
network: guest.state.clone().unwrap_or_default().network,
|
||||
exit_info: Some(GuestExitInfo { code }),
|
||||
error_info: None,
|
||||
domid: guest.state.clone().map(|x| x.domid).unwrap_or(u32::MAX),
|
||||
});
|
||||
|
||||
self.guests.update(id, guest).await?;
|
||||
self.guest_reconciler_notify.send(id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn evaluate(&mut self) -> Result<()> {
|
||||
select! {
|
||||
x = self.idm_receiver.recv() => match x {
|
||||
Some((domid, packet)) => {
|
||||
if let Some((id, _)) = self.idms.get(&domid) {
|
||||
self.handle_idm_packet(*id, packet).await?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
x = self.feed.recv() => match x {
|
||||
Ok(event) => {
|
||||
self.handle_feed_event(&event).await
|
||||
},
|
||||
Err(error) => {
|
||||
Err(error.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
loop {
|
||||
if let Err(error) = self.evaluate().await {
|
||||
error!("failed to evaluate daemon events: {}", error);
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use krata::idm::protocol::IdmPacket;
|
||||
use kratart::channel::ChannelService;
|
||||
use log::{error, warn};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{Receiver, Sender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
type ListenerMap = Arc<Mutex<HashMap<u32, Sender<(u32, IdmPacket)>>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonIdmHandle {
|
||||
listeners: ListenerMap,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonIdmSubscribeHandle {
|
||||
domid: u32,
|
||||
listeners: ListenerMap,
|
||||
}
|
||||
|
||||
impl DaemonIdmSubscribeHandle {
|
||||
pub async fn unsubscribe(&self) -> Result<()> {
|
||||
let mut guard = self.listeners.lock().await;
|
||||
let _ = guard.remove(&self.domid);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DaemonIdmHandle {
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
domid: u32,
|
||||
sender: Sender<(u32, IdmPacket)>,
|
||||
) -> Result<DaemonIdmSubscribeHandle> {
|
||||
let mut guard = self.listeners.lock().await;
|
||||
guard.insert(domid, sender);
|
||||
Ok(DaemonIdmSubscribeHandle {
|
||||
domid,
|
||||
listeners: self.listeners.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonIdmHandle {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonIdm {
|
||||
listeners: ListenerMap,
|
||||
receiver: Receiver<(u32, Vec<u8>)>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DaemonIdm {
|
||||
pub async fn new() -> Result<DaemonIdm> {
|
||||
let (service, _, receiver) = ChannelService::new("krata-channel".to_string(), None).await?;
|
||||
let task = service.launch().await?;
|
||||
let listeners = Arc::new(Mutex::new(HashMap::new()));
|
||||
Ok(DaemonIdm {
|
||||
receiver,
|
||||
task,
|
||||
listeners,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<DaemonIdmHandle> {
|
||||
let listeners = self.listeners.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
let mut buffers: HashMap<u32, BytesMut> = HashMap::new();
|
||||
if let Err(error) = self.process(&mut buffers).await {
|
||||
error!("failed to process idm: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(DaemonIdmHandle {
|
||||
listeners,
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(&mut self, buffers: &mut HashMap<u32, BytesMut>) -> Result<()> {
|
||||
loop {
|
||||
let Some((domid, data)) = self.receiver.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
||||
buffer.extend_from_slice(&data);
|
||||
if buffer.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let size = (buffer[0] as u16 | (buffer[1] as u16) << 8) as usize;
|
||||
let needed = size + 2;
|
||||
if buffer.len() < needed {
|
||||
continue;
|
||||
}
|
||||
let mut packet = buffer.split_to(needed);
|
||||
packet.advance(2);
|
||||
match IdmPacket::decode(packet) {
|
||||
Ok(packet) => {
|
||||
let guard = self.listeners.lock().await;
|
||||
if let Some(sender) = guard.get(&domid) {
|
||||
if let Err(error) = sender.try_send((domid, packet)) {
|
||||
warn!("dropped idm packet from domain {}: {}", domid, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(packet) => {
|
||||
warn!("received invalid packet from domain {}: {}", domid, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DaemonIdm {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
use std::{net::SocketAddr, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::Result;
|
||||
use console::{DaemonConsole, DaemonConsoleHandle};
|
||||
use control::RuntimeControlService;
|
||||
use db::GuestStore;
|
||||
use event::{DaemonEventContext, DaemonEventGenerator};
|
||||
use idm::{DaemonIdm, DaemonIdmHandle};
|
||||
use krata::{dial::ControlDialAddress, v1::control::control_service_server::ControlServiceServer};
|
||||
use kratart::Runtime;
|
||||
use log::info;
|
||||
use reconcile::guest::GuestReconciler;
|
||||
use tokio::{
|
||||
net::UnixListener,
|
||||
sync::mpsc::{channel, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use tonic::transport::{Identity, Server, ServerTlsConfig};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod console;
|
||||
pub mod control;
|
||||
pub mod db;
|
||||
pub mod event;
|
||||
pub mod idm;
|
||||
pub mod reconcile;
|
||||
|
||||
pub struct Daemon {
|
||||
store: String,
|
||||
guests: GuestStore,
|
||||
events: DaemonEventContext,
|
||||
guest_reconciler_task: JoinHandle<()>,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
generator_task: JoinHandle<()>,
|
||||
_idm: DaemonIdmHandle,
|
||||
console: DaemonConsoleHandle,
|
||||
}
|
||||
|
||||
const GUEST_RECONCILER_QUEUE_LEN: usize = 1000;
|
||||
|
||||
impl Daemon {
|
||||
pub async fn new(store: String, runtime: Runtime) -> Result<Self> {
|
||||
let guests_db_path = format!("{}/guests.db", store);
|
||||
let guests = GuestStore::open(&PathBuf::from(guests_db_path))?;
|
||||
let (guest_reconciler_notify, guest_reconciler_receiver) =
|
||||
channel::<Uuid>(GUEST_RECONCILER_QUEUE_LEN);
|
||||
let idm = DaemonIdm::new().await?;
|
||||
let idm = idm.launch().await?;
|
||||
let console = DaemonConsole::new().await?;
|
||||
let console = console.launch().await?;
|
||||
let (events, generator) =
|
||||
DaemonEventGenerator::new(guests.clone(), guest_reconciler_notify.clone(), idm.clone())
|
||||
.await?;
|
||||
let runtime_for_reconciler = runtime.dupe().await?;
|
||||
let guest_reconciler = GuestReconciler::new(
|
||||
guests.clone(),
|
||||
events.clone(),
|
||||
runtime_for_reconciler,
|
||||
guest_reconciler_notify.clone(),
|
||||
)?;
|
||||
|
||||
let guest_reconciler_task = guest_reconciler.launch(guest_reconciler_receiver).await?;
|
||||
let generator_task = generator.launch().await?;
|
||||
Ok(Self {
|
||||
store,
|
||||
guests,
|
||||
events,
|
||||
guest_reconciler_task,
|
||||
guest_reconciler_notify,
|
||||
generator_task,
|
||||
_idm: idm,
|
||||
console,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> {
|
||||
let control_service = RuntimeControlService::new(
|
||||
self.events.clone(),
|
||||
self.console.clone(),
|
||||
self.guests.clone(),
|
||||
self.guest_reconciler_notify.clone(),
|
||||
);
|
||||
|
||||
let mut server = Server::builder();
|
||||
|
||||
if let ControlDialAddress::Tls {
|
||||
host: _,
|
||||
port: _,
|
||||
insecure,
|
||||
} = &addr
|
||||
{
|
||||
let mut tls_config = ServerTlsConfig::new();
|
||||
if !insecure {
|
||||
let certificate_path = format!("{}/tls/daemon.pem", self.store);
|
||||
let key_path = format!("{}/tls/daemon.key", self.store);
|
||||
tls_config = tls_config.identity(Identity::from_pem(certificate_path, key_path));
|
||||
}
|
||||
server = server.tls_config(tls_config)?;
|
||||
}
|
||||
|
||||
let server = server.add_service(ControlServiceServer::new(control_service));
|
||||
info!("listening on address {}", addr);
|
||||
match addr {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
tokio::fs::remove_file(&path).await?;
|
||||
}
|
||||
let listener = UnixListener::bind(path)?;
|
||||
let stream = UnixListenerStream::new(listener);
|
||||
server.serve_with_incoming(stream).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure: _,
|
||||
} => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Daemon {
|
||||
fn drop(&mut self) {
|
||||
self.guest_reconciler_task.abort();
|
||||
self.generator_task.abort();
|
||||
}
|
||||
}
|
@ -1,352 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use krata::v1::{
|
||||
common::{
|
||||
guest_image_spec::Image, Guest, GuestErrorInfo, GuestExitInfo, GuestNetworkState,
|
||||
GuestState, GuestStatus,
|
||||
},
|
||||
control::GuestChangedEvent,
|
||||
};
|
||||
use kratart::{launch::GuestLaunchRequest, GuestInfo, Runtime};
|
||||
use log::{error, info, trace, warn};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Mutex, RwLock,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time::sleep,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
db::GuestStore,
|
||||
event::{DaemonEvent, DaemonEventContext},
|
||||
};
|
||||
|
||||
const PARALLEL_LIMIT: u32 = 5;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GuestReconcilerResult {
|
||||
Unchanged,
|
||||
Changed { rerun: bool },
|
||||
}
|
||||
|
||||
struct GuestReconcilerEntry {
|
||||
task: JoinHandle<()>,
|
||||
sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl Drop for GuestReconcilerEntry {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GuestReconciler {
|
||||
guests: GuestStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
tasks: Arc<Mutex<HashMap<Uuid, GuestReconcilerEntry>>>,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
reconcile_lock: Arc<RwLock<()>>,
|
||||
}
|
||||
|
||||
impl GuestReconciler {
|
||||
pub fn new(
|
||||
guests: GuestStore,
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
guest_reconciler_notify: Sender<Uuid>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
guests,
|
||||
events,
|
||||
runtime,
|
||||
tasks: Arc::new(Mutex::new(HashMap::new())),
|
||||
guest_reconciler_notify,
|
||||
reconcile_lock: Arc::new(RwLock::with_max_readers((), PARALLEL_LIMIT)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self, mut notify: Receiver<Uuid>) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
if let Err(error) = self.reconcile_runtime(true).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
|
||||
loop {
|
||||
select! {
|
||||
x = notify.recv() => match x {
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
|
||||
Some(uuid) => {
|
||||
if let Err(error) = self.launch_task_if_needed(uuid).await {
|
||||
error!("failed to start guest reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
let map = self.tasks.lock().await;
|
||||
if let Some(entry) = map.get(&uuid) {
|
||||
if let Err(error) = entry.sender.send(()).await {
|
||||
error!("failed to notify guest reconciler task {}: {}", uuid, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(5)) => {
|
||||
if let Err(error) = self.reconcile_runtime(false).await {
|
||||
error!("runtime reconciler failed: {}", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn reconcile_runtime(&self, initial: bool) -> Result<()> {
|
||||
let _permit = self.reconcile_lock.write().await;
|
||||
trace!("reconciling runtime");
|
||||
let runtime_guests = self.runtime.list().await?;
|
||||
let stored_guests = self.guests.list().await?;
|
||||
for (uuid, mut stored_guest) in stored_guests {
|
||||
let previous_guest = stored_guest.clone();
|
||||
let runtime_guest = runtime_guests.iter().find(|x| x.uuid == uuid);
|
||||
match runtime_guest {
|
||||
None => {
|
||||
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
|
||||
if state.status() == GuestStatus::Started {
|
||||
state.status = GuestStatus::Starting.into();
|
||||
}
|
||||
stored_guest.state = Some(state);
|
||||
}
|
||||
|
||||
Some(runtime) => {
|
||||
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
|
||||
if let Some(code) = runtime.state.exit_code {
|
||||
state.status = GuestStatus::Exited.into();
|
||||
state.exit_info = Some(GuestExitInfo { code });
|
||||
} else {
|
||||
state.status = GuestStatus::Started.into();
|
||||
}
|
||||
state.network = Some(guestinfo_to_networkstate(runtime));
|
||||
stored_guest.state = Some(state);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = stored_guest != previous_guest;
|
||||
|
||||
if changed || initial {
|
||||
self.guests.update(uuid, stored_guest).await?;
|
||||
let _ = self.guest_reconciler_notify.try_send(uuid);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reconcile(&self, uuid: Uuid) -> Result<bool> {
|
||||
let _runtime_reconcile_permit = self.reconcile_lock.read().await;
|
||||
let Some(mut guest) = self.guests.read(uuid).await? else {
|
||||
warn!(
|
||||
"notified of reconcile for guest {} but it didn't exist",
|
||||
uuid
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
info!("reconciling guest {}", uuid);
|
||||
|
||||
self.events
|
||||
.send(DaemonEvent::GuestChanged(GuestChangedEvent {
|
||||
guest: Some(guest.clone()),
|
||||
}))?;
|
||||
|
||||
let start_status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
|
||||
let result = match start_status {
|
||||
GuestStatus::Starting => self.start(uuid, &mut guest).await,
|
||||
GuestStatus::Exited => self.exited(&mut guest).await,
|
||||
GuestStatus::Destroying => self.destroy(uuid, &mut guest).await,
|
||||
_ => Ok(GuestReconcilerResult::Unchanged),
|
||||
};
|
||||
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
|
||||
guest.state.as_mut().unwrap().status = GuestStatus::Failed.into();
|
||||
guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo {
|
||||
message: error.to_string(),
|
||||
});
|
||||
warn!("failed to start guest {}: {}", guest.id, error);
|
||||
GuestReconcilerResult::Changed { rerun: false }
|
||||
}
|
||||
};
|
||||
|
||||
info!("reconciled guest {}", uuid);
|
||||
|
||||
let status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
|
||||
let destroyed = status == GuestStatus::Destroyed;
|
||||
|
||||
let rerun = if let GuestReconcilerResult::Changed { rerun } = result {
|
||||
let event = DaemonEvent::GuestChanged(GuestChangedEvent {
|
||||
guest: Some(guest.clone()),
|
||||
});
|
||||
|
||||
if destroyed {
|
||||
self.guests.remove(uuid).await?;
|
||||
let mut map = self.tasks.lock().await;
|
||||
map.remove(&uuid);
|
||||
} else {
|
||||
self.guests.update(uuid, guest.clone()).await?;
|
||||
}
|
||||
|
||||
self.events.send(event)?;
|
||||
rerun
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(rerun)
|
||||
}
|
||||
|
||||
async fn start(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
let Some(ref spec) = guest.spec else {
|
||||
return Err(anyhow!("guest spec not specified"));
|
||||
};
|
||||
|
||||
let Some(ref image) = spec.image else {
|
||||
return Err(anyhow!("image spec not provided"));
|
||||
};
|
||||
let oci = match image.image {
|
||||
Some(Image::Oci(ref oci)) => oci,
|
||||
None => {
|
||||
return Err(anyhow!("oci spec not specified"));
|
||||
}
|
||||
};
|
||||
|
||||
let task = spec.task.as_ref().cloned().unwrap_or_default();
|
||||
|
||||
let info = self
|
||||
.runtime
|
||||
.launch(GuestLaunchRequest {
|
||||
uuid: Some(uuid),
|
||||
name: if spec.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&spec.name)
|
||||
},
|
||||
image: &oci.image,
|
||||
vcpus: spec.vcpus,
|
||||
mem: spec.mem,
|
||||
env: task
|
||||
.environment
|
||||
.iter()
|
||||
.map(|x| (x.key.clone(), x.value.clone()))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
run: empty_vec_optional(task.command.clone()),
|
||||
debug: false,
|
||||
})
|
||||
.await?;
|
||||
info!("started guest {}", uuid);
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Started.into(),
|
||||
network: Some(guestinfo_to_networkstate(&info)),
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
domid: info.domid,
|
||||
});
|
||||
Ok(GuestReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
|
||||
async fn exited(&self, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
if let Some(ref mut state) = guest.state {
|
||||
state.set_status(GuestStatus::Destroying);
|
||||
Ok(GuestReconcilerResult::Changed { rerun: true })
|
||||
} else {
|
||||
Ok(GuestReconcilerResult::Unchanged)
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<GuestReconcilerResult> {
|
||||
if let Err(error) = self.runtime.destroy(uuid).await {
|
||||
trace!("failed to destroy runtime guest {}: {}", uuid, error);
|
||||
}
|
||||
|
||||
info!("destroyed guest {}", uuid);
|
||||
guest.state = Some(GuestState {
|
||||
status: GuestStatus::Destroyed.into(),
|
||||
network: None,
|
||||
exit_info: None,
|
||||
error_info: None,
|
||||
domid: guest.state.as_ref().map(|x| x.domid).unwrap_or(u32::MAX),
|
||||
});
|
||||
Ok(GuestReconcilerResult::Changed { rerun: false })
|
||||
}
|
||||
|
||||
async fn launch_task_if_needed(&self, uuid: Uuid) -> Result<()> {
|
||||
let mut map = self.tasks.lock().await;
|
||||
match map.entry(uuid) {
|
||||
Entry::Occupied(_) => {}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(self.launch_task(uuid).await?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn launch_task(&self, uuid: Uuid) -> Result<GuestReconcilerEntry> {
|
||||
let this = self.clone();
|
||||
let (sender, mut receiver) = channel(10);
|
||||
let task = tokio::task::spawn(async move {
|
||||
'notify_loop: loop {
|
||||
if receiver.recv().await.is_none() {
|
||||
break 'notify_loop;
|
||||
}
|
||||
|
||||
'rerun_loop: loop {
|
||||
let rerun = match this.reconcile(uuid).await {
|
||||
Ok(rerun) => rerun,
|
||||
Err(error) => {
|
||||
error!("failed to reconcile guest {}: {}", uuid, error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if rerun {
|
||||
continue 'rerun_loop;
|
||||
}
|
||||
break 'rerun_loop;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(GuestReconcilerEntry { task, sender })
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn guestinfo_to_networkstate(info: &GuestInfo) -> GuestNetworkState {
|
||||
GuestNetworkState {
|
||||
guest_ipv4: info.guest_ipv4.map(|x| x.to_string()).unwrap_or_default(),
|
||||
guest_ipv6: info.guest_ipv6.map(|x| x.to_string()).unwrap_or_default(),
|
||||
guest_mac: info.guest_mac.as_ref().cloned().unwrap_or_default(),
|
||||
gateway_ipv4: info.gateway_ipv4.map(|x| x.to_string()).unwrap_or_default(),
|
||||
gateway_ipv6: info.gateway_ipv6.map(|x| x.to_string()).unwrap_or_default(),
|
||||
gateway_mac: info.gateway_mac.as_ref().cloned().unwrap_or_default(),
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod guest;
|
@ -1,36 +0,0 @@
|
||||
[package]
|
||||
name = "krata-guest"
|
||||
description = "Guest services for the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
cgroups-rs = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.7" }
|
||||
krata-xenstore = { path = "../xen/xenstore", version = "^0.0.7" }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nix = { workspace = true, features = ["ioctl", "process", "fs"] }
|
||||
oci-spec = { workspace = true }
|
||||
path-absolutize = { workspace = true }
|
||||
rtnetlink = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sys-mount = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "krataguest"
|
||||
|
||||
[[bin]]
|
||||
name = "krataguest"
|
||||
path = "bin/init.rs"
|
@ -1,28 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use env_logger::Env;
|
||||
use krataguest::{death, init::GuestInit};
|
||||
use log::error;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env::set_var("RUST_BACKTRACE", "1");
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
|
||||
if env::var("KRATA_UNSAFE_ALWAYS_ALLOW_INIT").unwrap_or("0".to_string()) != "1" {
|
||||
let pid = std::process::id();
|
||||
if pid > 3 {
|
||||
return Err(anyhow!(
|
||||
"not running because the pid of {} indicates this is probably not \
|
||||
the right context for the init daemon. \
|
||||
run with KRATA_UNSAFE_ALWAYS_ALLOW_INIT=1 to bypass this check",
|
||||
pid
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut guest = GuestInit::new();
|
||||
if let Err(error) = guest.init().await {
|
||||
error!("failed to initialize guest: {}", error);
|
||||
death(127).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
use crate::{
|
||||
childwait::{ChildEvent, ChildWait},
|
||||
death,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use cgroups_rs::Cgroup;
|
||||
use krata::idm::{
|
||||
client::IdmClient,
|
||||
protocol::{idm_event::Event, IdmEvent, IdmExitEvent, IdmPacket},
|
||||
};
|
||||
use log::error;
|
||||
use nix::unistd::Pid;
|
||||
use tokio::select;
|
||||
|
||||
pub struct GuestBackground {
|
||||
idm: IdmClient,
|
||||
child: Pid,
|
||||
_cgroup: Cgroup,
|
||||
wait: ChildWait,
|
||||
}
|
||||
|
||||
impl GuestBackground {
|
||||
pub async fn new(idm: IdmClient, cgroup: Cgroup, child: Pid) -> Result<GuestBackground> {
|
||||
Ok(GuestBackground {
|
||||
idm,
|
||||
child,
|
||||
_cgroup: cgroup,
|
||||
wait: ChildWait::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = self.idm.receiver.recv() => match x {
|
||||
Some(_packet) => {
|
||||
|
||||
},
|
||||
|
||||
None => {
|
||||
error!("idm packet channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
event = self.wait.recv() => match event {
|
||||
Some(event) => self.child_event(event).await?,
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn child_event(&mut self, event: ChildEvent) -> Result<()> {
|
||||
if event.pid == self.child {
|
||||
self.idm
|
||||
.sender
|
||||
.send(IdmPacket {
|
||||
event: Some(IdmEvent {
|
||||
event: Some(Event::Exit(IdmExitEvent { code: event.status })),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
death(event.status).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
use std::{
|
||||
ptr::addr_of_mut,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use libc::{c_int, waitpid, WEXITSTATUS, WIFEXITED};
|
||||
use log::warn;
|
||||
use nix::unistd::Pid;
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
const CHILD_WAIT_QUEUE_LEN: usize = 10;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ChildEvent {
|
||||
pub pid: Pid,
|
||||
pub status: c_int,
|
||||
}
|
||||
|
||||
pub struct ChildWait {
|
||||
receiver: Receiver<ChildEvent>,
|
||||
signal: Arc<AtomicBool>,
|
||||
_task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl ChildWait {
|
||||
pub fn new() -> Result<ChildWait> {
|
||||
let (sender, receiver) = channel(CHILD_WAIT_QUEUE_LEN);
|
||||
let signal = Arc::new(AtomicBool::new(false));
|
||||
let mut processor = ChildWaitTask {
|
||||
sender,
|
||||
signal: signal.clone(),
|
||||
};
|
||||
let task = thread::spawn(move || {
|
||||
if let Err(error) = processor.process() {
|
||||
warn!("failed to process child updates: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(ChildWait {
|
||||
receiver,
|
||||
signal,
|
||||
_task: task,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn recv(&mut self) -> Option<ChildEvent> {
|
||||
self.receiver.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
struct ChildWaitTask {
|
||||
sender: Sender<ChildEvent>,
|
||||
signal: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ChildWaitTask {
|
||||
fn process(&mut self) -> Result<()> {
|
||||
loop {
|
||||
let mut status: c_int = 0;
|
||||
let pid = unsafe { waitpid(-1, addr_of_mut!(status), 0) };
|
||||
|
||||
if WIFEXITED(status) {
|
||||
let event = ChildEvent {
|
||||
pid: Pid::from_raw(pid),
|
||||
status: WEXITSTATUS(status),
|
||||
};
|
||||
let _ = self.sender.try_send(event);
|
||||
|
||||
if self.signal.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ChildWait {
|
||||
fn drop(&mut self) {
|
||||
self.signal.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
@ -1,590 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use cgroups_rs::{Cgroup, CgroupPid};
|
||||
use futures::stream::TryStreamExt;
|
||||
use ipnetwork::IpNetwork;
|
||||
use krata::ethtool::EthtoolHandle;
|
||||
use krata::idm::client::IdmClient;
|
||||
use krata::launchcfg::{LaunchInfo, LaunchNetwork};
|
||||
use libc::{sethostname, setsid, TIOCSCTTY};
|
||||
use log::{trace, warn};
|
||||
use nix::ioctl_write_int_bad;
|
||||
use nix::unistd::{dup2, execve, fork, ForkResult, Pid};
|
||||
use oci_spec::image::{Config, ImageConfiguration};
|
||||
use path_absolutize::Absolutize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use std::fs::{File, OpenOptions, Permissions};
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::linux::fs::MetadataExt;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::{chroot, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use sys_mount::{FilesystemType, Mount, MountFlags};
|
||||
use tokio::fs;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::background::GuestBackground;
|
||||
|
||||
const IMAGE_BLOCK_DEVICE_PATH: &str = "/dev/xvda";
|
||||
const CONFIG_BLOCK_DEVICE_PATH: &str = "/dev/xvdb";
|
||||
|
||||
const IMAGE_MOUNT_PATH: &str = "/image";
|
||||
const CONFIG_MOUNT_PATH: &str = "/config";
|
||||
const OVERLAY_MOUNT_PATH: &str = "/overlay";
|
||||
|
||||
const OVERLAY_IMAGE_BIND_PATH: &str = "/overlay/image";
|
||||
const OVERLAY_WORK_PATH: &str = "/overlay/work";
|
||||
const OVERLAY_UPPER_PATH: &str = "/overlay/upper";
|
||||
|
||||
const SYS_PATH: &str = "/sys";
|
||||
const PROC_PATH: &str = "/proc";
|
||||
const DEV_PATH: &str = "/dev";
|
||||
|
||||
const NEW_ROOT_PATH: &str = "/newroot";
|
||||
const NEW_ROOT_SYS_PATH: &str = "/newroot/sys";
|
||||
const NEW_ROOT_PROC_PATH: &str = "/newroot/proc";
|
||||
const NEW_ROOT_DEV_PATH: &str = "/newroot/dev";
|
||||
|
||||
const IMAGE_CONFIG_JSON_PATH: &str = "/config/image/config.json";
|
||||
const LAUNCH_CONFIG_JSON_PATH: &str = "/config/launch.json";
|
||||
|
||||
ioctl_write_int_bad!(set_controlling_terminal, TIOCSCTTY);
|
||||
|
||||
pub struct GuestInit {}
|
||||
|
||||
impl Default for GuestInit {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GuestInit {
|
||||
pub fn new() -> GuestInit {
|
||||
GuestInit {}
|
||||
}
|
||||
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
self.early_init().await?;
|
||||
|
||||
trace!("opening console descriptor");
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/console")
|
||||
{
|
||||
Ok(console) => self.map_console(&console)?,
|
||||
Err(error) => warn!("failed to open console: {}", error),
|
||||
};
|
||||
|
||||
let idm = IdmClient::open("/dev/hvc1")
|
||||
.await
|
||||
.map_err(|x| anyhow!("failed to open idm client: {}", x))?;
|
||||
self.mount_squashfs_images().await?;
|
||||
|
||||
let config = self.parse_image_config().await?;
|
||||
let launch = self.parse_launch_config().await?;
|
||||
|
||||
self.mount_new_root().await?;
|
||||
self.nuke_initrd().await?;
|
||||
self.bind_new_root().await?;
|
||||
|
||||
if let Some(hostname) = launch.hostname.clone() {
|
||||
let result = unsafe {
|
||||
sethostname(
|
||||
hostname.as_bytes().as_ptr() as *mut libc::c_char,
|
||||
hostname.len(),
|
||||
)
|
||||
};
|
||||
if result != 0 {
|
||||
warn!("failed to set hostname: {}", result);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(network) = &launch.network {
|
||||
trace!("initializing network");
|
||||
if let Err(error) = self.network_setup(network).await {
|
||||
warn!("failed to initialize network: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cfg) = config.config() {
|
||||
trace!("running guest task");
|
||||
self.run(cfg, &launch, idm).await?;
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"unable to determine what to execute, image config doesn't tell us"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn early_init(&mut self) -> Result<()> {
|
||||
trace!("early init");
|
||||
self.create_dir("/dev", Some(0o0755)).await?;
|
||||
self.create_dir("/proc", None).await?;
|
||||
self.create_dir("/sys", Some(0o0555)).await?;
|
||||
self.create_dir("/root", Some(0o0700)).await?;
|
||||
self.create_dir("/tmp", None).await?;
|
||||
self.create_dir("/run", Some(0o0755)).await?;
|
||||
self.mount_kernel_fs("devtmpfs", "/dev", "mode=0755", None)
|
||||
.await?;
|
||||
self.mount_kernel_fs("proc", "/proc", "", None).await?;
|
||||
self.mount_kernel_fs("sysfs", "/sys", "", None).await?;
|
||||
fs::symlink("/proc/self/fd", "/dev/fd").await?;
|
||||
fs::symlink("/proc/self/fd/0", "/dev/stdin").await?;
|
||||
fs::symlink("/proc/self/fd/1", "/dev/stdout").await?;
|
||||
fs::symlink("/proc/self/fd/2", "/dev/stderr").await?;
|
||||
self.mount_kernel_fs("cgroup2", "/sys/fs/cgroup", "", Some(MountFlags::RELATIME))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_dir(&mut self, path: &str, mode: Option<u32>) -> Result<()> {
|
||||
let path = Path::new(path);
|
||||
if !path.is_dir() {
|
||||
trace!("creating directory {:?}", path);
|
||||
fs::create_dir(path).await?;
|
||||
}
|
||||
if let Some(mode) = mode {
|
||||
let permissions = Permissions::from_mode(mode);
|
||||
trace!("setting directory {:?} permissions to {:?}", path, mode);
|
||||
fs::set_permissions(path, permissions).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_kernel_fs(
|
||||
&mut self,
|
||||
fstype: &str,
|
||||
path: &str,
|
||||
data: &str,
|
||||
flags: Option<MountFlags>,
|
||||
) -> Result<()> {
|
||||
trace!("mounting kernel fs {} to {}", fstype, path);
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual(fstype))
|
||||
.flags(MountFlags::NOEXEC | MountFlags::NOSUID | flags.unwrap_or(MountFlags::empty()))
|
||||
.data(data)
|
||||
.mount(fstype, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_console(&mut self, console: &File) -> Result<()> {
|
||||
trace!("mapping console");
|
||||
dup2(console.as_raw_fd(), 0)?;
|
||||
dup2(console.as_raw_fd(), 1)?;
|
||||
dup2(console.as_raw_fd(), 2)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_squashfs_images(&mut self) -> Result<()> {
|
||||
trace!("mounting squashfs images");
|
||||
let image_mount_path = Path::new(IMAGE_MOUNT_PATH);
|
||||
let config_mount_path = Path::new(CONFIG_MOUNT_PATH);
|
||||
self.mount_squashfs(Path::new(IMAGE_BLOCK_DEVICE_PATH), image_mount_path)
|
||||
.await?;
|
||||
self.mount_squashfs(Path::new(CONFIG_BLOCK_DEVICE_PATH), config_mount_path)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_squashfs(&mut self, from: &Path, to: &Path) -> Result<()> {
|
||||
trace!("mounting squashfs image {:?} to {:?}", from, to);
|
||||
if !to.is_dir() {
|
||||
fs::create_dir(to).await?;
|
||||
}
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("squashfs"))
|
||||
.flags(MountFlags::RDONLY)
|
||||
.mount(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_move_subtree(&mut self, from: &Path, to: &Path) -> Result<()> {
|
||||
trace!("moving subtree {:?} to {:?}", from, to);
|
||||
if !to.is_dir() {
|
||||
fs::create_dir(to).await?;
|
||||
}
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("none"))
|
||||
.flags(MountFlags::MOVE)
|
||||
.mount(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_new_root(&mut self) -> Result<()> {
|
||||
trace!("mounting new root");
|
||||
self.mount_overlay_tmpfs().await?;
|
||||
self.bind_image_to_overlay_tmpfs().await?;
|
||||
self.mount_overlay_to_new_root().await?;
|
||||
std::env::set_current_dir(NEW_ROOT_PATH)?;
|
||||
trace!("mounted new root");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_overlay_tmpfs(&mut self) -> Result<()> {
|
||||
fs::create_dir(OVERLAY_MOUNT_PATH).await?;
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("tmpfs"))
|
||||
.mount("tmpfs", OVERLAY_MOUNT_PATH)?;
|
||||
fs::create_dir(OVERLAY_UPPER_PATH).await?;
|
||||
fs::create_dir(OVERLAY_WORK_PATH).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn bind_image_to_overlay_tmpfs(&mut self) -> Result<()> {
|
||||
fs::create_dir(OVERLAY_IMAGE_BIND_PATH).await?;
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("none"))
|
||||
.flags(MountFlags::BIND | MountFlags::RDONLY)
|
||||
.mount(IMAGE_MOUNT_PATH, OVERLAY_IMAGE_BIND_PATH)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_overlay_to_new_root(&mut self) -> Result<()> {
|
||||
fs::create_dir(NEW_ROOT_PATH).await?;
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("overlay"))
|
||||
.flags(MountFlags::NOATIME)
|
||||
.data(&format!(
|
||||
"lowerdir={},upperdir={},workdir={}",
|
||||
OVERLAY_IMAGE_BIND_PATH, OVERLAY_UPPER_PATH, OVERLAY_WORK_PATH
|
||||
))
|
||||
.mount(format!("overlayfs:{}", OVERLAY_MOUNT_PATH), NEW_ROOT_PATH)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn parse_image_config(&mut self) -> Result<ImageConfiguration> {
|
||||
let image_config_path = Path::new(IMAGE_CONFIG_JSON_PATH);
|
||||
let content = fs::read_to_string(image_config_path).await?;
|
||||
let config = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn parse_launch_config(&mut self) -> Result<LaunchInfo> {
|
||||
trace!("parsing launch config");
|
||||
let launch_config = Path::new(LAUNCH_CONFIG_JSON_PATH);
|
||||
let content = fs::read_to_string(launch_config).await?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
async fn nuke_initrd(&mut self) -> Result<()> {
|
||||
trace!("nuking initrd");
|
||||
let initrd_dev = fs::metadata("/").await?.st_dev();
|
||||
for item in WalkDir::new("/")
|
||||
.same_file_system(true)
|
||||
.follow_links(false)
|
||||
.contents_first(true)
|
||||
{
|
||||
if item.is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let item = item?;
|
||||
let metadata = match item.metadata() {
|
||||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if metadata.st_dev() != initrd_dev {
|
||||
continue;
|
||||
}
|
||||
|
||||
if metadata.is_symlink() || metadata.is_file() {
|
||||
let _ = fs::remove_file(item.path()).await;
|
||||
trace!("deleting file {:?}", item.path());
|
||||
} else if metadata.is_dir() {
|
||||
let _ = fs::remove_dir(item.path()).await;
|
||||
trace!("deleting directory {:?}", item.path());
|
||||
}
|
||||
}
|
||||
trace!("nuked initrd");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn bind_new_root(&mut self) -> Result<()> {
|
||||
self.mount_move_subtree(Path::new(SYS_PATH), Path::new(NEW_ROOT_SYS_PATH))
|
||||
.await?;
|
||||
self.mount_move_subtree(Path::new(PROC_PATH), Path::new(NEW_ROOT_PROC_PATH))
|
||||
.await?;
|
||||
self.mount_move_subtree(Path::new(DEV_PATH), Path::new(NEW_ROOT_DEV_PATH))
|
||||
.await?;
|
||||
trace!("binding new root");
|
||||
Mount::builder()
|
||||
.fstype(FilesystemType::Manual("none"))
|
||||
.flags(MountFlags::BIND)
|
||||
.mount(".", "/")?;
|
||||
trace!("chrooting into new root");
|
||||
chroot(".")?;
|
||||
trace!("setting root as current directory");
|
||||
std::env::set_current_dir("/")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn network_setup(&mut self, network: &LaunchNetwork) -> Result<()> {
|
||||
trace!("setting up network for link");
|
||||
|
||||
let etc = PathBuf::from_str("/etc")?;
|
||||
if !etc.exists() {
|
||||
fs::create_dir(etc).await?;
|
||||
}
|
||||
let resolv = PathBuf::from_str("/etc/resolv.conf")?;
|
||||
let mut lines = vec!["# krata resolver configuration".to_string()];
|
||||
for nameserver in &network.resolver.nameservers {
|
||||
lines.push(format!("nameserver {}", nameserver));
|
||||
}
|
||||
|
||||
let mut conf = lines.join("\n");
|
||||
conf.push('\n');
|
||||
fs::write(resolv, conf).await?;
|
||||
self.network_configure_ethtool(network).await?;
|
||||
self.network_configure_link(network).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn network_configure_link(&mut self, network: &LaunchNetwork) -> Result<()> {
|
||||
let (connection, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let mut links = handle.link().get().match_name("lo".to_string()).execute();
|
||||
let Some(link) = links.try_next().await? else {
|
||||
warn!("unable to find link named lo");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
handle.link().set(link.header.index).up().execute().await?;
|
||||
|
||||
let ipv4_network: IpNetwork = network.ipv4.address.parse()?;
|
||||
let ipv4_gateway: Ipv4Addr = network.ipv4.gateway.parse()?;
|
||||
let ipv6_network: IpNetwork = network.ipv6.address.parse()?;
|
||||
let ipv6_gateway: Ipv6Addr = network.ipv6.gateway.parse()?;
|
||||
|
||||
let mut links = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(network.link.clone())
|
||||
.execute();
|
||||
let Some(link) = links.try_next().await? else {
|
||||
warn!("unable to find link named {}", network.link);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
handle
|
||||
.address()
|
||||
.add(link.header.index, ipv4_network.ip(), ipv4_network.prefix())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let ipv6_result = handle
|
||||
.address()
|
||||
.add(link.header.index, ipv6_network.ip(), ipv6_network.prefix())
|
||||
.execute()
|
||||
.await;
|
||||
|
||||
let ipv6_ready = match ipv6_result {
|
||||
Ok(()) => true,
|
||||
Err(error) => {
|
||||
warn!("unable to setup ipv6 network: {}", error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
handle.link().set(link.header.index).up().execute().await?;
|
||||
|
||||
handle
|
||||
.route()
|
||||
.add()
|
||||
.v4()
|
||||
.destination_prefix(Ipv4Addr::UNSPECIFIED, 0)
|
||||
.output_interface(link.header.index)
|
||||
.gateway(ipv4_gateway)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
if ipv6_ready {
|
||||
let ipv6_gw_result = handle
|
||||
.route()
|
||||
.add()
|
||||
.v6()
|
||||
.destination_prefix(Ipv6Addr::UNSPECIFIED, 0)
|
||||
.output_interface(link.header.index)
|
||||
.gateway(ipv6_gateway)
|
||||
.execute()
|
||||
.await;
|
||||
|
||||
if let Err(error) = ipv6_gw_result {
|
||||
warn!("failed to add ipv6 gateway route: {}", error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn network_configure_ethtool(&mut self, network: &LaunchNetwork) -> Result<()> {
|
||||
let mut handle = EthtoolHandle::new()?;
|
||||
handle.set_gso(&network.link, false)?;
|
||||
handle.set_tso(&network.link, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(&mut self, config: &Config, launch: &LaunchInfo, idm: IdmClient) -> Result<()> {
|
||||
let mut cmd = match config.cmd() {
|
||||
None => vec![],
|
||||
Some(value) => value.clone(),
|
||||
};
|
||||
|
||||
if launch.run.is_some() {
|
||||
cmd.clone_from(launch.run.as_ref().unwrap());
|
||||
}
|
||||
|
||||
if let Some(entrypoint) = config.entrypoint() {
|
||||
for item in entrypoint.iter().rev() {
|
||||
cmd.insert(0, item.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.is_empty() {
|
||||
cmd.push("/bin/sh".to_string());
|
||||
}
|
||||
|
||||
let path = cmd.remove(0);
|
||||
|
||||
let mut env = HashMap::new();
|
||||
if let Some(config_env) = config.env() {
|
||||
env.extend(GuestInit::env_map(config_env));
|
||||
}
|
||||
env.extend(launch.env.clone());
|
||||
env.insert("KRATA_CONTAINER".to_string(), "1".to_string());
|
||||
env.insert("TERM".to_string(), "vt100".to_string());
|
||||
|
||||
let path = GuestInit::resolve_executable(&env, path.into())?;
|
||||
let Some(file_name) = path.file_name() else {
|
||||
return Err(anyhow!("cannot get file name of command path"));
|
||||
};
|
||||
let Some(file_name) = file_name.to_str() else {
|
||||
return Err(anyhow!("cannot get file name of command path as str"));
|
||||
};
|
||||
cmd.insert(0, file_name.to_string());
|
||||
let env = GuestInit::env_list(env);
|
||||
|
||||
trace!("running guest command: {}", cmd.join(" "));
|
||||
|
||||
let path = CString::new(path.as_os_str().as_bytes())?;
|
||||
let cmd = GuestInit::strings_as_cstrings(cmd)?;
|
||||
let env = GuestInit::strings_as_cstrings(env)?;
|
||||
let mut working_dir = config
|
||||
.working_dir()
|
||||
.as_ref()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or("/".to_string());
|
||||
|
||||
if working_dir.is_empty() {
|
||||
working_dir = "/".to_string();
|
||||
}
|
||||
|
||||
let cgroup = self.init_cgroup().await?;
|
||||
self.fork_and_exec(idm, cgroup, working_dir, path, cmd, env)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn init_cgroup(&self) -> Result<Cgroup> {
|
||||
trace!("initializing cgroup");
|
||||
let hierarchy = cgroups_rs::hierarchies::auto();
|
||||
let cgroup = Cgroup::new(hierarchy, "krata-guest-task")?;
|
||||
cgroup.set_cgroup_type("threaded")?;
|
||||
trace!("initialized cgroup");
|
||||
Ok(cgroup)
|
||||
}
|
||||
|
||||
fn strings_as_cstrings(values: Vec<String>) -> Result<Vec<CString>> {
|
||||
let mut results: Vec<CString> = vec![];
|
||||
for value in values {
|
||||
results.push(CString::new(value.as_bytes().to_vec())?);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn resolve_executable(env: &HashMap<String, String>, path: PathBuf) -> Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
return Ok(path.absolutize()?.to_path_buf());
|
||||
}
|
||||
|
||||
if let Some(path_var) = env.get("PATH") {
|
||||
for item in path_var.split(':') {
|
||||
let mut exe_path: PathBuf = item.into();
|
||||
exe_path.push(&path);
|
||||
if exe_path.is_file() {
|
||||
return Ok(exe_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn env_list(env: HashMap<String, String>) -> Vec<String> {
|
||||
env.iter()
|
||||
.map(|(key, value)| format!("{}={}", key, value))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
async fn fork_and_exec(
|
||||
&mut self,
|
||||
idm: IdmClient,
|
||||
cgroup: Cgroup,
|
||||
working_dir: String,
|
||||
path: CString,
|
||||
cmd: Vec<CString>,
|
||||
env: Vec<CString>,
|
||||
) -> Result<()> {
|
||||
match unsafe { fork()? } {
|
||||
ForkResult::Parent { child } => self.background(idm, cgroup, child).await,
|
||||
ForkResult::Child => self.foreground(cgroup, working_dir, path, cmd, env).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn foreground(
|
||||
&mut self,
|
||||
cgroup: Cgroup,
|
||||
working_dir: String,
|
||||
path: CString,
|
||||
cmd: Vec<CString>,
|
||||
env: Vec<CString>,
|
||||
) -> Result<()> {
|
||||
GuestInit::set_controlling_terminal()?;
|
||||
std::env::set_current_dir(working_dir)?;
|
||||
cgroup.add_task(CgroupPid::from(std::process::id() as u64))?;
|
||||
execve(&path, &cmd, &env)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_controlling_terminal() -> Result<()> {
|
||||
unsafe {
|
||||
setsid();
|
||||
set_controlling_terminal(io::stdin().as_raw_fd(), 0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn background(&mut self, idm: IdmClient, cgroup: Cgroup, executed: Pid) -> Result<()> {
|
||||
let mut background = GuestBackground::new(idm, cgroup, executed).await?;
|
||||
background.run().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
use std::{os::raw::c_int, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::time::sleep;
|
||||
use xenstore::{XsdClient, XsdInterface};
|
||||
|
||||
pub mod background;
|
||||
pub mod childwait;
|
||||
pub mod init;
|
||||
|
||||
pub async fn death(code: c_int) -> Result<()> {
|
||||
let store = XsdClient::open().await?;
|
||||
store
|
||||
.write_string("krata/guest/exit-code", &code.to_string())
|
||||
.await?;
|
||||
drop(store);
|
||||
loop {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "krata"
|
||||
description = "Client library and common services for the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-reflect = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { workspace = true, features = ["term"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = { workspace = true }
|
||||
prost-build = { workspace = true }
|
||||
prost-reflect-build = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "krata"
|
||||
|
||||
[[example]]
|
||||
name = "ethtool"
|
||||
path = "examples/ethtool.rs"
|
@ -1,24 +0,0 @@
|
||||
use std::io::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut config = prost_build::Config::new();
|
||||
prost_reflect_build::Builder::new()
|
||||
.descriptor_pool("crate::DESCRIPTOR_POOL")
|
||||
.configure(
|
||||
&mut config,
|
||||
&[
|
||||
"proto/krata/v1/control.proto",
|
||||
"proto/krata/internal/idm.proto",
|
||||
],
|
||||
&["proto/"],
|
||||
)?;
|
||||
tonic_build::configure().compile_with_config(
|
||||
config,
|
||||
&[
|
||||
"proto/krata/v1/control.proto",
|
||||
"proto/krata/internal/idm.proto",
|
||||
],
|
||||
&["proto/"],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::ethtool::EthtoolHandle;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = env::args().collect::<Vec<String>>();
|
||||
let interface = args.get(1).unwrap();
|
||||
let mut handle = EthtoolHandle::new()?;
|
||||
handle.set_gso(interface, false)?;
|
||||
handle.set_tso(interface, false)?;
|
||||
Ok(())
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.internal.idm;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.internal.idm";
|
||||
option java_outer_classname = "IdmProto";
|
||||
|
||||
message IdmExitEvent {
|
||||
int32 code = 1;
|
||||
}
|
||||
|
||||
message IdmEvent {
|
||||
oneof event {
|
||||
IdmExitEvent exit = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message IdmPacket {
|
||||
IdmEvent event = 1;
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.v1.common;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.v1.common";
|
||||
option java_outer_classname = "CommonProto";
|
||||
|
||||
message Guest {
|
||||
string id = 1;
|
||||
GuestSpec spec = 2;
|
||||
GuestState state = 3;
|
||||
}
|
||||
|
||||
message GuestSpec {
|
||||
string name = 1;
|
||||
GuestImageSpec image = 2;
|
||||
uint32 vcpus = 3;
|
||||
uint64 mem = 4;
|
||||
GuestTaskSpec task = 5;
|
||||
repeated GuestSpecAnnotation annotations = 6;
|
||||
}
|
||||
|
||||
message GuestImageSpec {
|
||||
oneof image {
|
||||
GuestOciImageSpec oci = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GuestOciImageSpec {
|
||||
string image = 1;
|
||||
}
|
||||
|
||||
message GuestTaskSpec {
|
||||
repeated GuestTaskSpecEnvVar environment = 1;
|
||||
repeated string command = 2;
|
||||
}
|
||||
|
||||
message GuestTaskSpecEnvVar {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message GuestSpecAnnotation {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message GuestState {
|
||||
GuestStatus status = 1;
|
||||
GuestNetworkState network = 2;
|
||||
GuestExitInfo exit_info = 3;
|
||||
GuestErrorInfo error_info = 4;
|
||||
uint32 domid = 5;
|
||||
}
|
||||
|
||||
enum GuestStatus {
|
||||
GUEST_STATUS_UNKNOWN = 0;
|
||||
GUEST_STATUS_STARTING = 1;
|
||||
GUEST_STATUS_STARTED = 2;
|
||||
GUEST_STATUS_EXITED = 3;
|
||||
GUEST_STATUS_DESTROYING = 4;
|
||||
GUEST_STATUS_DESTROYED = 5;
|
||||
GUEST_STATUS_FAILED = 6;
|
||||
}
|
||||
|
||||
message GuestNetworkState {
|
||||
string guest_ipv4 = 1;
|
||||
string guest_ipv6 = 2;
|
||||
string guest_mac = 3;
|
||||
string gateway_ipv4 = 4;
|
||||
string gateway_ipv6 = 5;
|
||||
string gateway_mac = 6;
|
||||
}
|
||||
|
||||
message GuestExitInfo {
|
||||
int32 code = 1;
|
||||
}
|
||||
|
||||
message GuestErrorInfo {
|
||||
string message = 1;
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package krata.v1.control;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "dev.krata.proto.v1.control";
|
||||
option java_outer_classname = "ControlProto";
|
||||
|
||||
import "krata/v1/common.proto";
|
||||
|
||||
service ControlService {
|
||||
rpc CreateGuest(CreateGuestRequest) returns (CreateGuestReply);
|
||||
rpc DestroyGuest(DestroyGuestRequest) returns (DestroyGuestReply);
|
||||
rpc ResolveGuest(ResolveGuestRequest) returns (ResolveGuestReply);
|
||||
rpc ListGuests(ListGuestsRequest) returns (ListGuestsReply);
|
||||
rpc ConsoleData(stream ConsoleDataRequest) returns (stream ConsoleDataReply);
|
||||
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
|
||||
}
|
||||
|
||||
message CreateGuestRequest {
|
||||
krata.v1.common.GuestSpec spec = 1;
|
||||
}
|
||||
|
||||
message CreateGuestReply {
|
||||
string guest_id = 1;
|
||||
}
|
||||
|
||||
message DestroyGuestRequest {
|
||||
string guest_id = 1;
|
||||
}
|
||||
|
||||
message DestroyGuestReply {}
|
||||
|
||||
message ResolveGuestRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message ResolveGuestReply {
|
||||
krata.v1.common.Guest guest = 1;
|
||||
}
|
||||
|
||||
message ListGuestsRequest {}
|
||||
|
||||
message ListGuestsReply {
|
||||
repeated krata.v1.common.Guest guests = 1;
|
||||
}
|
||||
|
||||
message ConsoleDataRequest {
|
||||
string guest_id = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
message ConsoleDataReply {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
message WatchEventsRequest {}
|
||||
|
||||
message WatchEventsReply {
|
||||
oneof event {
|
||||
GuestChangedEvent guest_changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GuestChangedEvent {
|
||||
krata.v1.common.Guest guest = 1;
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
use crate::{dial::ControlDialAddress, v1::control::control_service_client::ControlServiceClient};
|
||||
#[cfg(not(unix))]
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
#[cfg(unix)]
|
||||
use tonic::transport::Uri;
|
||||
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
|
||||
#[cfg(unix)]
|
||||
use tower::service_fn;
|
||||
|
||||
pub struct ControlClientProvider {}
|
||||
|
||||
impl ControlClientProvider {
|
||||
pub async fn dial(addr: ControlDialAddress) -> Result<ControlServiceClient<Channel>> {
|
||||
let channel = match addr {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
#[cfg(not(unix))]
|
||||
return Err(anyhow!(
|
||||
"unix sockets are not supported on this platform (path {})",
|
||||
path
|
||||
));
|
||||
#[cfg(unix)]
|
||||
ControlClientProvider::dial_unix_socket(path).await?
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
Endpoint::try_from(format!("http://{}:{}", host, port))?
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure: _,
|
||||
} => {
|
||||
let tls_config = ClientTlsConfig::new().domain_name(&host);
|
||||
let address = format!("https://{}:{}", host, port);
|
||||
Channel::from_shared(address)?
|
||||
.tls_config(tls_config)?
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ControlServiceClient::new(channel))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn dial_unix_socket(path: String) -> Result<Channel> {
|
||||
// This URL is not actually used but is required to be specified.
|
||||
Ok(Endpoint::try_from(format!("unix://localhost/{}", path))?
|
||||
.connect_with_connector(service_fn(|uri: Uri| {
|
||||
let path = uri.path().to_string();
|
||||
UnixStream::connect(path)
|
||||
}))
|
||||
.await?)
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use url::{Host, Url};
|
||||
|
||||
pub const KRATA_DEFAULT_TCP_PORT: u16 = 4350;
|
||||
pub const KRATA_DEFAULT_TLS_PORT: u16 = 4353;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ControlDialAddress {
|
||||
UnixSocket {
|
||||
path: String,
|
||||
},
|
||||
Tcp {
|
||||
host: String,
|
||||
port: u16,
|
||||
},
|
||||
Tls {
|
||||
host: String,
|
||||
port: u16,
|
||||
insecure: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for ControlDialAddress {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url: Url = s.parse()?;
|
||||
|
||||
let host = url.host().unwrap_or(Host::Domain("localhost")).to_string();
|
||||
|
||||
match url.scheme() {
|
||||
"unix" => Ok(ControlDialAddress::UnixSocket {
|
||||
path: url.path().to_string(),
|
||||
}),
|
||||
|
||||
"tcp" => {
|
||||
let port = url.port().unwrap_or(KRATA_DEFAULT_TCP_PORT);
|
||||
Ok(ControlDialAddress::Tcp { host, port })
|
||||
}
|
||||
|
||||
"tls" | "tls-insecure" => {
|
||||
let insecure = url.scheme() == "tls-insecure";
|
||||
let port = url.port().unwrap_or(KRATA_DEFAULT_TLS_PORT);
|
||||
Ok(ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure,
|
||||
})
|
||||
}
|
||||
|
||||
_ => Err(anyhow!("unknown control address scheme: {}", url.scheme())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ControlDialAddress> for Url {
|
||||
fn from(val: ControlDialAddress) -> Self {
|
||||
match val {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
let mut url = Url::parse("unix:///").unwrap();
|
||||
url.set_path(&path);
|
||||
url
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
let mut url = Url::parse("tcp://").unwrap();
|
||||
url.set_host(Some(&host)).unwrap();
|
||||
if port != KRATA_DEFAULT_TCP_PORT {
|
||||
url.set_port(Some(port)).unwrap();
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure,
|
||||
} => {
|
||||
let mut url = Url::parse("tls://").unwrap();
|
||||
if insecure {
|
||||
url.set_scheme("tls-insecure").unwrap();
|
||||
}
|
||||
url.set_host(Some(&host)).unwrap();
|
||||
if port != KRATA_DEFAULT_TLS_PORT {
|
||||
url.set_port(Some(port)).unwrap();
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ControlDialAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let url: Url = self.clone().into();
|
||||
write!(f, "{}", url)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use std::{
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
ptr::addr_of_mut,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use libc::{ioctl, socket, AF_INET, SOCK_DGRAM};
|
||||
|
||||
#[repr(C)]
|
||||
struct EthtoolValue {
|
||||
cmd: u32,
|
||||
data: u32,
|
||||
}
|
||||
|
||||
const ETHTOOL_SGSO: u32 = 0x00000024;
|
||||
const ETHTOOL_STSO: u32 = 0x0000001f;
|
||||
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
const SIOCETHTOOL: libc::c_ulong = libc::SIOCETHTOOL;
|
||||
#[cfg(target_env = "musl")]
|
||||
const SIOCETHTOOL: libc::c_int = libc::SIOCETHTOOL as i32;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
struct EthtoolIfreq {
|
||||
ifr_name: [libc::c_char; libc::IF_NAMESIZE],
|
||||
ifr_data: libc::uintptr_t,
|
||||
}
|
||||
|
||||
impl EthtoolIfreq {
|
||||
fn new(interface: &str) -> EthtoolIfreq {
|
||||
let mut ifreq = EthtoolIfreq {
|
||||
ifr_name: [0; libc::IF_NAMESIZE],
|
||||
ifr_data: 0,
|
||||
};
|
||||
for (i, byte) in interface.as_bytes().iter().enumerate() {
|
||||
ifreq.ifr_name[i] = *byte as libc::c_char
|
||||
}
|
||||
ifreq
|
||||
}
|
||||
|
||||
fn set_value(&mut self, ptr: *mut libc::c_void) {
|
||||
self.ifr_data = ptr as libc::uintptr_t;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EthtoolHandle {
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
impl EthtoolHandle {
|
||||
pub fn new() -> Result<EthtoolHandle> {
|
||||
let fd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) };
|
||||
if fd == -1 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
|
||||
Ok(EthtoolHandle {
|
||||
fd: unsafe { OwnedFd::from_raw_fd(fd) },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_gso(&mut self, interface: &str, value: bool) -> Result<()> {
|
||||
self.set_value(interface, ETHTOOL_SGSO, if value { 1 } else { 0 })
|
||||
}
|
||||
|
||||
pub fn set_tso(&mut self, interface: &str, value: bool) -> Result<()> {
|
||||
self.set_value(interface, ETHTOOL_STSO, if value { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn set_value(&mut self, interface: &str, cmd: u32, value: u32) -> Result<()> {
|
||||
let mut ifreq = EthtoolIfreq::new(interface);
|
||||
let mut value = EthtoolValue { cmd, data: value };
|
||||
ifreq.set_value(addr_of_mut!(value) as *mut libc::c_void);
|
||||
let result = unsafe { ioctl(self.fd.as_raw_fd(), SIOCETHTOOL, addr_of_mut!(ifreq) as u64) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::v1::control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event, WatchEventsReply,
|
||||
WatchEventsRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::{error, trace, warn};
|
||||
use tokio::{sync::broadcast, task::JoinHandle, time::sleep};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{transport::Channel, Streaming};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventStream {
|
||||
sender: Arc<broadcast::Sender<Event>>,
|
||||
task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl EventStream {
|
||||
pub async fn open(client: ControlServiceClient<Channel>) -> Result<Self> {
|
||||
let (sender, _) = broadcast::channel(1000);
|
||||
let emit = sender.clone();
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = EventStream::process(client, emit).await {
|
||||
error!("failed to process event stream: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
sender: Arc::new(sender),
|
||||
task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
emit: broadcast::Sender<Event>,
|
||||
) -> Result<()> {
|
||||
let mut events: Option<Streaming<WatchEventsReply>> = None;
|
||||
loop {
|
||||
let mut stream = match events {
|
||||
Some(stream) => stream,
|
||||
None => {
|
||||
let result = client.watch_events(WatchEventsRequest {}).await;
|
||||
if let Err(error) = result {
|
||||
warn!("failed to watch events: {}", error);
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
result.unwrap().into_inner()
|
||||
}
|
||||
};
|
||||
|
||||
let Some(result) = stream.next().await else {
|
||||
events = None;
|
||||
continue;
|
||||
};
|
||||
|
||||
let reply = match result {
|
||||
Ok(reply) => reply,
|
||||
Err(error) => {
|
||||
trace!("event stream processing failed: {}", error);
|
||||
events = None;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(event) = reply.event else {
|
||||
events = Some(stream);
|
||||
continue;
|
||||
};
|
||||
let _ = emit.send(event);
|
||||
events = Some(stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EventStream {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::protocol::IdmPacket;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use log::{debug, error};
|
||||
use nix::sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg};
|
||||
use prost::Message;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{unix::AsyncFd, AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
const IDM_PACKET_QUEUE_LEN: usize = 100;
|
||||
|
||||
pub struct IdmClient {
|
||||
pub receiver: Receiver<IdmPacket>,
|
||||
pub sender: Sender<IdmPacket>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for IdmClient {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl IdmClient {
|
||||
pub async fn open<P: AsRef<Path>>(path: P) -> Result<IdmClient> {
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(false)
|
||||
.open(path)
|
||||
.await?;
|
||||
IdmClient::set_raw_port(&file)?;
|
||||
let (rx_sender, rx_receiver) = channel(IDM_PACKET_QUEUE_LEN);
|
||||
let (tx_sender, tx_receiver) = channel(IDM_PACKET_QUEUE_LEN);
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = IdmClient::process(file, rx_sender, tx_receiver).await {
|
||||
debug!("failed to handle idm client processing: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(IdmClient {
|
||||
receiver: rx_receiver,
|
||||
sender: tx_sender,
|
||||
task,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_raw_port(file: &File) -> Result<()> {
|
||||
let mut termios = tcgetattr(file)?;
|
||||
cfmakeraw(&mut termios);
|
||||
tcsetattr(file, SetArg::TCSANOW, &termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(
|
||||
file: File,
|
||||
sender: Sender<IdmPacket>,
|
||||
mut receiver: Receiver<IdmPacket>,
|
||||
) -> Result<()> {
|
||||
let mut file = AsyncFd::new(file)?;
|
||||
loop {
|
||||
select! {
|
||||
x = file.readable_mut() => match x {
|
||||
Ok(mut guard) => {
|
||||
let size = guard.get_inner_mut().read_u16_le().await?;
|
||||
if size == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut buffer = BytesMut::with_capacity(size as usize);
|
||||
guard.get_inner_mut().read_exact(&mut buffer).await?;
|
||||
match IdmPacket::decode(buffer) {
|
||||
Ok(packet) => {
|
||||
sender.send(packet).await?;
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
error!("received invalid idm packet: {}", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
return Err(anyhow!("failed to read idm client: {}", error));
|
||||
}
|
||||
},
|
||||
x = receiver.recv() => match x {
|
||||
Some(packet) => {
|
||||
let data = packet.encode_to_vec();
|
||||
if data.len() > u16::MAX as usize {
|
||||
error!("unable to send idm packet, packet size exceeded (tried to send {} bytes)", data.len());
|
||||
continue;
|
||||
}
|
||||
file.get_mut().write_u16_le(data.len() as u16).await?;
|
||||
file.get_mut().write_all(&data).await?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
#[cfg(unix)]
|
||||
pub mod client;
|
||||
pub mod protocol;
|
@ -1 +0,0 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/krata.internal.idm.rs"));
|
@ -1,36 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LaunchNetworkIpv4 {
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LaunchNetworkIpv6 {
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LaunchNetworkResolver {
|
||||
pub nameservers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LaunchNetwork {
|
||||
pub link: String,
|
||||
pub ipv4: LaunchNetworkIpv4,
|
||||
pub ipv6: LaunchNetworkIpv6,
|
||||
pub resolver: LaunchNetworkResolver,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct LaunchInfo {
|
||||
pub hostname: Option<String>,
|
||||
pub network: Option<LaunchNetwork>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub run: Option<Vec<String>>,
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use prost_reflect::DescriptorPool;
|
||||
|
||||
pub mod v1;
|
||||
|
||||
pub mod client;
|
||||
pub mod dial;
|
||||
pub mod events;
|
||||
pub mod idm;
|
||||
pub mod launchcfg;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethtool;
|
||||
|
||||
pub static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
|
||||
DescriptorPool::decode(
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")).as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
@ -1 +0,0 @@
|
||||
tonic::include_proto!("krata.v1.common");
|
@ -1 +0,0 @@
|
||||
tonic::include_proto!("krata.v1.control");
|
@ -1,2 +0,0 @@
|
||||
pub mod common;
|
||||
pub mod control;
|
15
crates/loopdev/Cargo.toml
Normal file
15
crates/loopdev/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "krata-loopdev"
|
||||
description = "Loop device handling library for krata"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "krataloopdev"
|
||||
|
||||
[dependencies]
|
||||
libc.workspace = true
|
348
crates/loopdev/src/lib.rs
Normal file
348
crates/loopdev/src/lib.rs
Normal file
@ -0,0 +1,348 @@
|
||||
use libc::{c_int, ioctl};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io,
|
||||
os::fd::{AsRawFd, IntoRawFd, RawFd},
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[cfg(all(not(target_os = "android"), not(target_env = "musl")))]
|
||||
type IoctlRequest = libc::c_ulong;
|
||||
#[cfg(any(target_os = "android", target_env = "musl"))]
|
||||
type IoctlRequest = libc::c_int;
|
||||
|
||||
const LOOP_CONTROL: &str = "/dev/loop-control";
|
||||
const LOOP_PREFIX: &str = "/dev/loop";
|
||||
|
||||
/// Loop control interface IOCTLs.
|
||||
const LOOP_CTL_GET_FREE: IoctlRequest = 0x4C82;
|
||||
|
||||
/// Loop device flags.
|
||||
const LO_FLAGS_READ_ONLY: u32 = 1;
|
||||
const LO_FLAGS_AUTOCLEAR: u32 = 4;
|
||||
const LO_FLAGS_PARTSCAN: u32 = 8;
|
||||
const LO_FLAGS_DIRECT_IO: u32 = 16;
|
||||
|
||||
/// Loop device IOCTLs.
|
||||
const LOOP_SET_FD: IoctlRequest = 0x4C00;
|
||||
const LOOP_CLR_FD: IoctlRequest = 0x4C01;
|
||||
const LOOP_SET_STATUS64: IoctlRequest = 0x4C04;
|
||||
const LOOP_SET_CAPACITY: IoctlRequest = 0x4C07;
|
||||
const LOOP_SET_DIRECT_IO: IoctlRequest = 0x4C08;
|
||||
|
||||
/// Interface which wraps a handle to the loop control device.
|
||||
#[derive(Debug)]
|
||||
pub struct LoopControl {
|
||||
dev_file: File,
|
||||
}
|
||||
|
||||
/// Translate ioctl results to errors if appropriate.
|
||||
fn translate_error(ret: i32) -> io::Result<i32> {
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
impl LoopControl {
|
||||
/// Open the loop control device.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any errors from physically opening the loop control device are
|
||||
/// bubbled up.
|
||||
pub fn open() -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
dev_file: OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(LOOP_CONTROL)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Requests the next available loop device from the kernel and opens it.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use krataloopdev::LoopControl;
|
||||
/// let lc = LoopControl::open().unwrap();
|
||||
/// let ld = lc.next_free().unwrap();
|
||||
/// println!("{}", ld.path().unwrap().display());
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any errors from opening the loop device are bubbled up.
|
||||
pub fn next_free(&self) -> io::Result<LoopDevice> {
|
||||
let dev_num = translate_error(unsafe {
|
||||
ioctl(
|
||||
self.dev_file.as_raw_fd() as c_int,
|
||||
LOOP_CTL_GET_FREE as IoctlRequest,
|
||||
)
|
||||
})?;
|
||||
LoopDevice::open(format!("{}{}", LOOP_PREFIX, dev_num))
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface to a loop device itself, e.g. `/dev/loop0`.
|
||||
#[derive(Debug)]
|
||||
pub struct LoopDevice {
|
||||
device: File,
|
||||
}
|
||||
|
||||
impl AsRawFd for LoopDevice {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.device.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRawFd for LoopDevice {
|
||||
fn into_raw_fd(self) -> RawFd {
|
||||
self.device.into_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl LoopDevice {
|
||||
/// Opens a loop device.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any errors from opening the underlying physical loop device are bubbled up.
|
||||
pub fn open<P: AsRef<Path>>(dev: P) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
device: OpenOptions::new().read(true).write(true).open(dev)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach a loop device to a file with the given options.
|
||||
pub fn with(&self) -> AttachOptions<'_> {
|
||||
AttachOptions {
|
||||
device: self,
|
||||
info: LoopInfo64::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables or disables Direct I/O mode.
|
||||
pub fn set_direct_io(&self, direct_io: bool) -> io::Result<()> {
|
||||
translate_error(unsafe {
|
||||
ioctl(
|
||||
self.device.as_raw_fd() as c_int,
|
||||
LOOP_SET_DIRECT_IO as IoctlRequest,
|
||||
if direct_io { 1 } else { 0 },
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attach the loop device to a fully-mapped file.
|
||||
pub fn attach_file<P: AsRef<Path>>(&self, backing_file: P) -> io::Result<()> {
|
||||
let info = LoopInfo64 {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self::attach_with_loop_info(self, backing_file, info)
|
||||
}
|
||||
|
||||
/// Attach the loop device to a file with `LoopInfo64`.
|
||||
fn attach_with_loop_info(
|
||||
&self,
|
||||
backing_file: impl AsRef<Path>,
|
||||
info: LoopInfo64,
|
||||
) -> io::Result<()> {
|
||||
let write_access = (info.lo_flags & LO_FLAGS_READ_ONLY) == 0;
|
||||
let bf = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(write_access)
|
||||
.open(backing_file)?;
|
||||
self.attach_fd_with_loop_info(bf, info)
|
||||
}
|
||||
|
||||
/// Attach the loop device to a file descriptor with `LoopInfo64`.
|
||||
fn attach_fd_with_loop_info(&self, bf: impl AsRawFd, info: LoopInfo64) -> io::Result<()> {
|
||||
translate_error(unsafe {
|
||||
ioctl(
|
||||
self.device.as_raw_fd() as c_int,
|
||||
LOOP_SET_FD as IoctlRequest,
|
||||
bf.as_raw_fd() as c_int,
|
||||
)
|
||||
})?;
|
||||
|
||||
let result = unsafe {
|
||||
ioctl(
|
||||
self.device.as_raw_fd() as c_int,
|
||||
LOOP_SET_STATUS64 as IoctlRequest,
|
||||
&info,
|
||||
)
|
||||
};
|
||||
|
||||
match translate_error(result) {
|
||||
Err(err) => {
|
||||
let _detach_err = self.detach();
|
||||
Err(err)
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path for the loop device.
|
||||
pub fn path(&self) -> Option<PathBuf> {
|
||||
let mut p = PathBuf::from("/proc/self/fd");
|
||||
p.push(self.device.as_raw_fd().to_string());
|
||||
std::fs::read_link(&p).ok()
|
||||
}
|
||||
|
||||
/// Detach a loop device.
|
||||
pub fn detach(&self) -> io::Result<()> {
|
||||
translate_error(unsafe {
|
||||
ioctl(
|
||||
self.device.as_raw_fd() as c_int,
|
||||
LOOP_CLR_FD as IoctlRequest,
|
||||
0,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a loop device's capacity.
|
||||
pub fn set_capacity(&self) -> io::Result<()> {
|
||||
translate_error(unsafe {
|
||||
ioctl(
|
||||
self.device.as_raw_fd() as c_int,
|
||||
LOOP_SET_CAPACITY as IoctlRequest,
|
||||
0,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the major device node number.
|
||||
pub fn major(&self) -> io::Result<u32> {
|
||||
self.device
|
||||
.metadata()
|
||||
.map(|m| unsafe { libc::major(m.rdev()) })
|
||||
}
|
||||
|
||||
/// Return the minor device node number.
|
||||
pub fn minor(&self) -> io::Result<u32> {
|
||||
self.device
|
||||
.metadata()
|
||||
.map(|m| unsafe { libc::minor(m.rdev()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct LoopInfo64 {
|
||||
lo_device: u64,
|
||||
lo_inode: u64,
|
||||
lo_rdevice: u64,
|
||||
lo_offset: u64,
|
||||
lo_sizelimit: u64,
|
||||
lo_number: u32,
|
||||
lo_encrypt_type: u32,
|
||||
lo_encrypt_key_size: u32,
|
||||
lo_flags: u32,
|
||||
lo_file_name: [u8; 64],
|
||||
lo_crypt_name: [u8; 64],
|
||||
lo_encrypt_key: [u8; 32],
|
||||
lo_init: [u64; 2],
|
||||
}
|
||||
|
||||
impl Default for LoopInfo64 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lo_device: 0,
|
||||
lo_inode: 0,
|
||||
lo_rdevice: 0,
|
||||
lo_offset: 0,
|
||||
lo_sizelimit: 0,
|
||||
lo_number: 0,
|
||||
lo_encrypt_type: 0,
|
||||
lo_encrypt_key_size: 0,
|
||||
lo_flags: 0,
|
||||
lo_file_name: [0; 64],
|
||||
lo_crypt_name: [0; 64],
|
||||
lo_encrypt_key: [0; 32],
|
||||
lo_init: [0, 2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct AttachOptions<'d> {
|
||||
device: &'d LoopDevice,
|
||||
info: LoopInfo64,
|
||||
}
|
||||
|
||||
impl AttachOptions<'_> {
|
||||
pub fn offset(mut self, offset: u64) -> Self {
|
||||
self.info.lo_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size_limit(mut self, size_limit: u64) -> Self {
|
||||
self.info.lo_sizelimit = size_limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn read_only(mut self, read_only: bool) -> Self {
|
||||
if read_only {
|
||||
self.info.lo_flags |= LO_FLAGS_READ_ONLY;
|
||||
} else {
|
||||
self.info.lo_flags &= !LO_FLAGS_READ_ONLY;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn autoclear(mut self, autoclear: bool) -> Self {
|
||||
if autoclear {
|
||||
self.info.lo_flags |= LO_FLAGS_AUTOCLEAR;
|
||||
} else {
|
||||
self.info.lo_flags &= !LO_FLAGS_AUTOCLEAR;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn part_scan(mut self, part_scan: bool) -> Self {
|
||||
if part_scan {
|
||||
self.info.lo_flags |= LO_FLAGS_PARTSCAN;
|
||||
} else {
|
||||
self.info.lo_flags &= !LO_FLAGS_PARTSCAN;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_direct_io(mut self, direct_io: bool) -> Self {
|
||||
if direct_io {
|
||||
self.info.lo_flags |= LO_FLAGS_DIRECT_IO;
|
||||
} else {
|
||||
self.info.lo_flags &= !LO_FLAGS_DIRECT_IO;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn direct_io(&self) -> bool {
|
||||
(self.info.lo_flags & LO_FLAGS_DIRECT_IO) == LO_FLAGS_DIRECT_IO
|
||||
}
|
||||
|
||||
pub fn attach(&self, backing_file: impl AsRef<Path>) -> io::Result<()> {
|
||||
self.device
|
||||
.attach_with_loop_info(backing_file, self.info.clone())?;
|
||||
if self.direct_io() {
|
||||
self.device.set_direct_io(self.direct_io())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn attach_fd(&self, backing_file_fd: impl AsRawFd) -> io::Result<()> {
|
||||
self.device
|
||||
.attach_fd_with_loop_info(backing_file_fd, self.info.clone())?;
|
||||
if self.direct_io() {
|
||||
self.device.set_direct_io(self.direct_io())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
[package]
|
||||
name = "krata-network"
|
||||
description = "Networking services for the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
etherparse = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.7" }
|
||||
krata-advmac = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rtnetlink = { workspace = true }
|
||||
smoltcp = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-tun = { workspace = true }
|
||||
udp-stream = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratanet"
|
||||
|
||||
[[bin]]
|
||||
name = "kratanet"
|
||||
path = "bin/network.rs"
|
||||
|
||||
[[example]]
|
||||
name = "ping"
|
||||
path = "examples/ping.rs"
|
@ -1,22 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use krata::dial::ControlDialAddress;
|
||||
use kratanet::NetworkService;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct NetworkArgs {
|
||||
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
|
||||
connection: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
let args = NetworkArgs::parse();
|
||||
let control_dial_address = ControlDialAddress::from_str(&args.connection)?;
|
||||
let mut service = NetworkService::new(control_dial_address).await?;
|
||||
service.watch().await
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use std::{net::Ipv6Addr, str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use kratanet::icmp::{IcmpClient, IcmpProtocol};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let client = IcmpClient::new(IcmpProtocol::Icmpv6)?;
|
||||
let payload: [u8; 4] = [12u8, 14u8, 16u8, 32u8];
|
||||
let result = client
|
||||
.ping6(
|
||||
Ipv6Addr::from_str("2606:4700:4700::1111")?,
|
||||
0,
|
||||
1,
|
||||
&payload,
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await?;
|
||||
println!("reply: {:?}", result);
|
||||
Ok(())
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::Guest,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
ListGuestsRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::warn;
|
||||
use smoltcp::wire::{EthernetAddress, Ipv4Cidr, Ipv6Cidr};
|
||||
use std::{collections::HashMap, str::FromStr, time::Duration};
|
||||
use tokio::{select, sync::broadcast::Receiver, time::sleep};
|
||||
use tonic::transport::Channel;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AutoNetworkWatcher {
|
||||
control: ControlServiceClient<Channel>,
|
||||
pub events: EventStream,
|
||||
known: HashMap<Uuid, NetworkMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkSide {
|
||||
pub ipv4: Ipv4Cidr,
|
||||
pub ipv6: Ipv6Cidr,
|
||||
pub mac: EthernetAddress,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkMetadata {
|
||||
pub domid: u32,
|
||||
pub uuid: Uuid,
|
||||
pub guest: NetworkSide,
|
||||
pub gateway: NetworkSide,
|
||||
}
|
||||
|
||||
impl NetworkMetadata {
|
||||
pub fn interface(&self) -> String {
|
||||
format!("vif{}.20", self.domid)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AutoNetworkChangeset {
|
||||
pub added: Vec<NetworkMetadata>,
|
||||
pub removed: Vec<NetworkMetadata>,
|
||||
}
|
||||
|
||||
impl AutoNetworkWatcher {
|
||||
pub async fn new(control: ControlServiceClient<Channel>) -> Result<AutoNetworkWatcher> {
|
||||
let client = control.clone();
|
||||
Ok(AutoNetworkWatcher {
|
||||
control,
|
||||
events: EventStream::open(client).await?,
|
||||
known: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> Result<Vec<NetworkMetadata>> {
|
||||
let mut all_guests: HashMap<Uuid, Guest> = HashMap::new();
|
||||
for guest in self
|
||||
.control
|
||||
.list_guests(ListGuestsRequest {})
|
||||
.await?
|
||||
.into_inner()
|
||||
.guests
|
||||
{
|
||||
let Ok(uuid) = Uuid::from_str(&guest.id) else {
|
||||
continue;
|
||||
};
|
||||
all_guests.insert(uuid, guest);
|
||||
}
|
||||
|
||||
let mut networks: Vec<NetworkMetadata> = Vec::new();
|
||||
for (uuid, guest) in &all_guests {
|
||||
let Some(ref state) = guest.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if state.domid == u32::MAX {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(ref network) = state.network else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_ipv4_cidr) = Ipv4Cidr::from_str(&network.guest_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_ipv6_cidr) = Ipv6Cidr::from_str(&network.guest_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(guest_mac) = EthernetAddress::from_str(&network.guest_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv4_cidr) = Ipv4Cidr::from_str(&network.gateway_ipv4) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_ipv6_cidr) = Ipv6Cidr::from_str(&network.gateway_ipv6) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(gateway_mac) = EthernetAddress::from_str(&network.gateway_mac) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
networks.push(NetworkMetadata {
|
||||
domid: state.domid,
|
||||
uuid: *uuid,
|
||||
guest: NetworkSide {
|
||||
ipv4: guest_ipv4_cidr,
|
||||
ipv6: guest_ipv6_cidr,
|
||||
mac: guest_mac,
|
||||
},
|
||||
gateway: NetworkSide {
|
||||
ipv4: gateway_ipv4_cidr,
|
||||
ipv6: gateway_ipv6_cidr,
|
||||
mac: gateway_mac,
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(networks)
|
||||
}
|
||||
|
||||
pub async fn read_changes(&mut self) -> Result<AutoNetworkChangeset> {
|
||||
let mut seen: Vec<Uuid> = Vec::new();
|
||||
let mut added: Vec<NetworkMetadata> = Vec::new();
|
||||
let mut removed: Vec<NetworkMetadata> = Vec::new();
|
||||
|
||||
let networks = match self.read().await {
|
||||
Ok(networks) => networks,
|
||||
Err(error) => {
|
||||
warn!("failed to read network changes: {}", error);
|
||||
return Ok(AutoNetworkChangeset { added, removed });
|
||||
}
|
||||
};
|
||||
|
||||
for network in networks {
|
||||
seen.push(network.uuid);
|
||||
if self.known.contains_key(&network.uuid) {
|
||||
continue;
|
||||
}
|
||||
let _ = self.known.insert(network.uuid, network.clone());
|
||||
added.push(network);
|
||||
}
|
||||
|
||||
let mut gone: Vec<Uuid> = Vec::new();
|
||||
for uuid in self.known.keys() {
|
||||
if seen.contains(uuid) {
|
||||
continue;
|
||||
}
|
||||
gone.push(*uuid);
|
||||
}
|
||||
|
||||
for uuid in &gone {
|
||||
let Some(network) = self.known.remove(uuid) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
removed.push(network);
|
||||
}
|
||||
|
||||
Ok(AutoNetworkChangeset { added, removed })
|
||||
}
|
||||
|
||||
pub async fn wait(&mut self, receiver: &mut Receiver<Event>) -> Result<()> {
|
||||
loop {
|
||||
select! {
|
||||
x = receiver.recv() => match x {
|
||||
Ok(Event::GuestChanged(_)) => {
|
||||
break;
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
warn!("failed to receive event: {}", error);
|
||||
}
|
||||
},
|
||||
|
||||
_ = sleep(Duration::from_secs(10)) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_unknown(&mut self, uuid: Uuid) -> Result<bool> {
|
||||
Ok(self.known.remove(&uuid).is_some())
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
use crate::autonet::NetworkMetadata;
|
||||
use crate::chandev::ChannelDevice;
|
||||
use crate::nat::Nat;
|
||||
use crate::proxynat::ProxyNatHandlerFactory;
|
||||
use crate::raw_socket::{AsyncRawSocketChannel, RawSocketHandle, RawSocketProtocol};
|
||||
use crate::vbridge::{BridgeJoinHandle, VirtualBridge};
|
||||
use crate::EXTRA_MTU;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use futures::TryStreamExt;
|
||||
use log::{info, trace, warn};
|
||||
use smoltcp::iface::{Config, Interface, SocketSet};
|
||||
use smoltcp::phy::Medium;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{HardwareAddress, IpCidr};
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{channel, Receiver};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const TX_CHANNEL_BUFFER_LEN: usize = 3000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkBackend {
|
||||
metadata: NetworkMetadata,
|
||||
bridge: VirtualBridge,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NetworkStackSelect {
|
||||
Receive(Option<BytesMut>),
|
||||
Send(Option<BytesMut>),
|
||||
}
|
||||
|
||||
struct NetworkStack<'a> {
|
||||
tx: Receiver<BytesMut>,
|
||||
kdev: AsyncRawSocketChannel,
|
||||
udev: ChannelDevice,
|
||||
interface: Interface,
|
||||
sockets: SocketSet<'a>,
|
||||
nat: Nat,
|
||||
bridge: BridgeJoinHandle,
|
||||
}
|
||||
|
||||
impl NetworkStack<'_> {
|
||||
async fn poll(&mut self) -> Result<bool> {
|
||||
let what = select! {
|
||||
biased;
|
||||
x = self.kdev.receiver.recv() => NetworkStackSelect::Receive(x),
|
||||
x = self.tx.recv() => NetworkStackSelect::Send(x),
|
||||
x = self.bridge.from_bridge_receiver.recv() => NetworkStackSelect::Send(x),
|
||||
x = self.bridge.from_broadcast_receiver.recv() => NetworkStackSelect::Send(x.ok()),
|
||||
};
|
||||
|
||||
match what {
|
||||
NetworkStackSelect::Receive(Some(packet)) => {
|
||||
if let Err(error) = self.bridge.to_bridge_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send guest packet to bridge: {}", error);
|
||||
}
|
||||
|
||||
if let Err(error) = self.nat.receive_sender.try_send(packet.clone()) {
|
||||
trace!("failed to send guest packet to nat: {}", error);
|
||||
}
|
||||
|
||||
self.udev.rx = Some(packet);
|
||||
self.interface
|
||||
.poll(Instant::now(), &mut self.udev, &mut self.sockets);
|
||||
}
|
||||
|
||||
NetworkStackSelect::Send(Some(packet)) => {
|
||||
if let Err(error) = self.kdev.sender.try_send(packet) {
|
||||
warn!("failed to transmit packet to interface: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkStackSelect::Receive(None) | NetworkStackSelect::Send(None) => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBackend {
|
||||
pub fn new(metadata: NetworkMetadata, bridge: VirtualBridge) -> Result<Self> {
|
||||
Ok(Self { metadata, bridge })
|
||||
}
|
||||
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
let interface = self.metadata.interface();
|
||||
let (connection, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let mut links = handle.link().get().match_name(interface.clone()).execute();
|
||||
let link = links.try_next().await?;
|
||||
if link.is_none() {
|
||||
return Err(anyhow!(
|
||||
"unable to find network interface named {}",
|
||||
interface
|
||||
));
|
||||
}
|
||||
let link = link.unwrap();
|
||||
handle.link().set(link.header.index).up().execute().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let mut stack = self.create_network_stack().await?;
|
||||
loop {
|
||||
if !stack.poll().await? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_network_stack(&self) -> Result<NetworkStack> {
|
||||
let interface = self.metadata.interface();
|
||||
let proxy = Box::new(ProxyNatHandlerFactory::new());
|
||||
let addresses: Vec<IpCidr> = vec![
|
||||
self.metadata.gateway.ipv4.into(),
|
||||
self.metadata.gateway.ipv6.into(),
|
||||
];
|
||||
let mut kdev =
|
||||
RawSocketHandle::bound_to_interface(&interface, RawSocketProtocol::Ethernet)?;
|
||||
let mtu = kdev.mtu_of_interface(&interface)? + EXTRA_MTU;
|
||||
let (tx_sender, tx_receiver) = channel::<BytesMut>(TX_CHANNEL_BUFFER_LEN);
|
||||
let mut udev = ChannelDevice::new(mtu, Medium::Ethernet, tx_sender.clone());
|
||||
let mac = self.metadata.gateway.mac;
|
||||
let nat = Nat::new(mtu, proxy, mac, addresses.clone(), tx_sender.clone())?;
|
||||
let hardware_addr = HardwareAddress::Ethernet(mac);
|
||||
let config = Config::new(hardware_addr);
|
||||
let mut iface = Interface::new(config, &mut udev, Instant::now());
|
||||
iface.update_ip_addrs(|addrs| {
|
||||
addrs
|
||||
.extend_from_slice(&addresses)
|
||||
.expect("failed to set ip addresses");
|
||||
});
|
||||
let sockets = SocketSet::new(vec![]);
|
||||
let handle = self.bridge.join(self.metadata.guest.mac).await?;
|
||||
let kdev = AsyncRawSocketChannel::new(mtu, kdev)?;
|
||||
Ok(NetworkStack {
|
||||
tx: tx_receiver,
|
||||
kdev,
|
||||
udev,
|
||||
interface: iface,
|
||||
sockets,
|
||||
nat,
|
||||
bridge: handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn launch(self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
info!(
|
||||
"launched network backend for krata guest {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
if let Err(error) = self.run().await {
|
||||
warn!(
|
||||
"network backend for krata guest {} failed: {}",
|
||||
self.metadata.uuid, error
|
||||
);
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetworkBackend {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
"destroyed network backend for krata guest {}",
|
||||
self.metadata.uuid
|
||||
);
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
// Referenced https://github.com/vi/wgslirpy/blob/master/crates/libwgslirpy/src/channelized_smoltcp_device.rs
|
||||
use bytes::BytesMut;
|
||||
use log::{debug, warn};
|
||||
use smoltcp::phy::{Checksum, Device, Medium};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
const TEAR_OFF_BUFFER_SIZE: usize = 65536;
|
||||
|
||||
pub struct ChannelDevice {
|
||||
pub mtu: usize,
|
||||
pub medium: Medium,
|
||||
pub tx: Sender<BytesMut>,
|
||||
pub rx: Option<BytesMut>,
|
||||
tear_off_buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl ChannelDevice {
|
||||
pub fn new(mtu: usize, medium: Medium, tx: Sender<BytesMut>) -> Self {
|
||||
Self {
|
||||
mtu,
|
||||
medium,
|
||||
tx,
|
||||
rx: None,
|
||||
tear_off_buffer: BytesMut::with_capacity(TEAR_OFF_BUFFER_SIZE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RxToken(pub BytesMut);
|
||||
|
||||
impl Device for ChannelDevice {
|
||||
type RxToken<'a> = RxToken where Self: 'a;
|
||||
type TxToken<'a> = &'a mut ChannelDevice where Self: 'a;
|
||||
|
||||
fn receive(
|
||||
&mut self,
|
||||
_timestamp: smoltcp::time::Instant,
|
||||
) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
|
||||
self.rx.take().map(|x| (RxToken(x), self))
|
||||
}
|
||||
|
||||
fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
|
||||
if self.tx.capacity() == 0 {
|
||||
debug!("ran out of transmission capacity");
|
||||
return None;
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
|
||||
let mut capabilities = smoltcp::phy::DeviceCapabilities::default();
|
||||
capabilities.medium = self.medium;
|
||||
capabilities.max_transmission_unit = self.mtu;
|
||||
capabilities.checksum = smoltcp::phy::ChecksumCapabilities::ignored();
|
||||
capabilities.checksum.tcp = Checksum::Tx;
|
||||
capabilities.checksum.ipv4 = Checksum::Tx;
|
||||
capabilities.checksum.icmpv4 = Checksum::Tx;
|
||||
capabilities.checksum.icmpv6 = Checksum::Tx;
|
||||
capabilities
|
||||
}
|
||||
}
|
||||
|
||||
impl smoltcp::phy::RxToken for RxToken {
|
||||
fn consume<R, F>(mut self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut [u8]) -> R,
|
||||
{
|
||||
f(&mut self.0[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> smoltcp::phy::TxToken for &'a mut ChannelDevice {
|
||||
fn consume<R, F>(self, len: usize, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut [u8]) -> R,
|
||||
{
|
||||
self.tear_off_buffer.resize(len, 0);
|
||||
let result = f(&mut self.tear_off_buffer[..]);
|
||||
let chunk = self.tear_off_buffer.split();
|
||||
if let Err(error) = self.tx.try_send(chunk) {
|
||||
warn!("failed to transmit packet: {}", error);
|
||||
}
|
||||
|
||||
if self.tear_off_buffer.capacity() < self.mtu {
|
||||
self.tear_off_buffer = BytesMut::with_capacity(TEAR_OFF_BUFFER_SIZE);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
|
||||
use advmac::MacAddr6;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use futures::TryStreamExt;
|
||||
use log::error;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use tokio::{select, task::JoinHandle};
|
||||
use tokio_tun::Tun;
|
||||
|
||||
use crate::vbridge::{BridgeJoinHandle, VirtualBridge};
|
||||
|
||||
const HOST_IPV4_ADDR: Ipv4Addr = Ipv4Addr::new(10, 75, 0, 1);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HostBridgeProcessSelect {
|
||||
Send(Option<BytesMut>),
|
||||
Receive(std::io::Result<usize>),
|
||||
}
|
||||
|
||||
pub struct HostBridge {
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl HostBridge {
|
||||
pub async fn new(mtu: usize, interface: String, bridge: &VirtualBridge) -> Result<HostBridge> {
|
||||
let tun = Tun::builder()
|
||||
.name(&interface)
|
||||
.tap(true)
|
||||
.mtu(mtu as i32)
|
||||
.packet_info(false)
|
||||
.try_build()?;
|
||||
|
||||
let (connection, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let mut mac = MacAddr6::random();
|
||||
mac.set_local(true);
|
||||
mac.set_multicast(false);
|
||||
|
||||
let mut links = handle.link().get().match_name(interface.clone()).execute();
|
||||
let link = links.try_next().await?;
|
||||
if link.is_none() {
|
||||
return Err(anyhow!(
|
||||
"unable to find network interface named {}",
|
||||
interface
|
||||
));
|
||||
}
|
||||
let link = link.unwrap();
|
||||
|
||||
handle
|
||||
.address()
|
||||
.add(link.header.index, IpAddr::V4(HOST_IPV4_ADDR), 16)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
handle
|
||||
.address()
|
||||
.add(link.header.index, IpAddr::V6(mac.to_link_local_ipv6()), 10)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
handle
|
||||
.link()
|
||||
.set(link.header.index)
|
||||
.address(mac.to_array().to_vec())
|
||||
.up()
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
let mac = EthernetAddress(mac.to_array());
|
||||
let bridge_handle = bridge.join(mac).await?;
|
||||
|
||||
let task = tokio::task::spawn(async move {
|
||||
if let Err(error) = HostBridge::process(mtu, tun, bridge_handle).await {
|
||||
error!("failed to process host bridge: {}", error);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(HostBridge { task })
|
||||
}
|
||||
|
||||
async fn process(mtu: usize, tun: Tun, mut bridge_handle: BridgeJoinHandle) -> Result<()> {
|
||||
let tear_off_size = 100 * mtu;
|
||||
let mut buffer: BytesMut = BytesMut::with_capacity(tear_off_size);
|
||||
loop {
|
||||
if buffer.capacity() < mtu {
|
||||
buffer = BytesMut::with_capacity(tear_off_size);
|
||||
}
|
||||
|
||||
buffer.resize(mtu, 0);
|
||||
let selection = select! {
|
||||
biased;
|
||||
x = tun.recv(&mut buffer) => HostBridgeProcessSelect::Receive(x),
|
||||
x = bridge_handle.from_bridge_receiver.recv() => HostBridgeProcessSelect::Send(x),
|
||||
x = bridge_handle.from_broadcast_receiver.recv() => HostBridgeProcessSelect::Send(x.ok()),
|
||||
};
|
||||
|
||||
match selection {
|
||||
HostBridgeProcessSelect::Send(Some(bytes)) => match tun.try_send(&bytes) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
if error.kind() == ErrorKind::WouldBlock {
|
||||
continue;
|
||||
}
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
|
||||
HostBridgeProcessSelect::Send(None) => {
|
||||
break;
|
||||
}
|
||||
|
||||
HostBridgeProcessSelect::Receive(result) => match result {
|
||||
Ok(len) => {
|
||||
if len == 0 {
|
||||
continue;
|
||||
}
|
||||
let packet = buffer.split_to(len);
|
||||
let _ = bridge_handle.to_bridge_sender.try_send(packet);
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
if error.kind() == ErrorKind::WouldBlock {
|
||||
continue;
|
||||
}
|
||||
|
||||
error!(
|
||||
"failed to receive data from tap device to bridge: {}",
|
||||
error
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HostBridge {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
use crate::raw_socket::{RawSocketHandle, RawSocketProtocol};
|
||||
use anyhow::{anyhow, Result};
|
||||
use etherparse::{
|
||||
IcmpEchoHeader, Icmpv4Header, Icmpv4Slice, Icmpv4Type, Icmpv6Header, Icmpv6Slice, Icmpv6Type,
|
||||
IpNumber, NetSlice, SlicedPacket,
|
||||
};
|
||||
use log::warn;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
os::fd::{FromRawFd, IntoRawFd},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::{oneshot, Mutex},
|
||||
task::JoinHandle,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IcmpProtocol {
|
||||
Icmpv4,
|
||||
Icmpv6,
|
||||
}
|
||||
|
||||
impl IcmpProtocol {
|
||||
pub fn to_socket_protocol(&self) -> RawSocketProtocol {
|
||||
match self {
|
||||
IcmpProtocol::Icmpv4 => RawSocketProtocol::Icmpv4,
|
||||
IcmpProtocol::Icmpv6 => RawSocketProtocol::Icmpv6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct IcmpHandlerToken(IpAddr, Option<u16>, u16);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IcmpReply {
|
||||
Icmpv4 {
|
||||
header: Icmpv4Header,
|
||||
echo: IcmpEchoHeader,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
|
||||
Icmpv6 {
|
||||
header: Icmpv6Header,
|
||||
echo: IcmpEchoHeader,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
type IcmpHandlerMap = Arc<Mutex<HashMap<IcmpHandlerToken, oneshot::Sender<IcmpReply>>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IcmpClient {
|
||||
socket: Arc<UdpSocket>,
|
||||
handlers: IcmpHandlerMap,
|
||||
task: Arc<JoinHandle<Result<()>>>,
|
||||
}
|
||||
|
||||
impl IcmpClient {
|
||||
pub fn new(protocol: IcmpProtocol) -> Result<IcmpClient> {
|
||||
let handle = RawSocketHandle::new(protocol.to_socket_protocol())?;
|
||||
let socket = unsafe { std::net::UdpSocket::from_raw_fd(handle.into_raw_fd()) };
|
||||
let socket: Arc<UdpSocket> = Arc::new(socket.try_into()?);
|
||||
let handlers = Arc::new(Mutex::new(HashMap::new()));
|
||||
let task = Arc::new(tokio::task::spawn(IcmpClient::process(
|
||||
protocol,
|
||||
socket.clone(),
|
||||
handlers.clone(),
|
||||
)));
|
||||
Ok(IcmpClient {
|
||||
socket,
|
||||
handlers,
|
||||
task,
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(
|
||||
protocol: IcmpProtocol,
|
||||
socket: Arc<UdpSocket>,
|
||||
handlers: IcmpHandlerMap,
|
||||
) -> Result<()> {
|
||||
let mut buffer = vec![0u8; 2048];
|
||||
loop {
|
||||
let (size, addr) = socket.recv_from(&mut buffer).await?;
|
||||
let packet = &buffer[0..size];
|
||||
|
||||
let (token, reply) = match protocol {
|
||||
IcmpProtocol::Icmpv4 => {
|
||||
let sliced = match SlicedPacket::from_ip(packet) {
|
||||
Ok(sliced) => sliced,
|
||||
Err(error) => {
|
||||
warn!("received icmp packet but failed to parse it: {}", error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(NetSlice::Ipv4(ipv4)) = sliced.net else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if ipv4.header().protocol() != IpNumber::ICMP {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(icmpv4) = Icmpv4Slice::from_slice(ipv4.payload().payload) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Icmpv4Type::EchoReply(echo) = icmpv4.header().icmp_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let token = IcmpHandlerToken(
|
||||
IpAddr::V4(ipv4.header().source_addr()),
|
||||
Some(echo.id),
|
||||
echo.seq,
|
||||
);
|
||||
let reply = IcmpReply::Icmpv4 {
|
||||
header: icmpv4.header(),
|
||||
echo,
|
||||
payload: icmpv4.payload().to_vec(),
|
||||
};
|
||||
(token, reply)
|
||||
}
|
||||
|
||||
IcmpProtocol::Icmpv6 => {
|
||||
let Ok(icmpv6) = Icmpv6Slice::from_slice(packet) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Icmpv6Type::EchoReply(echo) = icmpv6.header().icmp_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let SocketAddr::V6(addr) = addr else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let token = IcmpHandlerToken(IpAddr::V6(*addr.ip()), Some(echo.id), echo.seq);
|
||||
|
||||
let reply = IcmpReply::Icmpv6 {
|
||||
header: icmpv6.header(),
|
||||
echo,
|
||||
payload: icmpv6.payload().to_vec(),
|
||||
};
|
||||
(token, reply)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sender) = handlers.lock().await.remove(&token) {
|
||||
let _ = sender.send(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_handler(&self, token: IcmpHandlerToken) -> Result<oneshot::Receiver<IcmpReply>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if self
|
||||
.handlers
|
||||
.lock()
|
||||
.await
|
||||
.insert(token.clone(), tx)
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("duplicate icmp request: {:?}", token));
|
||||
}
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
async fn remove_handler(&self, token: IcmpHandlerToken) -> Result<()> {
|
||||
self.handlers.lock().await.remove(&token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ping4(
|
||||
&self,
|
||||
addr: Ipv4Addr,
|
||||
id: u16,
|
||||
seq: u16,
|
||||
payload: &[u8],
|
||||
deadline: Duration,
|
||||
) -> Result<Option<IcmpReply>> {
|
||||
let token = IcmpHandlerToken(IpAddr::V4(addr), Some(id), seq);
|
||||
let rx = self.add_handler(token.clone()).await?;
|
||||
|
||||
let echo = IcmpEchoHeader { id, seq };
|
||||
let mut header = Icmpv4Header::new(Icmpv4Type::EchoRequest(echo));
|
||||
header.update_checksum(payload);
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
header.write(&mut buffer)?;
|
||||
buffer.extend_from_slice(payload);
|
||||
|
||||
self.socket
|
||||
.send_to(&buffer, SocketAddr::V4(SocketAddrV4::new(addr, 0)))
|
||||
.await?;
|
||||
|
||||
let result = timeout(deadline, rx).await;
|
||||
self.remove_handler(token).await?;
|
||||
let reply = match result {
|
||||
Ok(Ok(packet)) => Some(packet),
|
||||
Ok(Err(err)) => return Err(anyhow!("failed to wait for icmp packet: {}", err)),
|
||||
Err(_) => None,
|
||||
};
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
pub async fn ping6(
|
||||
&self,
|
||||
addr: Ipv6Addr,
|
||||
id: u16,
|
||||
seq: u16,
|
||||
payload: &[u8],
|
||||
deadline: Duration,
|
||||
) -> Result<Option<IcmpReply>> {
|
||||
let token = IcmpHandlerToken(IpAddr::V6(addr), Some(id), seq);
|
||||
let rx = self.add_handler(token.clone()).await?;
|
||||
|
||||
let echo = IcmpEchoHeader { id, seq };
|
||||
let header = Icmpv6Header::new(Icmpv6Type::EchoRequest(echo));
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
header.write(&mut buffer)?;
|
||||
buffer.extend_from_slice(payload);
|
||||
|
||||
self.socket
|
||||
.send_to(&buffer, SocketAddr::V6(SocketAddrV6::new(addr, 0, 0, 0)))
|
||||
.await?;
|
||||
|
||||
let result = timeout(deadline, rx).await;
|
||||
self.remove_handler(token).await?;
|
||||
let reply = match result {
|
||||
Ok(Ok(packet)) => Some(packet),
|
||||
Ok(Err(err)) => return Err(anyhow!("failed to wait for icmp packet: {}", err)),
|
||||
Err(_) => None,
|
||||
};
|
||||
Ok(reply)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IcmpClient {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.task) <= 1 {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use autonet::{AutoNetworkChangeset, AutoNetworkWatcher, NetworkMetadata};
|
||||
use futures::{future::join_all, TryFutureExt};
|
||||
use hbridge::HostBridge;
|
||||
use krata::{
|
||||
client::ControlClientProvider,
|
||||
dial::ControlDialAddress,
|
||||
v1::{common::Guest, control::control_service_client::ControlServiceClient},
|
||||
};
|
||||
use log::warn;
|
||||
use tokio::{task::JoinHandle, time::sleep};
|
||||
use tonic::transport::Channel;
|
||||
use uuid::Uuid;
|
||||
use vbridge::VirtualBridge;
|
||||
|
||||
use crate::backend::NetworkBackend;
|
||||
|
||||
pub mod autonet;
|
||||
pub mod backend;
|
||||
pub mod chandev;
|
||||
pub mod hbridge;
|
||||
pub mod icmp;
|
||||
pub mod nat;
|
||||
pub mod pkt;
|
||||
pub mod proxynat;
|
||||
pub mod raw_socket;
|
||||
pub mod vbridge;
|
||||
|
||||
const HOST_BRIDGE_MTU: usize = 1500;
|
||||
pub const EXTRA_MTU: usize = 20;
|
||||
|
||||
pub struct NetworkService {
|
||||
pub control: ControlServiceClient<Channel>,
|
||||
pub guests: HashMap<Uuid, Guest>,
|
||||
pub backends: HashMap<Uuid, JoinHandle<()>>,
|
||||
pub bridge: VirtualBridge,
|
||||
pub hbridge: HostBridge,
|
||||
}
|
||||
|
||||
impl NetworkService {
|
||||
pub async fn new(control_address: ControlDialAddress) -> Result<NetworkService> {
|
||||
let control = ControlClientProvider::dial(control_address).await?;
|
||||
let bridge = VirtualBridge::new()?;
|
||||
let hbridge =
|
||||
HostBridge::new(HOST_BRIDGE_MTU + EXTRA_MTU, "krata0".to_string(), &bridge).await?;
|
||||
Ok(NetworkService {
|
||||
control,
|
||||
guests: HashMap::new(),
|
||||
backends: HashMap::new(),
|
||||
bridge,
|
||||
hbridge,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkService {
|
||||
pub async fn watch(&mut self) -> Result<()> {
|
||||
let mut watcher = AutoNetworkWatcher::new(self.control.clone()).await?;
|
||||
let mut receiver = watcher.events.subscribe();
|
||||
loop {
|
||||
let changeset = watcher.read_changes().await?;
|
||||
self.process_network_changeset(&mut watcher, changeset)
|
||||
.await?;
|
||||
watcher.wait(&mut receiver).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_network_changeset(
|
||||
&mut self,
|
||||
collector: &mut AutoNetworkWatcher,
|
||||
changeset: AutoNetworkChangeset,
|
||||
) -> Result<()> {
|
||||
for removal in &changeset.removed {
|
||||
if let Some(handle) = self.backends.remove(&removal.uuid) {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
let futures = changeset
|
||||
.added
|
||||
.iter()
|
||||
.map(|metadata| {
|
||||
self.add_network_backend(metadata)
|
||||
.map_err(|x| (metadata.clone(), x))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let mut failed: Vec<Uuid> = Vec::new();
|
||||
let mut launched: Vec<(Uuid, JoinHandle<()>)> = Vec::new();
|
||||
let results = join_all(futures).await;
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(launch) => {
|
||||
launched.push(launch);
|
||||
}
|
||||
|
||||
Err((metadata, error)) => {
|
||||
warn!(
|
||||
"failed to launch network backend for krata guest {}: {}",
|
||||
metadata.uuid, error
|
||||
);
|
||||
failed.push(metadata.uuid);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (uuid, handle) in launched {
|
||||
self.backends.insert(uuid, handle);
|
||||
}
|
||||
|
||||
for uuid in failed {
|
||||
collector.mark_unknown(uuid)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_network_backend(
|
||||
&self,
|
||||
metadata: &NetworkMetadata,
|
||||
) -> Result<(Uuid, JoinHandle<()>)> {
|
||||
let mut network = NetworkBackend::new(metadata.clone(), self.bridge.clone())?;
|
||||
network.init().await?;
|
||||
Ok((metadata.uuid, network.launch().await?))
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use bytes::BytesMut;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use super::key::NatKey;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NatHandlerContext {
|
||||
pub mtu: usize,
|
||||
pub key: NatKey,
|
||||
pub transmit_sender: Sender<BytesMut>,
|
||||
pub reclaim_sender: Sender<NatKey>,
|
||||
}
|
||||
|
||||
impl NatHandlerContext {
|
||||
pub fn try_transmit(&self, buffer: BytesMut) -> Result<()> {
|
||||
self.transmit_sender.try_send(buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reclaim(&self) -> Result<()> {
|
||||
let _ = self.reclaim_sender.try_send(self.key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait NatHandler: Send {
|
||||
async fn receive(&self, packet: &[u8]) -> Result<bool>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait NatHandlerFactory: Send {
|
||||
async fn nat(&self, context: NatHandlerContext) -> Option<Box<dyn NatHandler>>;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use smoltcp::wire::{EthernetAddress, IpEndpoint};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub enum NatKeyProtocol {
|
||||
Tcp,
|
||||
Udp,
|
||||
Icmp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub struct NatKey {
|
||||
pub protocol: NatKeyProtocol,
|
||||
pub client_mac: EthernetAddress,
|
||||
pub local_mac: EthernetAddress,
|
||||
pub client_ip: IpEndpoint,
|
||||
pub external_ip: IpEndpoint,
|
||||
}
|
||||
|
||||
impl Display for NatKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} -> {} {:?} {} -> {}",
|
||||
self.client_mac, self.local_mac, self.protocol, self.client_ip, self.external_ip
|
||||
)
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use self::handler::NatHandlerFactory;
|
||||
use self::processor::NatProcessor;
|
||||
use bytes::BytesMut;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use smoltcp::wire::IpCidr;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub mod handler;
|
||||
pub mod key;
|
||||
pub mod processor;
|
||||
pub mod table;
|
||||
|
||||
pub struct Nat {
|
||||
pub receive_sender: Sender<BytesMut>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Nat {
|
||||
pub fn new(
|
||||
mtu: usize,
|
||||
factory: Box<dyn NatHandlerFactory>,
|
||||
local_mac: EthernetAddress,
|
||||
local_cidrs: Vec<IpCidr>,
|
||||
transmit_sender: Sender<BytesMut>,
|
||||
) -> Result<Self> {
|
||||
let (receive_sender, task) =
|
||||
NatProcessor::launch(mtu, factory, local_mac, local_cidrs, transmit_sender)?;
|
||||
Ok(Self {
|
||||
receive_sender,
|
||||
task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Nat {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
@ -1,330 +0,0 @@
|
||||
use crate::pkt::RecvPacket;
|
||||
use crate::pkt::RecvPacketIp;
|
||||
use anyhow::Result;
|
||||
use bytes::BytesMut;
|
||||
use etherparse::Icmpv4Header;
|
||||
use etherparse::Icmpv4Type;
|
||||
use etherparse::Icmpv6Header;
|
||||
use etherparse::Icmpv6Type;
|
||||
use etherparse::IpNumber;
|
||||
use etherparse::IpPayloadSlice;
|
||||
use etherparse::Ipv4Slice;
|
||||
use etherparse::Ipv6Slice;
|
||||
use etherparse::SlicedPacket;
|
||||
use etherparse::TcpHeaderSlice;
|
||||
use etherparse::UdpHeaderSlice;
|
||||
use log::warn;
|
||||
use log::{debug, trace};
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use smoltcp::wire::IpAddress;
|
||||
use smoltcp::wire::IpCidr;
|
||||
use smoltcp::wire::IpEndpoint;
|
||||
use std::collections::hash_map::Entry;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::channel;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use super::handler::NatHandler;
|
||||
use super::handler::NatHandlerContext;
|
||||
use super::handler::NatHandlerFactory;
|
||||
use super::key::NatKey;
|
||||
use super::key::NatKeyProtocol;
|
||||
use super::table::NatTable;
|
||||
|
||||
const RECEIVE_CHANNEL_QUEUE_LEN: usize = 3000;
|
||||
const RECLAIM_CHANNEL_QUEUE_LEN: usize = 30;
|
||||
|
||||
pub struct NatProcessor {
|
||||
mtu: usize,
|
||||
local_mac: EthernetAddress,
|
||||
local_cidrs: Vec<IpCidr>,
|
||||
table: NatTable,
|
||||
factory: Box<dyn NatHandlerFactory>,
|
||||
transmit_sender: Sender<BytesMut>,
|
||||
reclaim_sender: Sender<NatKey>,
|
||||
reclaim_receiver: Receiver<NatKey>,
|
||||
receive_receiver: Receiver<BytesMut>,
|
||||
}
|
||||
|
||||
enum NatProcessorSelect {
|
||||
Reclaim(Option<NatKey>),
|
||||
ReceivedPacket(Option<BytesMut>),
|
||||
}
|
||||
|
||||
impl NatProcessor {
|
||||
pub fn launch(
|
||||
mtu: usize,
|
||||
factory: Box<dyn NatHandlerFactory>,
|
||||
local_mac: EthernetAddress,
|
||||
local_cidrs: Vec<IpCidr>,
|
||||
transmit_sender: Sender<BytesMut>,
|
||||
) -> Result<(Sender<BytesMut>, JoinHandle<()>)> {
|
||||
let (reclaim_sender, reclaim_receiver) = channel(RECLAIM_CHANNEL_QUEUE_LEN);
|
||||
let (receive_sender, receive_receiver) = channel(RECEIVE_CHANNEL_QUEUE_LEN);
|
||||
let mut processor = Self {
|
||||
mtu,
|
||||
local_mac,
|
||||
local_cidrs,
|
||||
factory,
|
||||
table: NatTable::new(),
|
||||
transmit_sender,
|
||||
reclaim_sender,
|
||||
receive_receiver,
|
||||
reclaim_receiver,
|
||||
};
|
||||
|
||||
let handle = tokio::task::spawn(async move {
|
||||
if let Err(error) = processor.process().await {
|
||||
warn!("nat processing failed: {}", error);
|
||||
}
|
||||
});
|
||||
|
||||
Ok((receive_sender, handle))
|
||||
}
|
||||
|
||||
pub async fn process(&mut self) -> Result<()> {
|
||||
loop {
|
||||
let selection = select! {
|
||||
x = self.reclaim_receiver.recv() => NatProcessorSelect::Reclaim(x),
|
||||
x = self.receive_receiver.recv() => NatProcessorSelect::ReceivedPacket(x),
|
||||
};
|
||||
|
||||
match selection {
|
||||
NatProcessorSelect::Reclaim(Some(key)) => {
|
||||
if self.table.inner.remove(&key).is_some() {
|
||||
debug!("reclaimed nat key: {}", key);
|
||||
}
|
||||
}
|
||||
|
||||
NatProcessorSelect::ReceivedPacket(Some(packet)) => {
|
||||
if let Ok(slice) = SlicedPacket::from_ethernet(&packet) {
|
||||
let Ok(packet) = RecvPacket::new(&packet, &slice) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.process_packet(&packet).await?;
|
||||
}
|
||||
}
|
||||
|
||||
NatProcessorSelect::ReceivedPacket(None) | NatProcessorSelect::Reclaim(None) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn process_reclaim(&mut self) -> Result<Option<NatKey>> {
|
||||
Ok(if let Some(key) = self.reclaim_receiver.recv().await {
|
||||
if self.table.inner.remove(&key).is_some() {
|
||||
debug!("reclaimed nat key: {}", key);
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn process_packet<'a>(&mut self, packet: &RecvPacket<'a>) -> Result<()> {
|
||||
let Some(ether) = packet.ether else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mac = EthernetAddress(ether.destination());
|
||||
if mac != self.local_mac {
|
||||
trace!(
|
||||
"received packet with destination {} which is not the local mac {}",
|
||||
mac,
|
||||
self.local_mac
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let key = match packet.ip {
|
||||
Some(RecvPacketIp::Ipv4(ipv4)) => self.extract_key_ipv4(packet, ipv4)?,
|
||||
Some(RecvPacketIp::Ipv6(ipv6)) => self.extract_key_ipv6(packet, ipv6)?,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let Some(key) = key else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for cidr in &self.local_cidrs {
|
||||
if cidr.contains_addr(&key.external_ip.addr) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let context = NatHandlerContext {
|
||||
mtu: self.mtu,
|
||||
key,
|
||||
transmit_sender: self.transmit_sender.clone(),
|
||||
reclaim_sender: self.reclaim_sender.clone(),
|
||||
};
|
||||
let handler: Option<&mut Box<dyn NatHandler>> = match self.table.inner.entry(key) {
|
||||
Entry::Occupied(entry) => Some(entry.into_mut()),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Some(handler) = self.factory.nat(context).await {
|
||||
debug!("creating nat entry for key: {}", key);
|
||||
Some(entry.insert(handler))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(handler) = handler {
|
||||
if !handler.receive(packet.raw).await? {
|
||||
self.reclaim_sender.try_send(key)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_key_ipv4<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
ipv4: &Ipv4Slice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let source_addr = IpAddress::Ipv4(ipv4.header().source_addr().into());
|
||||
let dest_addr = IpAddress::Ipv4(ipv4.header().destination_addr().into());
|
||||
Ok(match ipv4.header().protocol() {
|
||||
IpNumber::TCP => {
|
||||
self.extract_key_tcp(packet, source_addr, dest_addr, ipv4.payload())?
|
||||
}
|
||||
|
||||
IpNumber::UDP => {
|
||||
self.extract_key_udp(packet, source_addr, dest_addr, ipv4.payload())?
|
||||
}
|
||||
|
||||
IpNumber::ICMP => {
|
||||
self.extract_key_icmpv4(packet, source_addr, dest_addr, ipv4.payload())?
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extract_key_ipv6<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
ipv6: &Ipv6Slice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let source_addr = IpAddress::Ipv6(ipv6.header().source_addr().into());
|
||||
let dest_addr = IpAddress::Ipv6(ipv6.header().destination_addr().into());
|
||||
Ok(match ipv6.header().next_header() {
|
||||
IpNumber::TCP => {
|
||||
self.extract_key_tcp(packet, source_addr, dest_addr, ipv6.payload())?
|
||||
}
|
||||
|
||||
IpNumber::UDP => {
|
||||
self.extract_key_udp(packet, source_addr, dest_addr, ipv6.payload())?
|
||||
}
|
||||
|
||||
IpNumber::IPV6_ICMP => {
|
||||
self.extract_key_icmpv6(packet, source_addr, dest_addr, ipv6.payload())?
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extract_key_udp<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
source_addr: IpAddress,
|
||||
dest_addr: IpAddress,
|
||||
payload: &IpPayloadSlice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let Some(ether) = packet.ether else {
|
||||
return Ok(None);
|
||||
};
|
||||
let header = UdpHeaderSlice::from_slice(payload.payload)?;
|
||||
let source = IpEndpoint::new(source_addr, header.source_port());
|
||||
let dest = IpEndpoint::new(dest_addr, header.destination_port());
|
||||
Ok(Some(NatKey {
|
||||
protocol: NatKeyProtocol::Udp,
|
||||
client_mac: EthernetAddress(ether.source()),
|
||||
local_mac: EthernetAddress(ether.destination()),
|
||||
client_ip: source,
|
||||
external_ip: dest,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn extract_key_icmpv4<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
source_addr: IpAddress,
|
||||
dest_addr: IpAddress,
|
||||
payload: &IpPayloadSlice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let Some(ether) = packet.ether else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (header, _) = Icmpv4Header::from_slice(payload.payload)?;
|
||||
let Icmpv4Type::EchoRequest(_) = header.icmp_type else {
|
||||
return Ok(None);
|
||||
};
|
||||
let source = IpEndpoint::new(source_addr, 0);
|
||||
let dest = IpEndpoint::new(dest_addr, 0);
|
||||
Ok(Some(NatKey {
|
||||
protocol: NatKeyProtocol::Icmp,
|
||||
client_mac: EthernetAddress(ether.source()),
|
||||
local_mac: EthernetAddress(ether.destination()),
|
||||
client_ip: source,
|
||||
external_ip: dest,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn extract_key_icmpv6<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
source_addr: IpAddress,
|
||||
dest_addr: IpAddress,
|
||||
payload: &IpPayloadSlice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let Some(ether) = packet.ether else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (header, _) = Icmpv6Header::from_slice(payload.payload)?;
|
||||
let Icmpv6Type::EchoRequest(_) = header.icmp_type else {
|
||||
return Ok(None);
|
||||
};
|
||||
let source = IpEndpoint::new(source_addr, 0);
|
||||
let dest = IpEndpoint::new(dest_addr, 0);
|
||||
Ok(Some(NatKey {
|
||||
protocol: NatKeyProtocol::Icmp,
|
||||
client_mac: EthernetAddress(ether.source()),
|
||||
local_mac: EthernetAddress(ether.destination()),
|
||||
client_ip: source,
|
||||
external_ip: dest,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn extract_key_tcp<'a>(
|
||||
&mut self,
|
||||
packet: &RecvPacket<'a>,
|
||||
source_addr: IpAddress,
|
||||
dest_addr: IpAddress,
|
||||
payload: &IpPayloadSlice<'a>,
|
||||
) -> Result<Option<NatKey>> {
|
||||
let Some(ether) = packet.ether else {
|
||||
return Ok(None);
|
||||
};
|
||||
let header = TcpHeaderSlice::from_slice(payload.payload)?;
|
||||
let source = IpEndpoint::new(source_addr, header.source_port());
|
||||
let dest = IpEndpoint::new(dest_addr, header.destination_port());
|
||||
Ok(Some(NatKey {
|
||||
protocol: NatKeyProtocol::Tcp,
|
||||
client_mac: EthernetAddress(ether.source()),
|
||||
local_mac: EthernetAddress(ether.destination()),
|
||||
client_ip: source,
|
||||
external_ip: dest,
|
||||
}))
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{handler::NatHandler, key::NatKey};
|
||||
|
||||
pub struct NatTable {
|
||||
pub inner: HashMap<NatKey, Box<dyn NatHandler>>,
|
||||
}
|
||||
|
||||
impl Default for NatTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl NatTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use etherparse::{Ethernet2Slice, Ipv4Slice, Ipv6Slice, LinkSlice, NetSlice, SlicedPacket};
|
||||
|
||||
pub enum RecvPacketIp<'a> {
|
||||
Ipv4(&'a Ipv4Slice<'a>),
|
||||
Ipv6(&'a Ipv6Slice<'a>),
|
||||
}
|
||||
|
||||
pub struct RecvPacket<'a> {
|
||||
pub raw: &'a [u8],
|
||||
pub slice: &'a SlicedPacket<'a>,
|
||||
pub ether: Option<&'a Ethernet2Slice<'a>>,
|
||||
pub ip: Option<RecvPacketIp<'a>>,
|
||||
}
|
||||
|
||||
impl RecvPacket<'_> {
|
||||
pub fn new<'a>(raw: &'a [u8], slice: &'a SlicedPacket<'a>) -> Result<RecvPacket<'a>> {
|
||||
let ether = match slice.link {
|
||||
Some(LinkSlice::Ethernet2(ref ether)) => Some(ether),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let ip = match slice.net {
|
||||
Some(NetSlice::Ipv4(ref ipv4)) => Some(RecvPacketIp::Ipv4(ipv4)),
|
||||
Some(NetSlice::Ipv6(ref ipv6)) => Some(RecvPacketIp::Ipv6(ipv6)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let packet = RecvPacket {
|
||||
raw,
|
||||
slice,
|
||||
ether,
|
||||
ip,
|
||||
};
|
||||
Ok(packet)
|
||||
}
|
||||
}
|
@ -1,276 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use etherparse::{
|
||||
IcmpEchoHeader, Icmpv4Header, Icmpv4Type, Icmpv6Header, Icmpv6Type, IpNumber, Ipv4Slice,
|
||||
Ipv6Slice, NetSlice, PacketBuilder, SlicedPacket,
|
||||
};
|
||||
use log::{debug, trace, warn};
|
||||
use smoltcp::wire::IpAddress;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
icmp::{IcmpClient, IcmpProtocol, IcmpReply},
|
||||
nat::handler::{NatHandler, NatHandlerContext},
|
||||
};
|
||||
|
||||
const ICMP_PING_TIMEOUT_SECS: u64 = 20;
|
||||
const ICMP_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
pub struct ProxyIcmpHandler {
|
||||
rx_sender: Sender<BytesMut>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NatHandler for ProxyIcmpHandler {
|
||||
async fn receive(&self, data: &[u8]) -> Result<bool> {
|
||||
if self.rx_sender.is_closed() {
|
||||
Ok(true)
|
||||
} else {
|
||||
self.rx_sender.try_send(data.into())?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyIcmpSelect {
|
||||
Internal(BytesMut),
|
||||
Close,
|
||||
}
|
||||
|
||||
impl ProxyIcmpHandler {
|
||||
pub fn new(rx_sender: Sender<BytesMut>) -> Self {
|
||||
ProxyIcmpHandler { rx_sender }
|
||||
}
|
||||
|
||||
pub async fn spawn(
|
||||
&mut self,
|
||||
context: NatHandlerContext,
|
||||
rx_receiver: Receiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let client = IcmpClient::new(match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(_) => IcmpProtocol::Icmpv4,
|
||||
IpAddress::Ipv6(_) => IcmpProtocol::Icmpv6,
|
||||
})?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = ProxyIcmpHandler::process(client, rx_receiver, context).await {
|
||||
warn!("processing of icmp proxy failed: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(
|
||||
client: IcmpClient,
|
||||
mut rx_receiver: Receiver<BytesMut>,
|
||||
context: NatHandlerContext,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let deadline = tokio::time::sleep(Duration::from_secs(ICMP_TIMEOUT_SECS));
|
||||
let selection = select! {
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyIcmpSelect::Internal(data)
|
||||
} else {
|
||||
ProxyIcmpSelect::Close
|
||||
},
|
||||
_ = deadline => ProxyIcmpSelect::Close,
|
||||
};
|
||||
|
||||
match selection {
|
||||
ProxyIcmpSelect::Internal(data) => {
|
||||
let packet = SlicedPacket::from_ethernet(&data)?;
|
||||
let Some(ref net) = packet.net else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match net {
|
||||
NetSlice::Ipv4(ipv4) => {
|
||||
ProxyIcmpHandler::process_ipv4(&context, ipv4, &client).await?
|
||||
}
|
||||
|
||||
NetSlice::Ipv6(ipv6) => {
|
||||
ProxyIcmpHandler::process_ipv6(&context, ipv6, &client).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProxyIcmpSelect::Close => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.reclaim().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_ipv4(
|
||||
context: &NatHandlerContext,
|
||||
ipv4: &Ipv4Slice<'_>,
|
||||
client: &IcmpClient,
|
||||
) -> Result<()> {
|
||||
if ipv4.header().protocol() != IpNumber::ICMP {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (header, payload) = Icmpv4Header::from_slice(ipv4.payload().payload)?;
|
||||
if let Icmpv4Type::EchoRequest(echo) = header.icmp_type {
|
||||
let IpAddr::V4(external_ipv4) = context.key.external_ip.addr.into() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let context = context.clone();
|
||||
let client = client.clone();
|
||||
let payload = payload.to_vec();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(error) = ProxyIcmpHandler::process_echo_ipv4(
|
||||
context,
|
||||
client,
|
||||
external_ipv4,
|
||||
echo,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
trace!("icmp4 echo failed: {}", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_ipv6(
|
||||
context: &NatHandlerContext,
|
||||
ipv6: &Ipv6Slice<'_>,
|
||||
client: &IcmpClient,
|
||||
) -> Result<()> {
|
||||
if ipv6.header().next_header() != IpNumber::IPV6_ICMP {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (header, payload) = Icmpv6Header::from_slice(ipv6.payload().payload)?;
|
||||
if let Icmpv6Type::EchoRequest(echo) = header.icmp_type {
|
||||
let IpAddr::V6(external_ipv6) = context.key.external_ip.addr.into() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let context = context.clone();
|
||||
let client = client.clone();
|
||||
let payload = payload.to_vec();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(error) = ProxyIcmpHandler::process_echo_ipv6(
|
||||
context,
|
||||
client,
|
||||
external_ipv6,
|
||||
echo,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
trace!("icmp6 echo failed: {}", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_echo_ipv4(
|
||||
context: NatHandlerContext,
|
||||
client: IcmpClient,
|
||||
external_ipv4: Ipv4Addr,
|
||||
echo: IcmpEchoHeader,
|
||||
payload: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let reply = client
|
||||
.ping4(
|
||||
external_ipv4,
|
||||
echo.id,
|
||||
echo.seq,
|
||||
&payload,
|
||||
Duration::from_secs(ICMP_PING_TIMEOUT_SECS),
|
||||
)
|
||||
.await?;
|
||||
let Some(IcmpReply::Icmpv4 {
|
||||
header: _,
|
||||
echo,
|
||||
payload,
|
||||
}) = reply
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let packet = PacketBuilder::ethernet2(context.key.local_mac.0, context.key.client_mac.0);
|
||||
let packet = match (context.key.external_ip.addr, context.key.client_ip.addr) {
|
||||
(IpAddress::Ipv4(external_addr), IpAddress::Ipv4(client_addr)) => {
|
||||
packet.ipv4(external_addr.0, client_addr.0, 20)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("IP endpoint mismatch"));
|
||||
}
|
||||
};
|
||||
let packet = packet.icmpv4_echo_reply(echo.id, echo.seq);
|
||||
let buffer = BytesMut::with_capacity(packet.size(payload.len()));
|
||||
let mut writer = buffer.writer();
|
||||
packet.write(&mut writer, &payload)?;
|
||||
let buffer = writer.into_inner();
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit icmp packet: {}", error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_echo_ipv6(
|
||||
context: NatHandlerContext,
|
||||
client: IcmpClient,
|
||||
external_ipv6: Ipv6Addr,
|
||||
echo: IcmpEchoHeader,
|
||||
payload: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let reply = client
|
||||
.ping6(
|
||||
external_ipv6,
|
||||
echo.id,
|
||||
echo.seq,
|
||||
&payload,
|
||||
Duration::from_secs(ICMP_PING_TIMEOUT_SECS),
|
||||
)
|
||||
.await?;
|
||||
let Some(IcmpReply::Icmpv6 {
|
||||
header: _,
|
||||
echo,
|
||||
payload,
|
||||
}) = reply
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let packet = PacketBuilder::ethernet2(context.key.local_mac.0, context.key.client_mac.0);
|
||||
let packet = match (context.key.external_ip.addr, context.key.client_ip.addr) {
|
||||
(IpAddress::Ipv6(external_addr), IpAddress::Ipv6(client_addr)) => {
|
||||
packet.ipv6(external_addr.0, client_addr.0, 20)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("IP endpoint mismatch"));
|
||||
}
|
||||
};
|
||||
let packet = packet.icmpv6_echo_reply(echo.id, echo.seq);
|
||||
let buffer = BytesMut::with_capacity(packet.size(payload.len()));
|
||||
let mut writer = buffer.writer();
|
||||
packet.write(&mut writer, &payload)?;
|
||||
let buffer = writer.into_inner();
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit icmp packet: {}", error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use log::warn;
|
||||
|
||||
use tokio::sync::mpsc::channel;
|
||||
|
||||
use crate::proxynat::udp::ProxyUdpHandler;
|
||||
|
||||
use crate::nat::handler::{NatHandler, NatHandlerContext, NatHandlerFactory};
|
||||
use crate::nat::key::NatKeyProtocol;
|
||||
|
||||
use self::icmp::ProxyIcmpHandler;
|
||||
use self::tcp::ProxyTcpHandler;
|
||||
|
||||
mod icmp;
|
||||
mod tcp;
|
||||
mod udp;
|
||||
|
||||
const RX_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
|
||||
pub struct ProxyNatHandlerFactory {}
|
||||
|
||||
impl Default for ProxyNatHandlerFactory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyNatHandlerFactory {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NatHandlerFactory for ProxyNatHandlerFactory {
|
||||
async fn nat(&self, context: NatHandlerContext) -> Option<Box<dyn NatHandler>> {
|
||||
match context.key.protocol {
|
||||
NatKeyProtocol::Udp => {
|
||||
let (rx_sender, rx_receiver) = channel::<BytesMut>(RX_CHANNEL_QUEUE_LEN);
|
||||
let mut handler = ProxyUdpHandler::new(rx_sender);
|
||||
|
||||
if let Err(error) = handler.spawn(context, rx_receiver).await {
|
||||
warn!("unable to spawn udp proxy handler: {}", error);
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(handler))
|
||||
}
|
||||
}
|
||||
|
||||
NatKeyProtocol::Icmp => {
|
||||
let (rx_sender, rx_receiver) = channel::<BytesMut>(RX_CHANNEL_QUEUE_LEN);
|
||||
let mut handler = ProxyIcmpHandler::new(rx_sender);
|
||||
|
||||
if let Err(error) = handler.spawn(context, rx_receiver).await {
|
||||
warn!("unable to spawn icmp proxy handler: {}", error);
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(handler))
|
||||
}
|
||||
}
|
||||
|
||||
NatKeyProtocol::Tcp => {
|
||||
let (rx_sender, rx_receiver) = channel::<BytesMut>(RX_CHANNEL_QUEUE_LEN);
|
||||
let mut handler = ProxyTcpHandler::new(rx_sender);
|
||||
|
||||
if let Err(error) = handler.spawn(context, rx_receiver).await {
|
||||
warn!("unable to spawn tcp proxy handler: {}", error);
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(handler))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,466 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use bytes::BytesMut;
|
||||
use etherparse::{EtherType, Ethernet2Header};
|
||||
use log::{debug, warn};
|
||||
use smoltcp::{
|
||||
iface::{Config, Interface, SocketSet, SocketStorage},
|
||||
phy::Medium,
|
||||
socket::tcp::{self, SocketBuffer, State},
|
||||
time::Instant,
|
||||
wire::{HardwareAddress, IpAddress, IpCidr},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
select,
|
||||
sync::mpsc::channel,
|
||||
};
|
||||
use tokio::{sync::mpsc::Receiver, sync::mpsc::Sender};
|
||||
|
||||
use crate::{
|
||||
chandev::ChannelDevice,
|
||||
nat::handler::{NatHandler, NatHandlerContext},
|
||||
};
|
||||
|
||||
const TCP_BUFFER_SIZE: usize = 65535;
|
||||
const TCP_IP_BUFFER_QUEUE_LEN: usize = 3000;
|
||||
const TCP_ACCEPT_TIMEOUT_SECS: u64 = 120;
|
||||
const TCP_DANGLE_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
pub struct ProxyTcpHandler {
|
||||
rx_sender: Sender<BytesMut>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NatHandler for ProxyTcpHandler {
|
||||
async fn receive(&self, data: &[u8]) -> Result<bool> {
|
||||
if self.rx_sender.is_closed() {
|
||||
Ok(false)
|
||||
} else {
|
||||
self.rx_sender.try_send(data.into())?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProxyTcpAcceptSelect {
|
||||
Internal(BytesMut),
|
||||
TxIpPacket(BytesMut),
|
||||
TimePassed,
|
||||
DoNothing,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProxyTcpDataSelect {
|
||||
ExternalRecv(usize),
|
||||
ExternalSent(usize),
|
||||
InternalRecv(BytesMut),
|
||||
TxIpPacket(BytesMut),
|
||||
TimePassed,
|
||||
DoNothing,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProxyTcpFinishSelect {
|
||||
InternalRecv(BytesMut),
|
||||
TxIpPacket(BytesMut),
|
||||
Close,
|
||||
}
|
||||
|
||||
impl ProxyTcpHandler {
|
||||
pub fn new(rx_sender: Sender<BytesMut>) -> Self {
|
||||
ProxyTcpHandler { rx_sender }
|
||||
}
|
||||
|
||||
pub async fn spawn(
|
||||
&mut self,
|
||||
context: NatHandlerContext,
|
||||
rx_receiver: Receiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let external_addr = match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(addr) => {
|
||||
SocketAddr::new(IpAddr::V4(addr.0.into()), context.key.external_ip.port)
|
||||
}
|
||||
IpAddress::Ipv6(addr) => {
|
||||
SocketAddr::new(IpAddr::V6(addr.0.into()), context.key.external_ip.port)
|
||||
}
|
||||
};
|
||||
|
||||
let socket = TcpStream::connect(external_addr).await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = ProxyTcpHandler::process(context, socket, rx_receiver).await {
|
||||
warn!("processing of tcp proxy failed: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(
|
||||
context: NatHandlerContext,
|
||||
mut external_socket: TcpStream,
|
||||
mut rx_receiver: Receiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let (ip_sender, mut ip_receiver) = channel::<BytesMut>(TCP_IP_BUFFER_QUEUE_LEN);
|
||||
let mut external_buffer = vec![0u8; TCP_BUFFER_SIZE];
|
||||
|
||||
let mut device = ChannelDevice::new(
|
||||
context.mtu - Ethernet2Header::LEN,
|
||||
Medium::Ip,
|
||||
ip_sender.clone(),
|
||||
);
|
||||
let config = Config::new(HardwareAddress::Ip);
|
||||
|
||||
let tcp_rx_buffer = SocketBuffer::new(vec![0; TCP_BUFFER_SIZE]);
|
||||
let tcp_tx_buffer = SocketBuffer::new(vec![0; TCP_BUFFER_SIZE]);
|
||||
let internal_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer);
|
||||
let mut iface = Interface::new(config, &mut device, Instant::now());
|
||||
|
||||
iface.update_ip_addrs(|addrs| {
|
||||
let _ = addrs.push(IpCidr::new(context.key.external_ip.addr, 0));
|
||||
});
|
||||
|
||||
let mut sockets = SocketSet::new([SocketStorage::EMPTY]);
|
||||
let internal_socket_handle = sockets.add(internal_socket);
|
||||
let (mut external_r, mut external_w) = external_socket.split();
|
||||
|
||||
{
|
||||
let socket = sockets.get_mut::<tcp::Socket>(internal_socket_handle);
|
||||
socket.connect(
|
||||
iface.context(),
|
||||
context.key.client_ip,
|
||||
context.key.external_ip,
|
||||
)?;
|
||||
}
|
||||
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
|
||||
let mut sleeper: Option<tokio::time::Sleep> = None;
|
||||
loop {
|
||||
let socket = sockets.get_mut::<tcp::Socket>(internal_socket_handle);
|
||||
if socket.is_active() && socket.state() != State::SynSent {
|
||||
break;
|
||||
}
|
||||
|
||||
if socket.state() == State::Closed {
|
||||
break;
|
||||
}
|
||||
|
||||
let deadline = tokio::time::sleep(Duration::from_secs(TCP_ACCEPT_TIMEOUT_SECS));
|
||||
let selection = if let Some(sleep) = sleeper.take() {
|
||||
select! {
|
||||
biased;
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpAcceptSelect::Internal(data)
|
||||
} else {
|
||||
ProxyTcpAcceptSelect::Close
|
||||
},
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpAcceptSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpAcceptSelect::Close
|
||||
},
|
||||
_ = sleep => ProxyTcpAcceptSelect::TimePassed,
|
||||
_ = deadline => ProxyTcpAcceptSelect::Close,
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
biased;
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpAcceptSelect::Internal(data)
|
||||
} else {
|
||||
ProxyTcpAcceptSelect::Close
|
||||
},
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpAcceptSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpAcceptSelect::Close
|
||||
},
|
||||
_ = std::future::ready(()) => ProxyTcpAcceptSelect::DoNothing,
|
||||
_ = deadline => ProxyTcpAcceptSelect::Close,
|
||||
}
|
||||
};
|
||||
match selection {
|
||||
ProxyTcpAcceptSelect::TimePassed => {
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
}
|
||||
|
||||
ProxyTcpAcceptSelect::DoNothing => {
|
||||
sleeper = Some(tokio::time::sleep(Duration::from_micros(100)));
|
||||
}
|
||||
|
||||
ProxyTcpAcceptSelect::Internal(data) => {
|
||||
let (_, payload) = Ethernet2Header::from_slice(&data)?;
|
||||
device.rx = Some(payload.into());
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
}
|
||||
|
||||
ProxyTcpAcceptSelect::TxIpPacket(payload) => {
|
||||
let mut buffer = BytesMut::with_capacity(Ethernet2Header::LEN + payload.len());
|
||||
let header = Ethernet2Header {
|
||||
source: context.key.local_mac.0,
|
||||
destination: context.key.client_mac.0,
|
||||
ether_type: match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(_) => EtherType::IPV4,
|
||||
IpAddress::Ipv6(_) => EtherType::IPV6,
|
||||
},
|
||||
};
|
||||
buffer.extend_from_slice(&header.to_bytes());
|
||||
buffer.extend_from_slice(&payload);
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit tcp packet: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
ProxyTcpAcceptSelect::Close => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let accepted = if sockets
|
||||
.get_mut::<tcp::Socket>(internal_socket_handle)
|
||||
.is_active()
|
||||
{
|
||||
true
|
||||
} else {
|
||||
debug!("failed to accept tcp connection from client");
|
||||
false
|
||||
};
|
||||
|
||||
let mut already_shutdown = false;
|
||||
let mut sleeper: Option<tokio::time::Sleep> = None;
|
||||
loop {
|
||||
if !accepted {
|
||||
break;
|
||||
}
|
||||
|
||||
let socket = sockets.get_mut::<tcp::Socket>(internal_socket_handle);
|
||||
|
||||
match socket.state() {
|
||||
State::Closed
|
||||
| State::Listen
|
||||
| State::Closing
|
||||
| State::LastAck
|
||||
| State::TimeWait => {
|
||||
break;
|
||||
}
|
||||
State::FinWait1
|
||||
| State::SynSent
|
||||
| State::CloseWait
|
||||
| State::FinWait2
|
||||
| State::SynReceived
|
||||
| State::Established => {}
|
||||
}
|
||||
|
||||
let bytes_to_client = if socket.can_send() {
|
||||
socket.send_capacity() - socket.send_queue()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let (bytes_to_external, do_shutdown) = if socket.may_recv() {
|
||||
if let Ok(data) = socket.peek(TCP_BUFFER_SIZE) {
|
||||
if data.is_empty() {
|
||||
(None, false)
|
||||
} else {
|
||||
(Some(data), false)
|
||||
}
|
||||
} else {
|
||||
(None, false)
|
||||
}
|
||||
} else if !already_shutdown && matches!(socket.state(), State::CloseWait) {
|
||||
(None, true)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
let selection = if let Some(sleep) = sleeper.take() {
|
||||
if !do_shutdown {
|
||||
select! {
|
||||
biased;
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::InternalRecv(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = external_w.write(bytes_to_external.unwrap_or(b"")), if bytes_to_external.is_some() => ProxyTcpDataSelect::ExternalSent(x?),
|
||||
x = external_r.read(&mut external_buffer[..bytes_to_client]), if bytes_to_client > 0 => ProxyTcpDataSelect::ExternalRecv(x?),
|
||||
_ = sleep => ProxyTcpDataSelect::TimePassed,
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
biased;
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::InternalRecv(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
_ = external_w.shutdown() => ProxyTcpDataSelect::ExternalSent(0),
|
||||
x = external_r.read(&mut external_buffer[..bytes_to_client]), if bytes_to_client > 0 => ProxyTcpDataSelect::ExternalRecv(x?),
|
||||
_ = sleep => ProxyTcpDataSelect::TimePassed,
|
||||
}
|
||||
}
|
||||
} else if !do_shutdown {
|
||||
select! {
|
||||
biased;
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::InternalRecv(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = external_w.write(bytes_to_external.unwrap_or(b"")), if bytes_to_external.is_some() => ProxyTcpDataSelect::ExternalSent(x?),
|
||||
x = external_r.read(&mut external_buffer[..bytes_to_client]), if bytes_to_client > 0 => ProxyTcpDataSelect::ExternalRecv(x?),
|
||||
_ = std::future::ready(()) => ProxyTcpDataSelect::DoNothing,
|
||||
}
|
||||
} else {
|
||||
select! {
|
||||
biased;
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpDataSelect::InternalRecv(data)
|
||||
} else {
|
||||
ProxyTcpDataSelect::Close
|
||||
},
|
||||
_ = external_w.shutdown() => ProxyTcpDataSelect::ExternalSent(0),
|
||||
x = external_r.read(&mut external_buffer[..bytes_to_client]), if bytes_to_client > 0 => ProxyTcpDataSelect::ExternalRecv(x?),
|
||||
_ = std::future::ready(()) => ProxyTcpDataSelect::DoNothing,
|
||||
}
|
||||
};
|
||||
match selection {
|
||||
ProxyTcpDataSelect::ExternalRecv(size) => {
|
||||
if size == 0 {
|
||||
socket.close();
|
||||
} else {
|
||||
socket.send_slice(&external_buffer[..size])?;
|
||||
}
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::ExternalSent(size) => {
|
||||
if size == 0 {
|
||||
already_shutdown = true;
|
||||
} else {
|
||||
socket.recv(|_| (size, ()))?;
|
||||
}
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::InternalRecv(data) => {
|
||||
let (_, payload) = Ethernet2Header::from_slice(&data)?;
|
||||
device.rx = Some(payload.into());
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::TxIpPacket(payload) => {
|
||||
let mut buffer = BytesMut::with_capacity(Ethernet2Header::LEN + payload.len());
|
||||
let header = Ethernet2Header {
|
||||
source: context.key.local_mac.0,
|
||||
destination: context.key.client_mac.0,
|
||||
ether_type: match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(_) => EtherType::IPV4,
|
||||
IpAddress::Ipv6(_) => EtherType::IPV6,
|
||||
},
|
||||
};
|
||||
buffer.extend_from_slice(&header.to_bytes());
|
||||
buffer.extend_from_slice(&payload);
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit tcp packet: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::TimePassed => {
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::DoNothing => {
|
||||
sleeper = Some(tokio::time::sleep(Duration::from_micros(100)));
|
||||
}
|
||||
|
||||
ProxyTcpDataSelect::Close => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = external_socket.shutdown().await;
|
||||
drop(external_socket);
|
||||
|
||||
loop {
|
||||
let deadline = tokio::time::sleep(Duration::from_secs(TCP_DANGLE_TIMEOUT_SECS));
|
||||
tokio::pin!(deadline);
|
||||
|
||||
let selection = select! {
|
||||
biased;
|
||||
x = ip_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpFinishSelect::TxIpPacket(data)
|
||||
} else {
|
||||
ProxyTcpFinishSelect::Close
|
||||
},
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyTcpFinishSelect::InternalRecv(data)
|
||||
} else {
|
||||
ProxyTcpFinishSelect::Close
|
||||
},
|
||||
_ = deadline => ProxyTcpFinishSelect::Close,
|
||||
};
|
||||
|
||||
match selection {
|
||||
ProxyTcpFinishSelect::InternalRecv(data) => {
|
||||
let (_, payload) = Ethernet2Header::from_slice(&data)?;
|
||||
device.rx = Some(payload.into());
|
||||
iface.poll(Instant::now(), &mut device, &mut sockets);
|
||||
}
|
||||
|
||||
ProxyTcpFinishSelect::TxIpPacket(payload) => {
|
||||
let mut buffer = BytesMut::with_capacity(Ethernet2Header::LEN + payload.len());
|
||||
let header = Ethernet2Header {
|
||||
source: context.key.local_mac.0,
|
||||
destination: context.key.client_mac.0,
|
||||
ether_type: match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(_) => EtherType::IPV4,
|
||||
IpAddress::Ipv6(_) => EtherType::IPV6,
|
||||
},
|
||||
};
|
||||
buffer.extend_from_slice(&header.to_bytes());
|
||||
buffer.extend_from_slice(&payload);
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit tcp packet: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
ProxyTcpFinishSelect::Close => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.reclaim().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use etherparse::{PacketBuilder, SlicedPacket, UdpSlice};
|
||||
use log::{debug, warn};
|
||||
use smoltcp::wire::IpAddress;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
};
|
||||
use tokio::{sync::mpsc::Receiver, sync::mpsc::Sender};
|
||||
use udp_stream::UdpStream;
|
||||
|
||||
use crate::nat::handler::{NatHandler, NatHandlerContext};
|
||||
|
||||
const UDP_TIMEOUT_SECS: u64 = 60;
|
||||
|
||||
pub struct ProxyUdpHandler {
|
||||
rx_sender: Sender<BytesMut>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NatHandler for ProxyUdpHandler {
|
||||
async fn receive(&self, data: &[u8]) -> Result<bool> {
|
||||
if self.rx_sender.is_closed() {
|
||||
Ok(true)
|
||||
} else {
|
||||
self.rx_sender.try_send(data.into())?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyUdpSelect {
|
||||
External(usize),
|
||||
Internal(BytesMut),
|
||||
Close,
|
||||
}
|
||||
|
||||
impl ProxyUdpHandler {
|
||||
pub fn new(rx_sender: Sender<BytesMut>) -> Self {
|
||||
ProxyUdpHandler { rx_sender }
|
||||
}
|
||||
|
||||
pub async fn spawn(
|
||||
&mut self,
|
||||
context: NatHandlerContext,
|
||||
rx_receiver: Receiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let external_addr = match context.key.external_ip.addr {
|
||||
IpAddress::Ipv4(addr) => {
|
||||
SocketAddr::new(IpAddr::V4(addr.0.into()), context.key.external_ip.port)
|
||||
}
|
||||
IpAddress::Ipv6(addr) => {
|
||||
SocketAddr::new(IpAddr::V6(addr.0.into()), context.key.external_ip.port)
|
||||
}
|
||||
};
|
||||
|
||||
let socket = UdpStream::connect(external_addr).await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = ProxyUdpHandler::process(context, socket, rx_receiver).await {
|
||||
warn!("processing of udp proxy failed: {}", error);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(
|
||||
context: NatHandlerContext,
|
||||
mut socket: UdpStream,
|
||||
mut rx_receiver: Receiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let mut external_buffer = vec![0u8; 2048];
|
||||
|
||||
loop {
|
||||
let deadline = tokio::time::sleep(Duration::from_secs(UDP_TIMEOUT_SECS));
|
||||
let selection = select! {
|
||||
x = rx_receiver.recv() => if let Some(data) = x {
|
||||
ProxyUdpSelect::Internal(data)
|
||||
} else {
|
||||
ProxyUdpSelect::Close
|
||||
},
|
||||
x = socket.read(&mut external_buffer) => ProxyUdpSelect::External(x?),
|
||||
_ = deadline => ProxyUdpSelect::Close,
|
||||
};
|
||||
|
||||
match selection {
|
||||
ProxyUdpSelect::External(size) => {
|
||||
let data = &external_buffer[0..size];
|
||||
let packet =
|
||||
PacketBuilder::ethernet2(context.key.local_mac.0, context.key.client_mac.0);
|
||||
let packet = match (context.key.external_ip.addr, context.key.client_ip.addr) {
|
||||
(IpAddress::Ipv4(external_addr), IpAddress::Ipv4(client_addr)) => {
|
||||
packet.ipv4(external_addr.0, client_addr.0, 20)
|
||||
}
|
||||
(IpAddress::Ipv6(external_addr), IpAddress::Ipv6(client_addr)) => {
|
||||
packet.ipv6(external_addr.0, client_addr.0, 20)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("IP endpoint mismatch"));
|
||||
}
|
||||
};
|
||||
let packet =
|
||||
packet.udp(context.key.external_ip.port, context.key.client_ip.port);
|
||||
let buffer = BytesMut::with_capacity(packet.size(data.len()));
|
||||
let mut writer = buffer.writer();
|
||||
packet.write(&mut writer, data)?;
|
||||
let buffer = writer.into_inner();
|
||||
if let Err(error) = context.try_transmit(buffer) {
|
||||
debug!("failed to transmit udp packet: {}", error);
|
||||
}
|
||||
}
|
||||
ProxyUdpSelect::Internal(data) => {
|
||||
let packet = SlicedPacket::from_ethernet(&data)?;
|
||||
let Some(ref net) = packet.net else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ip) = net.ip_payload_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let udp = UdpSlice::from_slice(ip.payload)?;
|
||||
socket.write_all(udp.payload()).await?;
|
||||
}
|
||||
ProxyUdpSelect::Close => {
|
||||
drop(socket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.reclaim().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,317 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use log::{debug, warn};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::sync::Arc;
|
||||
use std::{io, mem};
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const RAW_SOCKET_TRANSMIT_QUEUE_LEN: usize = 3000;
|
||||
const RAW_SOCKET_RECEIVE_QUEUE_LEN: usize = 3000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RawSocketProtocol {
|
||||
Icmpv4,
|
||||
Icmpv6,
|
||||
Ethernet,
|
||||
}
|
||||
|
||||
impl RawSocketProtocol {
|
||||
pub fn to_socket_domain(&self) -> i32 {
|
||||
match self {
|
||||
RawSocketProtocol::Icmpv4 => libc::AF_INET,
|
||||
RawSocketProtocol::Icmpv6 => libc::AF_INET6,
|
||||
RawSocketProtocol::Ethernet => libc::AF_PACKET,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_socket_protocol(&self) -> u16 {
|
||||
match self {
|
||||
RawSocketProtocol::Icmpv4 => libc::IPPROTO_ICMP as u16,
|
||||
RawSocketProtocol::Icmpv6 => libc::IPPROTO_ICMPV6 as u16,
|
||||
RawSocketProtocol::Ethernet => (libc::ETH_P_ALL as u16).to_be(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_socket_type(&self) -> i32 {
|
||||
libc::SOCK_RAW
|
||||
}
|
||||
}
|
||||
|
||||
const SIOCGIFINDEX: libc::c_ulong = 0x8933;
|
||||
const SIOCGIFMTU: libc::c_ulong = 0x8921;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawSocketHandle {
|
||||
protocol: RawSocketProtocol,
|
||||
lower: libc::c_int,
|
||||
}
|
||||
|
||||
impl AsRawFd for RawSocketHandle {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.lower
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoRawFd for RawSocketHandle {
|
||||
fn into_raw_fd(self) -> RawFd {
|
||||
let fd = self.lower;
|
||||
mem::forget(self);
|
||||
fd
|
||||
}
|
||||
}
|
||||
|
||||
impl RawSocketHandle {
|
||||
pub fn new(protocol: RawSocketProtocol) -> io::Result<RawSocketHandle> {
|
||||
let lower = unsafe {
|
||||
let lower = libc::socket(
|
||||
protocol.to_socket_domain(),
|
||||
protocol.to_socket_type() | libc::SOCK_NONBLOCK,
|
||||
protocol.to_socket_protocol() as i32,
|
||||
);
|
||||
if lower == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
lower
|
||||
};
|
||||
|
||||
Ok(RawSocketHandle { protocol, lower })
|
||||
}
|
||||
|
||||
pub fn bound_to_interface(interface: &str, protocol: RawSocketProtocol) -> Result<Self> {
|
||||
let mut socket = RawSocketHandle::new(protocol)?;
|
||||
socket.bind_to_interface(interface)?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
pub fn bind_to_interface(&mut self, interface: &str) -> io::Result<()> {
|
||||
let mut ifreq = ifreq_for(interface);
|
||||
let sockaddr = libc::sockaddr_ll {
|
||||
sll_family: libc::AF_PACKET as u16,
|
||||
sll_protocol: self.protocol.to_socket_protocol(),
|
||||
sll_ifindex: ifreq_ioctl(self.lower, &mut ifreq, SIOCGIFINDEX)?,
|
||||
sll_hatype: 1,
|
||||
sll_pkttype: 0,
|
||||
sll_halen: 6,
|
||||
sll_addr: [0; 8],
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let res = libc::bind(
|
||||
self.lower,
|
||||
&sockaddr as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
);
|
||||
if res == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mtu_of_interface(&mut self, interface: &str) -> io::Result<usize> {
|
||||
let mut ifreq = ifreq_for(interface);
|
||||
ifreq_ioctl(self.lower, &mut ifreq, SIOCGIFMTU).map(|mtu| mtu as usize)
|
||||
}
|
||||
|
||||
pub fn recv(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
unsafe {
|
||||
let len = libc::recv(
|
||||
self.lower,
|
||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||
buffer.len(),
|
||||
0,
|
||||
);
|
||||
if len == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&self, buffer: &[u8]) -> io::Result<usize> {
|
||||
unsafe {
|
||||
let len = libc::send(
|
||||
self.lower,
|
||||
buffer.as_ptr() as *const libc::c_void,
|
||||
buffer.len(),
|
||||
0,
|
||||
);
|
||||
if len == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(len as usize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawSocketHandle {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.lower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
struct Ifreq {
|
||||
ifr_name: [libc::c_char; libc::IF_NAMESIZE],
|
||||
ifr_data: libc::c_int,
|
||||
}
|
||||
|
||||
fn ifreq_for(name: &str) -> Ifreq {
|
||||
let mut ifreq = Ifreq {
|
||||
ifr_name: [0; libc::IF_NAMESIZE],
|
||||
ifr_data: 0,
|
||||
};
|
||||
for (i, byte) in name.as_bytes().iter().enumerate() {
|
||||
ifreq.ifr_name[i] = *byte as libc::c_char
|
||||
}
|
||||
ifreq
|
||||
}
|
||||
|
||||
fn ifreq_ioctl(
|
||||
lower: libc::c_int,
|
||||
ifreq: &mut Ifreq,
|
||||
cmd: libc::c_ulong,
|
||||
) -> io::Result<libc::c_int> {
|
||||
unsafe {
|
||||
let res = libc::ioctl(lower, cmd as _, ifreq as *mut Ifreq);
|
||||
if res == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ifreq.ifr_data)
|
||||
}
|
||||
|
||||
pub struct AsyncRawSocketChannel {
|
||||
pub sender: Sender<BytesMut>,
|
||||
pub receiver: Receiver<BytesMut>,
|
||||
_task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
enum AsyncRawSocketChannelSelect {
|
||||
TransmitPacket(Option<BytesMut>),
|
||||
Readable(()),
|
||||
}
|
||||
|
||||
impl AsyncRawSocketChannel {
|
||||
pub fn new(mtu: usize, socket: RawSocketHandle) -> Result<AsyncRawSocketChannel> {
|
||||
let (transmit_sender, transmit_receiver) = channel(RAW_SOCKET_TRANSMIT_QUEUE_LEN);
|
||||
let (receive_sender, receive_receiver) = channel(RAW_SOCKET_RECEIVE_QUEUE_LEN);
|
||||
let task = AsyncRawSocketChannel::launch(mtu, socket, transmit_receiver, receive_sender)?;
|
||||
Ok(AsyncRawSocketChannel {
|
||||
sender: transmit_sender,
|
||||
receiver: receive_receiver,
|
||||
_task: Arc::new(task),
|
||||
})
|
||||
}
|
||||
|
||||
fn launch(
|
||||
mtu: usize,
|
||||
socket: RawSocketHandle,
|
||||
transmit_receiver: Receiver<BytesMut>,
|
||||
receive_sender: Sender<BytesMut>,
|
||||
) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
if let Err(error) =
|
||||
AsyncRawSocketChannel::process(mtu, socket, transmit_receiver, receive_sender).await
|
||||
{
|
||||
warn!("failed to process raw socket: {}", error);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async fn process(
|
||||
mtu: usize,
|
||||
socket: RawSocketHandle,
|
||||
mut transmit_receiver: Receiver<BytesMut>,
|
||||
receive_sender: Sender<BytesMut>,
|
||||
) -> Result<()> {
|
||||
let socket = unsafe { std::net::UdpSocket::from_raw_fd(socket.into_raw_fd()) };
|
||||
let socket = UdpSocket::from_std(socket)?;
|
||||
|
||||
let tear_off_size = 100 * mtu;
|
||||
let mut buffer: BytesMut = BytesMut::with_capacity(tear_off_size);
|
||||
loop {
|
||||
if buffer.capacity() < mtu {
|
||||
buffer = BytesMut::with_capacity(tear_off_size);
|
||||
}
|
||||
|
||||
let selection = select! {
|
||||
x = transmit_receiver.recv() => AsyncRawSocketChannelSelect::TransmitPacket(x),
|
||||
x = socket.readable() => AsyncRawSocketChannelSelect::Readable(x?),
|
||||
};
|
||||
|
||||
match selection {
|
||||
AsyncRawSocketChannelSelect::Readable(_) => {
|
||||
buffer.resize(mtu, 0);
|
||||
match socket.try_recv(&mut buffer) {
|
||||
Ok(len) => {
|
||||
if len == 0 {
|
||||
continue;
|
||||
}
|
||||
let packet = buffer.split_to(len);
|
||||
if let Err(error) = receive_sender.try_send(packet) {
|
||||
debug!(
|
||||
"failed to process received packet from raw socket: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ref error) => {
|
||||
if error.kind() == ErrorKind::WouldBlock {
|
||||
continue;
|
||||
}
|
||||
|
||||
// device no longer exists
|
||||
if error.raw_os_error() == Some(6) {
|
||||
break;
|
||||
}
|
||||
|
||||
return Err(anyhow!("failed to read from raw socket: {}", error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AsyncRawSocketChannelSelect::TransmitPacket(Some(packet)) => {
|
||||
match socket.try_send(&packet) {
|
||||
Ok(_len) => {}
|
||||
Err(ref error) => {
|
||||
if error.kind() == ErrorKind::WouldBlock {
|
||||
debug!("failed to transmit: would block");
|
||||
continue;
|
||||
}
|
||||
|
||||
// device no longer exists
|
||||
if error.raw_os_error() == Some(6) {
|
||||
break;
|
||||
}
|
||||
|
||||
return Err(anyhow!(
|
||||
"failed to write {} bytes to raw socket: {}",
|
||||
packet.len(),
|
||||
error
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AsyncRawSocketChannelSelect::TransmitPacket(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::BytesMut;
|
||||
use etherparse::{EtherType, Ethernet2Header, IpNumber, Ipv4Header, Ipv6Header, TcpHeader};
|
||||
use log::{debug, trace, warn};
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::broadcast::{
|
||||
channel as broadcast_channel, Receiver as BroadcastReceiver, Sender as BroadcastSender,
|
||||
};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
const TO_BRIDGE_QUEUE_LEN: usize = 3000;
|
||||
const FROM_BRIDGE_QUEUE_LEN: usize = 3000;
|
||||
const BROADCAST_QUEUE_LEN: usize = 3000;
|
||||
const MEMBER_LEAVE_QUEUE_LEN: usize = 30;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BridgeMember {
|
||||
pub from_bridge_sender: Sender<BytesMut>,
|
||||
}
|
||||
|
||||
pub struct BridgeJoinHandle {
|
||||
mac: EthernetAddress,
|
||||
pub to_bridge_sender: Sender<BytesMut>,
|
||||
pub from_bridge_receiver: Receiver<BytesMut>,
|
||||
pub from_broadcast_receiver: BroadcastReceiver<BytesMut>,
|
||||
member_leave_sender: Sender<EthernetAddress>,
|
||||
}
|
||||
|
||||
impl Drop for BridgeJoinHandle {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.member_leave_sender.try_send(self.mac) {
|
||||
warn!(
|
||||
"virtual bridge member {} failed to leave: {}",
|
||||
self.mac, error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VirtualBridgeMemberMap = Arc<Mutex<HashMap<EthernetAddress, BridgeMember>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VirtualBridge {
|
||||
to_bridge_sender: Sender<BytesMut>,
|
||||
from_broadcast_sender: BroadcastSender<BytesMut>,
|
||||
member_leave_sender: Sender<EthernetAddress>,
|
||||
members: VirtualBridgeMemberMap,
|
||||
_task: Arc<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
enum VirtualBridgeSelect {
|
||||
BroadcastSent,
|
||||
PacketReceived(Option<BytesMut>),
|
||||
MemberLeave(Option<EthernetAddress>),
|
||||
}
|
||||
|
||||
impl VirtualBridge {
|
||||
pub fn new() -> Result<VirtualBridge> {
|
||||
let (to_bridge_sender, to_bridge_receiver) = channel::<BytesMut>(TO_BRIDGE_QUEUE_LEN);
|
||||
let (member_leave_sender, member_leave_reciever) =
|
||||
channel::<EthernetAddress>(MEMBER_LEAVE_QUEUE_LEN);
|
||||
let (from_broadcast_sender, from_broadcast_receiver) =
|
||||
broadcast_channel(BROADCAST_QUEUE_LEN);
|
||||
|
||||
let members = Arc::new(Mutex::new(HashMap::new()));
|
||||
let handle = {
|
||||
let members = members.clone();
|
||||
let broadcast_rx_sender = from_broadcast_sender.clone();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(error) = VirtualBridge::process(
|
||||
members,
|
||||
member_leave_reciever,
|
||||
to_bridge_receiver,
|
||||
broadcast_rx_sender,
|
||||
from_broadcast_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("virtual bridge processing task failed: {}", error);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Ok(VirtualBridge {
|
||||
to_bridge_sender,
|
||||
from_broadcast_sender,
|
||||
member_leave_sender,
|
||||
members,
|
||||
_task: Arc::new(handle),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn join(&self, mac: EthernetAddress) -> Result<BridgeJoinHandle> {
|
||||
let (from_bridge_sender, from_bridge_receiver) = channel::<BytesMut>(FROM_BRIDGE_QUEUE_LEN);
|
||||
let member = BridgeMember { from_bridge_sender };
|
||||
|
||||
match self.members.lock().await.entry(mac) {
|
||||
Entry::Occupied(_) => {
|
||||
return Err(anyhow!("virtual bridge member {} already exists", mac));
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(member);
|
||||
}
|
||||
};
|
||||
debug!("virtual bridge member {} has joined", mac);
|
||||
Ok(BridgeJoinHandle {
|
||||
mac,
|
||||
member_leave_sender: self.member_leave_sender.clone(),
|
||||
from_bridge_receiver,
|
||||
from_broadcast_receiver: self.from_broadcast_sender.subscribe(),
|
||||
to_bridge_sender: self.to_bridge_sender.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process(
|
||||
members: VirtualBridgeMemberMap,
|
||||
mut member_leave_reciever: Receiver<EthernetAddress>,
|
||||
mut to_bridge_receiver: Receiver<BytesMut>,
|
||||
broadcast_rx_sender: BroadcastSender<BytesMut>,
|
||||
mut from_broadcast_receiver: BroadcastReceiver<BytesMut>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let selection = select! {
|
||||
biased;
|
||||
x = to_bridge_receiver.recv() => VirtualBridgeSelect::PacketReceived(x),
|
||||
_ = from_broadcast_receiver.recv() => VirtualBridgeSelect::BroadcastSent,
|
||||
x = member_leave_reciever.recv() => VirtualBridgeSelect::MemberLeave(x),
|
||||
};
|
||||
|
||||
match selection {
|
||||
VirtualBridgeSelect::PacketReceived(Some(mut packet)) => {
|
||||
let (header, payload) = match Ethernet2Header::from_slice(&packet) {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
debug!("virtual bridge failed to parse ethernet header: {}", error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// recalculate TCP checksums when routing packets.
|
||||
// the xen network backend / frontend drivers for linux
|
||||
// use checksum offloading but since we bypass some layers
|
||||
// of the kernel we have to do it ourselves.
|
||||
if header.ether_type == EtherType::IPV4 {
|
||||
let (ipv4, payload) = Ipv4Header::from_slice(payload)?;
|
||||
if ipv4.protocol == IpNumber::TCP {
|
||||
let (mut tcp, payload) = TcpHeader::from_slice(payload)?;
|
||||
tcp.checksum = tcp.calc_checksum_ipv4(&ipv4, payload)?;
|
||||
let tcp_header_offset = Ethernet2Header::LEN + ipv4.header_len();
|
||||
let mut header = &mut packet[tcp_header_offset..];
|
||||
tcp.write(&mut header)?;
|
||||
}
|
||||
} else if header.ether_type == EtherType::IPV6 {
|
||||
let (ipv6, payload) = Ipv6Header::from_slice(payload)?;
|
||||
if ipv6.next_header == IpNumber::TCP {
|
||||
let (mut tcp, payload) = TcpHeader::from_slice(payload)?;
|
||||
tcp.checksum = tcp.calc_checksum_ipv6(&ipv6, payload)?;
|
||||
let tcp_header_offset = Ethernet2Header::LEN + ipv6.header_len();
|
||||
let mut header = &mut packet[tcp_header_offset..];
|
||||
tcp.write(&mut header)?;
|
||||
}
|
||||
}
|
||||
|
||||
let destination = EthernetAddress(header.destination);
|
||||
if destination.is_multicast() {
|
||||
broadcast_rx_sender.send(packet)?;
|
||||
continue;
|
||||
}
|
||||
match members.lock().await.get(&destination) {
|
||||
Some(member) => {
|
||||
member.from_bridge_sender.try_send(packet)?;
|
||||
trace!(
|
||||
"sending bridged packet from {} to {}",
|
||||
EthernetAddress(header.source),
|
||||
EthernetAddress(header.destination)
|
||||
);
|
||||
}
|
||||
None => {
|
||||
trace!("no bridge member with address: {}", destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VirtualBridgeSelect::MemberLeave(Some(mac)) => {
|
||||
if members.lock().await.remove(&mac).is_some() {
|
||||
debug!("virtual bridge member {} has left", mac);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualBridgeSelect::PacketReceived(None) => break,
|
||||
VirtualBridgeSelect::MemberLeave(None) => {}
|
||||
VirtualBridgeSelect::BroadcastSent => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "krata-oci"
|
||||
description = "OCI services for the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-compression = { workspace = true, features = ["tokio", "gzip", "zstd"] }
|
||||
async-trait = { workspace = true }
|
||||
backhand = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
krata-tokio-tar = { workspace = true }
|
||||
log = { workspace = true }
|
||||
oci-spec = { workspace = true }
|
||||
path-clean = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha256 = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "krataoci"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "krataoci-squashify"
|
||||
path = "examples/squashify.rs"
|
@ -1,29 +0,0 @@
|
||||
use std::{env::args, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use env_logger::Env;
|
||||
use krataoci::{cache::ImageCache, compiler::ImageCompiler, name::ImageName};
|
||||
use tokio::fs;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
let image = ImageName::parse(&args().nth(1).unwrap())?;
|
||||
let seed = args().nth(2).map(PathBuf::from);
|
||||
|
||||
let cache_dir = PathBuf::from("krata-cache");
|
||||
if !cache_dir.exists() {
|
||||
fs::create_dir(&cache_dir).await?;
|
||||
}
|
||||
|
||||
let cache = ImageCache::new(&cache_dir)?;
|
||||
let compiler = ImageCompiler::new(&cache, seed)?;
|
||||
let info = compiler.compile(&image).await?;
|
||||
println!(
|
||||
"generated squashfs of {} to {}",
|
||||
image,
|
||||
info.image_squashfs.to_string_lossy()
|
||||
);
|
||||
Ok(())
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
use super::compiler::ImageInfo;
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use oci_spec::image::{ImageConfiguration, ImageManifest};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImageCache {
|
||||
cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
pub fn new(cache_dir: &Path) -> Result<ImageCache> {
|
||||
Ok(ImageCache {
|
||||
cache_dir: cache_dir.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn recall(&self, digest: &str) -> Result<Option<ImageInfo>> {
|
||||
let mut squashfs_path = self.cache_dir.clone();
|
||||
let mut config_path = self.cache_dir.clone();
|
||||
let mut manifest_path = self.cache_dir.clone();
|
||||
squashfs_path.push(format!("{}.squashfs", digest));
|
||||
manifest_path.push(format!("{}.manifest.json", digest));
|
||||
config_path.push(format!("{}.config.json", digest));
|
||||
Ok(
|
||||
if squashfs_path.exists() && manifest_path.exists() && config_path.exists() {
|
||||
let squashfs_metadata = fs::metadata(&squashfs_path).await?;
|
||||
let manifest_metadata = fs::metadata(&manifest_path).await?;
|
||||
let config_metadata = fs::metadata(&config_path).await?;
|
||||
if squashfs_metadata.is_file()
|
||||
&& manifest_metadata.is_file()
|
||||
&& config_metadata.is_file()
|
||||
{
|
||||
let manifest_text = fs::read_to_string(&manifest_path).await?;
|
||||
let manifest: ImageManifest = serde_json::from_str(&manifest_text)?;
|
||||
let config_text = fs::read_to_string(&config_path).await?;
|
||||
let config: ImageConfiguration = serde_json::from_str(&config_text)?;
|
||||
debug!("cache hit digest={}", digest);
|
||||
Some(ImageInfo::new(squashfs_path.clone(), manifest, config)?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug!("cache miss digest={}", digest);
|
||||
None
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn store(&self, digest: &str, info: &ImageInfo) -> Result<ImageInfo> {
|
||||
debug!("cache store digest={}", digest);
|
||||
let mut squashfs_path = self.cache_dir.clone();
|
||||
let mut manifest_path = self.cache_dir.clone();
|
||||
let mut config_path = self.cache_dir.clone();
|
||||
squashfs_path.push(format!("{}.squashfs", digest));
|
||||
manifest_path.push(format!("{}.manifest.json", digest));
|
||||
config_path.push(format!("{}.config.json", digest));
|
||||
fs::copy(&info.image_squashfs, &squashfs_path).await?;
|
||||
let manifest_text = serde_json::to_string_pretty(&info.manifest)?;
|
||||
fs::write(&manifest_path, manifest_text).await?;
|
||||
let config_text = serde_json::to_string_pretty(&info.config)?;
|
||||
fs::write(&config_path, config_text).await?;
|
||||
ImageInfo::new(
|
||||
squashfs_path.clone(),
|
||||
info.manifest.clone(),
|
||||
info.config.clone(),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,411 +0,0 @@
|
||||
use crate::cache::ImageCache;
|
||||
use crate::fetch::{OciImageDownloader, OciImageLayer};
|
||||
use crate::name::ImageName;
|
||||
use crate::registry::OciRegistryPlatform;
|
||||
use anyhow::{anyhow, Result};
|
||||
use backhand::compression::Compressor;
|
||||
use backhand::{FilesystemCompressor, FilesystemWriter, NodeHeader};
|
||||
use log::{debug, trace, warn};
|
||||
use oci_spec::image::{ImageConfiguration, ImageManifest};
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, ErrorKind, Read};
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_tar::{Archive, Entry};
|
||||
use uuid::Uuid;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub const IMAGE_SQUASHFS_VERSION: u64 = 2;
|
||||
|
||||
pub struct ImageInfo {
|
||||
pub image_squashfs: PathBuf,
|
||||
pub manifest: ImageManifest,
|
||||
pub config: ImageConfiguration,
|
||||
}
|
||||
|
||||
impl ImageInfo {
|
||||
pub fn new(
|
||||
squashfs: PathBuf,
|
||||
manifest: ImageManifest,
|
||||
config: ImageConfiguration,
|
||||
) -> Result<ImageInfo> {
|
||||
Ok(ImageInfo {
|
||||
image_squashfs: squashfs,
|
||||
manifest,
|
||||
config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageCompiler<'a> {
|
||||
cache: &'a ImageCache,
|
||||
seed: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ImageCompiler<'_> {
|
||||
pub fn new(cache: &ImageCache, seed: Option<PathBuf>) -> Result<ImageCompiler> {
|
||||
Ok(ImageCompiler { cache, seed })
|
||||
}
|
||||
|
||||
pub async fn compile(&self, image: &ImageName) -> Result<ImageInfo> {
|
||||
debug!("compile image={image}");
|
||||
let mut tmp_dir = std::env::temp_dir().clone();
|
||||
tmp_dir.push(format!("krata-compile-{}", Uuid::new_v4()));
|
||||
|
||||
let mut image_dir = tmp_dir.clone();
|
||||
image_dir.push("image");
|
||||
fs::create_dir_all(&image_dir).await?;
|
||||
|
||||
let mut layer_dir = tmp_dir.clone();
|
||||
layer_dir.push("layer");
|
||||
fs::create_dir_all(&layer_dir).await?;
|
||||
|
||||
let mut squash_file = tmp_dir.clone();
|
||||
squash_file.push("image.squashfs");
|
||||
let info = self
|
||||
.download_and_compile(image, &layer_dir, &image_dir, &squash_file)
|
||||
.await?;
|
||||
fs::remove_dir_all(&tmp_dir).await?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
async fn download_and_compile(
|
||||
&self,
|
||||
image: &ImageName,
|
||||
layer_dir: &Path,
|
||||
image_dir: &Path,
|
||||
squash_file: &Path,
|
||||
) -> Result<ImageInfo> {
|
||||
let downloader = OciImageDownloader::new(
|
||||
self.seed.clone(),
|
||||
layer_dir.to_path_buf(),
|
||||
OciRegistryPlatform::current(),
|
||||
);
|
||||
let resolved = downloader.resolve(image.clone()).await?;
|
||||
let cache_key = format!(
|
||||
"manifest={}:squashfs-version={}\n",
|
||||
resolved.digest, IMAGE_SQUASHFS_VERSION
|
||||
);
|
||||
let cache_digest = sha256::digest(cache_key);
|
||||
|
||||
if let Some(cached) = self.cache.recall(&cache_digest).await? {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let local = downloader.download(resolved).await?;
|
||||
for layer in &local.layers {
|
||||
debug!(
|
||||
"process layer digest={} compression={:?}",
|
||||
&layer.digest, layer.compression,
|
||||
);
|
||||
let whiteouts = self.process_layer_whiteout(layer, image_dir).await?;
|
||||
debug!(
|
||||
"process layer digest={} whiteouts={:?}",
|
||||
&layer.digest, whiteouts
|
||||
);
|
||||
let mut archive = layer.archive().await?;
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?;
|
||||
let mut maybe_whiteout_path_str =
|
||||
path.to_str().map(|x| x.to_string()).unwrap_or_default();
|
||||
if whiteouts.contains(&maybe_whiteout_path_str) {
|
||||
continue;
|
||||
}
|
||||
maybe_whiteout_path_str.push('/');
|
||||
if whiteouts.contains(&maybe_whiteout_path_str) {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = path.file_name() else {
|
||||
return Err(anyhow!("unable to get file name"));
|
||||
};
|
||||
let Some(name) = name.to_str() else {
|
||||
return Err(anyhow!("unable to get file name as string"));
|
||||
};
|
||||
|
||||
if name.starts_with(".wh.") {
|
||||
continue;
|
||||
} else {
|
||||
self.process_write_entry(&mut entry, layer, image_dir)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for layer in &local.layers {
|
||||
if layer.path.exists() {
|
||||
fs::remove_file(&layer.path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.squash(image_dir, squash_file)?;
|
||||
let info = ImageInfo::new(
|
||||
squash_file.to_path_buf(),
|
||||
local.image.manifest,
|
||||
local.config,
|
||||
)?;
|
||||
self.cache.store(&cache_digest, &info).await
|
||||
}
|
||||
|
||||
async fn process_layer_whiteout(
|
||||
&self,
|
||||
layer: &OciImageLayer,
|
||||
image_dir: &Path,
|
||||
) -> Result<Vec<String>> {
|
||||
let mut whiteouts = Vec::new();
|
||||
let mut archive = layer.archive().await?;
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
let path = entry.path()?;
|
||||
let Some(name) = path.file_name() else {
|
||||
return Err(anyhow!("unable to get file name"));
|
||||
};
|
||||
let Some(name) = name.to_str() else {
|
||||
return Err(anyhow!("unable to get file name as string"));
|
||||
};
|
||||
|
||||
if name.starts_with(".wh.") {
|
||||
let path = self
|
||||
.process_whiteout_entry(&entry, name, layer, image_dir)
|
||||
.await?;
|
||||
if let Some(path) = path {
|
||||
whiteouts.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(whiteouts)
|
||||
}
|
||||
|
||||
async fn process_whiteout_entry(
|
||||
&self,
|
||||
entry: &Entry<Archive<Pin<Box<dyn AsyncRead + Send>>>>,
|
||||
name: &str,
|
||||
layer: &OciImageLayer,
|
||||
image_dir: &Path,
|
||||
) -> Result<Option<String>> {
|
||||
let path = entry.path()?;
|
||||
let mut dst = self.check_safe_entry(path.clone(), image_dir)?;
|
||||
dst.pop();
|
||||
let mut path = path.to_path_buf();
|
||||
path.pop();
|
||||
|
||||
let opaque = name == ".wh..wh..opq";
|
||||
|
||||
if !opaque {
|
||||
let file = &name[4..];
|
||||
dst.push(file);
|
||||
path.push(file);
|
||||
self.check_safe_path(&dst, image_dir)?;
|
||||
}
|
||||
|
||||
trace!("whiteout entry layer={} path={:?}", &layer.digest, path,);
|
||||
|
||||
let whiteout = path
|
||||
.to_str()
|
||||
.ok_or(anyhow!("unable to convert path to string"))?
|
||||
.to_string();
|
||||
|
||||
if opaque {
|
||||
if dst.is_dir() {
|
||||
let mut reader = fs::read_dir(dst).await?;
|
||||
while let Some(entry) = reader.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_symlink() || path.is_file() {
|
||||
fs::remove_file(&path).await?;
|
||||
} else if path.is_dir() {
|
||||
fs::remove_dir_all(&path).await?;
|
||||
} else {
|
||||
return Err(anyhow!("opaque whiteout entry did not exist"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"whiteout opaque entry missing locally layer={} path={:?} local={:?}",
|
||||
&layer.digest,
|
||||
entry.path()?,
|
||||
dst,
|
||||
);
|
||||
}
|
||||
} else if dst.is_file() || dst.is_symlink() {
|
||||
fs::remove_file(&dst).await?;
|
||||
} else if dst.is_dir() {
|
||||
fs::remove_dir_all(&dst).await?;
|
||||
} else {
|
||||
debug!(
|
||||
"whiteout entry missing locally layer={} path={:?} local={:?}",
|
||||
&layer.digest,
|
||||
entry.path()?,
|
||||
dst,
|
||||
);
|
||||
}
|
||||
Ok(if opaque { None } else { Some(whiteout) })
|
||||
}
|
||||
|
||||
async fn process_write_entry(
|
||||
&self,
|
||||
entry: &mut Entry<Archive<Pin<Box<dyn AsyncRead + Send>>>>,
|
||||
layer: &OciImageLayer,
|
||||
image_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let uid = entry.header().uid()?;
|
||||
let gid = entry.header().gid()?;
|
||||
trace!(
|
||||
"unpack entry layer={} path={:?} type={:?} uid={} gid={}",
|
||||
&layer.digest,
|
||||
entry.path()?,
|
||||
entry.header().entry_type(),
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
entry.set_preserve_mtime(true);
|
||||
entry.set_preserve_permissions(true);
|
||||
entry.set_unpack_xattrs(true);
|
||||
if let Some(path) = entry.unpack_in(image_dir).await? {
|
||||
if !path.is_symlink() {
|
||||
std::os::unix::fs::chown(path, Some(uid as u32), Some(gid as u32))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_safe_entry(&self, path: Cow<Path>, image_dir: &Path) -> Result<PathBuf> {
|
||||
let mut dst = image_dir.to_path_buf();
|
||||
dst.push(path);
|
||||
if let Some(name) = dst.file_name() {
|
||||
if let Some(name) = name.to_str() {
|
||||
if name.starts_with(".wh.") {
|
||||
let copy = dst.clone();
|
||||
dst.pop();
|
||||
self.check_safe_path(&dst, image_dir)?;
|
||||
return Ok(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check_safe_path(&dst, image_dir)?;
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
fn check_safe_path(&self, dst: &Path, image_dir: &Path) -> Result<()> {
|
||||
let resolved = path_clean::clean(dst);
|
||||
if !resolved.starts_with(image_dir) {
|
||||
return Err(anyhow!("layer attempts to work outside image dir"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn squash(&self, image_dir: &Path, squash_file: &Path) -> Result<()> {
|
||||
let mut writer = FilesystemWriter::default();
|
||||
writer.set_compressor(FilesystemCompressor::new(Compressor::Gzip, None)?);
|
||||
let walk = WalkDir::new(image_dir).follow_links(false);
|
||||
for entry in walk {
|
||||
let entry = entry?;
|
||||
let rel = entry
|
||||
.path()
|
||||
.strip_prefix(image_dir)?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("failed to strip prefix of tmpdir"))?;
|
||||
let rel = format!("/{}", rel);
|
||||
trace!("squash write {}", rel);
|
||||
let typ = entry.file_type();
|
||||
let metadata = std::fs::symlink_metadata(entry.path())?;
|
||||
let uid = metadata.uid();
|
||||
let gid = metadata.gid();
|
||||
let mode = metadata.permissions().mode();
|
||||
let mtime = metadata.mtime();
|
||||
|
||||
if rel == "/" {
|
||||
writer.set_root_uid(uid);
|
||||
writer.set_root_gid(gid);
|
||||
writer.set_root_mode(mode as u16);
|
||||
continue;
|
||||
}
|
||||
|
||||
let header = NodeHeader {
|
||||
permissions: mode as u16,
|
||||
uid,
|
||||
gid,
|
||||
mtime: mtime as u32,
|
||||
};
|
||||
if typ.is_symlink() {
|
||||
let symlink = std::fs::read_link(entry.path())?;
|
||||
let symlink = symlink
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("failed to read symlink"))?;
|
||||
writer.push_symlink(symlink, rel, header)?;
|
||||
} else if typ.is_dir() {
|
||||
writer.push_dir(rel, header)?;
|
||||
} else if typ.is_file() {
|
||||
writer.push_file(ConsumingFileReader::new(entry.path()), rel, header)?;
|
||||
} else if typ.is_block_device() {
|
||||
let device = metadata.dev();
|
||||
writer.push_block_device(device as u32, rel, header)?;
|
||||
} else if typ.is_char_device() {
|
||||
let device = metadata.dev();
|
||||
writer.push_char_device(device as u32, rel, header)?;
|
||||
} else if typ.is_fifo() {
|
||||
writer.push_fifo(rel, header)?;
|
||||
} else if typ.is_socket() {
|
||||
writer.push_socket(rel, header)?;
|
||||
} else {
|
||||
return Err(anyhow!("invalid file type"));
|
||||
}
|
||||
}
|
||||
|
||||
let squash_file_path = squash_file
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("failed to convert squashfs string"))?;
|
||||
|
||||
let file = File::create(squash_file)?;
|
||||
let mut bufwrite = BufWriter::new(file);
|
||||
trace!("squash generate: {}", squash_file_path);
|
||||
writer.write(&mut bufwrite)?;
|
||||
std::fs::remove_dir_all(image_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsumingFileReader {
|
||||
path: PathBuf,
|
||||
file: Option<File>,
|
||||
}
|
||||
|
||||
impl ConsumingFileReader {
|
||||
fn new(path: &Path) -> ConsumingFileReader {
|
||||
ConsumingFileReader {
|
||||
path: path.to_path_buf(),
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for ConsumingFileReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.file.is_none() {
|
||||
self.file = Some(File::open(&self.path)?);
|
||||
}
|
||||
let Some(ref mut file) = self.file else {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
"file was not opened",
|
||||
));
|
||||
};
|
||||
file.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConsumingFileReader {
|
||||
fn drop(&mut self) {
|
||||
let file = self.file.take();
|
||||
drop(file);
|
||||
if let Err(error) = std::fs::remove_file(&self.path) {
|
||||
warn!("failed to delete consuming file {:?}: {}", self.path, error);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,281 +0,0 @@
|
||||
use super::{
|
||||
name::ImageName,
|
||||
registry::{OciRegistryClient, OciRegistryPlatform},
|
||||
};
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::tokio::bufread::{GzipDecoder, ZstdDecoder};
|
||||
use log::debug;
|
||||
use oci_spec::image::{
|
||||
Descriptor, ImageConfiguration, ImageIndex, ImageManifest, MediaType, ToDockerV2S2,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncRead, AsyncReadExt, BufReader, BufWriter},
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_tar::Archive;
|
||||
|
||||
pub struct OciImageDownloader {
|
||||
seed: Option<PathBuf>,
|
||||
storage: PathBuf,
|
||||
platform: OciRegistryPlatform,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OciImageLayerCompression {
|
||||
None,
|
||||
Gzip,
|
||||
Zstd,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OciImageLayer {
|
||||
pub path: PathBuf,
|
||||
pub digest: String,
|
||||
pub compression: OciImageLayerCompression,
|
||||
}
|
||||
|
||||
impl OciImageLayer {
|
||||
pub async fn decompress(&self) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
|
||||
let file = File::open(&self.path).await?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader: Pin<Box<dyn AsyncRead + Send>> = match self.compression {
|
||||
OciImageLayerCompression::None => Box::pin(reader),
|
||||
OciImageLayerCompression::Gzip => Box::pin(GzipDecoder::new(reader)),
|
||||
OciImageLayerCompression::Zstd => Box::pin(ZstdDecoder::new(reader)),
|
||||
};
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
pub async fn archive(&self) -> Result<Archive<Pin<Box<dyn AsyncRead + Send>>>> {
|
||||
let decompress = self.decompress().await?;
|
||||
Ok(Archive::new(decompress))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OciResolvedImage {
|
||||
pub name: ImageName,
|
||||
pub digest: String,
|
||||
pub manifest: ImageManifest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OciLocalImage {
|
||||
pub image: OciResolvedImage,
|
||||
pub config: ImageConfiguration,
|
||||
pub layers: Vec<OciImageLayer>,
|
||||
}
|
||||
|
||||
impl OciImageDownloader {
|
||||
pub fn new(
|
||||
seed: Option<PathBuf>,
|
||||
storage: PathBuf,
|
||||
platform: OciRegistryPlatform,
|
||||
) -> OciImageDownloader {
|
||||
OciImageDownloader {
|
||||
seed,
|
||||
storage,
|
||||
platform,
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_seed_json_blob<T: DeserializeOwned>(
|
||||
&self,
|
||||
descriptor: &Descriptor,
|
||||
) -> Result<Option<T>> {
|
||||
let digest = descriptor.digest();
|
||||
let Some((digest_type, digest_content)) = digest.split_once(':') else {
|
||||
return Err(anyhow!("digest content was not properly formatted"));
|
||||
};
|
||||
let want = format!("blobs/{}/{}", digest_type, digest_content);
|
||||
self.load_seed_json(&want).await
|
||||
}
|
||||
|
||||
async fn load_seed_json<T: DeserializeOwned>(&self, want: &str) -> Result<Option<T>> {
|
||||
let Some(ref seed) = self.seed else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let file = File::open(seed).await?;
|
||||
let mut archive = Archive::new(file);
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut entry = entry?;
|
||||
let path = String::from_utf8(entry.path_bytes().to_vec())?;
|
||||
if path == want {
|
||||
let mut content = String::new();
|
||||
entry.read_to_string(&mut content).await?;
|
||||
let data = serde_json::from_str::<T>(&content)?;
|
||||
return Ok(Some(data));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn extract_seed_blob(&self, descriptor: &Descriptor, to: &Path) -> Result<bool> {
|
||||
let Some(ref seed) = self.seed else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let digest = descriptor.digest();
|
||||
let Some((digest_type, digest_content)) = digest.split_once(':') else {
|
||||
return Err(anyhow!("digest content was not properly formatted"));
|
||||
};
|
||||
let want = format!("blobs/{}/{}", digest_type, digest_content);
|
||||
|
||||
let seed = File::open(seed).await?;
|
||||
let mut archive = Archive::new(seed);
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut entry = entry?;
|
||||
let path = String::from_utf8(entry.path_bytes().to_vec())?;
|
||||
if path == want {
|
||||
let file = File::create(to).await?;
|
||||
let mut bufwrite = BufWriter::new(file);
|
||||
tokio::io::copy(&mut entry, &mut bufwrite).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn resolve(&self, image: ImageName) -> Result<OciResolvedImage> {
|
||||
debug!("resolve manifest image={}", image);
|
||||
|
||||
if let Some(index) = self.load_seed_json::<ImageIndex>("index.json").await? {
|
||||
let mut found: Option<&Descriptor> = None;
|
||||
for manifest in index.manifests() {
|
||||
let Some(annotations) = manifest.annotations() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut image_name = annotations.get("io.containerd.image.name");
|
||||
if image_name.is_none() {
|
||||
image_name = annotations.get("org.opencontainers.image.ref.name");
|
||||
}
|
||||
|
||||
let Some(image_name) = image_name else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if *image_name != image.to_string() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(platform) = manifest.platform() {
|
||||
if *platform.architecture() != self.platform.arch
|
||||
|| *platform.os() != self.platform.os
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
found = Some(manifest);
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(found) = found {
|
||||
if let Some(manifest) = self.load_seed_json_blob(found).await? {
|
||||
debug!(
|
||||
"found seeded manifest image={} manifest={}",
|
||||
image,
|
||||
found.digest()
|
||||
);
|
||||
return Ok(OciResolvedImage {
|
||||
name: image,
|
||||
digest: found.digest().clone(),
|
||||
manifest,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?;
|
||||
let (manifest, digest) = client
|
||||
.get_manifest_with_digest(&image.name, &image.reference)
|
||||
.await?;
|
||||
Ok(OciResolvedImage {
|
||||
name: image,
|
||||
digest,
|
||||
manifest,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download(&self, image: OciResolvedImage) -> Result<OciLocalImage> {
|
||||
let config: ImageConfiguration;
|
||||
|
||||
let mut client = OciRegistryClient::new(image.name.registry_url()?, self.platform.clone())?;
|
||||
if let Some(seeded) = self
|
||||
.load_seed_json_blob::<ImageConfiguration>(image.manifest.config())
|
||||
.await?
|
||||
{
|
||||
config = seeded;
|
||||
} else {
|
||||
let config_bytes = client
|
||||
.get_blob(&image.name.name, image.manifest.config())
|
||||
.await?;
|
||||
config = serde_json::from_slice(&config_bytes)?;
|
||||
}
|
||||
let mut layers = Vec::new();
|
||||
for layer in image.manifest.layers() {
|
||||
layers.push(self.acquire_layer(&image.name, layer, &mut client).await?);
|
||||
}
|
||||
Ok(OciLocalImage {
|
||||
image,
|
||||
config,
|
||||
layers,
|
||||
})
|
||||
}
|
||||
|
||||
async fn acquire_layer(
|
||||
&self,
|
||||
image: &ImageName,
|
||||
layer: &Descriptor,
|
||||
client: &mut OciRegistryClient,
|
||||
) -> Result<OciImageLayer> {
|
||||
debug!(
|
||||
"acquire layer digest={} size={}",
|
||||
layer.digest(),
|
||||
layer.size()
|
||||
);
|
||||
let mut layer_path = self.storage.clone();
|
||||
layer_path.push(format!("{}.layer", layer.digest()));
|
||||
|
||||
let seeded = self.extract_seed_blob(layer, &layer_path).await?;
|
||||
if !seeded {
|
||||
let file = File::create(&layer_path).await?;
|
||||
let size = client.write_blob_to_file(&image.name, layer, file).await?;
|
||||
if layer.size() as u64 != size {
|
||||
return Err(anyhow!(
|
||||
"downloaded layer size differs from size in manifest",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut media_type = layer.media_type().clone();
|
||||
|
||||
// docker layer compatibility
|
||||
if media_type.to_string() == MediaType::ImageLayerGzip.to_docker_v2s2()? {
|
||||
media_type = MediaType::ImageLayerGzip;
|
||||
}
|
||||
|
||||
let compression = match media_type {
|
||||
MediaType::ImageLayer => OciImageLayerCompression::None,
|
||||
MediaType::ImageLayerGzip => OciImageLayerCompression::Gzip,
|
||||
MediaType::ImageLayerZstd => OciImageLayerCompression::Zstd,
|
||||
other => return Err(anyhow!("found layer with unknown media type: {}", other)),
|
||||
};
|
||||
Ok(OciImageLayer {
|
||||
path: layer_path,
|
||||
digest: layer.digest().clone(),
|
||||
compression,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod cache;
|
||||
pub mod compiler;
|
||||
pub mod fetch;
|
||||
pub mod name;
|
||||
pub mod registry;
|
@ -1,88 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::fmt;
|
||||
use url::Url;
|
||||
|
||||
const DOCKER_HUB_MIRROR: &str = "mirror.gcr.io";
|
||||
const DEFAULT_IMAGE_TAG: &str = "latest";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ImageName {
|
||||
pub hostname: String,
|
||||
pub port: Option<u16>,
|
||||
pub name: String,
|
||||
pub reference: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ImageName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(port) = self.port {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}/{}:{}",
|
||||
self.hostname, port, self.name, self.reference
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}/{}:{}", self.hostname, self.name, self.reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ImageName {
|
||||
fn default() -> Self {
|
||||
Self::parse(&format!("{}", uuid::Uuid::new_v4().as_hyphenated()))
|
||||
.expect("UUID hyphenated must be valid name")
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageName {
|
||||
pub fn parse(name: &str) -> Result<Self> {
|
||||
let full_name = name.to_string();
|
||||
let name = full_name.clone();
|
||||
let (mut hostname, mut name) = name
|
||||
.split_once('/')
|
||||
.map(|x| (x.0.to_string(), x.1.to_string()))
|
||||
.unwrap_or_else(|| (DOCKER_HUB_MIRROR.to_string(), format!("library/{}", name)));
|
||||
|
||||
// heuristic to find any docker hub image formats
|
||||
// that may be in the hostname format. for example:
|
||||
// abc/xyz:latest will trigger this if check, but abc.io/xyz:latest will not,
|
||||
// and neither will abc/hello/xyz:latest
|
||||
if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 {
|
||||
name = format!("{}/{}", hostname, name);
|
||||
hostname = DOCKER_HUB_MIRROR.to_string();
|
||||
}
|
||||
|
||||
let (hostname, port) = if let Some((hostname, port)) = hostname
|
||||
.split_once(':')
|
||||
.map(|x| (x.0.to_string(), x.1.to_string()))
|
||||
{
|
||||
(hostname, Some(str::parse(&port)?))
|
||||
} else {
|
||||
(hostname, None)
|
||||
};
|
||||
let (name, reference) = name
|
||||
.split_once(':')
|
||||
.map(|x| (x.0.to_string(), x.1.to_string()))
|
||||
.unwrap_or((name.to_string(), DEFAULT_IMAGE_TAG.to_string()));
|
||||
Ok(ImageName {
|
||||
hostname,
|
||||
port,
|
||||
name,
|
||||
reference,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn registry_url(&self) -> Result<Url> {
|
||||
let hostname = if let Some(port) = self.port {
|
||||
format!("{}:{}", self.hostname, port)
|
||||
} else {
|
||||
self.hostname.clone()
|
||||
};
|
||||
let url = if self.hostname.starts_with("localhost") {
|
||||
format!("http://{}", hostname)
|
||||
} else {
|
||||
format!("https://{}", hostname)
|
||||
};
|
||||
Ok(Url::parse(&url)?)
|
||||
}
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::Bytes;
|
||||
use oci_spec::image::{Arch, Descriptor, ImageIndex, ImageManifest, MediaType, Os, ToDockerV2S2};
|
||||
use reqwest::{Client, RequestBuilder, Response, StatusCode};
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OciRegistryPlatform {
|
||||
pub os: Os,
|
||||
pub arch: Arch,
|
||||
}
|
||||
|
||||
impl OciRegistryPlatform {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const CURRENT_ARCH: Arch = Arch::Amd64;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
const CURRENT_ARCH: Arch = Arch::ARM64;
|
||||
|
||||
pub fn new(os: Os, arch: Arch) -> OciRegistryPlatform {
|
||||
OciRegistryPlatform { os, arch }
|
||||
}
|
||||
|
||||
pub fn current() -> OciRegistryPlatform {
|
||||
OciRegistryPlatform {
|
||||
os: Os::Linux,
|
||||
arch: OciRegistryPlatform::CURRENT_ARCH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OciRegistryClient {
|
||||
agent: Client,
|
||||
url: Url,
|
||||
platform: OciRegistryPlatform,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
impl OciRegistryClient {
|
||||
pub fn new(url: Url, platform: OciRegistryPlatform) -> Result<OciRegistryClient> {
|
||||
Ok(OciRegistryClient {
|
||||
agent: Client::new(),
|
||||
url,
|
||||
platform,
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn call(&mut self, mut req: RequestBuilder) -> Result<Response> {
|
||||
if let Some(ref token) = self.token {
|
||||
req = req.bearer_auth(token);
|
||||
}
|
||||
let req_first_try = req.try_clone().ok_or(anyhow!("request is not clonable"))?;
|
||||
let response = self.agent.execute(req_first_try.build()?).await?;
|
||||
if response.status() == StatusCode::UNAUTHORIZED && self.token.is_none() {
|
||||
let Some(www_authenticate) = response.headers().get("www-authenticate") else {
|
||||
return Err(anyhow!("not authorized to perform this action"));
|
||||
};
|
||||
|
||||
let www_authenticate = www_authenticate.to_str()?;
|
||||
if !www_authenticate.starts_with("Bearer ") {
|
||||
return Err(anyhow!("unknown authentication scheme"));
|
||||
}
|
||||
|
||||
let details = &www_authenticate[7..];
|
||||
let details = details
|
||||
.split(',')
|
||||
.map(|x| x.split('='))
|
||||
.map(|mut x| (x.next(), x.next()))
|
||||
.filter(|(key, value)| key.is_some() && value.is_some())
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
key.unwrap().trim().to_lowercase(),
|
||||
value.unwrap().trim().to_string(),
|
||||
)
|
||||
})
|
||||
.map(|(key, value)| (key, value.trim_matches('\"').to_string()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let realm = details.get("realm");
|
||||
let service = details.get("service");
|
||||
let scope = details.get("scope");
|
||||
if realm.is_none() || service.is_none() || scope.is_none() {
|
||||
return Err(anyhow!(
|
||||
"unknown authentication scheme: realm, service, and scope are required"
|
||||
));
|
||||
}
|
||||
let mut url = Url::parse(realm.unwrap())?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("service", service.unwrap())
|
||||
.append_pair("scope", scope.unwrap());
|
||||
let token_response = self.agent.get(url.clone()).send().await?;
|
||||
if token_response.status() != StatusCode::OK {
|
||||
return Err(anyhow!(
|
||||
"failed to acquire token via {}: status {}",
|
||||
url,
|
||||
token_response.status()
|
||||
));
|
||||
}
|
||||
let token_bytes = token_response.bytes().await?;
|
||||
let token = serde_json::from_slice::<serde_json::Value>(&token_bytes)?;
|
||||
let token = token
|
||||
.get("token")
|
||||
.and_then(|x| x.as_str())
|
||||
.ok_or(anyhow!("token key missing from response"))?;
|
||||
self.token = Some(token.to_string());
|
||||
return Ok(self.agent.execute(req.bearer_auth(token).build()?).await?);
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"request to {} failed: status {}",
|
||||
req.build()?.url(),
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get_blob<N: AsRef<str>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
descriptor: &Descriptor,
|
||||
) -> Result<Bytes> {
|
||||
let url = self.url.join(&format!(
|
||||
"/v2/{}/blobs/{}",
|
||||
name.as_ref(),
|
||||
descriptor.digest()
|
||||
))?;
|
||||
let response = self.call(self.agent.get(url.as_str())).await?;
|
||||
Ok(response.bytes().await?)
|
||||
}
|
||||
|
||||
pub async fn write_blob_to_file<N: AsRef<str>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
descriptor: &Descriptor,
|
||||
mut dest: File,
|
||||
) -> Result<u64> {
|
||||
let url = self.url.join(&format!(
|
||||
"/v2/{}/blobs/{}",
|
||||
name.as_ref(),
|
||||
descriptor.digest()
|
||||
))?;
|
||||
let mut response = self.call(self.agent.get(url.as_str())).await?;
|
||||
let mut size: u64 = 0;
|
||||
while let Some(chunk) = response.chunk().await? {
|
||||
dest.write_all(&chunk).await?;
|
||||
size += chunk.len() as u64;
|
||||
}
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn get_raw_manifest_with_digest<N: AsRef<str>, R: AsRef<str>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
reference: R,
|
||||
) -> Result<(ImageManifest, String)> {
|
||||
let url = self.url.join(&format!(
|
||||
"/v2/{}/manifests/{}",
|
||||
name.as_ref(),
|
||||
reference.as_ref()
|
||||
))?;
|
||||
let accept = format!(
|
||||
"{}, {}, {}, {}",
|
||||
MediaType::ImageManifest.to_docker_v2s2()?,
|
||||
MediaType::ImageManifest,
|
||||
MediaType::ImageIndex,
|
||||
MediaType::ImageIndex.to_docker_v2s2()?,
|
||||
);
|
||||
let response = self
|
||||
.call(self.agent.get(url.as_str()).header("Accept", &accept))
|
||||
.await?;
|
||||
let digest = response
|
||||
.headers()
|
||||
.get("Docker-Content-Digest")
|
||||
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
let manifest = serde_json::from_str(&response.text().await?)?;
|
||||
Ok((manifest, digest))
|
||||
}
|
||||
|
||||
pub async fn get_manifest_with_digest<N: AsRef<str>, R: AsRef<str>>(
|
||||
&mut self,
|
||||
name: N,
|
||||
reference: R,
|
||||
) -> Result<(ImageManifest, String)> {
|
||||
let url = self.url.join(&format!(
|
||||
"/v2/{}/manifests/{}",
|
||||
name.as_ref(),
|
||||
reference.as_ref()
|
||||
))?;
|
||||
let accept = format!(
|
||||
"{}, {}, {}, {}",
|
||||
MediaType::ImageManifest.to_docker_v2s2()?,
|
||||
MediaType::ImageManifest,
|
||||
MediaType::ImageIndex,
|
||||
MediaType::ImageIndex.to_docker_v2s2()?,
|
||||
);
|
||||
let response = self
|
||||
.call(self.agent.get(url.as_str()).header("Accept", &accept))
|
||||
.await?;
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.ok_or_else(|| anyhow!("registry response did not have a Content-Type header"))?
|
||||
.to_str()?;
|
||||
if content_type == MediaType::ImageIndex.to_string()
|
||||
|| content_type == MediaType::ImageIndex.to_docker_v2s2()?
|
||||
{
|
||||
let index = serde_json::from_str(&response.text().await?)?;
|
||||
let descriptor = self
|
||||
.pick_manifest(index)
|
||||
.ok_or_else(|| anyhow!("unable to pick manifest from index"))?;
|
||||
return self
|
||||
.get_raw_manifest_with_digest(name, descriptor.digest())
|
||||
.await;
|
||||
}
|
||||
let digest = response
|
||||
.headers()
|
||||
.get("Docker-Content-Digest")
|
||||
.ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
let manifest = serde_json::from_str(&response.text().await?)?;
|
||||
Ok((manifest, digest))
|
||||
}
|
||||
|
||||
fn pick_manifest(&mut self, index: ImageIndex) -> Option<Descriptor> {
|
||||
for item in index.manifests() {
|
||||
if let Some(platform) = item.platform() {
|
||||
if *platform.os() == self.platform.os
|
||||
&& *platform.architecture() == self.platform.arch
|
||||
{
|
||||
return Some(item.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
[package]
|
||||
name = "krata-runtime"
|
||||
description = "Runtime for running guests on the krata hypervisor."
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
backhand = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
krata = { path = "../krata", version = "^0.0.7" }
|
||||
krata-advmac = { workspace = true }
|
||||
krata-oci = { path = "../oci", version = "^0.0.7" }
|
||||
log = { workspace = true }
|
||||
loopdev-3 = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
krata-xenclient = { path = "../xen/xenclient", version = "^0.0.7" }
|
||||
krata-xenevtchn = { path = "../xen/xenevtchn", version = "^0.0.7" }
|
||||
krata-xengnt = { path = "../xen/xengnt", version = "^0.0.7" }
|
||||
krata-xenstore = { path = "../xen/xenstore", version = "^0.0.7" }
|
||||
|
||||
[lib]
|
||||
name = "kratart"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "kratart-squashify"
|
||||
path = "examples/squashify.rs"
|
||||
|
||||
[[example]]
|
||||
name = "kratart-channel"
|
||||
path = "examples/channel.rs"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user