├── .build.yml ├── .clang-format ├── .editorconfig ├── .firebaserc ├── .github ├── FUNDING.yml ├── actions │ └── rpm_spec_files │ │ ├── Dockerfile │ │ ├── action.yml │ │ └── entrypoint.sh ├── lockdown.yml └── workflows │ ├── build_images.sh │ ├── build_images.yaml │ ├── rpm_spec_files.yaml │ └── website.yaml ├── .gitignore ├── .gn ├── .vscode └── settings.json ├── BUILD.gn ├── LICENSE ├── README.md ├── Vagrantfile ├── build ├── BUILDCONFIG.gn ├── bin_proxy.py ├── copy_target_outputs.gni ├── force_varlink.go ├── go.gni ├── go.py ├── go_deps.py ├── install.gni ├── installers │ ├── install_build_results.py │ └── install_with_rpm_ostree.py ├── makepkg.gni ├── minimal-toolchain │ └── BUILD.gn ├── parse_release.py ├── python-shim.sh ├── python_binary.gni ├── python_binary.py ├── rpmbuild.gni ├── selinux.gni ├── selinux.py ├── source_archive.py ├── substitute_file.gni ├── substitute_file.py ├── symlink.gni ├── symlink.py └── write_release.py ├── cmd ├── nsbox-host │ ├── enter.go │ ├── main.go │ ├── reload_exports.go │ ├── service.go │ └── varlink_util.go ├── nsbox-invoker │ └── main.go ├── nsbox │ ├── config.go │ ├── create.go │ ├── delete.go │ ├── images.go │ ├── info.go │ ├── kill.go │ ├── list.go │ ├── main.go │ ├── rename.go │ ├── run.go │ ├── set_default.go │ └── version.go └── nsboxd │ └── main.go ├── data ├── getty-override.conf ├── nsbox-container.target ├── nsbox-init.service ├── scripts │ ├── nsbox-apply-env.sh │ ├── nsbox-enter-run.sh │ ├── nsbox-enter-setup.sh │ └── nsbox-init.sh └── wants-networkd.conf ├── firebase.json ├── go.mod ├── go.sum ├── images ├── BUILD.gn ├── arch │ ├── Dockerfile │ ├── metadata.json │ ├── playbook.yaml │ └── roles │ │ └── main │ │ ├── files │ │ ├── PKGBUILD │ │ └── nsbox-trigger.hook │ │ ├── tasks │ │ ├── build_guest_tools.yaml │ │ └── main.yaml │ │ └── vars │ │ └── guest_tools.yaml ├── debian │ ├── Dockerfile │ ├── metadata.json │ ├── playbook.yaml │ └── roles │ │ └── main │ │ └── tasks │ │ └── main.yaml ├── fedora │ ├── metadata.json │ ├── playbook.yaml │ └── roles │ │ └── main │ │ ├── files │ │ ├── nsbox-guest-tools.spec │ │ └── nsbox_trigger.py │ │ ├── tasks │ │ ├── build_guest_tools.yaml │ │ └── main.yaml │ │ └── vars │ │ └── guest_tools.yaml └── images.gni ├── internal ├── args │ ├── args.go │ └── array.go ├── config │ └── host_config.template.go ├── container │ ├── container.go │ └── info.go ├── create │ └── create.go ├── daemon │ ├── direct.go │ └── transient.go ├── gtkicons │ ├── gtkicons.go │ ├── nsbox-gtkicons.c │ └── nsbox-gtkicons.h ├── image │ └── image.go ├── integration │ └── xdgdesktop.go ├── inventory │ └── inventory.go ├── kill │ └── kill.go ├── log │ └── log.go ├── network │ ├── firewalld.go │ └── network.go ├── nsbus │ └── nsbus.go ├── nspawn │ └── builder.go ├── paths │ └── paths.go ├── ptyservice │ ├── client.go │ └── service.go ├── release │ └── release.go ├── selinux │ └── selinux.go ├── session │ ├── enter.go │ ├── enter_nsenter.go │ ├── enter_systemd.go │ ├── nsbox-ptyfwd.c │ ├── nsbox-ptyfwd.h │ └── setup.go ├── transport │ └── transport.go ├── userdata │ ├── check_privs.go │ └── userdata.go ├── varlink │ ├── dev.nsbox.varlink │ └── stub.go └── varlinkhost │ └── varlinkhost.go ├── misc ├── dev.nsbox.policy ├── dev.nsbox.rules └── profile.d-nsbox.sh ├── mock-f32-x64.cfg ├── packaging └── fedora │ ├── BUILD.gn │ └── nsbox.spec ├── sepolicy ├── BUILD.gn ├── nsbox.fc └── nsbox.te ├── tests ├── main.exp ├── playbooks │ └── fedora.yaml └── tests.exp ├── utils └── nsbox-bender.py └── web ├── .vuepress ├── config.js ├── public │ ├── favicon.ico │ ├── nsbox.svg │ ├── robots.txt │ └── text.svg └── theme │ ├── LICENSE │ ├── components │ ├── AlgoliaSearchBox.vue │ ├── DropdownLink.vue │ ├── DropdownTransition.vue │ ├── Home.vue │ ├── NavLink.vue │ ├── NavLinks.vue │ ├── Navbar.vue │ ├── Page.vue │ ├── PageEdit.vue │ ├── PageNav.vue │ ├── Sidebar.vue │ ├── SidebarButton.vue │ ├── SidebarGroup.vue │ ├── SidebarLink.vue │ └── SidebarLinks.vue │ ├── global-components │ └── Badge.vue │ ├── index.js │ ├── layouts │ ├── 404.vue │ └── Layout.vue │ ├── noopModule.js │ ├── styles │ ├── arrow.styl │ ├── code.styl │ ├── custom-blocks.styl │ ├── index.styl │ ├── mobile.styl │ ├── palette.styl │ ├── toc.styl │ └── wrapper.styl │ └── util │ └── index.js ├── README.md ├── faq.md ├── guide.md ├── images.md ├── package.json ├── recipes.md └── yarn.lock /.build.yml: -------------------------------------------------------------------------------- 1 | image: fedora/35 2 | secrets: 3 | - 618623af-4ab3-4e03-a115-56ca5b1e9c12 4 | sources: 5 | - https://git.sr.ht/~refi64/nsbox 6 | triggers: 7 | - action: email 8 | condition: failure 9 | to: 'nsbox-devel <~refi64/nsbox-devel@lists.sr.ht>' 10 | tasks: 11 | - mirror: | 12 | ssh-keyscan -t ed25519 github.com > ~/.ssh/known_hosts 13 | ssh-keygen -lf ~/.ssh/known_hosts | awk '{print $2}' | \ 14 | grep SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU 15 | 16 | cd nsbox 17 | git push git@github.com:refi64/nsbox.git main 18 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | AlignEscapedNewlines: Left 2 | ColumnLimit: 90 3 | IncludeBlocks: Regroup 4 | PointerAlignment: Right 5 | SpacesBeforeTrailingComments: 2 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.gn{,i}] 4 | indent_size = 2 5 | indent_style = space 6 | 7 | [*.go] 8 | indent_size = 2 9 | indent_style = tab 10 | tab_width = 2 11 | 12 | [*.py] 13 | indent_size = 4 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "refi64-hosting" 4 | }, 5 | "targets": { 6 | "refi64-hosting": { 7 | "hosting": { 8 | "nsbox-site": [ 9 | "nsbox-site" 10 | ] 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://refi64.com/funds.html'] 13 | -------------------------------------------------------------------------------- /.github/actions/rpm_spec_files/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.fedoraproject.org/fedora:34 2 | RUN dnf install -y git go ninja-build unzip && dnf clean all 3 | RUN \ 4 | curl -o gn.zip -L https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/latest && \ 5 | unzip gn.zip gn && install -Dm 755 gn /usr/local/bin/gn && rm -f gn.zip gn 6 | COPY entrypoint.sh /entrypoint.sh 7 | ENTRYPOINT ["/entrypoint.sh"] 8 | -------------------------------------------------------------------------------- /.github/actions/rpm_spec_files/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Push prebuilt spec files' 2 | description: 'Push prebuilt spec files to the nsbox-bot spec files repository' 3 | inputs: 4 | branch: 5 | description: 'The build branch' 6 | required: true 7 | token: 8 | description: 'The nsbox-bot Git access token' 9 | required: true 10 | runs: 11 | using: docker 12 | image: Dockerfile 13 | args: 14 | - '${{ inputs.token }}' 15 | -------------------------------------------------------------------------------- /.github/actions/rpm_spec_files/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git_token="$1" 4 | 5 | set -ex 6 | 7 | gn_args=(fedora_package=true) 8 | if [[ "$GITHUB_REF" == *"/stable" ]]; then 9 | git_branch=stable 10 | gn_args+=(is_stable_build=true) 11 | elif [[ "$GITHUB_REF" == *"/staging" ]]; then 12 | git_branch=staging 13 | gn_args+=(is_stable_build=true) 14 | else 15 | git_branch=master 16 | fi 17 | 18 | go mod vendor -v 19 | 20 | gn gen out --args="${gn_args[*]}" 21 | ninja -C out rpm/nsbox{.spec,-sources.tar} 22 | 23 | git clone https://github%40nsbox.dev@github.com/nsbox-bot/rpm-spec-files 24 | if git rev-parse --verify origin/$git_branch 2>/dev/null; then 25 | git checkout $git_branch 26 | else 27 | git checkout -b $git_branch 28 | fi 29 | 30 | cp \ 31 | out/rpm/nsbox.spec \ 32 | out/rpm/nsbox-sources.tar \ 33 | rpm-spec-files 34 | cd rpm-spec-files 35 | git config user.email 'github@nsbox.dev' 36 | git config user.name 'nsbox-bot' 37 | git add . 38 | git commit -am "Automated push at $(date)" 39 | 40 | cat >askpass.sh <<'EOF' 41 | echo "$git_token" 42 | EOF 43 | chmod +x askpass.sh 44 | 45 | export git_token 46 | GIT_ASKPASS=./askpass.sh git push 47 | -------------------------------------------------------------------------------- /.github/lockdown.yml: -------------------------------------------------------------------------------- 1 | only: pulls 2 | comment: > 3 | This repository uses [GerritHub](https://gerrithub.io/) for code review. Please submit 4 | your changes there instead. 5 | 6 | If you're not familiar with Gerrit, there is a generic walkthrough 7 | [here](https://gerrit-review.googlesource.com/Documentation/intro-gerrit-walkthrough.html), 8 | where `gerrithost` is replaced with `review.gerrithub.io`. In addition, 9 | [this guide](https://gerrit-review.googlesource.com/Documentation/intro-gerrit-walkthrough-github.html) 10 | is available for users familiar with GitHub pull requests. 11 | -------------------------------------------------------------------------------- /.github/workflows/build_images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | dnf install -y ansible-bender buildah findutils git ninja-build podman unzip 6 | sed -i 's/"overlay"/"vfs"/;s/^mountopt/#mountopt/' /etc/containers/storage.conf 7 | sed -i 's/# \(cgroup_manager = \).*$/\1"cgroupfs"/' /usr/share/containers/containers.conf 8 | 9 | export _BUILDAH_STARTED_IN_USERNS="" BUILDAH_ISOLATION=chroot 10 | 11 | curl -o gn.zip -L https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/latest 12 | unzip gn.zip gn 13 | install -Dm 755 gn /usr/local/bin/gn 14 | rm -f gn.zip gn 15 | 16 | # XXX: partly copied from .github/actions/rpm_spec_files/entrypoint.sh 17 | gn_args=() 18 | if [[ "$GITHUB_REF" == *"/stable" ]]; then 19 | gn_args+=(is_stable_build=true) 20 | bender=nsbox-bender 21 | elif [[ "$GITHUB_REF" == *"/staging" ]]; then 22 | gn_args+=(is_stable_build=true) 23 | staging=1 24 | bender=nsbox-bender 25 | else 26 | bender=nsbox-edge-bender 27 | fi 28 | 29 | gn gen out --args="${gn_args[*]}" 30 | ninja -C out "$bender" :install_share_release 31 | 32 | IMAGES_TO_BUILD=( 33 | arch 34 | debian:buster 35 | debian:bullseye 36 | fedora:33 37 | fedora:34 38 | fedora:35 39 | ) 40 | 41 | for image in "${IMAGES_TO_BUILD[@]}"; do 42 | out/install/bin/"$bender" images/"$image" 43 | done 44 | 45 | list_images() { 46 | podman images --format '{{.Repository}}:{{.Tag}}' | grep '^gcr.io/nsbox-data' | grep -Ev -- '-(bud|failed)$' 47 | } 48 | 49 | push() { 50 | (set -x; podman push "$@") 51 | } 52 | 53 | echo "$GCR_JSON_KEY" | podman login gcr.io -u _json_key --password-stdin 54 | 55 | for image in $(list_images); do 56 | if [[ -n "$staging" ]]; then 57 | push "$image" "$(echo "$image" | sed 's/\(:.*\)stable/\1staging/')" 58 | else 59 | push "$image" 60 | fi 61 | done 62 | -------------------------------------------------------------------------------- /.github/workflows/build_images.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push the images 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - staging 7 | - stable 8 | paths: 9 | - '.github/workflows/build_images.*' 10 | - 'images/**' 11 | - 'utils/nsbox-bender.py' 12 | schedule: 13 | - cron: '0 0 * * *' 14 | 15 | jobs: 16 | build: 17 | name: 'Build and push the images' 18 | runs-on: ubuntu-latest 19 | container: 20 | image: registry.fedoraproject.org/fedora:34 21 | options: '--privileged' 22 | steps: 23 | - name: 'Checkout the nsbox repo' 24 | uses: actions/checkout@v1 25 | - name: 'Build the containers' 26 | run: ./.github/workflows/build_images.sh 27 | env: 28 | GCR_JSON_KEY: ${{secrets.GCR_JSON_KEY}} 29 | -------------------------------------------------------------------------------- /.github/workflows/rpm_spec_files.yaml: -------------------------------------------------------------------------------- 1 | name: Push prebuilt spec files 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - staging 7 | - stable 8 | paths-ignore: 9 | - '.firebaserc' 10 | - 'firebase.json' 11 | - 'web/**' 12 | 13 | jobs: 14 | push: 15 | name: Push the spec files 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: 'Checkout the nsbox repo' 19 | uses: actions/checkout@v1 20 | - name: 'Run the push' 21 | uses: ./.github/actions/rpm_spec_files 22 | with: 23 | token: '${{ secrets.RPM_SPEC_FILES_TOKEN }}' 24 | -------------------------------------------------------------------------------- /.github/workflows/website.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy the website 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - '.github/workflows/website.yaml' 8 | - '.firebaserc' 9 | - 'firebase.json' 10 | - 'web/**' 11 | 12 | jobs: 13 | build: 14 | name: Build and deploy the website 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout the nsbox repo' 18 | uses: actions/checkout@v1 19 | - name: 'Setup Node' 20 | uses: actions/setup-node@v1 21 | - name: 'Download the build dependencies' 22 | run: 'yarn --cwd web' 23 | - name: 'Build the website' 24 | run: 'yarn --cwd web build' 25 | - name: 'Deploy to Firebase' 26 | uses: w9jds/firebase-action@v1.1.0 27 | with: 28 | args: 'deploy --only hosting' 29 | env: 30 | FIREBASE_TOKEN: '${{ secrets.FIREBASE_TOKEN }}' 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Note that these globs are used as ignores for Vagrant 2 | # Try to make sure these are all valid at the shell level too 3 | .firebase/ 4 | .vagrant/ 5 | out*/ 6 | tests/playbooks/*.retry 7 | vendor/ 8 | web/node_modules/ 9 | web/.vuepress/dist/ 10 | -------------------------------------------------------------------------------- /.gn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | buildconfig = "//build/BUILDCONFIG.gn" 6 | script_executable = rebase_path("//build/python-shim.sh") 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/out-arch/**": true, 4 | "out/**": true 5 | }, 6 | "files.watcherExclude": { 7 | "**/.vuepress/dist/**": true, 8 | "out-arch/**": true, 9 | "out/**": true 10 | }, 11 | "editor.smoothScrolling": true, 12 | "[vue]": { 13 | "editor.formatOnSave": false 14 | }, 15 | "[json]": { 16 | "editor.formatOnSave": false 17 | }, 18 | "go.lintFlags": [ 19 | "internal", 20 | "cmd" 21 | ], 22 | "python.autoComplete.extraPaths": [ 23 | "build" 24 | ], 25 | "python.formatting.provider": "yapf" 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nsbox 2 | 3 | nsbox is a multi-purpose, nspawn-powered container manager. Please see the 4 | [website](https://nsbox.dev) for more user-friendly information and documentation. 5 | 6 | ## Links 7 | 8 | - [sr.ht project](https://sr.ht/~refi64/nsbox/) 9 | - [GitHub Mirror](https://github.com/refi64/nsbox/) (will be decomissioned in the future) 10 | - [Legacy ora.pm issues board](https://ora.pm/project/211667/kanban) (superceded by the 11 | sr.ht project, will be decomissioned in the future) 12 | 13 | ## Build dependencies 14 | 15 | You need: 16 | 17 | - [Google's GN](https://gn.googlesource.com/gn) to generate the build files. Building this 18 | from source is pretty simple, see the instructions on the site for more info. 19 | - [Ninja](https://ninja-build.org/) to actually...build stuff. 20 | - The [Go compiler](http://golang.org). 21 | - GCC or Clang for compiling cgo code. 22 | - Python 3, which is used to run some of the build scripts. 23 | - The systemd development headers. 24 | 25 | ## Building the code 26 | 27 | Run: 28 | 29 | ```bash 30 | $ go mod vendor 31 | $ gn gen out 32 | $ ninja -C out 33 | ``` 34 | 35 | The resulting files should all be under out/install. Then, you can run 36 | `build/install.py out` to install to /usr/local (or set `--prefix` and/or `--destdir`, with the 37 | usual meanings). 38 | 39 | ### Build configuration 40 | 41 | Run `gn args --list out` to see all the configuration arguments nsbox supports. You can use 42 | these options to set the saved paths (e.g. the libexec directory) to your distro's preferred 43 | locations. 44 | 45 | ## Building the website 46 | 47 | Run: 48 | 49 | ```bash 50 | $ cd web 51 | $ yarn 52 | # Run a development web server: 53 | $ yarn run dev 54 | # Build the production docs: 55 | $ yarn run build 56 | ``` 57 | 58 | ## Contributing 59 | 60 | ### Submitting Patches 61 | 62 | Please see the [guide for submitting patches on 63 | git.sr.ht](https://man.sr.ht/git.sr.ht/#sending-patches-upstream). (If you choose to use 64 | `git send-email`, the patches should be sent to 65 | [~refi64/nsbox-devel@lists.sr.ht](https://lists.sr.ht/~refi64/nsbox-devel).) 66 | 67 | ### Coding Guidelines 68 | 69 | TODO 70 | 71 | ### Running the tests 72 | 73 | **These are not currently functional!** I'm doing a major overhaul to the way tests work. 74 | 75 | Unit testing is done by running [Expect](https://www.tcl.tk/man/expect5.31/expect.1.html) scripts inside 76 | an isolated environment. **Do not run the tests on your host system, as they will modify your containers.** 77 | 78 | Vagrant is used to manage the virtual environments (as a VM is required to test SELinux integration). 79 | The libvirt provider is required. 80 | 81 | Run: 82 | 83 | ```bash 84 | vagrant up 85 | ``` 86 | 87 | to bring up and provision the box (this includes building and installing nsbox inside). Once that is complete, 88 | you can run: 89 | 90 | ```bash 91 | vagrant ssh -c /vagrant/tests/main.exp 92 | ``` 93 | 94 | to run the unit tests. 95 | 96 | TODO: document test runner 97 | 98 | ## Misc. notes 99 | 100 | ### Updating the theme 101 | 102 | ```bash 103 | $ git -C VUEPRESS/packages/@vuepress/theme-default diff --relative v.PREV ':(exclude)__tests__' |\ 104 | git apply --reject --directory web/.vuepress/theme 105 | ``` 106 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | def define_fedora_vm(config, version) 2 | config.vm.define "fedora-#{version}" do |fedora| 3 | fedora.vm.box = "fedora/#{version}-cloud-base" 4 | fedora.vm.provision 'ansible' do |ansible| 5 | ansible.playbook = 'tests/playbooks/fedora.yaml' 6 | end 7 | end 8 | end 9 | 10 | Vagrant.configure('2') do |config| 11 | exclude = File.readlines('.gitignore').reject{|l| l.start_with? '#'}.map(&:strip).map(&Dir.method(:glob)).flatten(1) 12 | config.vm.synced_folder '.', '/vagrant', type: :rsync, rsync__exclude: exclude 13 | 14 | config.vm.provider :libvirt do |libvirt, override| 15 | libvirt.memory = 1024 16 | override.vm.synced_folder '.', '/vagrant', type: :nfs 17 | end 18 | 19 | define_fedora_vm config, '32' 20 | define_fedora_vm config, '33' 21 | end 22 | -------------------------------------------------------------------------------- /build/bin_proxy.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Just runs an executable in the given directory. 6 | 7 | import os 8 | import sys 9 | 10 | os.chdir(sys.argv[1]) 11 | os.execvp(sys.argv[2], sys.argv[2:]) 12 | -------------------------------------------------------------------------------- /build/copy_target_outputs.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("copy_target_outputs") { 6 | all_copy_targets = [] 7 | 8 | foreach(dep, invoker.deps) { 9 | copy_target = "${target_name}_" + string_replace(dep, "/", "_") 10 | copy_target = string_replace(copy_target, ":", "_") 11 | 12 | all_copy_targets += [ ":$copy_target" ] 13 | 14 | copy(copy_target) { 15 | sources = get_target_outputs(dep) 16 | forward_variables_from(invoker, [ "outputs" ]) 17 | deps = [ dep ] 18 | } 19 | } 20 | 21 | group(target_name) { 22 | deps = all_copy_targets 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /build/force_varlink.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // We want the go interface generator sources vendored, but: 6 | // - This can't manually be added to go.mod because the next tidy will drop it. 7 | // - This can't be imported from our main modules because it's not actually a library. 8 | // So I did the next best thing: create a stub .go file that imports it, that way it'll 9 | // be scanned by 'go mod' and be added normally. 10 | 11 | package force_varlink 12 | 13 | import ( 14 | _ "github.com/varlink/go/cmd/varlink-go-interface-generator" 15 | ) 16 | -------------------------------------------------------------------------------- /build/go.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | go_target_dir = "$root_build_dir/gofiles" 6 | 7 | template("go_binary") { 8 | action(target_name) { 9 | forward_variables_from(invoker, [ "deps" ]) 10 | 11 | outputs = [ "$target_out_dir/$target_name" ] 12 | depfile = "$target_out_dir/$target_name.d" 13 | 14 | script = "//build/go.py" 15 | args = [ 16 | "--go", 17 | go_exe, 18 | "--go-cache", 19 | rebase_path("$root_build_dir/gocache"), 20 | "--package", 21 | invoker.package, 22 | "--gofiles-root", 23 | rebase_path(go_target_dir, root_build_dir), 24 | "--out-bin", 25 | rebase_path(outputs[0], root_build_dir), 26 | "--out-dep", 27 | rebase_path(depfile, root_build_dir), 28 | ] 29 | 30 | if (defined(invoker.static) && invoker.static) { 31 | args += [ "--static" ] 32 | } 33 | 34 | if (enable_selinux) { 35 | args += [ "--selinux" ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /build/go.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Calls the go compiler, but also generates a Makefile-style .d file containing the Go package's 6 | # dependencies, that way GN can track them. 7 | 8 | from go_deps import go_list 9 | 10 | from pathlib import Path 11 | 12 | import argparse 13 | import os 14 | import subprocess 15 | import sys 16 | 17 | 18 | def load_deps(args, tags): 19 | deps = go_list(args.go, args.package, cwd=str(args.gofiles_root), vendor=True, 20 | tags=tags) 21 | 22 | bin_relative_to_depfile = os.path.relpath(args.out_bin, args.out_dep) 23 | 24 | makefile_deps = set() 25 | 26 | for dep in deps: 27 | import_path = dep['ImportPath'] 28 | directory = Path(dep['Dir']).resolve() 29 | 30 | try: 31 | directory.relative_to(args.gofiles_root) 32 | except ValueError: 33 | continue 34 | 35 | for key, value in dep.items(): 36 | if key.endswith('Files') and not key.startswith('Ignored'): 37 | makefile_deps |= set((directory / f).relative_to(Path().resolve()) for f in value) 38 | 39 | with open(args.out_dep, 'w') as fp: 40 | print(f'{args.out_bin}: {" ".join(map(str, makefile_deps))}', file=fp) 41 | 42 | 43 | def build(args, tags): 44 | command = [args.go, 'build', '-mod=vendor', '-o', os.path.abspath(args.out_bin)] 45 | env = os.environ.copy() 46 | 47 | env['GOCACHE'] = args.go_cache 48 | 49 | if args.static: 50 | env['CGO_ENABLED'] = '0' 51 | # We have to add buildmode=exe because distros like to use buildmode=pie for 52 | # building Go binaries, but it causes statically-linked binaries like nsbox-host 53 | # to segfault. 54 | command.extend(['-ldflags', '-extldflags "-static"', '-buildmode=exe']) 55 | 56 | if tags: 57 | command.append(f'-tags={" ".join(tags)}') 58 | 59 | command.append(args.package) 60 | 61 | process = subprocess.Popen(command, cwd=str(args.gofiles_root), env=env) 62 | ret = process.wait() 63 | if ret: 64 | sys.exit(ret) 65 | 66 | 67 | def main(): 68 | parser = argparse.ArgumentParser() 69 | parser.add_argument('--go') 70 | parser.add_argument('--go-cache') 71 | parser.add_argument('--gofiles-root', type=lambda x: Path(x).resolve()) 72 | parser.add_argument('--package') 73 | parser.add_argument('--out-bin') 74 | parser.add_argument('--out-dep') 75 | parser.add_argument('--selinux', action='store_true', default=False) 76 | parser.add_argument('--static', action='store_true', default=False) 77 | 78 | args = parser.parse_args() 79 | 80 | tags = set() 81 | if args.selinux: 82 | tags.add('selinux') 83 | 84 | load_deps(args, tags) 85 | build(args, tags) 86 | 87 | 88 | if __name__ == '__main__': 89 | main() 90 | -------------------------------------------------------------------------------- /build/go_deps.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Provides some basic helpers for parsing the JSON output of 'go list -mod=...' commands. 6 | 7 | import json 8 | import subprocess 9 | 10 | def go_list(go, package, *, cwd=None, vendor=False, tags=set()): 11 | mod_arg = 'vendor' if vendor else 'readonly' 12 | 13 | process = subprocess.run([go, 'list', f'-mod={mod_arg}', '-json', '-deps', 14 | f'-tags={" ".join(tags)}', package], 15 | stdout=subprocess.PIPE, universal_newlines=True, check=True, 16 | cwd=cwd) 17 | 18 | # Change formatting to be in list form. (Go list prints JSON objects back-to-back.) 19 | dep_json = '[' + process.stdout.replace('\n}', '\n},').rstrip(',\n') + ']' 20 | return json.loads(dep_json) 21 | -------------------------------------------------------------------------------- /build/install.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | install_dir = "$root_out_dir/install" 6 | 7 | template("install_files") { 8 | copy(target_name) { 9 | if (defined(invoker.deps)) { 10 | deps = invoker.deps 11 | } else { 12 | deps = [] 13 | } 14 | 15 | if (defined(invoker.sources)) { 16 | sources = invoker.sources 17 | } else { 18 | sources = [] 19 | } 20 | 21 | if (defined(invoker.targets)) { 22 | foreach(target, invoker.targets) { 23 | sources += get_target_outputs(target) 24 | } 25 | 26 | deps += invoker.targets 27 | } 28 | 29 | if (defined(invoker.output_prefix)) { 30 | output_prefix = invoker.output_prefix 31 | } else { 32 | output_prefix = install_dir 33 | } 34 | 35 | outputs = [ "$output_prefix/${invoker.output}" ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /build/installers/install_build_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | 8 | from pathlib import Path 9 | 10 | import argparse 11 | import os 12 | import shutil 13 | import stat 14 | 15 | 16 | def install_files(source, target, skip=set()): 17 | for item in os.scandir(source): 18 | if item.name in skip: 19 | continue 20 | 21 | target_item = target / item.name 22 | 23 | if item.is_dir(): 24 | target_item.mkdir(mode=0o755, parents=True, exist_ok=True) 25 | install_files(item.path, target / item.name) 26 | else: 27 | st = item.stat() 28 | if st.st_mode & stat.S_IXUSR: 29 | target_perms = 0o755 30 | else: 31 | target_perms = 0o644 32 | 33 | print(f'{item.path} -> {target_item} ({oct(target_perms)})') 34 | shutil.copy(item.path, target_item) 35 | target_item.chmod(target_perms) 36 | 37 | 38 | def main(): 39 | parser = argparse.ArgumentParser() 40 | 41 | parser.add_argument('outdir', help='The build directory to install', default='out', type=Path) 42 | parser.add_argument('--destdir', help='The destdir to install into', default='/', type=Path) 43 | parser.add_argument('--prefix', help='The prefix to install into', default='/usr/local', 44 | type=Path) 45 | 46 | args = parser.parse_args() 47 | 48 | install_files(args.outdir / 'install' / 'etc', args.destdir / 'etc') 49 | install_files(args.outdir / 'install', args.destdir / args.prefix.relative_to('/'), 50 | skip={'etc'}) 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /build/installers/install_with_rpm_ostree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | 8 | from pathlib import Path 9 | import argparse 10 | import json 11 | import os 12 | import re 13 | import subprocess 14 | import sys 15 | 16 | 17 | def rpm_ostree(args): 18 | prefix = ['flatpak-spawn', '--host'] if os.path.exists('/run/host/nsbox') else [] 19 | return [*prefix, 'rpm-ostree', *args] 20 | 21 | 22 | def read_fedora_release(): 23 | with open('/etc/os-release') as fp: 24 | for line in fp: 25 | if line.startswith('VERSION_ID='): 26 | return int(line.split('=', 1)[1]) 27 | 28 | assert False 29 | 30 | 31 | def get_matching_rpms(args): 32 | build_dir = Path(__file__).parent.parent 33 | rpm_dir = args.outdir / 'rpm' 34 | 35 | release_process = subprocess.run([sys.executable, str(build_dir / 'parse_release.py'), 36 | f'--root={build_dir.parent}', f'--branch={args.branch}'], 37 | stdout=subprocess.PIPE, check=True, universal_newlines=True) 38 | release = json.loads(release_process.stdout) 39 | release_version = args.version or release['version'] 40 | 41 | fedora = read_fedora_release() 42 | 43 | rpms = [] 44 | 45 | for child in rpm_dir.iterdir(): 46 | name = child.name 47 | keep = ( 48 | not ('debug' in name or 'guest-tools' in name 49 | or 'bender' in name or 'alias' in name 50 | or name.endswith('.src.rpm')) 51 | and release_version in name 52 | and (not name.startswith('nsbox-edge-') if args.branch == 'stable' 53 | else release['commit'] in name) 54 | ) 55 | if not keep: 56 | continue 57 | 58 | rpms.append(child) 59 | 60 | return rpms 61 | 62 | 63 | def get_packages_to_uninstall(args): 64 | rpm_ostree_state_process = subprocess.run(rpm_ostree(['status', '--json']), 65 | stdout=subprocess.PIPE, check=True, 66 | universal_newlines=True) 67 | rpm_ostree_state = json.loads(rpm_ostree_state_process.stdout) 68 | 69 | booted_deloyment = [deployment for deployment in rpm_ostree_state['deployments'] if deployment['booted']][0] 70 | 71 | if args.branch == 'stable': 72 | package_re = re.compile(r'(nsbox(?!-edge)(?:-[a-z]+)*)') 73 | else: 74 | package_re = re.compile(r'(nsbox-edge(?:-[a-z]+)*)') 75 | 76 | requested = booted_deloyment['requested-packages'] + booted_deloyment['requested-local-packages'] 77 | to_uninstall = [] 78 | 79 | for package in requested: 80 | match = package_re.match(package) 81 | if match is None: 82 | continue 83 | 84 | to_uninstall.append(package) 85 | 86 | return to_uninstall 87 | 88 | 89 | def main(): 90 | parser = argparse.ArgumentParser() 91 | parser.add_argument('outdir', help='The build directory to install', default='out', type=Path) 92 | parser.add_argument('--branch', help='The release branch', default='edge', choices=['edge', 'stable']) 93 | parser.add_argument('--version', help='The release version') 94 | parser.add_argument('--extra-args', help='Extra arguments to pass to rpm-ostree', default=[], nargs='*') 95 | args = parser.parse_args() 96 | 97 | rpms = get_matching_rpms(args) 98 | to_uninstall = get_packages_to_uninstall(args) 99 | 100 | command = ['install'] 101 | command.extend(f'--uninstall={package}' for package in to_uninstall) 102 | command.extend(args.extra_args) 103 | command.extend(map(str, rpms)) 104 | 105 | subprocess.run(rpm_ostree(command), check=True) 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /build/makepkg.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("makepkg") { 6 | action(target_name) { 7 | pool = "//:console" 8 | 9 | if (defined(invoker.package_name)) { 10 | package_name = invoker.package_name 11 | } else { 12 | package_name = invoker.target_name 13 | } 14 | 15 | forward_variables_from(invoker, 16 | [ 17 | "deps", 18 | "release", 19 | "version", 20 | ]) 21 | 22 | if (defined(invoker.pkgbuild)) { 23 | pkgbuild = invoker.pkgbuild 24 | } else { 25 | pkgbuild = "PKGBUILD" 26 | } 27 | 28 | sources = [ pkgbuild ] 29 | 30 | if (defined(invoker.sources)) { 31 | sources += invoker.sources 32 | # sources += get_target_outputs(":${target_name}_sources") 33 | } 34 | 35 | # binary_packages = [package_name] 36 | # if (defined(invoker.has_debug) && invoker.has_debug) { 37 | # binary_packages += ["$package_name-debuginfo", "$package_name-debugsource"] 38 | # } 39 | 40 | # if (defined(invoker.extra_packages)) { 41 | # binary_packages += invoker.extra_packages 42 | # } 43 | 44 | outputs = 45 | [ "$target_gen_dir/$package_name-$version-$release-any.pkg.tar.zst" ] 46 | 47 | pkgdest = rebase_path(target_gen_dir) 48 | builddir = rebase_path("$target_gen_dir/build-$target_name") 49 | buildfile_dir = rebase_path(get_path_info(pkgbuild, "dir"), root_out_dir) 50 | buildfile = get_path_info(pkgbuild, "file") 51 | 52 | script = "//build/bin_proxy.py" 53 | args = [ 54 | buildfile_dir, 55 | makepkg_exe, 56 | "-f", 57 | "BUILDFILE=$buildfile", 58 | "BUILDDIR=$builddir", 59 | "PKGDEST=$pkgdest", 60 | "PKGEXT=.pkg.tar.zst", 61 | ] 62 | 63 | if (defined(invoker.vars)) { 64 | foreach(var, invoker.vars) { 65 | args += [ "${var[0]}=${var[1]}" ] 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /build/minimal-toolchain/BUILD.gn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Implements a minimal toolchain, just what we need to build Go code but nothing more. 6 | 7 | toolchain("minimal-toolchain") { 8 | tool("copy") { 9 | command = "cp -af {{source}} {{output}}" 10 | description = "COPY {{source}} {{output}}" 11 | } 12 | 13 | tool("stamp") { 14 | command = "touch {{output}}" 15 | description = "STAMP {{output}}" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build/parse_release.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Parses the date of the latest git commit to generate the version. 6 | 7 | import argparse 8 | import json 9 | import subprocess 10 | import time 11 | 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('--root') 16 | parser.add_argument('--branch', choices=['stable', 'edge']) 17 | parser.add_argument('--override-commit') 18 | 19 | args = parser.parse_args() 20 | 21 | git_version_cmd = ['git', '-C', args.root, 'log', '-1', f'--format=%ct.%h'] 22 | if args.override_commit is not None: 23 | git_version_cmd.append(args.override_commit) 24 | 25 | version_proc = subprocess.run(git_version_cmd, 26 | stdout=subprocess.PIPE, 27 | check=True, 28 | universal_newlines=True, 29 | cwd=args.root) 30 | version_proc_parts = version_proc.stdout.strip().split('.') 31 | assert len(version_proc_parts) == 2, version_proc_parts 32 | 33 | data = { 34 | 'version': 35 | time.strftime('%y.%m.%d', time.gmtime(int(version_proc_parts[0]))), 36 | 'commit': 37 | version_proc_parts[1], 38 | } 39 | 40 | if args.branch == 'edge': 41 | git_rev_list_cmd = [ 42 | 'git', 'rev-list', '--count', args.override_commit or 'HEAD' 43 | ] 44 | rev_count_proc = subprocess.run(git_rev_list_cmd, 45 | stdout=subprocess.PIPE, 46 | check=True, 47 | universal_newlines=True, 48 | cwd=args.root) 49 | rev_count = rev_count_proc.stdout.strip() 50 | 51 | data['version'] += f'.{rev_count}' 52 | 53 | print(json.dumps(data)) 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /build/python-shim.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | # Wraps python3 to avoid generating bytecode files. 8 | 9 | exec python3 -B "$@" 10 | -------------------------------------------------------------------------------- /build/python_binary.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("python_binary") { 6 | action(target_name) { 7 | forward_variables_from(invoker, [ "deps" ]) 8 | 9 | script = "//build/python_binary.py" 10 | sources = [ invoker.source ] 11 | outputs = [ 12 | invoker.output, 13 | "${invoker.python_files_dir}/$target_name.py", 14 | "${invoker.python_files_dir}/$target_name.pyc", 15 | ] 16 | args = [ 17 | "--script", 18 | rebase_path(sources[0], root_build_dir), 19 | "--out-wrapper", 20 | rebase_path(outputs[0], root_build_dir), 21 | "--out-py", 22 | rebase_path(outputs[1], root_build_dir), 23 | "--out-pyc", 24 | rebase_path(outputs[2], root_build_dir), 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /build/python_binary.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Generates a "Python binary" by compiling the Python to bytecode and adding a wrapper 6 | # script. 7 | 8 | import argparse 9 | import os 10 | import py_compile 11 | import shlex 12 | import shutil 13 | 14 | 15 | SCRIPT_WRAPPER = ''' 16 | #!/bin/bash 17 | 18 | python3 "$(dirname "$0")/"{0} "$@" 19 | '''.lstrip() 20 | 21 | 22 | def main(): 23 | parser = argparse.ArgumentParser() 24 | 25 | parser.add_argument('--script') 26 | parser.add_argument('--out-wrapper') 27 | parser.add_argument('--out-py') 28 | parser.add_argument('--out-pyc') 29 | 30 | args = parser.parse_args() 31 | 32 | shutil.copy(args.script, args.out_py) 33 | py_compile.compile(args.out_py, args.out_pyc) 34 | 35 | relative_compiled_script = os.path.relpath(args.out_pyc, 36 | os.path.dirname(args.out_wrapper)) 37 | 38 | with open(args.out_wrapper, 'w') as fp: 39 | fp.write(SCRIPT_WRAPPER.format(shlex.quote(relative_compiled_script))) 40 | 41 | os.chmod(args.out_wrapper, 0o755) 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /build/rpmbuild.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("rpmbuild") { 6 | if (defined(invoker.sources)) { 7 | copy("${target_name}_sources") { 8 | forward_variables_from(invoker, 9 | [ 10 | "sources", 11 | "deps", 12 | ]) 13 | outputs = [ "$target_gen_dir/{{source_file_part}}" ] 14 | } 15 | } 16 | 17 | action(target_name) { 18 | pool = "//:console" 19 | 20 | if (defined(invoker.package_name)) { 21 | package_name = invoker.package_name 22 | } else { 23 | package_name = invoker.target_name 24 | } 25 | 26 | forward_variables_from(invoker, 27 | [ 28 | "version", 29 | "release", 30 | ]) 31 | 32 | release += ".fc$fedora_rpm_target_release" 33 | if (release_branch == "edge") { 34 | release += ".$release_commit" 35 | } 36 | 37 | deps = [] 38 | if (defined(invoker.sources)) { 39 | deps += [ ":${target_name}_sources" ] 40 | } 41 | if (defined(invoker.deps)) { 42 | deps += invoker.deps 43 | } 44 | 45 | topdir = "$target_gen_dir/rpmbuild" 46 | 47 | absolute_topdir = rebase_path(topdir) 48 | 49 | sources = [ invoker.spec ] 50 | if (defined(invoker.sources)) { 51 | sources += get_target_outputs(":${target_name}_sources") 52 | } 53 | 54 | binary_packages = [] 55 | noarch_packages = [] 56 | 57 | if (defined(invoker.noarch) && invoker.noarch) { 58 | noarch_packages += [ package_name ] 59 | } else { 60 | binary_packages += [ package_name ] 61 | } 62 | 63 | if (defined(invoker.has_debug) && invoker.has_debug) { 64 | binary_packages += [ 65 | "$package_name-debuginfo", 66 | "$package_name-debugsource", 67 | ] 68 | } 69 | 70 | if (defined(invoker.extra_binary_packages)) { 71 | binary_packages += invoker.extra_binary_packages 72 | } 73 | 74 | if (defined(invoker.extra_noarch_packages)) { 75 | noarch_packages += invoker.extra_noarch_packages 76 | } 77 | 78 | if (binary_packages != []) { 79 | if (target_cpu == "x86") { 80 | arch = "x86" 81 | } else if (target_cpu == "x64") { 82 | arch = "x86_64" 83 | } else if (target_cpu == "arm") { 84 | arch = "armv7hl" 85 | } else if (target_cpu == "arm64") { 86 | arch = "aarch64" 87 | } else if (target_cpu == "mipsel") { 88 | arch = "mipsel" 89 | } else { 90 | assert(false, "unknown target arch $target_cpu") 91 | } 92 | } 93 | 94 | outputs = [ "$topdir/SRPMS/$package_name-$version-$release.src.rpm" ] 95 | foreach(pkg, binary_packages) { 96 | outputs += [ "$topdir/RPMS/$arch/$pkg-$version-$release.$arch.rpm" ] 97 | } 98 | foreach(pkg, noarch_packages) { 99 | outputs += [ "$topdir/RPMS/noarch/$pkg-$version-$release.noarch.rpm" ] 100 | } 101 | 102 | script = "//build/bin_proxy.py" 103 | args = [ 104 | ".", 105 | rpmbuild_exe, 106 | "-ba", 107 | rebase_path(invoker.spec), 108 | "--define", 109 | "_topdir $absolute_topdir", 110 | "--undefine", 111 | "_disable_source_fetch", 112 | ] 113 | 114 | if (defined(invoker.sources)) { 115 | srcdir = rebase_path( 116 | get_label_info(":${target_name}_sources", "target_gen_dir")) 117 | args += [ 118 | "--define", 119 | "_sourcedir $srcdir", 120 | ] 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /build/selinux.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("selinux_package") { 6 | action(target_name) { 7 | script = "//build/selinux.py" 8 | 9 | if (defined(invoker.name)) { 10 | name = invoker.name 11 | } else { 12 | name = target_name 13 | } 14 | 15 | forward_variables_from(invoker, 16 | [ 17 | "sources", 18 | "deps", 19 | ]) 20 | 21 | te = "" 22 | fc = "" 23 | foreach(source, sources) { 24 | ext = get_path_info(source, "extension") 25 | if (ext == "te") { 26 | assert(te == "") 27 | te = source 28 | } else if (ext == "fc") { 29 | assert(fc == "") 30 | fc = source 31 | } 32 | } 33 | 34 | assert(te != "") 35 | 36 | scratch_dir = "$target_gen_dir/$name-policy" 37 | outputs = [ "$target_out_dir/$name.pp.bz2" ] 38 | 39 | args = [ 40 | "--make", 41 | selinux_make, 42 | "--makefile", 43 | selinux_makefile, 44 | "--variant", 45 | selinux_variant, 46 | "--out", 47 | rebase_path(outputs[0], root_build_dir), 48 | "--scratch-dir", 49 | rebase_path(scratch_dir, root_build_dir), 50 | "--te", 51 | rebase_path(te, root_build_dir), 52 | "--fc", 53 | rebase_path(fc, root_build_dir), 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /build/selinux.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from pathlib import Path 6 | 7 | import argparse 8 | import bz2 9 | import os 10 | import shutil 11 | import subprocess 12 | 13 | 14 | def run_make(args, *targets): 15 | subprocess.run([args.make, f'NAME={args.variant}', '-f', args.makefile, *targets], 16 | cwd=args.scratch_dir, check=True, stdout=subprocess.DEVNULL) 17 | 18 | 19 | def compress(args): 20 | policy_file = os.path.join(args.scratch_dir, 21 | os.path.splitext(os.path.basename(args.out))[0]) 22 | 23 | with open(policy_file, 'rb') as policy, open(args.out, 'wb') as out: 24 | compressor = bz2.BZ2Compressor() 25 | 26 | while True: 27 | data = policy.read(2048) 28 | if not data: 29 | break 30 | 31 | out.write(compressor.compress(data)) 32 | 33 | out.write(compressor.flush()) 34 | 35 | 36 | def main(): 37 | parser = argparse.ArgumentParser() 38 | 39 | parser.add_argument('--make') 40 | parser.add_argument('--makefile') 41 | parser.add_argument('--variant') 42 | parser.add_argument('--out') 43 | parser.add_argument('--scratch-dir') 44 | parser.add_argument('--te') 45 | parser.add_argument('--fc') 46 | 47 | args = parser.parse_args() 48 | 49 | try: 50 | shutil.rmtree(args.scratch_dir) 51 | except FileNotFoundError: 52 | pass 53 | 54 | os.makedirs(args.scratch_dir, exist_ok=True) 55 | 56 | try: 57 | shutil.copy(args.te, args.scratch_dir) 58 | if args.fc: 59 | shutil.copy(args.fc, args.scratch_dir) 60 | 61 | run_make(args) 62 | compress(args) 63 | run_make(args, 'clean') 64 | 65 | finally: 66 | shutil.rmtree(args.scratch_dir) 67 | 68 | 69 | if __name__ == '__main__': 70 | main() 71 | -------------------------------------------------------------------------------- /build/source_archive.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import argparse 6 | import os.path 7 | import subprocess 8 | import tarfile 9 | 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('--source-root') 14 | parser.add_argument('--prefix') 15 | parser.add_argument('--out-tar') 16 | parser.add_argument('--out-dep') 17 | parser.add_argument('--include-vendor', action='store_true') 18 | 19 | args = parser.parse_args() 20 | dep_parent = os.path.dirname(args.out_dep) 21 | 22 | files_process = subprocess.run( 23 | ['git', 'ls-files', '-oc', '-X', '.gitignore'], 24 | cwd=args.source_root, 25 | check=True, 26 | stdout=subprocess.PIPE, 27 | universal_newlines=True) 28 | files = set(files_process.stdout.splitlines()) 29 | 30 | removed_process = subprocess.run(['git', 'ls-files', '-d'], 31 | cwd=args.source_root, 32 | check=True, 33 | stdout=subprocess.PIPE, 34 | universal_newlines=True) 35 | files -= set(removed_process.stdout.splitlines()) 36 | 37 | if args.include_vendor: 38 | for root, _, vendored in os.walk( 39 | os.path.join(args.source_root, 'vendor')): 40 | files.update( 41 | os.path.relpath(os.path.join(root, file), args.source_root) 42 | for file in vendored) 43 | 44 | # XXX: Avoid a weird issue where the out dependency file's parent dirs don't exist yet. 45 | os.makedirs(os.path.dirname(args.out_dep), exist_ok=True) 46 | 47 | deps = set() 48 | 49 | with tarfile.open(args.out_tar, 'w') as tar: 50 | for file in files: 51 | tar.add(os.path.join(args.source_root, file), 52 | os.path.join(args.prefix, file)) 53 | deps.add(os.path.relpath(file)) 54 | 55 | with open(args.out_dep, 'w') as dep: 56 | print(f'{args.out_tar}: {" ".join(deps)}', file=dep) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /build/substitute_file.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("substitute_file") { 6 | action(target_name) { 7 | forward_variables_from(invoker, [ "deps" ]) 8 | 9 | sources = [ invoker.source ] 10 | 11 | if (defined(invoker.output)) { 12 | outputs = [ invoker.output ] 13 | } else { 14 | outputs = [ "$target_out_dir/$target_name" ] 15 | } 16 | 17 | script = "//build/substitute_file.py" 18 | 19 | args = [ 20 | "--source", 21 | rebase_path(invoker.source, root_build_dir), 22 | "--dest", 23 | rebase_path(outputs[0], root_build_dir), 24 | ] 25 | 26 | if (defined(invoker.vars)) { 27 | foreach(var, invoker.vars) { 28 | args += [ 29 | "--var", 30 | var[0], 31 | var[1], 32 | ] 33 | } 34 | } 35 | 36 | if (defined(invoker.file_vars)) { 37 | foreach(file_var, invoker.file_vars) { 38 | args += [ 39 | "--file-var", 40 | file_var[0], 41 | rebase_path(file_var[1], root_build_dir), 42 | ] 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /build/substitute_file.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Runs template substitutions over a file. 6 | 7 | import argparse 8 | import string 9 | 10 | 11 | class CustomTemplate(string.Template): 12 | delimiter = '@' 13 | 14 | 15 | def main(): 16 | parser = argparse.ArgumentParser() 17 | 18 | parser.add_argument('--source') 19 | parser.add_argument('--dest') 20 | parser.add_argument('--var', nargs=2, action='append', default=[], dest='vars') 21 | parser.add_argument('--file-var', nargs=2, action='append', default=[], dest='file_vars') 22 | 23 | args = parser.parse_args() 24 | 25 | substitutions = {} 26 | 27 | for var, value in args.vars: 28 | substitutions[var] = value 29 | 30 | for var, path in args.file_vars: 31 | with open(path) as fp: 32 | substitutions[var] = fp.read() 33 | 34 | with open(args.source) as source, open(args.dest, 'w') as dest: 35 | for line in source: 36 | if line.strip(): 37 | line = CustomTemplate(line).substitute(substitutions) 38 | 39 | dest.write(line) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /build/symlink.gni: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | template("symlink") { 6 | action(target_name) { 7 | stamp = "$target_gen_dir/$target_name.stamp" 8 | 9 | script = "//build/symlink.py" 10 | args = [ 11 | invoker.value, 12 | rebase_path(invoker.name, root_build_dir), 13 | rebase_path(stamp, root_build_dir), 14 | ] 15 | sources = [] 16 | outputs = [ stamp ] 17 | 18 | forward_variables_from(invoker, [ "deps" ]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build/symlink.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Just symlinks a file. This script's boring-ness is only matched by bin_proxy.py. 6 | 7 | import os 8 | import sys 9 | 10 | _, source, dest, stamp = sys.argv 11 | 12 | if os.path.islink(dest): 13 | os.unlink(dest) 14 | 15 | os.makedirs(os.path.dirname(dest), exist_ok=True) 16 | os.symlink(source, dest) 17 | 18 | with open(stamp, 'w') as fp: 19 | pass 20 | -------------------------------------------------------------------------------- /build/write_release.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Writes the VERSION and RELEASE files for use by other targets. 6 | 7 | import argparse 8 | import time 9 | 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('--version') 14 | parser.add_argument('--branch') 15 | parser.add_argument('--out-version') 16 | parser.add_argument('--out-branch') 17 | 18 | args = parser.parse_args() 19 | 20 | with open(args.out_version, 'w') as fp: 21 | fp.write(args.version) 22 | 23 | with open(args.out_branch, 'w') as fp: 24 | fp.write(args.branch) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /cmd/nsbox-host/enter.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/pkg/errors" 12 | "github.com/refi64/nsbox/internal/args" 13 | "github.com/refi64/nsbox/internal/log" 14 | "github.com/refi64/nsbox/internal/session" 15 | ) 16 | 17 | type enterCommand struct { 18 | stdin string 19 | stdout string 20 | stderr string 21 | uid int 22 | cwd string 23 | noReplay bool 24 | } 25 | 26 | func newEnterCommand(app args.App) subcommands.Command { 27 | return args.WrapSimpleCommand(app, &enterCommand{}) 28 | } 29 | 30 | func (*enterCommand) Name() string { 31 | return "enter" 32 | } 33 | 34 | func (*enterCommand) Synopsis() string { 35 | return "enter a session" 36 | } 37 | 38 | func (*enterCommand) Usage() string { 39 | return "" 40 | } 41 | 42 | func (cmd *enterCommand) SetFlags(fs *flag.FlagSet) { 43 | fs.StringVar(&cmd.stdin, "stdin", "", "") 44 | fs.StringVar(&cmd.stdout, "stdout", "", "") 45 | fs.StringVar(&cmd.stderr, "stderr", "", "") 46 | fs.IntVar(&cmd.uid, "uid", -1, "") 47 | fs.StringVar(&cmd.cwd, "cwd", "", "") 48 | fs.BoolVar(&cmd.noReplay, "no-replay", false, "") 49 | } 50 | 51 | func (cmd *enterCommand) ParsePositional(fs *flag.FlagSet) error { 52 | if fs.NArg() == 0 { 53 | return errors.New("expected a command") 54 | } 55 | 56 | if cmd.cwd == "" || cmd.uid == -1 { 57 | return errors.New("missing arguments") 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (cmd *enterCommand) Execute(_ args.App, fs *flag.FlagSet) subcommands.ExitStatus { 64 | if fs.NArg() == 0 { 65 | log.Alert("expected a command") 66 | return subcommands.ExitUsageError 67 | } 68 | 69 | err := session.ConnectPtys(cmd.stdin, cmd.stdout, cmd.stderr) 70 | if err == nil { 71 | err = session.SetupContainerSession(cmd.uid, cmd.cwd, cmd.noReplay, fs.Args()) 72 | } 73 | 74 | return args.HandleError(err) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/nsbox-host/main.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "os" 10 | 11 | "github.com/google/subcommands" 12 | "github.com/refi64/nsbox/internal/args" 13 | ) 14 | 15 | const internalEnv = "NSBOX_INTERNAL" 16 | 17 | type nsboxHostApp struct{} 18 | 19 | func (app *nsboxHostApp) PreexecHook(cmd subcommands.Command, fs *flag.FlagSet) {} 20 | func (app *nsboxHostApp) SetGlobalFlags(fs *flag.FlagSet) {} 21 | 22 | func main() { 23 | app := &nsboxHostApp{} 24 | 25 | subcommands.Register(subcommands.HelpCommand(), "") 26 | subcommands.Register(subcommands.FlagsCommand(), "") 27 | subcommands.Register(subcommands.CommandsCommand(), "") 28 | 29 | subcommands.Register(newReloadExportsCommand(app), "") 30 | 31 | if os.Getenv(internalEnv) != "" { 32 | subcommands.Register(newServiceCommand(app), "") 33 | subcommands.Register(newEnterCommand(app), "") 34 | 35 | os.Unsetenv(internalEnv) 36 | } 37 | 38 | args.Execute(app) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/nsbox-host/reload_exports.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | 11 | "github.com/google/subcommands" 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/args" 14 | devnsbox "github.com/refi64/nsbox/internal/varlink" 15 | ) 16 | 17 | func reloadExports() error { 18 | conn, err := varlinkConnect() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | defer conn.Close() 24 | 25 | if err := devnsbox.NotifyReloadExports().Call(context.Background(), conn); err != nil { 26 | return errors.Wrap(err, "failed to send reload exports message") 27 | } 28 | 29 | return nil 30 | } 31 | 32 | type reloadExportsCommand struct{} 33 | 34 | func newReloadExportsCommand(app args.App) subcommands.Command { 35 | return args.WrapSimpleCommand(app, &reloadExportsCommand{}) 36 | } 37 | 38 | func (*reloadExportsCommand) Name() string { 39 | return "reload-exports" 40 | } 41 | 42 | func (*reloadExportsCommand) Synopsis() string { 43 | return "reload all the files exported to the host." 44 | } 45 | 46 | func (*reloadExportsCommand) Usage() string { 47 | return `reload-exports: 48 | Reload all the files exported to the host. 49 | ` 50 | } 51 | 52 | func (*reloadExportsCommand) SetFlags(fs *flag.FlagSet) { 53 | } 54 | 55 | func (*reloadExportsCommand) ParsePositional(fs *flag.FlagSet) error { 56 | return args.ExpectArgs(fs) 57 | } 58 | 59 | func (*reloadExportsCommand) Execute(_ args.App, fs *flag.FlagSet) subcommands.ExitStatus { 60 | return args.HandleError(reloadExports()) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/nsbox-host/service.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "os" 11 | 12 | "github.com/coreos/go-systemd/v22/daemon" 13 | "github.com/google/subcommands" 14 | "github.com/pkg/errors" 15 | "github.com/refi64/nsbox/internal/args" 16 | "github.com/refi64/nsbox/internal/ptyservice" 17 | devnsbox "github.com/refi64/nsbox/internal/varlink" 18 | ) 19 | 20 | func startPtyServiceAndNotifyHost(name string) error { 21 | if err := ptyservice.StartPtyService(name); err != nil { 22 | return errors.Wrap(err, "failed to start pty service") 23 | } 24 | 25 | conn, err := varlinkConnect() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // NOTE: the connection is not closed because we will generally never die normally: 31 | // - If an error occurs, then it needs to be logged before the connection is closed, at 32 | // which point nsboxd dies before we can log the message. On error, nsbox-host dies 33 | // anyway. 34 | // - If no error occurs, this will run forever until killed. 35 | 36 | if err := devnsbox.NotifyStart().Call(context.Background(), conn); err != nil { 37 | return errors.Wrap(err, "failed to notify of start") 38 | } 39 | 40 | if os.Getenv("NOTIFY_SOCKET") != "" { 41 | if _, err := daemon.SdNotify(true, daemon.SdNotifyReady); err != nil { 42 | return err 43 | } 44 | } 45 | 46 | select {} 47 | } 48 | 49 | type serviceCommand struct { 50 | container string 51 | } 52 | 53 | func newServiceCommand(app args.App) subcommands.Command { 54 | return args.WrapSimpleCommand(app, &serviceCommand{}) 55 | } 56 | 57 | func (*serviceCommand) Name() string { 58 | return "service" 59 | } 60 | 61 | func (*serviceCommand) Synopsis() string { 62 | return "starts the PTY service" 63 | } 64 | 65 | func (*serviceCommand) Usage() string { 66 | return "" 67 | } 68 | 69 | func (*serviceCommand) SetFlags(fs *flag.FlagSet) { 70 | } 71 | 72 | func (cmd *serviceCommand) ParsePositional(fs *flag.FlagSet) error { 73 | return args.ExpectArgs(fs, &cmd.container) 74 | } 75 | 76 | func (cmd *serviceCommand) Execute(_ args.App, fs *flag.FlagSet) subcommands.ExitStatus { 77 | return args.HandleError(startPtyServiceAndNotifyHost(cmd.container)) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/nsbox-host/varlink_util.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/refi64/nsbox/internal/paths" 12 | "github.com/varlink/go/varlink" 13 | ) 14 | 15 | func varlinkConnect() (*varlink.Connection, error) { 16 | conn, err := varlink.NewConnection(context.Background(), "unix:///run/host/nsbox/"+paths.HostServiceSocketName) 17 | if err != nil { 18 | return nil, errors.Wrap(err, "failed to connect to host socket") 19 | } 20 | 21 | return conn, nil 22 | } 23 | -------------------------------------------------------------------------------- /cmd/nsbox-invoker/main.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/refi64/nsbox/internal/log" 11 | "github.com/refi64/nsbox/internal/paths" 12 | "github.com/refi64/nsbox/internal/userdata" 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | func main() { 17 | // Usage: nsbox-invoker :: 18 | 19 | args := os.Args[1:] 20 | if len(args) < 3 { 21 | log.Fatal("This is an internal tool!!") 22 | } 23 | 24 | command := args[0] 25 | args = args[1:] 26 | 27 | environ := os.Environ() 28 | 29 | for idx, env := range args { 30 | if env == "::" { 31 | args = args[idx+1:] 32 | break 33 | } 34 | 35 | name, _ := userdata.SplitEnv(env) 36 | if userdata.IsWhitelisted(name) { 37 | environ = append(environ, env) 38 | } else { 39 | log.Fatal("non-whitelisted environment variable:", env) 40 | } 41 | } 42 | 43 | if len(args) == 0 { 44 | log.Fatal("end of environment not found") 45 | } 46 | 47 | nsbox, err := paths.GetMainExecutable() 48 | if err != nil { 49 | log.Fatal("failed to find nsbox binary:", err) 50 | } 51 | 52 | cmd := []string{nsbox, command} 53 | cmd = append(cmd, args...) 54 | 55 | err = unix.Exec(cmd[0], cmd, environ) 56 | log.Fatal("exec:", err) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/nsbox/create.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/refi64/nsbox/internal/args" 12 | "github.com/refi64/nsbox/internal/container" 13 | "github.com/refi64/nsbox/internal/create" 14 | ) 15 | 16 | type createCommand struct { 17 | image string 18 | name string 19 | tar string 20 | boot bool 21 | } 22 | 23 | func newCreateCommand(app args.App) subcommands.Command { 24 | return args.WrapSimpleCommand(app, &createCommand{}) 25 | } 26 | 27 | func (*createCommand) Name() string { 28 | return "create" 29 | } 30 | 31 | func (*createCommand) Synopsis() string { 32 | return "create a new container" 33 | } 34 | 35 | func (*createCommand) Usage() string { 36 | return `create [-boot] [-tar ] : 37 | Creates a new container with the given name from the given image. You can provide an initial 38 | container config to it by passing various arguments. 39 | ` 40 | } 41 | 42 | func (cmd *createCommand) SetFlags(fs *flag.FlagSet) { 43 | fs.StringVar(&cmd.tar, "tar", "", "Override the image contents with this tar file") 44 | fs.BoolVar(&cmd.boot, "boot", false, "Make the container a booted container") 45 | } 46 | 47 | func (cmd *createCommand) ParsePositional(fs *flag.FlagSet) error { 48 | return args.ExpectArgs(fs, &cmd.image, &cmd.name) 49 | } 50 | 51 | func (cmd *createCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 52 | config := container.Config{ 53 | Image: cmd.image, 54 | Boot: cmd.boot, 55 | } 56 | 57 | err := create.CreateContainer(app.(*nsboxApp).usrdata, cmd.name, cmd.tar, config) 58 | return args.HandleError(err) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/nsbox/delete.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/google/subcommands" 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/args" 15 | "github.com/refi64/nsbox/internal/container" 16 | "github.com/refi64/nsbox/internal/inventory" 17 | ) 18 | 19 | type deleteCommand struct { 20 | name string 21 | yes bool 22 | } 23 | 24 | func newDeleteCommand(app args.App) subcommands.Command { 25 | return args.WrapSimpleCommand(app, &deleteCommand{}) 26 | } 27 | 28 | func (*deleteCommand) Name() string { 29 | return "delete" 30 | } 31 | 32 | func (*deleteCommand) Synopsis() string { 33 | return "delete a container" 34 | } 35 | 36 | func (*deleteCommand) Usage() string { 37 | return `delete [-y] 38 | Permanently deletes the given container. 39 | ` 40 | } 41 | 42 | func (cmd *deleteCommand) SetFlags(fs *flag.FlagSet) { 43 | fs.BoolVar(&cmd.yes, "y", false, "Don't ask to confirm whether or not to delete the container") 44 | } 45 | 46 | func (cmd *deleteCommand) ParsePositional(fs *flag.FlagSet) error { 47 | return args.ExpectArgs(fs, &cmd.name) 48 | } 49 | 50 | func (cmd *deleteCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 51 | ct, err := container.Open(app.(*nsboxApp).usrdata, cmd.name) 52 | if err != nil { 53 | return args.HandleError(err) 54 | } 55 | 56 | def, err := inventory.DefaultContainer(app.(*nsboxApp).usrdata) 57 | if err != nil { 58 | return args.HandleError(err) 59 | } 60 | 61 | if def != nil && def.Name == ct.Name { 62 | return args.HandleError(errors.New("Cannot delete the default container.")) 63 | } 64 | 65 | if !cmd.yes { 66 | fmt.Printf("Are you sure you want to PERMANENTLY delete %s? (y/n) ", cmd.name) 67 | 68 | var resp string 69 | fmt.Scanln(&resp) 70 | if strings.ToLower(resp) != "y" { 71 | return subcommands.ExitSuccess 72 | } 73 | } 74 | 75 | return args.HandleError(ct.LockAndDelete(container.NoWaitForLock)) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/nsbox/images.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // XXX: This is really similar to list.go... 6 | 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/google/subcommands" 16 | "github.com/refi64/nsbox/internal/args" 17 | "github.com/refi64/nsbox/internal/image" 18 | ) 19 | 20 | type imagesCommand struct { 21 | patterns []string 22 | } 23 | 24 | func newImagesCommand(app args.App) subcommands.Command { 25 | return args.WrapSimpleCommand(app, &imagesCommand{}) 26 | } 27 | 28 | func (*imagesCommand) Name() string { 29 | return "images" 30 | } 31 | 32 | func (*imagesCommand) Synopsis() string { 33 | return "list images" 34 | } 35 | 36 | func (*imagesCommand) Usage() string { 37 | return `list [...]: 38 | Lists all the available images. If a pattern is given, list only containers whose names 39 | match one of the given patterns. 40 | ` 41 | } 42 | 43 | func (*imagesCommand) SetFlags(fs *flag.FlagSet) {} 44 | 45 | func (cmd *imagesCommand) ParsePositional(fs *flag.FlagSet) error { 46 | cmd.patterns = fs.Args() 47 | return nil 48 | } 49 | 50 | func (cmd *imagesCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 51 | var images []*image.Image 52 | images, err := image.List() 53 | if err != nil { 54 | return args.HandleError(err) 55 | } 56 | 57 | for _, img := range images { 58 | if len(cmd.patterns) != 0 { 59 | var match bool 60 | 61 | for _, arg := range fs.Args() { 62 | match, err = filepath.Match(arg, filepath.Base(img.RootPath)) 63 | if match { 64 | break 65 | } 66 | 67 | if err != nil { 68 | return args.HandleError(err) 69 | } 70 | } 71 | 72 | if !match { 73 | continue 74 | } 75 | } 76 | 77 | name := filepath.Base(img.RootPath) 78 | 79 | if len(img.ValidTags) != 0 { 80 | fmt.Printf("%s:%s\n", name, strings.Join(img.ValidTags, ",")) 81 | } else { 82 | fmt.Println(name) 83 | } 84 | } 85 | 86 | return subcommands.ExitSuccess 87 | } 88 | -------------------------------------------------------------------------------- /cmd/nsbox/info.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/refi64/nsbox/internal/args" 12 | "github.com/refi64/nsbox/internal/container" 13 | ) 14 | 15 | type infoCommand struct { 16 | name string 17 | } 18 | 19 | func newInfoCommand(app args.App) subcommands.Command { 20 | return args.WrapSimpleCommand(app, &infoCommand{}) 21 | } 22 | 23 | func (*infoCommand) Name() string { 24 | return "info" 25 | } 26 | 27 | func (*infoCommand) Synopsis() string { 28 | return "show container info" 29 | } 30 | 31 | func (*infoCommand) Usage() string { 32 | return `info 33 | Show information about the given container, including its running state and configuration. 34 | ` 35 | } 36 | 37 | func (*infoCommand) SetFlags(fs *flag.FlagSet) {} 38 | 39 | func (cmd *infoCommand) ParsePositional(fs *flag.FlagSet) error { 40 | return args.ExpectArgs(fs, &cmd.name) 41 | } 42 | 43 | func (cmd *infoCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 44 | err := container.OpenAndShowInfo(app.(*nsboxApp).usrdata, cmd.name) 45 | return args.HandleError(err) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nsbox/kill.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/refi64/nsbox/internal/args" 12 | "github.com/refi64/nsbox/internal/container" 13 | "github.com/refi64/nsbox/internal/kill" 14 | ) 15 | 16 | type killCommand struct { 17 | container string 18 | signal kill.Signal 19 | all bool 20 | } 21 | 22 | func newKillCommand(app args.App) subcommands.Command { 23 | return args.WrapSimpleCommand(app, &killCommand{signal: kill.SigPoweroff}) 24 | } 25 | 26 | func (*killCommand) Name() string { 27 | return "kill" 28 | } 29 | 30 | func (*killCommand) Synopsis() string { 31 | return "kill a container" 32 | } 33 | 34 | func (*killCommand) Usage() string { 35 | return `kill [-signal signal] [-all] : 36 | Kill the container using the given signal. If the signal is not given, then poweroff is the 37 | default for booted containers, and sigterm is the default for non-booted containers. 38 | ` 39 | } 40 | 41 | func (cmd *killCommand) SetFlags(fs *flag.FlagSet) { 42 | fs.Var(&cmd.signal, "signal", "The signal to use to kill the container") 43 | fs.BoolVar(&cmd.all, "all", false, "Send the signal to all processes, not just the leader") 44 | } 45 | 46 | func (cmd *killCommand) ParsePositional(fs *flag.FlagSet) error { 47 | return args.ExpectArgs(fs, &cmd.container) 48 | } 49 | 50 | func (cmd *killCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 51 | usrdata := app.(*nsboxApp).usrdata 52 | 53 | ct, err := container.Open(usrdata, cmd.container) 54 | if err != nil { 55 | return args.HandleError(err) 56 | } 57 | 58 | err = kill.KillContainer(app.(*nsboxApp).usrdata, ct, cmd.signal, cmd.all) 59 | return args.HandleError(err) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/nsbox/list.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "path/filepath" 10 | 11 | "github.com/google/subcommands" 12 | "github.com/refi64/nsbox/internal/args" 13 | "github.com/refi64/nsbox/internal/container" 14 | "github.com/refi64/nsbox/internal/inventory" 15 | "github.com/refi64/nsbox/internal/log" 16 | ) 17 | 18 | type listCommand struct { 19 | patterns []string 20 | } 21 | 22 | func newListCommand(app args.App) subcommands.Command { 23 | return args.WrapSimpleCommand(app, &listCommand{}) 24 | } 25 | 26 | func (*listCommand) Name() string { 27 | return "list" 28 | } 29 | 30 | func (*listCommand) Synopsis() string { 31 | return "list containers" 32 | } 33 | 34 | func (*listCommand) Usage() string { 35 | return `list [...]: 36 | Lists all the available containers. If a pattern is given, list only containers whose names 37 | match one of the given patterns. 38 | ` 39 | } 40 | 41 | func (*listCommand) SetFlags(fs *flag.FlagSet) {} 42 | 43 | func (cmd *listCommand) ParsePositional(fs *flag.FlagSet) error { 44 | cmd.patterns = fs.Args() 45 | return nil 46 | } 47 | 48 | func (cmd *listCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 49 | var containers []*container.Container 50 | containers, err := inventory.List(app.(*nsboxApp).usrdata) 51 | if err != nil { 52 | return args.HandleError(err) 53 | } 54 | 55 | for _, ct := range containers { 56 | if len(cmd.patterns) != 0 { 57 | var match bool 58 | 59 | for _, arg := range fs.Args() { 60 | match, err = filepath.Match(arg, ct.Name) 61 | if match { 62 | break 63 | } 64 | 65 | if err != nil { 66 | return args.HandleError(err) 67 | } 68 | } 69 | 70 | if !match { 71 | continue 72 | } 73 | } 74 | 75 | log.Info(ct.Name) 76 | } 77 | 78 | return subcommands.ExitSuccess 79 | } 80 | -------------------------------------------------------------------------------- /cmd/nsbox/rename.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "os" 10 | 11 | "github.com/google/subcommands" 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/args" 14 | "github.com/refi64/nsbox/internal/container" 15 | "github.com/refi64/nsbox/internal/inventory" 16 | "github.com/refi64/nsbox/internal/userdata" 17 | ) 18 | 19 | type renameCommand struct { 20 | current string 21 | new string 22 | } 23 | 24 | func newRenameCommand(app args.App) subcommands.Command { 25 | return args.WrapSimpleCommand(app, &renameCommand{}) 26 | } 27 | 28 | func (*renameCommand) Name() string { 29 | return "rename" 30 | } 31 | 32 | func (*renameCommand) Synopsis() string { 33 | return "rename a container" 34 | } 35 | 36 | func (*renameCommand) Usage() string { 37 | return `rename 38 | Rename the given container to a new name. 39 | ` 40 | } 41 | 42 | func (*renameCommand) SetFlags(fs *flag.FlagSet) {} 43 | 44 | func (cmd *renameCommand) ParsePositional(fs *flag.FlagSet) error { 45 | return args.ExpectArgs(fs, &cmd.current, &cmd.new) 46 | } 47 | 48 | func isDefaultContainer(usrdata *userdata.Userdata, name string) (bool, error) { 49 | def, err := inventory.DefaultContainer(usrdata) 50 | if err != nil { 51 | return false, err 52 | } 53 | 54 | return def.Name == name, nil 55 | } 56 | 57 | func (cmd *renameCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 58 | usrdata := app.(*nsboxApp).usrdata 59 | 60 | ct, err := container.Open(usrdata, cmd.current) 61 | if err != nil { 62 | return args.HandleError(err) 63 | } 64 | 65 | err = ct.LockUntilProcessDeath(container.FullContainerLock, container.NoWaitForLock) 66 | if err != nil { 67 | return args.HandleError(err) 68 | } 69 | 70 | isDefault, err := isDefaultContainer(usrdata, cmd.current) 71 | if err != nil { 72 | return args.HandleError(errors.Wrap(err, "checking default container")) 73 | } 74 | 75 | err = ct.Rename(cmd.new) 76 | if err != nil { 77 | if os.IsExist(err) { 78 | err = errors.New("new name already exists") 79 | } 80 | 81 | return args.HandleError(err) 82 | } 83 | 84 | if isDefault { 85 | // XXX: This is a bit racy, but in the worst-case scenario, the default container 86 | // ends up set to a now-deleted container. 87 | err = inventory.SetDefaultContainer(usrdata, cmd.new) 88 | if err != nil { 89 | return args.HandleError(err) 90 | } 91 | } 92 | 93 | return subcommands.ExitSuccess 94 | } 95 | -------------------------------------------------------------------------------- /cmd/nsbox/run.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "os" 10 | 11 | "github.com/google/subcommands" 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/args" 14 | "github.com/refi64/nsbox/internal/container" 15 | "github.com/refi64/nsbox/internal/daemon" 16 | "github.com/refi64/nsbox/internal/inventory" 17 | "github.com/refi64/nsbox/internal/log" 18 | "github.com/refi64/nsbox/internal/session" 19 | ) 20 | 21 | type runCommand struct { 22 | container string 23 | restart bool 24 | noReplay bool 25 | command []string 26 | } 27 | 28 | func newRunCommand(app args.App) subcommands.Command { 29 | return args.WrapSimpleCommand(app, &runCommand{}) 30 | } 31 | 32 | func (*runCommand) Name() string { 33 | return "run" 34 | } 35 | 36 | func (*runCommand) Synopsis() string { 37 | return "run a container" 38 | } 39 | 40 | func (*runCommand) Usage() string { 41 | return `run [] []: 42 | Run a command within container. If a command is not given, the shell will be run. If a 43 | container is not given or is -, the default container will be run. 44 | ` 45 | } 46 | 47 | func (cmd *runCommand) SetFlags(fs *flag.FlagSet) { 48 | fs.BoolVar(&cmd.restart, "restart", false, "Restart the container if it's already running") 49 | fs.BoolVar(&cmd.noReplay, "no-replay", false, "Don't attempt to replay any updated Ansible playbooks") 50 | } 51 | 52 | func (cmd *runCommand) ParsePositional(fs *flag.FlagSet) error { 53 | if len(fs.Args()) >= 1 { 54 | cmd.container = fs.Args()[0] 55 | cmd.command = fs.Args()[1:] 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (cmd *runCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 62 | var ct *container.Container 63 | var err error 64 | 65 | usrdata := app.(*nsboxApp).usrdata 66 | 67 | if cmd.container == "" || cmd.container == "-" { 68 | ct, err = inventory.DefaultContainer(usrdata) 69 | if ct == nil { 70 | err = errors.New("no default container is set") 71 | } 72 | } else { 73 | ct, err = container.Open(usrdata, cmd.container) 74 | } 75 | 76 | if err != nil { 77 | return args.HandleError(err) 78 | } 79 | 80 | if err := daemon.RunContainerViaTransientUnit(ct, cmd.restart, usrdata); err != nil { 81 | return args.HandleError(err) 82 | } 83 | 84 | log.Debug("Container presumed to be ready, entering...") 85 | 86 | exitCode, err := session.EnterContainer(ct, cmd.command, usrdata, cmd.noReplay, app.(*nsboxApp).workdir) 87 | if err != nil { 88 | return args.HandleError(err) 89 | } 90 | 91 | os.Exit(exitCode) 92 | return subcommands.ExitSuccess // ? 93 | } 94 | -------------------------------------------------------------------------------- /cmd/nsbox/set_default.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/refi64/nsbox/internal/args" 12 | "github.com/refi64/nsbox/internal/inventory" 13 | ) 14 | 15 | type setDefaultCommand struct { 16 | newDefault string 17 | } 18 | 19 | func newSetDefaultCommand(app args.App) subcommands.Command { 20 | return args.WrapSimpleCommand(app, &setDefaultCommand{}) 21 | } 22 | 23 | func (*setDefaultCommand) Name() string { 24 | return "set-default" 25 | } 26 | 27 | func (*setDefaultCommand) Synopsis() string { 28 | return "set the default container" 29 | } 30 | 31 | func (*setDefaultCommand) Usage() string { 32 | return `set-default []: 33 | Set the defualt container to the value of default. If - or the empty string is given as the 34 | new default, the default container will be unset. 35 | ` 36 | } 37 | 38 | func (*setDefaultCommand) SetFlags(fs *flag.FlagSet) {} 39 | 40 | func (cmd *setDefaultCommand) ParsePositional(fs *flag.FlagSet) error { 41 | return args.ExpectArgs(fs, &cmd.newDefault) 42 | } 43 | 44 | func (cmd *setDefaultCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 45 | if cmd.newDefault == "-" { 46 | cmd.newDefault = "" 47 | } 48 | 49 | if err := inventory.SetDefaultContainer(app.(*nsboxApp).usrdata, cmd.newDefault); err != nil { 50 | return args.HandleError(err) 51 | } 52 | 53 | return subcommands.ExitSuccess 54 | } 55 | -------------------------------------------------------------------------------- /cmd/nsbox/version.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/google/subcommands" 11 | "github.com/refi64/nsbox/internal/args" 12 | "github.com/refi64/nsbox/internal/log" 13 | "github.com/refi64/nsbox/internal/release" 14 | ) 15 | 16 | type versionCommand struct { 17 | } 18 | 19 | func newVersionCommand(app args.App) subcommands.Command { 20 | return args.WrapSimpleCommand(app, &versionCommand{}) 21 | } 22 | 23 | func (*versionCommand) Name() string { 24 | return "version" 25 | } 26 | 27 | func (*versionCommand) Synopsis() string { 28 | return "show the nsbox version" 29 | } 30 | 31 | func (*versionCommand) Usage() string { 32 | return `version 33 | Show the current nsbox version. 34 | ` 35 | } 36 | 37 | func (*versionCommand) SetFlags(fs *flag.FlagSet) {} 38 | 39 | func (cmd *versionCommand) ParsePositional(fs *flag.FlagSet) error { 40 | return nil 41 | } 42 | 43 | func (cmd *versionCommand) Execute(app args.App, fs *flag.FlagSet) subcommands.ExitStatus { 44 | rel, err := release.Read() 45 | if err != nil { 46 | return args.HandleError(err) 47 | } 48 | 49 | log.Infof("%s (%v)", rel.Version, rel.Branch) 50 | 51 | return args.HandleError(err) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/nsboxd/main.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/refi64/nsbox/internal/container" 11 | "github.com/refi64/nsbox/internal/daemon" 12 | "github.com/refi64/nsbox/internal/log" 13 | "github.com/refi64/nsbox/internal/userdata" 14 | ) 15 | 16 | func main() { 17 | log.SetFlags(flag.CommandLine) 18 | flag.Parse() 19 | 20 | if flag.NArg() != 1 { 21 | log.Fatal("invalid arguments") 22 | } 23 | 24 | usrdata, err := userdata.BeneathSudo() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | ct, err := container.Open(usrdata, flag.Arg(0)) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | ct.ApplyEnvironFilter(usrdata) 35 | 36 | if err := daemon.RunContainerDirectNspawn(ct, usrdata); err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /data/getty-override.conf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Auto-login for the nsbox user. 6 | 7 | [Service] 8 | EnvironmentFile=/run/host/nsbox/shared-env 9 | 10 | # XXX: A temporary hack to make it not run shell init scripts, 11 | # until we do custom PAM auth later on. 12 | Environment=HOME=/tmp 13 | PrivateTmp=true 14 | 15 | ExecStart= 16 | ExecStart=-/sbin/agetty -o '-p -- \\u' --noclear --autologin $NSBOX_USER --keep-baud console 115200,38400,9600 $TERM 17 | -------------------------------------------------------------------------------- /data/nsbox-container.target: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [Unit] 6 | Description=nsbox container target 7 | Requires=default.target nsbox-init.service 8 | After=default.target nsbox-init.service 9 | AllowIsolate=yes 10 | -------------------------------------------------------------------------------- /data/nsbox-init.service: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [Unit] 6 | Description=Initialize the nsbox environment 7 | Wants=console-getty.service default.target 8 | After=console-getty.service default.target 9 | 10 | [Service] 11 | Type=notify 12 | ExecStart=/run/host/nsbox/scripts/nsbox-init.sh 13 | -------------------------------------------------------------------------------- /data/scripts/nsbox-apply-env.sh: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | while read _line; do 6 | export "$_line" 7 | done < /run/host/nsbox/shared-env 8 | -------------------------------------------------------------------------------- /data/scripts/nsbox-enter-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | set -e 8 | trap 'echo "$BASH_SOURCE:$LINENO: $BASH_COMMAND" failed, sorry.' ERR 9 | 10 | unset SUDO_COMMAND SUDO_USER SUDO_UID SUDO_GID 11 | . /run/host/nsbox/scripts/nsbox-apply-env.sh 12 | 13 | cwd="$1" 14 | shift 1 15 | 16 | if [[ -d "$cwd" ]]; then 17 | if [[ "$cwd" != */ ]]; then 18 | cwd="$cwd/" 19 | fi 20 | 21 | if [[ "$NSBOX_HOME_LINK_TARGET_ADJUST_CWD" == "1" \ 22 | && "$cwd" == "/$NSBOX_HOME_LINK_TARGET"/* ]]; then 23 | cwd="$NSBOX_HOME_LINK_NAME/${cwd#/$NSBOX_HOME_LINK_TARGET/}" 24 | fi 25 | 26 | cd "$cwd" 27 | fi 28 | 29 | if [[ -n "$NSBOX_BOOTED" ]]; then 30 | # Booted systems have their own XDG_RUNTIME_DIR, we need to symlink relevant stuff inside. 31 | 32 | if [[ -n "$WAYLAND_DISPLAY" && ! -e "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]]; then 33 | ln -sf "/run/host/nsbox/usr-run/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR" 34 | fi 35 | 36 | if [[ ! -e "$XDG_RUNTIME_DIR/pulse" ]]; then 37 | ln -sf "/run/host/nsbox/usr-run/pulse" "$XDG_RUNTIME_DIR" 38 | fi 39 | 40 | host_pipewire=/run/host/nsbox/usr-run/pipewire-0 41 | if [[ ! -e "$XDG_RUNTIME_DIR/pipewire-0" && -e "$host_pipewire" ]]; then 42 | ln -sf "$host_pipewire" "$XDG_RUNTIME_DIR" 43 | fi 44 | fi 45 | 46 | exec "$@" 47 | -------------------------------------------------------------------------------- /data/scripts/nsbox-enter-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | set -e 8 | trap 'echo "$BASH_SOURCE:$LINENO: $BASH_COMMAND" failed, sorry.' ERR 9 | 10 | . /run/host/nsbox/scripts/nsbox-apply-env.sh 11 | 12 | integrity_root=/var/lib/nsbox-container-state/ansible 13 | mkdir -p $integrity_root 14 | 15 | get_images_to_update() { 16 | while [[ $# -gt 0 ]]; do 17 | local image=$1 18 | local integrity_file=$integrity_root/$image.sha256 19 | 20 | # If the current image has no previous sha256s saved, or any of them have changed since last 21 | # time, then this image and all its dependents will have their playbooks re-run. 22 | if [[ ! -f "$integrity_file" ]] || ! sha256sum --status -c $integrity_file; then 23 | # Generate a new integrity file. 24 | find /run/host/nsbox/images/$image -type f | xargs sha256sum > $integrity_file.tmp 25 | # It will be renamed below if the ansible playbook runs successfully. 26 | 27 | echo "$@" 28 | return 29 | fi 30 | 31 | shift 32 | done 33 | } 34 | 35 | if [[ -z "$NSBOX_NO_REPLAY" ]]; then 36 | to_update="$(get_images_to_update $NSBOX_IMAGE_CHAIN)" 37 | if [[ -n "$to_update" ]]; then 38 | # XXX: duplicated from nsbox-bender.py. 39 | branch="$(cat /run/host/nsbox/release/BRANCH)" 40 | version="$(cat /run/host/nsbox/release/VERSION)" 41 | 42 | if [[ "$branch" == "edge" ]]; then 43 | product_name="nsbox-edge" 44 | else 45 | product_name="nsbox" 46 | fi 47 | 48 | extra_vars=() 49 | extra_vars+=(ansible_python_interpreter=/usr/bin/python3) 50 | extra_vars+=(nsbox_branch=$branch) 51 | extra_vars+=(nsbox_version=$version) 52 | extra_vars+=(nsbox_product_name=$product_name) 53 | 54 | for image in $to_update; do 55 | ANSIBLE_STDOUT_CALLBACK=default ansible-playbook \ 56 | --connection=local --inventory=localhost, --extra-vars "${extra_vars[*]}" --skip-tags bend \ 57 | /run/host/nsbox/images/$image/playbook.yaml 58 | # XXX: ugly duplication, but this is getting nuked in favor of a Python variant later on anyway 59 | mv $integrity_root/$image.sha256{.tmp,} 60 | done 61 | fi 62 | fi 63 | 64 | exec runuser -s /bin/bash -- - "$NSBOX_USER" /run/host/nsbox/scripts/nsbox-enter-run.sh "$@" 65 | -------------------------------------------------------------------------------- /data/scripts/nsbox-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | set -e 8 | trap 'echo "$BASH_SOURCE:$LINENO: $BASH_COMMAND" failed, sorry.; exit 1' ERR 9 | 10 | . /run/host/nsbox/scripts/nsbox-apply-env.sh 11 | 12 | user="$NSBOX_USER" 13 | uid="$NSBOX_UID" 14 | shell="$NSBOX_SHELL" 15 | 16 | rm -f /var/mail/"$user" 17 | 18 | if id "$user" &>/dev/null; then 19 | usermod -d "$NSBOX_HOME" -u "$uid" -s "$shell" "$user" >/dev/null 20 | else 21 | useradd -d "$NSBOX_HOME" -MU -u "$uid" -s "$shell" "$user" >/dev/null 22 | fi 23 | 24 | currently_can_sudo=$(id -Gnz "$user" | grep -Fqxz "$NSBOX_SUDO_GROUP" && echo 1 ||:) 25 | 26 | if [[ -n "$NSBOX_CAN_SUDO" && -z "$currently_can_sudo" ]]; then 27 | gpasswd -a "$user" "$NSBOX_SUDO_GROUP" >/dev/null 28 | elif [[ -z "$NSBOX_CAN_SUDO" && -n "$currently_can_sudo" ]]; then 29 | gpasswd -d "$user" "$NSBOX_SUDO_GROUP" >/dev/null 30 | fi 31 | 32 | if [[ -d /run/host/nsbox/mail ]]; then 33 | rm -f /var/mail/"$user" 34 | ln -s /run/host/nsbox/mail /var/mail/"$user" 35 | fi 36 | 37 | update=1 38 | 39 | # XXX: shadow file hacks suck, but the only real workarond is to define a custom 40 | # NSS module that asks the host, which...is not very fun. 41 | if [[ -f /run/host/nsbox/shadow-custom-pass ]]; then 42 | # https://stackoverflow.com/questions/407523/escape-a-string-for-a-sed-replace-pattern 43 | pass=$(sed -e 's/[\/&]/\\&/g' /run/host/nsbox/shadow-custom-pass) 44 | sed "s/^\($user\):[^:]*/\1:$pass/" /etc/shadow > /etc/shadow.x 45 | unset pass 46 | elif [[ -f /run/host/nsbox/shadow-entry ]]; then 47 | grep -v "^$user" /etc/shadow > /etc/shadow.x 48 | cat /run/host/nsbox/shadow-entry >> /etc/shadow.x 49 | else 50 | update= 51 | fi 52 | 53 | if [[ -n "$update" ]]; then 54 | rm -f /run/host/nsbox/shadow-{custom-pass,entry} 55 | mv /etc/shadow{.x,} 56 | chmod 000 /etc/shadow 57 | fi 58 | 59 | if [[ "$NSBOX_BOOTED" == "1" ]]; then 60 | hostnamectl set-hostname "$HOSTNAME" 61 | else 62 | echo "$HOSTNAME" > /etc/hostname 63 | fi 64 | 65 | ln -sf {/run/host,}/etc/locale.conf 66 | 67 | if [[ -n "$NSBOX_HOME_LINK_TARGET" ]]; then 68 | [[ -e /home ]] && rm -d /home ||: 69 | ln -s "$NSBOX_HOME_LINK_TARGET" /home 70 | fi 71 | 72 | if [[ -n "$NSBOX_BOOTED" ]]; then 73 | rm -f "$XDG_RUNTIME_DIR"/wayland-* 74 | fi 75 | 76 | mknod -m 666 /dev/fuse c 10 229 ||: 77 | 78 | NSBOX_INTERNAL=1 exec /run/host/nsbox/bin/nsbox-host service "$NSBOX_CONTAINER" 79 | -------------------------------------------------------------------------------- /data/wants-networkd.conf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Override on top of nsbox-container.target to start it after 6 | # networkd/resolved. 7 | 8 | [Unit] 9 | Wants=systemd-networkd.service systemd-resolved.service 10 | After=systemd-networkd.service systemd-resolved.service 11 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "nsbox-site", 4 | "public": "web/.vuepress/dist", 5 | "ignore": [ 6 | "firebase.json", 7 | "**/.*", 8 | "**/node_modules/**" 9 | ], 10 | "rewrites": [ 11 | { 12 | "source": "**", 13 | "destination": "/index.html" 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/refi64/nsbox 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 7 | github.com/artyom/untar v1.0.0 8 | github.com/briandowns/spinner v1.9.0 9 | github.com/coreos/go-systemd/v22 v22.0.0 10 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f 11 | github.com/creack/pty v1.1.9 12 | github.com/dustin/go-humanize v1.0.0 13 | github.com/godbus/dbus/v5 v5.0.3 14 | github.com/google/go-containerregistry v0.0.0-20200320200342-35f57d7d4930 15 | github.com/google/subcommands v1.2.0 16 | github.com/google/uuid v1.1.1 17 | github.com/opencontainers/selinux v1.4.0 18 | github.com/pkg/errors v0.9.1 19 | github.com/refi64/go-lxtempdir v0.0.0-20190815193640-e8f0a4e7825f 20 | github.com/varlink/go v0.3.0 21 | github.com/vishvananda/netlink v1.1.0 22 | golang.org/x/crypto v0.0.0-20200320181102-891825fb96df 23 | golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae 24 | ) 25 | -------------------------------------------------------------------------------- /images/BUILD.gn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import("images.gni") 6 | 7 | template("nsbox_image") { 8 | action(target_name) { 9 | pool = "//:console" 10 | 11 | forward_variables_from(invoker, [ "deps" ]) 12 | 13 | image_root = rebase_path(invoker.image, root_build_dir) 14 | 15 | script = "//utils/nsbox-bender.py" 16 | outputs = [ "$root_out_dir/images/$target_name.tar" ] 17 | args = [ 18 | "--force-color", 19 | "--export", 20 | rebase_path(outputs[0], root_build_dir), 21 | "--override-nsbox-version", 22 | release_version, 23 | "--override-nsbox-branch", 24 | release_branch, 25 | "--builder", 26 | image_builder, 27 | ] 28 | 29 | if (defined(invoker.tag) && invoker.tag != "") { 30 | args += [ "$image_root:${invoker.tag}" ] 31 | } else { 32 | args += [ image_root ] 33 | } 34 | 35 | sources = [ 36 | "${invoker.image}/metadata.json", 37 | "${invoker.image}/playbook.yaml", 38 | ] 39 | 40 | if (defined(invoker.image_files)) { 41 | foreach(file, invoker.image_files) { 42 | sources += [ "${invoker.image}/$file" ] 43 | } 44 | } 45 | 46 | if (defined(invoker.local) && invoker.local) { 47 | sources += [ "${invoker.image}/Dockerfile" ] 48 | } 49 | } 50 | } 51 | 52 | foreach(def, image_definitions) { 53 | versions = [] 54 | 55 | if (defined(def.versions)) { 56 | versions = def.versions 57 | } else { 58 | versions = [ "" ] 59 | } 60 | 61 | foreach(version, versions) { 62 | target_base = "${def.name}" 63 | if (version != "") { 64 | target_base += "-$version" 65 | } 66 | 67 | nsbox_image("$target_base-image") { 68 | image = def.name 69 | if (version != "") { 70 | tag = version 71 | } 72 | 73 | image_files = common_image_files 74 | 75 | if (defined(def.local) && def.local) { 76 | image_files += common_local_image_files 77 | } 78 | 79 | if (defined(def.extra_image_files)) { 80 | image_files += def.extra_image_files 81 | } 82 | } 83 | } 84 | } 85 | 86 | group("images") { 87 | deps = [] 88 | 89 | foreach(def, image_definitions) { 90 | if (defined(def.versions)) { 91 | foreach(version, def.versions) { 92 | deps += [ ":${def.name}-$version-image" ] 93 | } 94 | } else { 95 | deps += [ ":${def.name}-image" ] 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /images/arch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/archlinux 2 | RUN pacman -Syu --noconfirm base python3 3 | -------------------------------------------------------------------------------- /images/arch/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "@local", 3 | "remote": "registry.nsbox.dev/arch:{nsbox_branch}", 4 | "target": "gcr.io/nsbox-data/arch:{nsbox_branch}", 5 | "valid_tags": [] 6 | } 7 | -------------------------------------------------------------------------------- /images/arch/playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: false 3 | vars: 4 | ansible_bender: 5 | layering: false 6 | 7 | roles: 8 | - main 9 | -------------------------------------------------------------------------------- /images/arch/roles/main/files/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=nsbox-guest-tools 2 | pkgver=$NSBOX_VERSION 3 | pkgrel=1 4 | pkgdesc='The guest tools for nsbox-managed containers' 5 | arch=('any') 6 | license=('mpl-2.0') 7 | depends=(ansible grep inetutils sudo vte-common) 8 | source=(nsbox-trigger.hook) 9 | sha256sums=(SKIP) 10 | 11 | package() { 12 | install -Dm 644 nsbox-trigger.hook -t "${pkgdir}/usr/share/libalpm/hooks" 13 | mkdir -p "${pkgdir}/usr/bin" 14 | ln -s /run/host/nsbox/bin/nsbox-host "${pkgdir}/usr/bin/nsbox-host" 15 | } 16 | -------------------------------------------------------------------------------- /images/arch/roles/main/files/nsbox-trigger.hook: -------------------------------------------------------------------------------- 1 | [Trigger] 2 | Type = Package 3 | Operation = Install 4 | Operation = Upgrade 5 | Operation = Remove 6 | Target = usr/share/applications/*.desktop 7 | 8 | [Action] 9 | Description = Asking nsbox to reload the exports... 10 | When = PostTransaction 11 | Exec = /run/host/nsbox/bin/nsbox-host reload-exports 12 | -------------------------------------------------------------------------------- /images/arch/roles/main/tasks/build_guest_tools.yaml: -------------------------------------------------------------------------------- 1 | - include_vars: 2 | file: guest_tools.yaml 3 | name: guest_tools 4 | 5 | # We don't want to pull in the entire base-devel for makepkg, so just grab the absolute 6 | # bare necessities. 7 | - name: Install guest tools build requirements (this may take a while) 8 | pacman: 9 | name: 10 | - binutils 11 | - fakeroot 12 | 13 | - name: Remove the deprecated guest tools if present 14 | pacman: 15 | name: nsbox-edge-guest-tools 16 | state: absent 17 | 18 | - name: Check the available guest tools version 19 | shell: pacman -Qi nsbox-guest-tools | grep -Po '^Version\s+:\s+\K[^-]*' 20 | ignore_errors: true 21 | register: guest_tools_test 22 | args: 23 | warn: false 24 | 25 | - when: >- 26 | guest_tools_test.rc != 0 27 | or (guest_tools_test.stdout_lines | first) is version(guest_tools.min_version, '<') 28 | block: 29 | - name: Create a temporary build directory 30 | tempfile: 31 | state: directory 32 | prefix: nsbox 33 | register: buildroot 34 | 35 | - name: Copy the files for the guest tools 36 | copy: 37 | src: '{{ item }}' 38 | # Trailing / will create the intermediate directories as needed. 39 | dest: '{{ buildroot.path }}/' 40 | loop: 41 | - nsbox-trigger.hook 42 | - PKGBUILD 43 | 44 | - name: Build and install the guest tools (this may take a while) 45 | # XXX: Overriding EUID to trick makepkg into thinking we're not root is ugly but the 46 | # simplest solution 47 | shell: env EUID=1 NSBOX_VERSION={{ nsbox_version | quote }} makepkg -si --noconfirm 48 | args: 49 | chdir: '{{ buildroot.path }}' 50 | 51 | always: 52 | - file: 53 | path: '{{ buildroot.path }}' 54 | state: absent 55 | when: buildroot is defined 56 | -------------------------------------------------------------------------------- /images/arch/roles/main/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Update the package cache 2 | pacman: 3 | update_cache: yes 4 | tags: bend 5 | 6 | - import_tasks: build_guest_tools.yaml 7 | 8 | - name: Fix the setuid bit on sudo 9 | file: 10 | path: /usr/bin/sudo 11 | mode: 'u+s' 12 | 13 | - name: Allow wheel to sudo 14 | lineinfile: 15 | path: /etc/sudoers 16 | regexp: '# (%wheel ALL=\(ALL\) ALL)' 17 | line: '\1' 18 | backrefs: true 19 | 20 | - name: Preserve the package cache 21 | file: 22 | path: /usr/share/libalpm/hooks/package-cleanup.hook 23 | state: absent 24 | -------------------------------------------------------------------------------- /images/arch/roles/main/vars/guest_tools.yaml: -------------------------------------------------------------------------------- 1 | min_version: 20.07.06.282 2 | -------------------------------------------------------------------------------- /images/debian/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE_TAG 2 | FROM docker.io/debian:$IMAGE_TAG 3 | RUN apt update && apt install -y python3 python3-apt && apt clean && rm -rf /var/lib/apt/lists/* 4 | -------------------------------------------------------------------------------- /images/debian/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "@local", 3 | "remote": "registry.nsbox.dev/debian:{nsbox_branch}-{image_tag}", 4 | "target": "gcr.io/nsbox-data/debian:{nsbox_branch}-{image_tag}", 5 | "sudo_group": "sudo", 6 | "valid_tags": ["buster", "bullseye"] 7 | } 8 | -------------------------------------------------------------------------------- /images/debian/playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: false 3 | vars: 4 | ansible_bender: 5 | layering: false 6 | 7 | roles: 8 | - main 9 | -------------------------------------------------------------------------------- /images/debian/roles/main/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Delete the Docker-only apt config files 2 | file: 3 | path: '/etc/systemd/system/{{ item }}' 4 | state: absent 5 | loop: 6 | - docker-autoremove-suggests 7 | - docker-clean 8 | - docker-gzip-indexes 9 | - docker-no-languages 10 | 11 | - name: Delete the apt upgrade timers 12 | file: 13 | path: '/etc/systemd/system/timers.target.wants/' 14 | state: absent 15 | 16 | - name: Check the package cache 17 | find: 18 | paths: '/var/lib/apt/lists' 19 | register: package_cache 20 | 21 | - name: Update the package cache 22 | apt: 23 | force_apt_get: yes 24 | update_cache: yes 25 | when: '"bend" not in ansible_skip_tags or package_cache.matched|int == 0' 26 | 27 | - name: Install required packages (this may take a while) 28 | apt: 29 | force_apt_get: yes 30 | name: 31 | - ansible 32 | - hostname 33 | - man 34 | - libnss-myhostname 35 | - locales-all 36 | - sudo 37 | - systemd 38 | 39 | - name: Use nss-myhostname for hostname resolution before dns 40 | lineinfile: 41 | path: /etc/nsswitch.conf 42 | regexp: '^(hosts:\s+files)\s+(dns)(\s+myhostname)?$' 43 | line: '\1 myhostname \2' 44 | backrefs: true 45 | 46 | - name: Clear the apt cache 47 | shell: 'apt clean all && rm -rf /var/lib/apt/lists/*' 48 | args: 49 | warn: false 50 | tags: bend 51 | -------------------------------------------------------------------------------- /images/fedora/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "registry.fedoraproject.org/fedora:{image_tag}", 3 | "remote": "registry.nsbox.dev/fedora:{nsbox_branch}-{image_tag}", 4 | "target": "gcr.io/nsbox-data/fedora:{nsbox_branch}-{image_tag}", 5 | "valid_tags": ["32", "33", "34", "35"] 6 | } 7 | -------------------------------------------------------------------------------- /images/fedora/playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: false 3 | vars: 4 | ansible_bender: 5 | layering: false 6 | 7 | roles: 8 | - main 9 | -------------------------------------------------------------------------------- /images/fedora/roles/main/files/nsbox-guest-tools.spec: -------------------------------------------------------------------------------- 1 | Name: nsbox-guest-tools 2 | Version: %{nsbox_version} 3 | Release: 1 4 | Summary: Tools for nsbox host integration 5 | License: MPL-2.0 6 | URL: https://nsbox.dev/ 7 | BuildArch: noarch 8 | Provides: dnf-plugin-nsbox = %{version}-%{release} 9 | BuildRequires: python3-rpm-macros 10 | Requires: python3-dnf-plugins-core 11 | 12 | Requires: ansible 13 | Requires: findutils 14 | Requires: glibc-all-langpacks 15 | Requires: hostname 16 | Requires: man-pages 17 | Requires: nsbox-guest-tools 18 | Requires: iso-codes 19 | Requires: systemd 20 | Requires: sudo 21 | Requires: vte-profile 22 | 23 | Source0: nsbox_trigger.py 24 | 25 | %description 26 | Guest tools for nsbox containers that allow integration with the host system. 27 | 28 | %build 29 | cp %{SOURCE0} . 30 | %{__python3} -m compileall nsbox_trigger.py 31 | %{__python3} -O -m compileall nsbox_trigger.py 32 | 33 | %install 34 | install -Dm 644 -t %{buildroot}/%{python3_sitelib}/dnf-plugins %{_builddir}/nsbox_trigger.py 35 | install -Dm 644 -t %{buildroot}/%{python3_sitelib}/dnf-plugins/__pycache__ %{_builddir}/__pycache__/nsbox_trigger.*.pyc 36 | mkdir -p %{buildroot}/%{_bindir} 37 | ln -s /run/host/nsbox/bin/nsbox-host %{buildroot}/%{_bindir}/nsbox-host 38 | 39 | %files 40 | %{_bindir}/nsbox-host 41 | %{python3_sitelib}/dnf-plugins/nsbox_trigger.py 42 | %{python3_sitelib}/dnf-plugins/__pycache__/nsbox_trigger.*.pyc 43 | -------------------------------------------------------------------------------- /images/fedora/roles/main/files/nsbox_trigger.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Notifies the host to refresh the desktop files. 6 | 7 | from dnfpluginscore import logger 8 | import dnf 9 | import dnf.cli 10 | import subprocess 11 | 12 | 13 | class NsboxTrigger(dnf.Plugin): 14 | name = 'nsbox-trigger' 15 | 16 | def transaction(self): 17 | logger.debug('Notifying nsbox host of updates...') 18 | subprocess.run(['/run/host/nsbox/bin/nsbox-host', 'reload-exports']) 19 | -------------------------------------------------------------------------------- /images/fedora/roles/main/tasks/build_guest_tools.yaml: -------------------------------------------------------------------------------- 1 | - include_vars: 2 | file: guest_tools.yaml 3 | name: guest_tools 4 | 5 | - name: Remove the deprecated guest tools repository 6 | file: 7 | path: /etc/yum.repos.d/nsbox-edge.repo 8 | state: absent 9 | 10 | - name: Check for guest tools build requirements 11 | shell: "rpm -q {{ guest_tools.build_requires | join(' ') }}" 12 | ignore_errors: true 13 | register: build_requires_test 14 | args: 15 | warn: false 16 | 17 | - name: Install guest tools build requirements (this may take a while) 18 | when: build_requires_test.rc != 0 19 | dnf: 20 | name: '{{ guest_tools.build_requires }}' 21 | update_cache: true 22 | 23 | - name: Check the available guest tools version 24 | shell: "rpm -q --queryformat '%{VERSION}' nsbox-guest-tools" 25 | ignore_errors: true 26 | register: guest_tools_test 27 | args: 28 | warn: false 29 | 30 | - when: >- 31 | guest_tools_test.rc != 0 32 | or (guest_tools_test.stdout_lines | first) is version(guest_tools.min_version, '<') 33 | block: 34 | - name: Create a temporary build directory 35 | tempfile: 36 | state: directory 37 | prefix: nsbox 38 | register: rpm_topdir 39 | 40 | - set_fact: 41 | gpg_home: '{{ rpm_topdir.path }}/gpg' 42 | gpg_keygen: '{{ rpm_topdir.path }}/keygen' 43 | gpg_pubkey: '{{ rpm_topdir.path }}/pub.asc' 44 | 45 | - name: Create a GPG home directory 46 | file: 47 | path: '{{ gpg_home }}' 48 | state: directory 49 | mode: '0700' 50 | 51 | - name: Create the gpg script 52 | copy: 53 | dest: '{{ gpg_keygen }}' 54 | content: | 55 | Key-Type: 1 56 | Key-Length: 2048 57 | Name-Real: {{ guest_tools.gpg_name }} 58 | Name-Comment: Auto-generated by the image playbook 59 | Name-Email: {{ guest_tools.gpg_email }} 60 | Expire-Date: 0 61 | %no-protection 62 | %commit 63 | 64 | - name: Generate the gpg key 65 | shell: gpg --homedir {{ gpg_home }} --batch --generate-key {{ gpg_keygen }} 66 | 67 | - name: Export the gpg public key 68 | shell: > 69 | gpg 70 | --homedir {{ gpg_home }} 71 | --armor 72 | --output {{ gpg_pubkey }} 73 | --export '{{ guest_tools.gpg_email }}' 74 | 75 | - name: Copy the files for the guest tools 76 | copy: 77 | src: '{{ item }}' 78 | # Trailing / will create the intermediate directories as needed. 79 | dest: '{{ rpm_topdir.path }}/SOURCES/' 80 | loop: 81 | # XXX: Copying everything to SOURCES is a bit ugly, but it works... 82 | - nsbox-guest-tools.spec 83 | - nsbox_trigger.py 84 | 85 | - name: Build the guest tools 86 | shell: >- 87 | rpmbuild 88 | --define {{ ('nsbox_version ' + nsbox_version) | quote }} 89 | --define {{ ('_topdir ' + rpm_topdir.path) | quote }} 90 | -bb nsbox-guest-tools.spec 91 | args: 92 | chdir: '{{ rpm_topdir.path }}/SOURCES' 93 | 94 | - name: Locate the guest tools 95 | find: 96 | paths: '{{ rpm_topdir.path }}/RPMS/noarch' 97 | patterns: 'nsbox-guest-tools*.rpm' 98 | register: guest_tools_rpm 99 | 100 | - name: Sign the guest tools 101 | shell: >- 102 | rpm 103 | --define '_signature gpg' 104 | --define {{ ('_gpg_path ' + gpg_home) | quote }} 105 | --define {{ ('_gpg_name ' + guest_tools.gpg_name) | quote }} 106 | --addsign {{ (guest_tools_rpm.files | first).path }} 107 | args: 108 | warn: false 109 | 110 | - name: Install the gpg key 111 | rpm_key: 112 | state: present 113 | key: '{{ gpg_pubkey }}' 114 | 115 | - name: Install the guest tools (this may take a while) 116 | dnf: 117 | name: '{{ (guest_tools_rpm.files | first).path }}' 118 | state: present 119 | update_cache: true 120 | 121 | always: 122 | - rpm_key: 123 | state: absent 124 | key: '{{ gpg_pubkey }}' 125 | ignore_errors: true 126 | 127 | - file: 128 | state: absent 129 | path: '{{ rpm_topdir.path }}' 130 | when: rpm_topdir is defined 131 | -------------------------------------------------------------------------------- /images/fedora/roles/main/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Create tmpfiles.d's user config directory 2 | file: 3 | path: /etc/tmpfiles.d 4 | state: directory 5 | 6 | - name: Mask tmpfiles.d's selinux setup 7 | file: 8 | src: /dev/null 9 | dest: /etc/tmpfiles.d/selinux-policy.conf 10 | state: link 11 | 12 | - name: Require the documentation to be installed 13 | lineinfile: 14 | path: /etc/dnf/dnf.conf 15 | line: tsflags=nodocs 16 | state: absent 17 | 18 | - import_tasks: build_guest_tools.yaml 19 | 20 | - name: Clear the dnf cache 21 | shell: 'dnf clean all' 22 | args: 23 | warn: false 24 | tags: bend 25 | -------------------------------------------------------------------------------- /images/fedora/roles/main/vars/guest_tools.yaml: -------------------------------------------------------------------------------- 1 | gpg_name: nsbox-guest-tools 2 | gpg_email: fedora-guest-tools@nsbox.dev 3 | min_version: 20.07.06.282 4 | build_requires: 5 | - python3-rpm-macros 6 | - rpm-build 7 | - rpm-sign 8 | -------------------------------------------------------------------------------- /images/images.gni: -------------------------------------------------------------------------------- 1 | common_image_files = [ 2 | "metadata.json", 3 | "playbook.yaml", 4 | "roles/main/tasks/main.yaml", 5 | ] 6 | 7 | common_local_image_files = [ "Dockerfile" ] 8 | 9 | image_definitions = [ 10 | { 11 | name = "fedora" 12 | versions = [ 13 | "33", 14 | "34", 15 | "35", 16 | ] 17 | extra_image_files = [ 18 | "roles/main/files/nsbox_trigger.py", 19 | "roles/main/files/nsbox-guest-tools.spec", 20 | "roles/main/tasks/build_guest_tools.yaml", 21 | "roles/main/vars/guest_tools.yaml", 22 | ] 23 | }, 24 | { 25 | name = "debian" 26 | local = true 27 | versions = [ 28 | "buster", 29 | "bullseye", 30 | ] 31 | }, 32 | { 33 | name = "arch" 34 | local = true 35 | extra_image_files = [ 36 | "roles/main/files/nsbox-trigger.hook", 37 | "roles/main/files/PKGBUILD", 38 | "roles/main/tasks/build_guest_tools.yaml", 39 | "roles/main/vars/guest_tools.yaml", 40 | ] 41 | }, 42 | ] 43 | -------------------------------------------------------------------------------- /internal/args/args.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package args 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "os" 11 | 12 | "github.com/google/subcommands" 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/log" 15 | ) 16 | 17 | type App interface { 18 | PreexecHook(cmd subcommands.Command, fs *flag.FlagSet) 19 | SetGlobalFlags(fs *flag.FlagSet) 20 | } 21 | 22 | func setGlobalFlags(app App, fs *flag.FlagSet) { 23 | log.SetFlags(fs) 24 | app.SetGlobalFlags(fs) 25 | } 26 | 27 | // A wrapper over subcommands.Command with a slightly simplified API. 28 | type SimpleCommand interface { 29 | Name() string 30 | Synopsis() string 31 | Usage() string 32 | SetFlags(fs *flag.FlagSet) 33 | ParsePositional(fs *flag.FlagSet) error 34 | Execute(app App, fs *flag.FlagSet) subcommands.ExitStatus 35 | } 36 | 37 | type simpleCommandWrapper struct { 38 | app App 39 | simple SimpleCommand 40 | } 41 | 42 | func WrapSimpleCommand(app App, simple SimpleCommand) subcommands.Command { 43 | return &simpleCommandWrapper{app, simple} 44 | } 45 | 46 | func (wrapper *simpleCommandWrapper) Name() string { 47 | return wrapper.simple.Name() 48 | } 49 | 50 | func (wrapper *simpleCommandWrapper) Synopsis() string { 51 | return wrapper.simple.Synopsis() 52 | } 53 | 54 | func (wrapper *simpleCommandWrapper) Usage() string { 55 | return wrapper.simple.Usage() 56 | } 57 | 58 | func (wrapper *simpleCommandWrapper) SetFlags(fs *flag.FlagSet) { 59 | setGlobalFlags(wrapper.app, fs) 60 | wrapper.simple.SetFlags(fs) 61 | } 62 | 63 | func (wrapper *simpleCommandWrapper) Execute(_ context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 64 | if err := wrapper.simple.ParsePositional(fs); err != nil { 65 | log.Alert(err) 66 | fs.Usage() 67 | return subcommands.ExitUsageError 68 | } 69 | 70 | wrapper.app.PreexecHook(wrapper, fs) 71 | return wrapper.simple.Execute(wrapper.app, fs) 72 | } 73 | 74 | func ExpectArgs(fs *flag.FlagSet, args ...*string) error { 75 | if fs.NArg() != len(args) { 76 | return errors.Errorf("expected %d arg(s), got %d", len(args), fs.NArg()) 77 | } 78 | 79 | for i, arg := range fs.Args() { 80 | *args[i] = arg 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func HandleError(err error) subcommands.ExitStatus { 87 | if err != nil { 88 | log.Alert(err) 89 | return subcommands.ExitFailure 90 | } 91 | 92 | return subcommands.ExitSuccess 93 | } 94 | 95 | func Execute(app App) { 96 | setGlobalFlags(app, flag.CommandLine) 97 | 98 | flag.Parse() 99 | 100 | ctx := context.Background() 101 | os.Exit(int(subcommands.Execute(ctx))) 102 | } 103 | -------------------------------------------------------------------------------- /internal/args/array.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package args 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/refi64/nsbox/internal/log" 12 | ) 13 | 14 | type arrayTransformKind int 15 | 16 | const ( 17 | arrayTransformAdd arrayTransformKind = iota 18 | arrayTransformDel 19 | arrayTransformSet 20 | ) 21 | 22 | var ( 23 | arrayTransformKindToChar = map[arrayTransformKind]byte{ 24 | arrayTransformAdd: '+', 25 | arrayTransformDel: '-', 26 | arrayTransformSet: ':', 27 | } 28 | 29 | charToArrayTransformKind = map[byte]arrayTransformKind{ 30 | '+': arrayTransformAdd, 31 | '-': arrayTransformDel, 32 | ':': arrayTransformSet, 33 | } 34 | ) 35 | 36 | type ArrayTransformValue struct { 37 | kind arrayTransformKind 38 | items []string 39 | } 40 | 41 | func (value ArrayTransformValue) String() string { 42 | return string(arrayTransformKindToChar[value.kind]) + strings.Join(value.items, ",") 43 | } 44 | 45 | func (value *ArrayTransformValue) Set(arg string) error { 46 | if len(arg) == 0 { 47 | return nil 48 | } 49 | 50 | kind, ok := charToArrayTransformKind[arg[0]] 51 | if !ok { 52 | return errors.New("invalid array transform") 53 | } 54 | 55 | value.kind = kind 56 | 57 | if len(arg) == 1 { 58 | return nil 59 | } 60 | 61 | parts := strings.Split(arg[1:], ",") 62 | for _, part := range parts { 63 | if len(part) == 0 { 64 | return errors.New("items must not be empty") 65 | } 66 | } 67 | 68 | value.items = parts 69 | return nil 70 | } 71 | 72 | // Converts a slice of values to a map of keys to nil. 73 | func sliceToMap(items []string) map[string]interface{} { 74 | result := map[string]interface{}{} 75 | 76 | for _, item := range items { 77 | result[item] = nil 78 | } 79 | 80 | return result 81 | } 82 | 83 | func (value ArrayTransformValue) Apply(target *[]string) { 84 | switch value.kind { 85 | case arrayTransformAdd: 86 | // Map of present items to nil (to use like a set). 87 | presentItems := sliceToMap(*target) 88 | 89 | for _, item := range value.items { 90 | _, found := presentItems[item] 91 | 92 | if !found { 93 | *target = append(*target, item) 94 | } else { 95 | log.Alertf("item %s was already present", item) 96 | } 97 | } 98 | 99 | case arrayTransformDel: 100 | givenItems := sliceToMap(value.items) 101 | newTarget := []string{} 102 | 103 | for _, item := range *target { 104 | _, found := givenItems[item] 105 | if !found { 106 | newTarget = append(newTarget, item) 107 | } 108 | } 109 | 110 | *target = newTarget 111 | 112 | case arrayTransformSet: 113 | *target = value.items 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /internal/config/host_config.template.go: -------------------------------------------------------------------------------- 1 | // This file was auto-generated. DO NOT EDIT. 2 | // Use 'gn args' to change the directories there instead. 3 | 4 | package config 5 | 6 | const BinDir = "@BIN_DIR" 7 | const ConfigDir = "@CONFIG_DIR" 8 | const LibexecDir = "@LIBEXEC_DIR" 9 | const ShareDir = "@SHARE_DIR" 10 | const StateDir = "@STATE_DIR" 11 | 12 | const ProductName = "@PRODUCT_NAME" 13 | 14 | const EnableSudo = "@ENABLE_SUDO" == "true" 15 | const EnableCvtsudoers = "@ENABLE_CVTSUDOERS" == "true" 16 | const SudoGroup = "@SUDO_GROUP" 17 | -------------------------------------------------------------------------------- /internal/container/info.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package container 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "strings" 11 | "text/tabwriter" 12 | "time" 13 | 14 | "github.com/coreos/go-systemd/v22/dbus" 15 | "github.com/coreos/go-systemd/v22/machine1" 16 | "github.com/dustin/go-humanize" 17 | "github.com/refi64/nsbox/internal/log" 18 | "github.com/refi64/nsbox/internal/userdata" 19 | ) 20 | 21 | func boolYesNo(value bool) string { 22 | if value { 23 | return "yes" 24 | } else { 25 | return "no" 26 | } 27 | } 28 | 29 | func (ct Container) ShowInfo(usrdata *userdata.Userdata) error { 30 | systemd, err := dbus.New() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | machined, err := machine1.New() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | machineName := ct.MachineName(usrdata) 41 | 42 | unitMemory, err := systemd.GetServiceProperty(fmt.Sprintf("nsbox-%s.service", machineName), "MemoryCurrent") 43 | if err != nil { 44 | log.Debug("failed to get unit MemoryCurrent:", err) 45 | } 46 | 47 | machineProps, err := machined.DescribeMachine(machineName) 48 | if err != nil { 49 | log.Debug("failed to describe machine:", err) 50 | } 51 | 52 | writer := tabwriter.NewWriter(os.Stdout, 0, 2, 1, ' ', tabwriter.AlignRight) 53 | defer writer.Flush() 54 | 55 | fmt.Fprintln(writer, "Name:\t", ct.Name) 56 | fmt.Fprintln(writer, "Booted:\t", boolYesNo(ct.Config.Boot)) 57 | 58 | fmt.Fprintln(writer, "Shares cgroups:\t", boolYesNo(ct.Config.ShareCgroupfs)) 59 | fmt.Fprintln(writer, "Virtual network:\t", boolYesNo(ct.Config.VirtualNetwork)) 60 | 61 | fmt.Fprintln(writer, "Shared devices:\t", strings.Join(ct.Config.ShareDevices, ", ")) 62 | 63 | fmt.Fprintln(writer, "XDG desktop exports:\t", strings.Join(ct.Config.XdgDesktopExports, ", ")) 64 | fmt.Fprintln(writer, "XDG desktop extra:\t", strings.Join(ct.Config.XdgDesktopExtra, ", ")) 65 | 66 | if machineProps != nil { 67 | usec := machineProps["Timestamp"].(uint64) 68 | tm := time.Unix(int64(usec)/int64(time.Second/time.Microsecond), 0) 69 | fmt.Fprintf(writer, "Running:\t since %s (%s)\n", tm.Format(time.RFC1123), humanize.Time(tm)) 70 | } else { 71 | fmt.Fprintln(writer, "Running:\t no") 72 | } 73 | 74 | if unitMemory != nil { 75 | memory := unitMemory.Value.Value().(uint64) 76 | fmt.Fprintln(writer, "Memory:\t", humanize.Bytes(memory)) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func OpenAndShowInfo(usrdata *userdata.Userdata, name string) error { 83 | ct, err := Open(usrdata, name) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return ct.ShowInfo(usrdata) 89 | } 90 | -------------------------------------------------------------------------------- /internal/gtkicons/gtkicons.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // This ugly thing tries to dynamically load GTK+ and have it find icons for exports. 6 | 7 | package gtkicons 8 | 9 | import ( 10 | "unsafe" 11 | 12 | "github.com/coreos/pkg/dlopen" 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/log" 15 | ) 16 | 17 | // #include "nsbox-gtkicons.h" 18 | // #include 19 | import "C" 20 | 21 | var ( 22 | gtkHandle *dlopen.LibHandle 23 | 24 | newSym unsafe.Pointer 25 | unrefSym unsafe.Pointer 26 | setSearchPathSym unsafe.Pointer 27 | getIconSizesSym unsafe.Pointer 28 | lookupIconSym unsafe.Pointer 29 | getFilenameSym unsafe.Pointer 30 | ) 31 | 32 | type LookupContext struct { 33 | iconTheme *C.GtkIconTheme 34 | Path string 35 | } 36 | 37 | type Icon struct { 38 | Root string 39 | Path string 40 | Size int 41 | } 42 | 43 | func loadGtk() error { 44 | gtk, err := dlopen.GetHandle([]string{"libgtk-3.so", "libgtk-3.so.0"}) 45 | if err != nil { 46 | return errors.Wrap(err, "failed to open gtk3") 47 | } 48 | 49 | // We never bother to close the handle, because this will all be freed when the program dies. 50 | 51 | symbols := map[string]*unsafe.Pointer{ 52 | "gtk_icon_theme_new": &newSym, 53 | "g_object_unref": &unrefSym, 54 | "gtk_icon_theme_set_search_path": &setSearchPathSym, 55 | "gtk_icon_theme_get_icon_sizes": &getIconSizesSym, 56 | "gtk_icon_theme_lookup_icon": &lookupIconSym, 57 | "gtk_icon_info_get_filename": &getFilenameSym, 58 | } 59 | 60 | for name, target := range symbols { 61 | *target, err = gtk.GetSymbolPointer(name) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | 67 | gtkHandle = gtk 68 | return nil 69 | } 70 | 71 | func CreateContext(path string) (*LookupContext, error) { 72 | if gtkHandle == nil { 73 | if err := loadGtk(); err != nil { 74 | return nil, errors.Wrap(err, "failed to load gtk3") 75 | } 76 | } 77 | 78 | var ctx LookupContext 79 | ctx.Path = path 80 | 81 | ctx.iconTheme = C.nsbox_gtk_icon_theme_new(newSym) 82 | 83 | cpath := C.CString(path) 84 | defer C.free(unsafe.Pointer(cpath)) 85 | C.nsbox_gtk_icon_theme_set_search_path(setSearchPathSym, ctx.iconTheme, cpath) 86 | 87 | return &ctx, nil 88 | } 89 | 90 | func (ctx *LookupContext) Destroy() { 91 | C.nsbox_g_object_unref(unrefSym, unsafe.Pointer(ctx.iconTheme)) 92 | } 93 | 94 | func (ctx *LookupContext) lookupIconBySize(icon string, cicon *C.char, size int) (info Icon, err error) { 95 | iconInfo := C.nsbox_gtk_icon_theme_lookup_icon(lookupIconSym, ctx.iconTheme, cicon, C.int(size), 0) 96 | if iconInfo == nil { 97 | err = errors.Errorf("Could not find %s@%d", icon, size) 98 | return 99 | } 100 | 101 | defer C.nsbox_g_object_unref(unrefSym, unsafe.Pointer(iconInfo)) 102 | 103 | cpath := C.nsbox_gtk_icon_info_get_filename(getFilenameSym, iconInfo) 104 | path := C.GoString(cpath) 105 | info = Icon{ctx.Path, path, size} 106 | return 107 | } 108 | 109 | func (ctx *LookupContext) FindIcon(icon string) []Icon { 110 | var result []Icon 111 | 112 | cicon := C.CString(icon) 113 | defer C.free(unsafe.Pointer(cicon)) 114 | 115 | iconSizes := C.nsbox_gtk_icon_theme_get_icon_sizes(getIconSizesSym, ctx.iconTheme, cicon) 116 | defer C.free(unsafe.Pointer(iconSizes)) 117 | 118 | if *iconSizes == 0 { 119 | // If the icon is in pixmaps, it will not be found by get_icon_sizes. 120 | // Therefore, try to look it up manually. 121 | info, err := ctx.lookupIconBySize(icon, cicon, 0) 122 | if err == nil { 123 | result = append(result, info) 124 | } 125 | } 126 | 127 | for *iconSizes != 0 { 128 | info, err := ctx.lookupIconBySize(icon, cicon, int(*iconSizes)) 129 | if err != nil { 130 | log.Alert(err) 131 | } else { 132 | result = append(result, info) 133 | } 134 | 135 | iconSizes = (*C.int)(unsafe.Pointer(uintptr(unsafe.Pointer(iconSizes)) + unsafe.Sizeof(*iconSizes))) 136 | } 137 | 138 | return result 139 | } 140 | -------------------------------------------------------------------------------- /internal/gtkicons/nsbox-gtkicons.c: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #include "nsbox-gtkicons.h" 6 | 7 | GtkIconTheme *nsbox_gtk_icon_theme_new(void *actual_func) { 8 | typedef GtkIconTheme *(*gtk_icon_theme_new_type)(void); 9 | return ((gtk_icon_theme_new_type)actual_func)(); 10 | } 11 | 12 | void nsbox_g_object_unref(void *actual_func, void *object) { 13 | typedef void (*g_object_unref_type)(void *); 14 | ((g_object_unref_type)actual_func)(object); 15 | } 16 | 17 | void nsbox_gtk_icon_theme_set_search_path(void *actual_func, GtkIconTheme *icon_theme, 18 | const gchar *path) { 19 | typedef void (*gtk_icon_theme_set_search_path_type)(GtkIconTheme *, const gchar **, 20 | gint); 21 | ((gtk_icon_theme_set_search_path_type)actual_func)(icon_theme, &path, 1); 22 | } 23 | 24 | gint *nsbox_gtk_icon_theme_get_icon_sizes(void *actual_func, GtkIconTheme *icon_theme, 25 | const gchar *icon_name) { 26 | typedef gint *(*gtk_icon_theme_get_icon_sizes_type)(GtkIconTheme *, const gchar *); 27 | return ((gtk_icon_theme_get_icon_sizes_type)actual_func)(icon_theme, icon_name); 28 | } 29 | 30 | GtkIconInfo *nsbox_gtk_icon_theme_lookup_icon(void *actual_func, GtkIconTheme *icon_theme, 31 | const gchar *icon_name, gint size, 32 | int flags) { 33 | typedef GtkIconInfo *(*gtk_icon_theme_lookup_icon_type)(GtkIconTheme *, const gchar *, 34 | gint, int); 35 | return ((gtk_icon_theme_lookup_icon_type)actual_func)(icon_theme, icon_name, size, 36 | flags); 37 | } 38 | 39 | const gchar *nsbox_gtk_icon_info_get_filename(void *actual_func, GtkIconInfo *icon_info) { 40 | typedef const gchar *(*gtk_icon_info_get_filename_type)(GtkIconInfo *); 41 | return ((gtk_icon_info_get_filename_type)actual_func)(icon_info); 42 | } 43 | -------------------------------------------------------------------------------- /internal/gtkicons/nsbox-gtkicons.h: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | #pragma once 6 | 7 | typedef struct GtkIconTheme GtkIconTheme; 8 | typedef struct GtkIconInfo GtkIconInfo; 9 | typedef char gchar; 10 | typedef int gint; 11 | 12 | GtkIconTheme *nsbox_gtk_icon_theme_new(void *actual_func); 13 | 14 | void nsbox_g_object_unref(void *actual_func, void *object); 15 | 16 | // Unlike the others here, this is not an identical function signature, because we only 17 | // ever pass *one* path from Go land and it would be more difficult to try to pass the 18 | // actual array of strings vs passing one string and having C land use that as an array. 19 | void nsbox_gtk_icon_theme_set_search_path(void *actual_func, GtkIconTheme *icon_theme, 20 | const gchar *path); 21 | 22 | gint *nsbox_gtk_icon_theme_get_icon_sizes(void *actual_func, GtkIconTheme *icon_theme, 23 | const gchar *icon_name); 24 | 25 | GtkIconInfo *nsbox_gtk_icon_theme_lookup_icon(void *actual_func, GtkIconTheme *icon_theme, 26 | const gchar *icon_name, gint size, 27 | int flags); 28 | 29 | const gchar *nsbox_gtk_icon_info_get_filename(void *actual_func, GtkIconInfo *icon_info); 30 | -------------------------------------------------------------------------------- /internal/inventory/inventory.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package inventory 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/container" 15 | "github.com/refi64/nsbox/internal/log" 16 | "github.com/refi64/nsbox/internal/paths" 17 | "github.com/refi64/nsbox/internal/userdata" 18 | ) 19 | 20 | func List(usrdata *userdata.Userdata) ([]*container.Container, error) { 21 | containers := []*container.Container{} 22 | 23 | inventory := paths.ContainerInventory(usrdata) 24 | items, err := ioutil.ReadDir(inventory) 25 | if err != nil { 26 | if os.IsNotExist(err) { 27 | log.Debug("container directory does not exist") 28 | return containers, nil 29 | } 30 | 31 | return nil, errors.Wrap(err, "failed to read container inventory") 32 | } 33 | 34 | for _, item := range items { 35 | if strings.HasSuffix(item.Name(), container.StageSuffix) { 36 | log.Debug("skipping item ", item.Name()) 37 | continue 38 | } 39 | 40 | stat, err := os.Stat(filepath.Join(inventory, item.Name())) 41 | if err != nil { 42 | return nil, errors.Wrapf(err, "failed to stat %s", item.Name()) 43 | } 44 | 45 | if stat.Mode().IsDir() { 46 | ct, err := container.Open(usrdata, item.Name()) 47 | if err != nil { 48 | log.Alertf("WARNING: failed to open %s: %v", item.Name(), err) 49 | continue 50 | } 51 | 52 | containers = append(containers, ct) 53 | } else { 54 | log.Debug("skipping non-file ", item.Name()) 55 | } 56 | } 57 | 58 | return containers, nil 59 | } 60 | 61 | func DefaultContainer(usrdata *userdata.Userdata) (*container.Container, error) { 62 | path := paths.ContainerDefault(usrdata) 63 | 64 | if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { 65 | return nil, nil 66 | } 67 | 68 | target, err := os.Readlink(path) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return container.OpenPath(path, filepath.Base(target)) 74 | } 75 | 76 | func SetDefaultContainer(usrdata *userdata.Userdata, name string) error { 77 | if name != "" && name != "-" { 78 | ct, err := container.Open(usrdata, name) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | defaultPath := paths.ContainerDefault(usrdata) 84 | defaultTmp := defaultPath + ".tmp" 85 | 86 | if err := os.Remove(defaultTmp); err != nil && !os.IsNotExist(err) { 87 | return errors.Wrap(err, "failed to unlink old temporary default link") 88 | } 89 | 90 | if err := os.Symlink(ct.Path, defaultTmp); err != nil { 91 | return errors.Wrap(err, "failed to symlink new temporary default container") 92 | } 93 | 94 | if err := os.Rename(defaultTmp, defaultPath); err != nil { 95 | return errors.Wrap(err, "failed to rename temporary link") 96 | } 97 | } else { 98 | if err := os.Remove(paths.ContainerDefault(usrdata)); err != nil && !os.IsNotExist(err) { 99 | return errors.Wrap(err, "failed to unlink old default container") 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/kill/kill.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package kill 6 | 7 | import ( 8 | "os" 9 | "strings" 10 | 11 | "github.com/coreos/go-systemd/v22/machine1" 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/container" 14 | "github.com/refi64/nsbox/internal/log" 15 | "github.com/refi64/nsbox/internal/userdata" 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // Package unix doesn't provide SIGRTMIN. 20 | 21 | // #include 22 | import "C" 23 | 24 | type Signal unix.Signal 25 | 26 | var ( 27 | // systemd sends SIGRTMIN + 4 to signify poweroff and SIGINT for reboot. 28 | SigPoweroff = Signal(C.SIGRTMIN + 4) 29 | SigKill = Signal(unix.SIGKILL) 30 | 31 | sigToString = map[Signal]string{ 32 | SigPoweroff: "poweroff", 33 | SigKill: "sigkill", 34 | } 35 | 36 | stringToSig = map[string]Signal{ 37 | "poweroff": SigPoweroff, 38 | "kill": SigKill, 39 | "sigkill": SigKill, 40 | } 41 | ) 42 | 43 | func (sig Signal) String() string { 44 | return sigToString[sig] 45 | } 46 | 47 | func (sig *Signal) Set(value string) error { 48 | newSig, ok := stringToSig[strings.ToLower(value)] 49 | if !ok { 50 | return errors.New("does not exist") 51 | } 52 | 53 | *sig = newSig 54 | return nil 55 | } 56 | 57 | func KillContainer(usrdata *userdata.Userdata, ct *container.Container, signal Signal, all bool) error { 58 | if ct.Config.Boot && all { 59 | return errors.New("-a/--all is not supported for booted containers") 60 | } 61 | 62 | machined, err := machine1.New() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | log.Debugf("sending signal %d to %s", int(signal), ct.Name) 68 | 69 | machineName := ct.MachineName(usrdata) 70 | 71 | // machined's SELinux policies don't allow us to ask it to kill the leader process of a 72 | // container that machined didn't start. However, when "all" is given, machined just 73 | // forwards the kill request to systemd, which can kill any cgroup associated with a 74 | // systemd service. That means for "all", we can just forward the request to machined, 75 | // but otherwise, we need to send the kill signal ourselves. 76 | 77 | if all { 78 | if err := machined.KillMachine(machineName, "all", unix.Signal(signal)); err != nil { 79 | return errors.Wrap(err, "failed to ask machined to kill container") 80 | } 81 | } else { 82 | props, err := machined.DescribeMachine(machineName) 83 | if err != nil { 84 | return errors.Wrap(err, "failed to describe machine") 85 | } 86 | 87 | leader, err := os.FindProcess(int(props["Leader"].(uint32))) 88 | if err != nil { 89 | return errors.Wrap(err, "failed to find leader process") 90 | } 91 | 92 | log.Debug("leader process is", leader.Pid) 93 | 94 | if err := leader.Signal(unix.Signal(signal)); err != nil { 95 | return errors.Wrap(err, "failed to signal leader process") 96 | } 97 | } 98 | 99 | lock, err := ct.Lock(container.RunLock, container.WaitForLock) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | lock.Release() 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package log 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | // Why not use another logger? Well, we have to specific requirements we want to settle: 14 | // - We don't want a timestamp prefix, since logs will ever only go to the CLI (where the prefix 15 | // is insignificant) or to the journal (where timestamps are already added). 16 | // - We want basic leveled logs. 17 | // - We *don't* need overly fancy functionality. 18 | // glog doesn't allow disabling the timestamps, logrus outputs weird stuff when logging to a 19 | // non-TTY (the journal), etc. 20 | 21 | var verbose bool 22 | 23 | func SetFlags(fs *flag.FlagSet) { 24 | fs.BoolVar(&verbose, "v", verbose, "Be verbose") 25 | } 26 | 27 | func Verbose() bool { 28 | return verbose 29 | } 30 | 31 | func SetVerbose(newVerbose bool) { 32 | verbose = newVerbose 33 | } 34 | 35 | func Info(args ...interface{}) { 36 | fmt.Println(args...) 37 | } 38 | 39 | func Infof(format string, args ...interface{}) { 40 | fmt.Printf(format+"\n", args...) 41 | } 42 | 43 | func Debug(args ...interface{}) { 44 | if verbose { 45 | Alert(args...) 46 | } 47 | } 48 | 49 | func Debugf(format string, args ...interface{}) { 50 | if verbose { 51 | Alertf(format, args...) 52 | } 53 | } 54 | 55 | func Alert(args ...interface{}) { 56 | fmt.Fprintln(os.Stderr, args...) 57 | } 58 | 59 | func Alertf(format string, args ...interface{}) { 60 | fmt.Fprintf(os.Stderr, format+"\n", args...) 61 | } 62 | 63 | func Fatal(args ...interface{}) { 64 | Alert(args...) 65 | os.Exit(1) 66 | } 67 | 68 | func Fatalf(format string, args ...interface{}) { 69 | Alertf(format, args...) 70 | os.Exit(1) 71 | } 72 | -------------------------------------------------------------------------------- /internal/network/firewalld.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package network 6 | 7 | import ( 8 | godbus "github.com/godbus/dbus/v5" 9 | "github.com/refi64/nsbox/internal/log" 10 | ) 11 | 12 | type firewalld struct { 13 | systemBus *godbus.Conn 14 | firewalld godbus.BusObject 15 | } 16 | 17 | const ( 18 | firewalldService = "org.fedoraproject.FirewallD1" 19 | firewalldManagerObject = "/org/fedoraproject/FirewallD1" 20 | 21 | firewalldManagerIface = "org.fedoraproject.FirewallD1" 22 | firewalldZoneIface = "org.fedoraproject.FirewallD1.zone" 23 | 24 | firewalldManagerVersionProp = firewalldManagerIface + ".version" 25 | firewalldZoneChangeInterfaceMethod = firewalldZoneIface + ".changeZoneOfInterface" 26 | firewalldZoneRemoveInterfaceMethod = firewalldZoneIface + ".removeInterface" 27 | 28 | defaultCallFlags = godbus.FlagNoAutoStart 29 | 30 | trustedZone = "trusted" 31 | ) 32 | 33 | func newFirewalld() *firewalld { 34 | systemBus, err := godbus.SystemBus() 35 | if err != nil { 36 | log.Debug("Failed to acquire system bus:", err) 37 | return nil 38 | } 39 | 40 | object := systemBus.Object(firewalldService, firewalldManagerObject) 41 | if _, err := object.GetProperty(firewalldManagerVersionProp); err != nil { 42 | log.Debug("Failed to get firewalld version:", err) 43 | return nil 44 | } 45 | 46 | // firewalld is now confirmed present. 47 | return &firewalld{systemBus: systemBus, firewalld: object} 48 | } 49 | 50 | func (fw *firewalld) TrustInterface(iface string) error { 51 | call := fw.firewalld.Call(firewalldZoneChangeInterfaceMethod, defaultCallFlags, 52 | trustedZone, iface) 53 | return call.Err 54 | } 55 | 56 | func (fw *firewalld) UntrustInterface(iface string) error { 57 | call := fw.firewalld.Call(firewalldZoneRemoveInterfaceMethod, defaultCallFlags, 58 | trustedZone, iface) 59 | return call.Err 60 | } 61 | 62 | func (fw *firewalld) Close() error { 63 | if err := fw.systemBus.Close(); err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/network/network.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package network 6 | 7 | import ( 8 | "math/rand" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/vishvananda/netlink" 12 | ) 13 | 14 | type Firewall interface { 15 | TrustInterface(string) error 16 | UntrustInterface(string) error 17 | Close() error 18 | } 19 | 20 | func GetFirewall() Firewall { 21 | if fw := newFirewalld(); fw != nil { 22 | return fw 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // Short prefix, because otherwise the name will be too long (IFNAMSIZ is only 16). 29 | const nsboxPrefix = "nx-" 30 | 31 | func GenerateUniqueLinkName(base string, assumedPrefix string) (string, error) { 32 | // XXX: This is a tad racy, as it's technically possible someone else 33 | // might claim this link name right before we do. 34 | // The "algorithm" is also stupid as heck, but it should generally work. 35 | 36 | links, err := netlink.LinkList() 37 | if err != nil { 38 | return "", errors.Wrap(err, "getting netlink list") 39 | } 40 | 41 | linkNames := map[string]interface{}{} 42 | for _, link := range links { 43 | linkNames[link.Attrs().Name] = nil 44 | } 45 | 46 | maxBaseLength := netlink.IFNAMSIZ - len(assumedPrefix) - len(nsboxPrefix) - 1 47 | 48 | name := nsboxPrefix 49 | if len(base) > maxBaseLength { 50 | name += base[:maxBaseLength] 51 | } else { 52 | name += base 53 | } 54 | 55 | unique := false 56 | 57 | for i := len(name) - 1; i >= len(nsboxPrefix); i-- { 58 | if _, exists := linkNames[name]; exists { 59 | randLetter := rune(rand.Intn(26) + 'a') 60 | name = name[:i] + string(randLetter) + name[i+1:] 61 | } else { 62 | unique = true 63 | break 64 | } 65 | } 66 | 67 | if !unique { 68 | return "", errors.Errorf("could not generate unique interface name for %s", base) 69 | } 70 | 71 | return name, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/paths/paths.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package paths 6 | 7 | // NOTE: several of the below variables are set in host_paths.go, which is generated by the 8 | // build scripts (see BUILD.gn in the root directory). 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/refi64/nsbox/internal/config" 16 | "github.com/refi64/nsbox/internal/userdata" 17 | ) 18 | 19 | const InContainerPrivPath = "/var/lib/.nsbox-priv" 20 | const HostServiceSocketName = "host-service.sock" 21 | const PtyServiceSocketName = "pty-service.sock" 22 | const StorageRoot = config.StateDir + "/nsbox" 23 | 24 | func ContainerDefault(usrdata *userdata.Userdata) string { 25 | return filepath.Join(StorageRoot, usrdata.User.Username, "default") 26 | } 27 | 28 | func ContainerInventory(usrdata *userdata.Userdata) string { 29 | return filepath.Join(StorageRoot, usrdata.User.Username, "inventory") 30 | } 31 | 32 | func ContainerData(usrdata *userdata.Userdata, name string) string { 33 | return filepath.Join(ContainerInventory(usrdata), name) 34 | } 35 | 36 | func GetExecutablePath() (self string, err error) { 37 | self, err = os.Executable() 38 | if err != nil { 39 | return 40 | } 41 | 42 | self, err = filepath.EvalSymlinks(self) 43 | if err != nil { 44 | return 45 | } 46 | 47 | self, err = filepath.Abs(self) 48 | return 49 | } 50 | 51 | func getPathRelativeToInstallRoot(subpaths ...string) (string, error) { 52 | self, err := GetExecutablePath() 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | parts := []string{filepath.Dir(self), ".."} 58 | 59 | // XXX: this is kinda hacked in. 60 | if strings.HasSuffix(self, "nsboxd") || strings.HasSuffix(self, "nsbox-invoker") { 61 | // nsboxd is in ROOT/libexec/nsbox, so a level further down than the nsbox CLI. 62 | parts = append(parts, "..") 63 | } 64 | 65 | parts = append(parts, subpaths...) 66 | 67 | path := filepath.Clean(filepath.Join(parts...)) 68 | if _, err := os.Stat(path); err != nil { 69 | return "", err 70 | } 71 | 72 | return path, nil 73 | } 74 | 75 | func GetSystemImagesDir() (string, error) { 76 | return getPathRelativeToInstallRoot(config.ShareDir, config.ProductName, "images") 77 | } 78 | 79 | func GetSystemImageDir(name string) (string, error) { 80 | systemImages, err := GetSystemImagesDir() 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return filepath.Join(systemImages, name), nil 86 | } 87 | 88 | func GetCustomImagesDir() string { 89 | return filepath.Join(config.ConfigDir, "nsbox", "images") 90 | } 91 | 92 | func GetCustomImageDir(name string) string { 93 | return filepath.Join(GetCustomImagesDir(), name) 94 | } 95 | 96 | func GetReleaseDataDir() (string, error) { 97 | return getPathRelativeToInstallRoot(config.ShareDir, config.ProductName, "release") 98 | } 99 | 100 | func GetDataDir() (string, error) { 101 | return getPathRelativeToInstallRoot(config.ShareDir, config.ProductName, "data") 102 | } 103 | 104 | func GetMainExecutable() (string, error) { 105 | return getPathRelativeToInstallRoot(config.BinDir, config.ProductName) 106 | } 107 | 108 | func GetPrivateExecutable(name string) (string, error) { 109 | return getPathRelativeToInstallRoot(config.LibexecDir, config.ProductName, name) 110 | } 111 | -------------------------------------------------------------------------------- /internal/ptyservice/client.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package ptyservice 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "os" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/container" 14 | "github.com/refi64/nsbox/internal/log" 15 | "github.com/refi64/nsbox/internal/paths" 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | func OpenPtyInContainer(ct *container.Container) (*os.File, error) { 20 | ptySocketPath := ct.StorageChild(paths.InContainerPrivPath, paths.PtyServiceSocketName) 21 | 22 | conn, err := net.Dial("unix", ptySocketPath) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "failed to connect to pty service") 25 | } 26 | 27 | connFile, err := conn.(*net.UnixConn).File() 28 | if err != nil { 29 | return nil, errors.Wrap(err, "failed to access connection file") 30 | } 31 | 32 | // Expecting a max-16 byte path & one 4-byte fd. 33 | pathBuffer := make([]byte, maxPathSize) 34 | controlBuffer := make([]byte, unix.CmsgSpace(4)) 35 | 36 | // If you look at (_, _, _) long enough it looks like a kaomoji. 37 | pathLen, _, _, _, err := unix.Recvmsg(int(connFile.Fd()), pathBuffer, controlBuffer, 0) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "failed to receive pty message") 40 | } 41 | 42 | path := string(pathBuffer[:pathLen]) 43 | 44 | controlMessages, err := unix.ParseSocketControlMessage(controlBuffer) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "failed to parse pty control message") 47 | } 48 | 49 | if len(controlMessages) != 1 { 50 | return nil, errors.Errorf("unexpected %f control messages from pty service", len(controlMessages)) 51 | } 52 | 53 | fds, err := unix.ParseUnixRights(&controlMessages[0]) 54 | if err != nil { 55 | return nil, errors.Wrap(err, "failed to parse pty control rights message") 56 | } 57 | 58 | if len(fds) != 1 { 59 | return nil, errors.Errorf("unexpected %d fds from pty service", len(fds)) 60 | } 61 | 62 | fd := fds[0] 63 | 64 | file := os.NewFile(uintptr(fd), path) 65 | if file == nil { 66 | panic(fmt.Sprint("given invalid fd by pty service: ", fd)) 67 | } 68 | 69 | log.Debugf("pty service sent %d, %s", fd, path) 70 | 71 | return file, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/ptyservice/service.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // The PTY service runs inside the container, and it's responsible for sending a PTY FD over a 6 | // Unix domain socket, that way nsbox can attach it to the user's terminal session. 7 | package ptyservice 8 | 9 | import ( 10 | "fmt" 11 | "net" 12 | "os" 13 | 14 | "github.com/creack/pty" 15 | "github.com/pkg/errors" 16 | "github.com/refi64/nsbox/internal/log" 17 | "github.com/refi64/nsbox/internal/paths" 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | // If we have a PTY over this size, something is very, very wrong... 22 | // (And if a bug report is filed because *I* was wrong, this comment will have officially aged 23 | // more poorly than the posts /r/iamverysmart of the poster when they were 10 years younger.) 24 | const maxPathSize = 16 25 | 26 | func openPty() (int, string, error) { 27 | master, slave, err := pty.Open() 28 | if err != nil { 29 | return 0, "", err 30 | } 31 | 32 | // We don't need the slave end, but we do need the name. 33 | slavePath := slave.Name() 34 | slave.Close() 35 | 36 | return int(master.Fd()), slavePath, nil 37 | } 38 | 39 | func handlePtyRequest(conn net.Conn) { 40 | defer conn.Close() 41 | 42 | fd, path, err := openPty() 43 | if err != nil { 44 | fmt.Fprintln(os.Stderr, "failed to open pty: ", err) 45 | return 46 | } 47 | 48 | defer unix.Close(fd) 49 | 50 | bytePath := []byte(path) 51 | if len(bytePath) > maxPathSize { 52 | fmt.Fprintln(os.Stderr, "path too long: ", path) 53 | return 54 | } 55 | 56 | rights := unix.UnixRights(fd) 57 | 58 | connFile, err := conn.(*net.UnixConn).File() 59 | if err != nil { 60 | fmt.Fprintln(os.Stderr, "failed to access file behind connection: ", err) 61 | return 62 | } 63 | 64 | if err := unix.Sendmsg(int(connFile.Fd()), []byte(path), rights, nil, 0); err != nil { 65 | fmt.Fprintln(os.Stderr, "failed to send reply: ", err) 66 | return 67 | } 68 | } 69 | 70 | func StartPtyService(name string) error { 71 | socketPath := "/run/host/nsbox/" + paths.PtyServiceSocketName 72 | 73 | if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { 74 | return errors.Wrap(err, "failed to remove old pty service socket") 75 | } 76 | 77 | listener, err := net.Listen("unix", socketPath) 78 | if err != nil { 79 | return errors.Wrap(err, "failed to listen on pty service socket") 80 | } 81 | 82 | go func() { 83 | for { 84 | conn, err := listener.Accept() 85 | if err != nil { 86 | log.Fatal("failed to accept pty service connection: ", err) 87 | break 88 | } 89 | 90 | go handlePtyRequest(conn) 91 | } 92 | }() 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/release/release.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package release 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/paths" 15 | ) 16 | 17 | type Branch int 18 | 19 | const ( 20 | StableBranch Branch = iota 21 | EdgeBranch 22 | ) 23 | 24 | func (branch Branch) String() string { 25 | switch branch { 26 | case StableBranch: 27 | return "stable" 28 | case EdgeBranch: 29 | return "edge" 30 | default: 31 | return "invalid" 32 | } 33 | } 34 | 35 | type ReleaseInfo struct { 36 | Branch Branch 37 | Version string 38 | } 39 | 40 | func Read() (*ReleaseInfo, error) { 41 | releaseDir, err := paths.GetReleaseDataDir() 42 | if err != nil { 43 | return nil, errors.Wrap(err, "failed to get release path") 44 | } 45 | 46 | var release ReleaseInfo 47 | var branchString string 48 | 49 | releaseFiles := map[string]*string{ 50 | "BRANCH": &branchString, 51 | "VERSION": &release.Version, 52 | } 53 | 54 | for releaseFile, target := range releaseFiles { 55 | file, err := os.Open(filepath.Join(releaseDir, releaseFile)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | defer file.Close() 61 | 62 | bytes, err := ioutil.ReadAll(file) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | *target = strings.TrimSpace(string(bytes)) 68 | } 69 | 70 | switch branchString { 71 | case "stable": 72 | release.Branch = StableBranch 73 | case "edge": 74 | release.Branch = EdgeBranch 75 | default: 76 | return nil, errors.Errorf("invalid release file branch: %s", branchString) 77 | } 78 | 79 | return &release, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/selinux/selinux.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package selinux 6 | 7 | import ( 8 | "io" 9 | "strings" 10 | 11 | "github.com/opencontainers/selinux/go-selinux" 12 | "github.com/pkg/errors" 13 | "github.com/refi64/nsbox/internal/log" 14 | ) 15 | 16 | const systemRole = "system_r" 17 | const spcType = "spc_t" 18 | 19 | func Enforcing() bool { 20 | return selinux.EnforceMode() == selinux.Enforcing 21 | } 22 | 23 | func Enabled() bool { 24 | return selinux.EnforceMode() != selinux.Disabled 25 | } 26 | 27 | func GetCurrentLabel() (string, error) { 28 | label, err := selinux.ExecLabel() 29 | if err == io.EOF { 30 | label, err = selinux.CurrentLabel() 31 | } 32 | 33 | return label, err 34 | } 35 | 36 | func GetExecLabel(currentLabel string) (string, error) { 37 | parts := strings.Split(currentLabel, ":") 38 | if len(parts) != 4 && len(parts) != 5 { 39 | return "", errors.Errorf("invalid SELinux label: %s") 40 | } 41 | 42 | parts[1] = systemRole 43 | parts[2] = spcType 44 | 45 | return strings.Join(parts, ":"), nil 46 | } 47 | 48 | func SetExecProcessContextContainer() error { 49 | if !Enabled() { 50 | log.Debug("SELinux is disabled") 51 | return nil 52 | } 53 | 54 | currentLabel, err := GetCurrentLabel() 55 | if err != nil { 56 | return errors.Wrap(err, "get current label") 57 | } 58 | 59 | newLabel, err := GetExecLabel(currentLabel) 60 | log.Debug("SELinux exec transition", currentLabel, "->", newLabel) 61 | 62 | if err := selinux.SetExecLabel(newLabel); err != nil { 63 | return errors.Wrap(err, "failed to set label") 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/session/enter_nsenter.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package session 6 | 7 | import ( 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "syscall" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/refi64/nsbox/internal/container" 15 | "github.com/refi64/nsbox/internal/log" 16 | "github.com/refi64/nsbox/internal/selinux" 17 | "github.com/refi64/nsbox/internal/userdata" 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | // We add the "import C" here, because enter.go has it, so if CGO_ENABLED=0, then enter.go 22 | // would not be built BUT this file still would be, resulting in undefined symbol errors. 23 | 24 | import "C" 25 | 26 | type nsenterSessionHandle struct { 27 | process *os.Process 28 | } 29 | 30 | // A door that enters the container environment via nsenter. 31 | type nsenterDoor struct{} 32 | 33 | func convertStateToProcessExit(state *os.ProcessState) (*processExitStatus, error) { 34 | // XXX: syscall is deprecated, but this cast will fail if it directly jumps to 35 | // unix.WaitStatus. 36 | waitStatus := unix.WaitStatus(state.Sys().(syscall.WaitStatus)) 37 | 38 | if waitStatus.Signaled() { 39 | return &processExitStatus{exitType: processExitSignaled, result: int(waitStatus.Signal())}, nil 40 | } else if waitStatus.Exited() { 41 | return &processExitStatus{exitType: processExitNormal, result: waitStatus.ExitStatus()}, nil 42 | } else { 43 | return nil, errors.Errorf("Unexpected wait status %d", int(waitStatus)) 44 | } 45 | } 46 | 47 | func (handle *nsenterSessionHandle) Signal(signal os.Signal) error { 48 | return handle.process.Signal(signal) 49 | } 50 | 51 | func (handle *nsenterSessionHandle) Wait() (*processExitStatus, error) { 52 | state, err := handle.process.Wait() 53 | if err != nil { 54 | // Handle ExitError in convertStateToProcessExit. 55 | if _, ok := err.(*exec.ExitError); !ok { 56 | return nil, errors.Wrap(err, "waiting for nsenter") 57 | } 58 | } 59 | 60 | return convertStateToProcessExit(state) 61 | } 62 | 63 | func (handle *nsenterSessionHandle) Destroy() {} 64 | 65 | func (door *nsenterDoor) Enter(ct *container.Container, 66 | spec *containerEntrySpec, usrdata *userdata.Userdata) (sessionHandle, error) { 67 | leader, err := ct.Leader(usrdata) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "getting leader process") 70 | } 71 | 72 | if err := os.Setenv("NSBOX_INTERNAL", "1"); err != nil { 73 | return nil, errors.Wrap(err, "set NSBOX_INTERNAL") 74 | } 75 | 76 | nsenterCmd := []string{"nsenter", "-at", strconv.Itoa(int(leader))} 77 | nsenterCmd = append(nsenterCmd, spec.buildNsboxHostCommand()...) 78 | 79 | log.Debug("running:", nsenterCmd) 80 | 81 | if err := selinux.SetExecProcessContextContainer(); err != nil { 82 | log.Alert("failed to set exec context to enter container:", err) 83 | } 84 | 85 | // If there's no pty, we can exec the command directly. 86 | if spec.ptyPath == "" { 87 | nsenter, err := exec.LookPath("nsenter") 88 | if err != nil { 89 | return nil, errors.Wrap(err, "failed to find nsenter") 90 | } 91 | 92 | if err := unix.Exec(nsenter, nsenterCmd, os.Environ()); err != nil { 93 | return nil, errors.Wrap(err, "failed to exec into namespace") 94 | } 95 | 96 | panic("should not reach here") 97 | } 98 | 99 | cmd := exec.Command(nsenterCmd[0], nsenterCmd[1:]...) 100 | cmd.Stdout = os.Stdout 101 | cmd.Stderr = os.Stderr 102 | cmd.Stdin = os.Stdin 103 | 104 | if err := cmd.Start(); err != nil { 105 | return nil, err 106 | } 107 | 108 | return &nsenterSessionHandle{process: cmd.Process}, nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/session/nsbox-ptyfwd.c: -------------------------------------------------------------------------------- 1 | #include "nsbox-ptyfwd.h" 2 | 3 | #define _GNU_SOURCE 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define FMT(...) \ 11 | ({ \ 12 | char *_p = NULL; \ 13 | asprintf(&_p, __VA_ARGS__); \ 14 | _p; \ 15 | }) 16 | 17 | char *nsbox_forward_pty(int dstfd, int srcfd) { 18 | for (;;) { 19 | char buffer[4 * 1024 * 1024] = {0}; 20 | ssize_t bytes_read = read(srcfd, &buffer, sizeof(buffer)); 21 | 22 | if (bytes_read == -1) { 23 | // EIO gets returned from transient TTY issues, so ignore it. 24 | if (errno == EINTR || errno == EIO) { 25 | continue; 26 | } else { 27 | return FMT("Failed to read: %s", strerror(errno)); 28 | } 29 | } else if (bytes_read == 0) { 30 | return NULL; 31 | } 32 | 33 | size_t offs = 0; 34 | while (offs < bytes_read) { 35 | ssize_t bytes_written = write(dstfd, &buffer + offs, bytes_read - offs); 36 | if (bytes_written == -1) { 37 | if (errno == EINTR) { 38 | continue; 39 | } else { 40 | return FMT("Failed to write: %s", strerror(errno)); 41 | } 42 | } 43 | 44 | offs += bytes_written; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/session/nsbox-ptyfwd.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | char *nsbox_forward_pty(int dstfd, int srcfd); 4 | -------------------------------------------------------------------------------- /internal/session/setup.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | // Bind to the host form within a container. 6 | package session 7 | 8 | import ( 9 | "os" 10 | 11 | "github.com/pkg/errors" 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | func ConnectPtys(stdinPty, stdoutPty, stderrPty string) error { 16 | // Order is significant, the indexes map to the file descriptors. 17 | ptys := []string{stdinPty, stdoutPty, stderrPty} 18 | 19 | for fd, pty := range ptys { 20 | if pty == "" { 21 | continue 22 | } 23 | 24 | if fd == 0 { 25 | if _, err := unix.Setsid(); err != nil { 26 | if errno, ok := err.(unix.Errno); !ok || errno != unix.EPERM { 27 | // EPERM means this is already a session leader. 28 | return errors.Wrapf(err, "failed to setsid") 29 | } 30 | } 31 | } 32 | 33 | // Some ANSI codes can be *written* to stdin, e.g. https://vt100.net/docs/vt510-rm/DECCKM.html 34 | // In addition, it's possible to *read* from stdin... Therefore, flags is always RDWR. 35 | ptyFd, err := unix.Open(pty, os.O_RDWR, 0) 36 | if err != nil { 37 | return errors.Wrapf(err, "failed to open pty %s for %d", pty, fd) 38 | } 39 | 40 | if err := unix.Dup2(ptyFd, fd); err != nil { 41 | return errors.Wrapf(err, "failed to dup %s onto %d", pty, fd) 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func SetupContainerSession(uid int, cwd string, noReplay bool, execCommand []string) error { 49 | if noReplay { 50 | if err := os.Setenv("NSBOX_NO_REPLAY", "1"); err != nil { 51 | return errors.Wrap(err, "set NSBOX_NO_REPLAY") 52 | } 53 | } 54 | 55 | script := "/run/host/nsbox/scripts/nsbox-enter-setup.sh" 56 | execCmdline := append([]string{script, cwd}, execCommand...) 57 | if err := unix.Exec(script, execCmdline, os.Environ()); err != nil { 58 | return errors.Wrap(err, "failed to exec command") 59 | } 60 | 61 | panic("should not reach here") 62 | } 63 | -------------------------------------------------------------------------------- /internal/varlink/dev.nsbox.varlink: -------------------------------------------------------------------------------- 1 | # This file is currently very boring. It will probably become less boring later on, but still 2 | # will probably be boring. 3 | 4 | interface dev.nsbox 5 | 6 | # Notify the host that the container is fully initialized. 7 | method NotifyStart() -> () 8 | 9 | # Notify the host that potentially exported files have been updated. 10 | method NotifyReloadExports() -> () 11 | -------------------------------------------------------------------------------- /internal/varlink/stub.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | /* 6 | This is here for two reason: 7 | - So 'go mod' won't get confused by an empty directory. 8 | - To make IDE autocomplete / analysis work properly. 9 | It contains a stub subset of the methods in the full generated Varlink interface 10 | */ 11 | 12 | package devnsbox 13 | 14 | import ( 15 | "context" 16 | 17 | "github.com/varlink/go/varlink" 18 | ) 19 | 20 | type NotifyStart_methods interface { 21 | Call(ctx context.Context, c *varlink.Connection) error 22 | } 23 | 24 | func NotifyStart() NotifyStart_methods 25 | 26 | type NotifyReloadExports_methods interface { 27 | Call(ctx context.Context, c *varlink.Connection) error 28 | } 29 | 30 | func NotifyReloadExports() NotifyReloadExports_methods 31 | 32 | type VarlinkCall interface { 33 | ReplyNotifyStart(ctx context.Context) error 34 | ReplyNotifyReloadExports(ctx context.Context) error 35 | } 36 | 37 | type iface interface { 38 | NotifyStart(ctx context.Context, c VarlinkCall) error 39 | NotifyReloadExports(ctx context.Context, c VarlinkCall) error 40 | } 41 | 42 | type VarlinkInterface struct { 43 | iface 44 | } 45 | 46 | func (VarlinkInterface) VarlinkDispatch(ctx context.Context, call varlink.Call, methodname string) error 47 | func (VarlinkInterface) VarlinkGetName() string 48 | func (VarlinkInterface) VarlinkGetDescription() string 49 | 50 | func VarlinkNew(m iface) *VarlinkInterface { return nil } 51 | -------------------------------------------------------------------------------- /internal/varlinkhost/varlinkhost.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | package varlinkhost 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/coreos/go-systemd/v22/daemon" 11 | "github.com/refi64/nsbox/internal/container" 12 | "github.com/refi64/nsbox/internal/integration" 13 | "github.com/refi64/nsbox/internal/log" 14 | devnsbox "github.com/refi64/nsbox/internal/varlink" 15 | ) 16 | 17 | type VarlinkHost struct { 18 | devnsbox.VarlinkInterface 19 | 20 | container *container.Container 21 | } 22 | 23 | func (host *VarlinkHost) NotifyStart(ctx context.Context, call devnsbox.VarlinkCall) error { 24 | log.Debug("received NotifyStart()") 25 | 26 | if _, err := daemon.SdNotify(true, daemon.SdNotifyReady); err != nil { 27 | log.Alert("notifying systemd of start", err) 28 | return err 29 | } 30 | 31 | return call.ReplyNotifyStart(ctx) 32 | } 33 | 34 | func (host *VarlinkHost) NotifyReloadExports(ctx context.Context, call devnsbox.VarlinkCall) error { 35 | log.Debug("received NotifyReloadExports()") 36 | 37 | if err := integration.UpdateDesktopFiles(host.container); err != nil { 38 | log.Alert("updating desktop files", err) 39 | return err 40 | } 41 | 42 | return call.ReplyNotifyReloadExports(ctx) 43 | } 44 | 45 | func New(ct *container.Container) *devnsbox.VarlinkInterface { 46 | host := VarlinkHost{container: ct} 47 | return devnsbox.VarlinkNew(&host) 48 | } 49 | -------------------------------------------------------------------------------- /misc/dev.nsbox.rules: -------------------------------------------------------------------------------- 1 | polkit.addRule(function (action, subject) { 2 | if ((action.id == '@RDNS_NAME.info' || action.id == '@RDNS_NAME.list' 3 | || action.id == '@RDNS_NAME.run' || action.id == '@RDNS_NAME.images') 4 | && subject.active && subject.local && subject.isInGroup('wheel')) { 5 | return polkit.Result.YES 6 | } 7 | 8 | return polkit.Result.NOT_HANDLED 9 | }) 10 | -------------------------------------------------------------------------------- /misc/profile.d-nsbox.sh: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # Updates XDG_DATA_DIRS with nsbox container exports. 6 | 7 | for dir in @STATE_DIR/nsbox/$USER/inventory/*; do 8 | [ -d "$dir/exports/share" ] || continue 9 | 10 | export XDG_DATA_DIRS="$XDG_DATA_DIRS:$dir/exports/share/" 11 | done 12 | -------------------------------------------------------------------------------- /mock-f32-x64.cfg: -------------------------------------------------------------------------------- 1 | config_opts['releasever'] = '32' 2 | config_opts['target_arch'] = 'x86_64' 3 | config_opts['legal_host_arches'] = ('x86_64',) 4 | 5 | include('/etc/mock/templates/fedora-branched.tpl') 6 | 7 | # since I usually build in a container so nspawn can't register 8 | config_opts['use_nspawn'] = False 9 | config_opts['dnf.conf'] += ''' 10 | [copr:copr.fedorainfracloud.org:refi64:gn] 11 | name=Copr repo for gn owned by refi64 12 | baseurl=https://copr-be.cloud.fedoraproject.org/results/refi64/gn/fedora-$releasever-$basearch/ 13 | type=rpm-md 14 | skip_if_unavailable=True 15 | gpgcheck=1 16 | gpgkey=https://copr-be.cloud.fedoraproject.org/results/refi64/gn/pubkey.gpg 17 | repo_gpgcheck=0 18 | enabled=1 19 | enabled_metadata=1 20 | ''' 21 | -------------------------------------------------------------------------------- /packaging/fedora/BUILD.gn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import("//build/copy_target_outputs.gni") 6 | import("//build/rpmbuild.gni") 7 | import("//build/substitute_file.gni") 8 | import("//build/symlink.gni") 9 | 10 | action("archive") { 11 | outputs = [ "$target_out_dir/nsbox-sources.tar" ] 12 | depfile = "$target_gen_dir/nsbox-sources.d" 13 | 14 | script = "//build/source_archive.py" 15 | args = [ 16 | "--source-root", 17 | rebase_path("//", root_build_dir), 18 | "--prefix", 19 | "$product_name-$release_version", 20 | "--out-tar", 21 | rebase_path(outputs[0], root_build_dir), 22 | "--out-dep", 23 | rebase_path(depfile, root_build_dir), 24 | "--include-vendor", 25 | ] 26 | } 27 | 28 | substitute_file("nsbox.spec") { 29 | deps = [ "//:release_files" ] 30 | 31 | source = "nsbox.spec" 32 | 33 | vars = [ 34 | [ 35 | "PRODUCT_NAME", 36 | product_name, 37 | ], 38 | [ 39 | "RDNS_NAME", 40 | rdns_name, 41 | ], 42 | [ 43 | "VERSION", 44 | release_version, 45 | ], 46 | [ 47 | "COMMIT", 48 | release_commit, 49 | ], 50 | ] 51 | } 52 | 53 | rpmbuild("rpm") { 54 | package_name = product_name 55 | 56 | version = release_version 57 | release = "1" 58 | has_debug = true 59 | 60 | extra_binary_packages = [ "$product_name-bender" ] 61 | extra_noarch_packages = [ "$product_name-selinux" ] 62 | 63 | if (!is_stable_build) { 64 | extra_binary_packages += [ 65 | "nsbox-edge-alias", 66 | "nsbox-edge-bender-alias", 67 | ] 68 | } 69 | 70 | archive_source = get_target_outputs(":archive") 71 | substituted_spec = get_target_outputs(":nsbox.spec") 72 | 73 | spec = substituted_spec[0] 74 | sources = [ archive_source[0] ] 75 | deps = [ 76 | ":archive", 77 | ":nsbox.spec", 78 | ] 79 | } 80 | 81 | copy_target_outputs("install") { 82 | deps = [ 83 | ":archive", 84 | ":nsbox.spec", 85 | ":rpm", 86 | ] 87 | outputs = [ "$root_build_dir/rpm/{{source_file_part}}" ] 88 | } 89 | 90 | group("fedora") { 91 | deps = [ ":install" ] 92 | } 93 | -------------------------------------------------------------------------------- /sepolicy/BUILD.gn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import("//build/install.gni") 6 | import("//build/selinux.gni") 7 | import("//build/substitute_file.gni") 8 | 9 | selinux_vars = [ 10 | [ 11 | "PRODUCT_NAME", 12 | product_name, 13 | ], 14 | [ 15 | "TYPE_PREFIX", 16 | string_replace(product_name, "-", "_"), 17 | ], 18 | [ 19 | "VERSION", 20 | release_version, 21 | ], 22 | 23 | [ 24 | "PREFIX", 25 | prefix, 26 | ], 27 | [ 28 | "LIBEXEC_DIR", 29 | libexec_dir, 30 | ], 31 | [ 32 | "SHARE_DIR", 33 | share_dir, 34 | ], 35 | ] 36 | 37 | substitute_file("nsbox.te") { 38 | source = "nsbox.te" 39 | output = "$target_out_dir/$product_name.te" 40 | vars = selinux_vars 41 | } 42 | 43 | substitute_file("nsbox.fc") { 44 | source = "nsbox.fc" 45 | output = "$target_out_dir/$product_name.fc" 46 | vars = selinux_vars 47 | } 48 | 49 | selinux_package("nsbox_policy") { 50 | name = product_name 51 | sources = get_target_outputs(":nsbox.te") + get_target_outputs(":nsbox.fc") 52 | deps = [ 53 | ":nsbox.fc", 54 | ":nsbox.te", 55 | ] 56 | } 57 | 58 | group("sepolicy") { 59 | deps = [ ":nsbox_policy" ] 60 | } 61 | 62 | install_files("install_sepolicy") { 63 | targets = [ ":nsbox_policy" ] 64 | output = "$share_dir/selinux/packages/{{source_file_part}}" 65 | } 66 | -------------------------------------------------------------------------------- /sepolicy/nsbox.fc: -------------------------------------------------------------------------------- 1 | @PREFIX/@LIBEXEC_DIR/@PRODUCT_NAME/nsboxd -- gen_context(system_u:object_r:@{TYPE_PREFIX}_nsboxd_exec_t,s0) 2 | -------------------------------------------------------------------------------- /sepolicy/nsbox.te: -------------------------------------------------------------------------------- 1 | policy_module(@PRODUCT_NAME, @VERSION) 2 | 3 | require { 4 | type bin_t; 5 | type home_root_t; 6 | type init_t; 7 | type kernel_t; 8 | type mail_spool_t; 9 | type passwd_file_t; 10 | type shadow_t; 11 | type spc_t; 12 | type sysctl_net_t; 13 | type var_lib_t; 14 | type user_tmp_t; 15 | } 16 | 17 | type @{TYPE_PREFIX}_nsboxd_t; 18 | type @{TYPE_PREFIX}_nsboxd_exec_t; 19 | init_daemon_domain(@{TYPE_PREFIX}_nsboxd_t, @{TYPE_PREFIX}_nsboxd_exec_t) 20 | 21 | # To transition to spt_t when exec-ing nspawn 22 | spec_domtrans_pattern(@{TYPE_PREFIX}_nsboxd_t, bin_t, spc_t) 23 | domain_entry_file(spc_t, bin_t) 24 | 25 | optional_policy(` 26 | unconfined_domain(@{TYPE_PREFIX}_nsboxd_t) 27 | ') 28 | -------------------------------------------------------------------------------- /tests/playbooks/fedora.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: true 3 | tasks: 4 | - name: Add the gn COPR repo 5 | yum_repository: 6 | name: nsbox-edge 7 | description: 'Copr repo for gn owned by refi64' 8 | baseurl: https://copr-be.cloud.fedoraproject.org/results/refi64/gn/fedora-$releasever-$basearch/ 9 | gpgkey: https://copr-be.cloud.fedoraproject.org/results/refi64/gn/pubkey.gpg 10 | become: true 11 | 12 | - name: Install the dependencies 13 | dnf: 14 | name: 15 | - container-selinux 16 | - expect 17 | - gcc 18 | - git 19 | - gn 20 | - golang-bin 21 | - go-rpm-macros 22 | - ninja-build 23 | - python3 24 | - rpm-build 25 | - selinux-policy-devel 26 | - sudo 27 | - systemd-container 28 | - systemd-devel 29 | - tcllib 30 | state: latest 31 | become: true 32 | 33 | - name: Clear the build directory 34 | file: 35 | path: /out 36 | state: absent 37 | become: true 38 | 39 | - name: Create the build directory 40 | file: 41 | path: /out 42 | state: directory 43 | # Make it world-writable, so gn can touch it later. 44 | mode: '0777' 45 | become: true 46 | 47 | - name: Build the nsbox RPMs 48 | shell: | 49 | set -e 50 | gn gen ../out --args='fedora_package=true fedora_rpm_target_release="{{ ansible_distribution_version }}"' 51 | ninja -C ../out fedora 52 | touch ../out/.built 53 | args: 54 | chdir: /vagrant 55 | creates: /out/.built 56 | 57 | - name: Find the nsbox RPMs 58 | find: 59 | paths: /out/rpm 60 | patterns: 'nsbox-edge-(\d|alias|selinux).*(noarch|{{ ansible_machine }})\.rpm' 61 | recurse: false 62 | use_regex: true 63 | register: nsbox_rpms 64 | 65 | - name: Remove any old nsbox installations 66 | dnf: 67 | name: 68 | - nsbox-edge 69 | - nsbox-edge-alias 70 | - nsbox-edge-selinux 71 | state: absent 72 | become: true 73 | 74 | - name: Install nsbox 75 | dnf: 76 | name: "{{ nsbox_rpms.files | map(attribute='path') | list }}" 77 | state: latest 78 | become: true 79 | -------------------------------------------------------------------------------- /web/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | themeConfig: { 3 | logo: '/nsbox.svg', 4 | nav: [ 5 | {text: 'Home', link: '/'}, 6 | {text: 'Guide', link: '/guide/'}, 7 | {text: 'Images', link: '/images/'}, 8 | {text: 'FAQ', link: '/faq/'}, 9 | {text: 'Recipes', link: '/recipes/'}, 10 | {text: 'Issues', link: 'https://ora.pm/project/211667/kanban'}, 11 | {text: 'Source', link: 'https://github.com/refi64/nsbox'}, 12 | ], 13 | sidebar: 'auto', 14 | smoothScroll: true, 15 | algolia: { 16 | apiKey: '38206f8f24dcc0d443c475ef4d13fac4', 17 | indexName: 'nsbox', 18 | }, 19 | }, 20 | title: 'nsbox', 21 | description: 'A powerful pet container manager', 22 | head: [ 23 | [ 24 | 'link', { 25 | rel: 'preconnect', 26 | href: 'https://fonts.googleapis.com/', 27 | crossorigin: '' 28 | } 29 | ], 30 | [ 31 | 'link', { 32 | rel: 'stylesheet', 33 | href: 34 | 'https://fonts.googleapis.com/css2?family=Dosis:wght@500;600&display=swap' 35 | } 36 | ], 37 | ], 38 | plugins: [['@vuepress/google-analytics', {'ga': 'UA-55018880-2'}]], 39 | } 40 | -------------------------------------------------------------------------------- /web/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refi64/nsbox/8d9c0ebcb73932e8a581e08ed9b371a2b5a645ed/web/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /web/.vuepress/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /web/.vuepress/theme/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Yuxi (Evan) You 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/DropdownTransition.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/NavLink.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 88 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/Page.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/PageEdit.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 109 | 110 | 144 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/PageNav.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 147 | 148 | 164 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 66 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/SidebarButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 82 | 83 | 141 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/SidebarLink.vue: -------------------------------------------------------------------------------- 1 | 103 | 104 | 138 | -------------------------------------------------------------------------------- /web/.vuepress/theme/components/SidebarLinks.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 103 | -------------------------------------------------------------------------------- /web/.vuepress/theme/global-components/Badge.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /web/.vuepress/theme/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Theme API. 4 | module.exports = (options, ctx) => { 5 | const { themeConfig, siteConfig } = ctx 6 | 7 | // resolve algolia 8 | const isAlgoliaSearch = ( 9 | themeConfig.algolia 10 | || Object 11 | .keys(siteConfig.locales && themeConfig.locales || {}) 12 | .some(base => themeConfig.locales[base].algolia) 13 | ) 14 | 15 | const enableSmoothScroll = themeConfig.smoothScroll === true 16 | 17 | return { 18 | alias () { 19 | return { 20 | '@AlgoliaSearchBox': isAlgoliaSearch 21 | ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') 22 | : path.resolve(__dirname, 'noopModule.js') 23 | } 24 | }, 25 | 26 | plugins: [ 27 | ['@vuepress/active-header-links', options.activeHeaderLinks], 28 | '@vuepress/search', 29 | '@vuepress/plugin-nprogress', 30 | ['container', { 31 | type: 'tip', 32 | defaultTitle: { 33 | '/': 'TIP', 34 | '/zh/': '提示' 35 | } 36 | }], 37 | ['container', { 38 | type: 'warning', 39 | defaultTitle: { 40 | '/': 'WARNING', 41 | '/zh/': '注意' 42 | } 43 | }], 44 | ['container', { 45 | type: 'danger', 46 | defaultTitle: { 47 | '/': 'WARNING', 48 | '/zh/': '警告' 49 | } 50 | }], 51 | ['container', { 52 | type: 'details', 53 | before: info => `
${info ? `${info}` : ''}\n`, 54 | after: () => '
\n' 55 | }], 56 | ['smooth-scroll', enableSmoothScroll] 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/.vuepress/theme/layouts/404.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /web/.vuepress/theme/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 152 | -------------------------------------------------------------------------------- /web/.vuepress/theme/noopModule.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/arrow.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | .arrow 4 | display inline-block 5 | width 0 6 | height 0 7 | &.up 8 | border-left 4px solid transparent 9 | border-right 4px solid transparent 10 | border-bottom 6px solid $arrowBgColor 11 | &.down 12 | border-left 4px solid transparent 13 | border-right 4px solid transparent 14 | border-top 6px solid $arrowBgColor 15 | &.right 16 | border-top 4px solid transparent 17 | border-bottom 4px solid transparent 18 | border-left 6px solid $arrowBgColor 19 | &.left 20 | border-top 4px solid transparent 21 | border-bottom 4px solid transparent 22 | border-right 6px solid $arrowBgColor 23 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/code.styl: -------------------------------------------------------------------------------- 1 | {$contentClass} 2 | code 3 | padding 0.25rem 0.5rem 4 | margin 0 5 | font-size 0.85em 6 | background-color rgba(27,31,35,0.05) 7 | border-radius 3px 8 | .token 9 | &.deleted 10 | color #EC5975 11 | &.inserted 12 | color $accentColor 13 | 14 | {$contentClass} 15 | pre, pre[class*="language-"] 16 | line-height 1.4 17 | padding 1.25rem 1.5rem 18 | margin 0.85rem 0 19 | background-color $codeBgColor 20 | border-radius 6px 21 | overflow auto 22 | code 23 | color #fff 24 | padding 0 25 | background-color transparent 26 | border-radius 0 27 | 28 | div[class*="language-"] 29 | position relative 30 | background-color $codeBgColor 31 | border-radius 6px 32 | .highlight-lines 33 | user-select none 34 | padding-top 1.3rem 35 | position absolute 36 | top 0 37 | left 0 38 | width 100% 39 | line-height 1.4 40 | .highlighted 41 | background-color rgba(0, 0, 0, 66%) 42 | pre, pre[class*="language-"] 43 | background transparent 44 | position relative 45 | z-index 1 46 | &::before 47 | position absolute 48 | z-index 3 49 | top 0.8em 50 | right 1em 51 | font-size 0.75rem 52 | color rgba(255, 255, 255, 0.4) 53 | &:not(.line-numbers-mode) 54 | .line-numbers-wrapper 55 | display none 56 | &.line-numbers-mode 57 | .highlight-lines .highlighted 58 | position relative 59 | &:before 60 | content ' ' 61 | position absolute 62 | z-index 3 63 | left 0 64 | top 0 65 | display block 66 | width $lineNumbersWrapperWidth 67 | height 100% 68 | background-color rgba(0, 0, 0, 66%) 69 | pre 70 | padding-left $lineNumbersWrapperWidth + 1 rem 71 | vertical-align middle 72 | .line-numbers-wrapper 73 | position absolute 74 | top 0 75 | width $lineNumbersWrapperWidth 76 | text-align center 77 | color rgba(255, 255, 255, 0.3) 78 | padding 1.25rem 0 79 | line-height 1.4 80 | br 81 | user-select none 82 | .line-number 83 | position relative 84 | z-index 4 85 | user-select none 86 | font-size 0.85em 87 | &::after 88 | content '' 89 | position absolute 90 | z-index 2 91 | top 0 92 | left 0 93 | width $lineNumbersWrapperWidth 94 | height 100% 95 | border-radius 6px 0 0 6px 96 | border-right 1px solid rgba(0, 0, 0, 66%) 97 | background-color $codeBgColor 98 | 99 | 100 | for lang in $codeLang 101 | div{'[class~="language-' + lang + '"]'} 102 | &:before 103 | content ('' + lang) 104 | 105 | div[class~="language-javascript"] 106 | &:before 107 | content "js" 108 | 109 | div[class~="language-typescript"] 110 | &:before 111 | content "ts" 112 | 113 | div[class~="language-markup"] 114 | &:before 115 | content "html" 116 | 117 | div[class~="language-markdown"] 118 | &:before 119 | content "md" 120 | 121 | div[class~="language-json"]:before 122 | content "json" 123 | 124 | div[class~="language-ruby"]:before 125 | content "rb" 126 | 127 | div[class~="language-python"]:before 128 | content "py" 129 | 130 | div[class~="language-bash"]:before 131 | content "sh" 132 | 133 | div[class~="language-php"]:before 134 | content "php" 135 | 136 | @import '~prismjs/themes/prism-tomorrow.css' 137 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/custom-blocks.styl: -------------------------------------------------------------------------------- 1 | .custom-block 2 | background-color #f3f5f7 3 | 4 | .custom-block-title 5 | font-family Dosis, sans-serif 6 | letter-spacing 0.4px 7 | font-size 1.4rem 8 | font-weight 600 9 | margin-bottom -0.4rem 10 | &.tip, &.warning, &.danger 11 | padding .1rem 1.5rem 12 | border-left-width .5rem 13 | border-left-style solid 14 | margin 1rem 0 15 | &.tip 16 | border-color $accentColor 17 | &.warning 18 | border-color #88008e 19 | .custom-block-title 20 | color #88008e 21 | &.danger 22 | border-color #b10000 23 | .custom-block-title 24 | color d#b10000 25 | &.details 26 | display block 27 | position relative 28 | border-radius 2px 29 | margin 1.6em 0 30 | padding 1.6em 31 | h4 32 | margin-top 0 33 | figure, p 34 | &:last-child 35 | margin-bottom 0 36 | padding-bottom 0 37 | summary 38 | outline none 39 | cursor pointer 40 | 41 | @media (prefers-color-scheme: dark) 42 | .custom-block 43 | background-color #363636 44 | &.warning 45 | border-color #f970ff 46 | .custom-block-title 47 | color #f970ff 48 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/mobile.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | $mobileSidebarWidth = $sidebarWidth * 0.82 4 | 5 | // narrow desktop / iPad 6 | @media (max-width: $MQNarrow) 7 | .sidebar 8 | font-size 15px 9 | width $mobileSidebarWidth 10 | .page 11 | padding-left $mobileSidebarWidth 12 | 13 | // wide mobile 14 | @media (max-width: $MQMobile) 15 | :not(.feature) > h2, :not(.feature) > h3 16 | padding-left 1rem 17 | .sidebar 18 | top 0 19 | padding-top $navbarHeight 20 | transform translateX(-100%) 21 | transition transform .2s ease 22 | .page 23 | padding-left 0 24 | .theme-container 25 | &.sidebar-open 26 | .sidebar 27 | transform translateX(0) 28 | &.no-navbar 29 | .sidebar 30 | padding-top: 0 31 | 32 | // narrow mobile 33 | @media (max-width: $MQMobileNarrow) 34 | h1 35 | font-size 1.9rem 36 | {$contentClass} 37 | div[class*="language-"] 38 | margin 0.85rem -1.5rem 39 | border-radius 0 40 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $accentColor = #099749 2 | $contentClass = '.theme-default-content' 3 | $textColor = black 4 | 5 | $darkBackgroundColor = #191919 6 | $darkTextColor = white 7 | 8 | accentFont() 9 | font-family Dosis, sans-serif 10 | letter-spacing 0.4px 11 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/toc.styl: -------------------------------------------------------------------------------- 1 | .table-of-contents 2 | .badge 3 | vertical-align middle 4 | -------------------------------------------------------------------------------- /web/.vuepress/theme/styles/wrapper.styl: -------------------------------------------------------------------------------- 1 | $wrapper 2 | max-width $contentWidth 3 | margin 0 auto 4 | padding 2rem 2.5rem 5 | @media (max-width: $MQNarrow) 6 | padding 2rem 7 | @media (max-width: $MQMobileNarrow) 8 | padding 1.5rem 9 | 10 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: ηsbοχ 4 | heroLabel: nsbox 5 | tagline: A powerful pet container manager 6 | actionText: Get Started → 7 | actionLink: /guide/ 8 | features: 9 | - title: Integrated 10 | details: Your containers can access the D-Bus instances, manipulate devices, 11 | and even export installed applications onto your host system. 12 | - title: Flexible 13 | details: Containers can optionally run their own init, and you can create custom 14 | base images for your containers built on Ansible playbooks. 15 | - title: Adaptable 16 | details: nsbox was built so that any improvements to the base images will be 17 | retroactively applied to already-existing containers. 18 | --- 19 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nsbox-web", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MPL-2.0", 6 | "private": true, 7 | "dependencies": { 8 | "@vuepress/plugin-active-header-links": "^1.2.0", 9 | "@vuepress/plugin-google-analytics": "^1.2.0", 10 | "@vuepress/plugin-nprogress": "^1.2.0", 11 | "@vuepress/plugin-search": "^1.2.0", 12 | "lodash": "^4.17.15", 13 | "stylus": "^0.54.5", 14 | "stylus-loader": "^3.0.2", 15 | "vuepress": "^1.1.0", 16 | "vuepress-plugin-container": "^2.0.2", 17 | "vuepress-plugin-smooth-scroll": "^0.0.3" 18 | }, 19 | "scripts": { 20 | "dev": "vuepress dev", 21 | "build": "vuepress build" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/recipes.md: -------------------------------------------------------------------------------- 1 | # Recipies 2 | 3 | Here are some examples of things you might be able to use nsbox for. 4 | 5 | ## Running Docker inside an nsbox container 6 | 7 | ::: warning 8 | If you're on Fedora 31+, note that [cgroups v2 is enabled by default]( 9 | https://www.redhat.com/sysadmin/fedora-31-control-group-v2), which breaks Docker. 10 | In order to use cgroups v1 instead, you can add the `systemd.unified_cgroup_hierarchy=0` 11 | kernel parameter option to your boot command line, either [temporarily]( 12 | https://docs.fedoraproject.org/en-US/fedora/rawhide/system-administrators-guide/kernel-module-driver-configuration/Working_with_the_GRUB_2_Boot_Loader/#sec-Making_Temporary_Changes_to_a_GRUB_2_Menu) 13 | or [permanently](https://fedoramagazine.org/setting-kernel-command-line-arguments-with-fedora-30/). 14 | ::: 15 | 16 | In order to run Docker inside an nsbox container, you need two things: 17 | 18 | - [Virtual networking.](docs.md#virtual-networking) 19 | - A system call filter that allows kernel keyring access. 20 | 21 | Both of these can be accomplished with a single config command: 22 | 23 | ```bash 24 | $ nsbox-edge config -virtual-network -syscall-filters=':@default,@keyring' my-container 25 | ``` 26 | 27 | ## Per-container VPNs 28 | 29 | Similarly to the above, you can use virtual networking to get VPN connections that are active 30 | per-container. Let's say you want to use OpenVPN. You can set up OpenVPN to run as a system 31 | service on startup and if it were on the host: 32 | 33 | ```bash 34 | $ sudo cp my-config.conf /etc/openvpn/client.conf 35 | # Setup password auth as usual 36 | $ systemctl enable --now openvpn-client@client 37 | ``` 38 | 39 | provided you make sure the container is configured with: 40 | 41 | ```bash 42 | $ nsbox-edge config -virtual-network my-container 43 | ``` 44 | 45 | In addition, you'll need some browser installed inside it, such as Firefox, which can then 46 | be exported onto the host: 47 | 48 | ```bash 49 | $ nsbox-edge config -xdg-desktop-exports='+firefox' 50 | ``` 51 | 52 | Now, whenever you open the VPN container's Firefox, it'll automatically be running in a 53 | container-local VPN. 54 | --------------------------------------------------------------------------------