Compare commits

..

23 Commits

Author SHA1 Message Date
Aiden McClelland
68f401bfa3 Feature/start tunnel (#3037)
* fix live-build resolv.conf

* improved debuggability

* wip: start-tunnel

* fixes for trixie and tor

* non-free-firmware on trixie

* wip

* web server WIP

* wip: tls refactor

* FE patchdb, mocks, and most endpoints

* fix editing records and patch mocks

* refactor complete

* finish api

* build and formatter update

* minor change toi viewing addresses and fix build

* fixes

* more providers

* endpoint for getting config

* fix tests

* api fixes

* wip: separate port forward controller into parts

* simplify iptables rules

* bump sdk

* misc fixes

* predict next subnet and ip, use wan ips, and form validation

* refactor: break big components apart and address todos (#3043)

* refactor: break big components apart and address todos

* starttunnel readme, fix pf mocks, fix adding tor domain in startos

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* better tui

* tui tweaks

* fix: address comments

* better regex for subnet

* fixes

* better validation

* handle rpc errors

* build fixes

* fix: address comments (#3044)

* fix: address comments

* fix unread notification mocks

* fix row click for notification

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix raspi build

* fix build

* fix build

* fix build

* fix build

* try to fix build

* fix tests

* fix tests

* fix rsync tests

* delete useless effectful test

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
2025-11-07 10:12:05 +00:00
Matt Hill
1ea525feaa make textarea rows configurable (#3042)
* make textarea rows configurable

* add comments

* better defaults
2025-10-31 11:46:49 -06:00
Alex Inkin
57c4a7527e fix: make CPU meter not go to 11 (#3038) 2025-10-30 16:13:39 -06:00
Aiden McClelland
5aa9c045e1 fix live-build resolv.conf (#3035)
* fix live-build resolv.conf

* improved debuggability
2025-09-24 22:44:25 -06:00
Matt Hill
6f1900f3bb limit adding gateway to StartTunnel, better copy around Tor SSL (#3033)
* limit adding gateway to StartTunnel, better copy around Tor SSL

* properly differentiate ssl

* exclude disconnected gateways

* better error handling

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-09-24 13:22:26 -06:00
Aiden McClelland
bc62de795e bugfixes for alpha.10 (#3032)
* bugfixes for alpha.10

* bump raspi kernel

* rpi kernel bump

* alpha.11
2025-09-23 22:42:17 +00:00
Alex Inkin
c62ca4b183 fix: make long dropdown options wrap (#3031) 2025-09-21 06:06:33 -06:00
Alex Inkin
876e5bc683 fix: fix overflowing interface table (#3027) 2025-09-20 07:10:58 -06:00
Alex Inkin
b99f3b73cd fix: make logs page take up all space (#3030) 2025-09-20 07:10:28 -06:00
Matt Hill
7eecf29449 fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color (#3025)
* refector addresses to not need gateways array

* fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color

* fix: fix action results textfields

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-17 10:32:20 -06:00
Mariusz Kogen
1d331d7810 Fix file permissions for developer key and auth cookie (#3024)
* fix permissions

* include read for group
2025-09-16 09:09:33 -06:00
Aiden McClelland
68414678d8 sdk updates; beta.39 (#3022)
* sdk updates; beta.39

* beta.40
2025-09-11 15:47:48 -06:00
Aiden McClelland
2f6b9dac26 Bugfix/dns recursion (#3023)
* fix dns recursion and localhost

* additional fix
2025-09-11 15:47:38 -06:00
Aiden McClelland
d1812d875b fix dns recursion and localhost (#3021) 2025-09-11 12:35:12 -06:00
Aiden McClelland
723dea100f add more gateway info to hostnameInfo (#3019) 2025-09-10 12:16:35 -06:00
Matt Hill
c4419ed31f show correct gateway name when adding public domain 2025-09-10 09:57:39 -06:00
Matt Hill
754ab86e51 only show http for tor if protocol is http 2025-09-10 09:36:03 -06:00
Mariusz Kogen
04dab532cd Motd Redesign - Visual and Structural Upgrade (#3018)
New 040 motd
2025-09-10 06:36:27 +00:00
Matt Hill
add01ebc68 Gateways, domains, and new service interface (#3001)
* add support for inbound proxies

* backend changes

* fix file type

* proxy -> tunnel, implement backend apis

* wip start-tunneld

* add domains and gateways, remove routers, fix docs links

* dont show hidden actions

* show and test dns

* edit instead of chnage acme and change gateway

* refactor: domains page

* refactor: gateways page

* domains and acme refactor

* certificate authorities

* refactor public/private gateways

* fix fe types

* domains mostly finished

* refactor: add file control to form service

* add ip util to sdk

* domains api + migration

* start service interface page, WIP

* different options for clearnet domains

* refactor: styles for interfaces page

* minor

* better placeholder for no addresses

* start sorting addresses

* best address logic

* comments

* fix unnecessary export

* MVP of service interface page

* domains preferred

* fix: address comments

* only translations left

* wip: start-tunnel & fix build

* forms for adding domain, rework things based on new ideas

* fix: dns testing

* public domain, max width, descriptions for dns

* nix StartOS domains, implement public and private domains at interface scope

* restart tor instead of reset

* better icon for restart tor

* dns

* fix sort functions for public and private domains

* with todos

* update types

* clean up tech debt, bump dependencies

* revert to ts-rs v9

* fix all types

* fix dns form

* add missing translations

* it builds

* fix: comments (#3009)

* fix: comments

* undo default

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix: refactor legacy components (#3010)

* fix: comments

* fix: refactor legacy components

* remove default again

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* more translations

* wip

* fix deadlock

* coukd work

* simple renaming

* placeholder for empty service interfaces table

* honor hidden form values

* remove logs

* reason instead of description

* fix dns

* misc fixes

* implement toggling gateways for service interface

* fix showing dns records

* move status column in service list

* remove unnecessary truthy check

* refactor: refactor forms components and remove legacy Taiga UI package (#3012)

* handle wh file uploads

* wip: debugging tor

* socks5 proxy working

* refactor: fix multiple comments (#3013)

* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix file_stream and remove non-terminating test

* clean  up logs

* support for sccache

* fix gha sccache

* more marketplace translations

* install wizard clarity

* stub hostnameInfo in migration

* fix address info after setup, fix styling on SI page, new 040 release notes

* remove tor logs from os

* misc fixes

* reset tor still not functioning...

* update ts

* minor styling and wording

* chore: some fixes (#3015)

* fix gateway renames

* different handling for public domains

* styling fixes

* whole navbar should not be clickable on service show page

* timeout getState request

* remove links from changelog

* misc fixes from pairing

* use custom name for gateway in more places

* fix dns parsing

* closes #3003

* closes #2999

* chore: some fixes (#3017)

* small copy change

* revert hardcoded error for testing

* dont require port forward if gateway is public

* use old wan ip when not available

* fix .const hanging on undefined

* fix test

* fix doc test

* fix renames

* update deps

* allow specifying dependency metadata directly

* temporarily make dependencies not cliackable in marketplace listings

* fix socks bind

* fix test

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-10 03:43:51 +00:00
Mariusz Kogen
1cc9a1a30b build(cli): harden build-cli.sh (zig check, env defaults, GIT_HASH) (#3016) 2025-09-09 14:18:52 -06:00
Dominion5254
92a1de7500 remove entire service package directory on hard uninstall (#3007)
* remove entire service package directory on hard uninstall

* fix package path
2025-08-12 15:46:01 -06:00
Alex Inkin
a6fedcff80 fix: extract correct manifest in updating state (#3004) 2025-08-01 22:51:38 -06:00
Alex Inkin
55eb999305 fix: update notifications design (#3000) 2025-07-29 22:17:59 -06:00
332 changed files with 18193 additions and 10510 deletions

View File

@@ -28,6 +28,7 @@ on:
- aarch64
- aarch64-nonfree
- raspberrypi
- riscv64
deploy:
type: choice
description: Deploy
@@ -45,7 +46,7 @@ on:
- next/*
env:
NODEJS_VERSION: "22.17.1"
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
@@ -62,6 +63,7 @@ jobs:
"aarch64": ["aarch64"],
"aarch64-nonfree": ["aarch64"],
"raspberrypi": ["aarch64"],
"riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64"]
}')[github.event.inputs.platform || 'ALL']
}}
@@ -100,6 +102,12 @@ jobs:
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Use Beta Toolchain
run: rustup default beta
- name: Setup Cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Make
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
env:
@@ -139,6 +147,7 @@ jobs:
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
"riscv64": "buildjet-8vcpu-ubuntu-2204",
}')[matrix.platform]
)
)[github.event.inputs.runner == 'fast']
@@ -152,6 +161,7 @@ jobs:
"aarch64": "aarch64",
"aarch64-nonfree": "aarch64",
"raspberrypi": "aarch64",
"riscv64": "riscv64",
}')[matrix.platform]
}}
steps:

View File

@@ -11,7 +11,7 @@ on:
- next/*
env:
NODEJS_VERSION: "22.17.1"
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: dev-unstable
jobs:
@@ -27,5 +27,11 @@ jobs:
with:
node-version: ${{ env.NODEJS_VERSION }}
- name: Use Beta Toolchain
run: rustup default beta
- name: Setup Cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build And Run Tests
run: make test

View File

@@ -25,9 +25,9 @@ docker buildx create --use
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
nvm alias default 22 # this prevents your machine from reverting back to another version
nvm install 24
nvm use 24
nvm alias default 24 # this prevents your machine from reverting back to another version
```
## Cloning the repository

129
Makefile
View File

@@ -5,15 +5,17 @@ PLATFORM_FILE := $(shell ./check-platform.sh)
ENVIRONMENT_FILE := $(shell ./check-environment.sh)
GIT_HASH_FILE := $(shell ./check-git-hash.sh)
VERSION_FILE := $(shell ./check-version.sh)
BASENAME := $(shell ./basename.sh)
BASENAME := $(shell PROJECT=startos ./basename.sh)
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi)
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./basename.sh)
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./basename.sh)
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html
FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
BUILD_SRC := $(call ls-files, build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
DEBIAN_SRC := $(call ls-files, debian/)
IMAGE_RECIPE_SRC := $(call ls-files, image-recipe/)
STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
@@ -21,18 +23,23 @@ WEB_SHARED_SRC := $(call ls-files, web/projects/shared) $(call ls-files, web/pro
WEB_UI_SRC := $(call ls-files, web/projects/ui)
WEB_SETUP_WIZARD_SRC := $(call ls-files, web/projects/setup-wizard)
WEB_INSTALL_WIZARD_SRC := $(call ls-files, web/projects/install-wizard)
WEB_START_TUNNEL_SRC := $(call ls-files, web/projects/start-tunnel)
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
GZIP_BIN := $(shell which pigz || which gzip)
TAR_BIN := $(shell which gtar || which tar)
COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox container-runtime/rootfs.$(ARCH).squashfs
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
COMPILED_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox container-runtime/rootfs.$(ARCH).squashfs
STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
$(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \
echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \
fi) \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; \
echo cargo-deps/$(ARCH)-unknown-linux-musl/release/flamegraph; \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
fi') \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
fi')
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
REBUILD_TYPES = 1
ifeq ($(REMOTE),)
@@ -56,12 +63,12 @@ endif
.DELETE_ON_ERROR:
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel
all: $(ALL_TARGETS)
all: $(STARTOS_TARGETS)
touch:
touch $(ALL_TARGETS)
touch $(STARTOS_TARGETS)
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
@@ -109,40 +116,74 @@ test-container-runtime: container-runtime/node_modules/.package-lock.json $(call
cli:
./core/install-cli.sh
registry:
./core/build-registrybox.sh
registry: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox
tunnel:
./core/build-tunnelbox.sh
install-registry: $(REGISTRY_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox,$(DESTDIR)/usr/bin/start-registrybox)
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registryd)
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/startos/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE)
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-registrybox.sh
tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/startos/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
$(call mkdir,$(DESTDIR)/usr/lib/startos/scripts)
$(call cp,build/lib/scripts/forward-port,$(DESTDIR)/usr/lib/startos/scripts/forward-port)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) web/dist/static/start-tunnel/index.html
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-tunnelbox.sh
deb: results/$(BASENAME).deb
debian/control: build/lib/depends build/lib/conflicts
./debuild/control.sh
results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS)
results/$(BASENAME).deb: dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
registry-deb: results/$(REGISTRY_BASENAME).deb
results/$(REGISTRY_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
tunnel-deb: results/$(TUNNEL_BASENAME).deb
results/$(TUNNEL_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS)
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables ./build/os-compat/run-compat.sh ./dpkg-build.sh
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
squashfs: results/$(BASENAME).squashfs
results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_SRC) results/$(BASENAME).deb
REQUIRES=debian ./build/os-compat/run-compat.sh ./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
# For creating os images. DO NOT USE
install: $(ALL_TARGETS)
install: $(STARTOS_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin)
$(call mkdir,$(DESTDIR)/usr/sbin)
$(call cp,core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
fi
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
fi
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
@@ -161,7 +202,7 @@ install: $(ALL_TARGETS)
$(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware)
update-overlay: $(ALL_TARGETS)
update-overlay: $(STARTOS_TARGETS)
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
@@ -170,10 +211,10 @@ update-overlay: $(ALL_TARGETS)
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
$(call ssh,"sudo systemctl start startd")
wormhole: core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox
wormhole: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
@echo "Paste the following command into the shell of your StartOS server:"
@echo
@wormhole send core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
@wormhole send core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
wormhole-deb: results/$(BASENAME).deb
@echo "Paste the following command into the shell of your StartOS server:"
@@ -187,16 +228,16 @@ wormhole-squashfs: results/$(BASENAME).squashfs
@echo
@wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img ./$(BASENAME).squashfs'"'"'\n", $$3 }'
update: $(ALL_TARGETS)
update: $(STARTOS_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
update-startbox: core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox # only update binary (faster than full update)
update-startbox: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox # only update binary (faster than full update)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(call cp,core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox,/media/startos/next/usr/bin/startbox)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,/media/startos/next/usr/bin/startbox)
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true')
update-deb: results/$(BASENAME).deb # better than update, but only available from debian
@@ -215,7 +256,7 @@ update-squashfs: results/$(BASENAME).squashfs
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs)
$(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img /media/startos/images/next.rootfs')
emulate-reflash: $(ALL_TARGETS)
emulate-reflash: $(STARTOS_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
@@ -261,22 +302,22 @@ container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/pa
./container-runtime/install-dist-deps.sh
touch container-runtime/dist/node_modules/.package-lock.json
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox
ARCH=$(ARCH) REQUIRES=linux ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh
build/lib/depends build/lib/conflicts: build/dpkg-deps/*
build/dpkg-deps/generate.sh
build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(shell ls build/dpkg-deps/*)
PLATFORM=$(PLATFORM) ARCH=$(ARCH) build/dpkg-deps/generate.sh
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
./download-firmware.sh $(PLATFORM)
core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-startbox.sh
touch core/target/$(ARCH)-unknown-linux-musl/$(PROFILE)/startbox
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE)
core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE)
ARCH=$(ARCH) ./core/build-containerbox.sh
touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox
touch core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox
web/package-lock.json: web/package.json sdk/baseDist/package.json
npm --prefix web i
@@ -303,8 +344,12 @@ web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_S
npm --prefix web run build:install
touch web/dist/raw/install-wizard/index.html
$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE)
./compress-uis.sh
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:tunnel
touch web/dist/raw/start-tunnel/index.html
web/dist/static/%/index.html: web/dist/raw/%/index.html
./compress-uis.sh $*
web/config.json: $(GIT_HASH_FILE) web/config-sample.json
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
@@ -331,11 +376,11 @@ ui: web/dist/raw/ui
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep:
ARCH=aarch64 ./build-cargo-dep.sh pi-beep
cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console:
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console:
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console
cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs:
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs:
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
cargo-deps/$(ARCH)-unknown-linux-musl/release/flamegraph:
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph:
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh flamegraph

59
START-TUNNEL.md Normal file
View File

@@ -0,0 +1,59 @@
# StartTunnel
A self-hosted Wiregaurd VPN optimized for creating VLANs and reverse tunneling to personal servers.
You can think of StartTunnel as "virtual router in the cloud"
Use it for private, remote access, to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address.
## Installation
1. Rent a low cost VPS. For most use cases, the cheapest option should be enough.
- It must have a dedicated public IP address.
- For (CPU), memory (RAM), and storage (disk), choose the minimum spec.
- For transfer (bandwidth), it depends on (1) your use case and (2) your home Internet's _upload_ speed. Even if you intend to serve large files or stream content from your server, there is no reason to pay for speeds that exceed your home Internet's upload speed.
1. Provision the VPS with the latest version of Debian.
1. Access the VPS via SSH.
1. Install StartTunnel:
@TODO
## Features
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique Wireguard config that must be copied, downloaded, or scanned into the device.
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
## CLI
By default, StartTunnel is managed via the `start-tunnel` command line interface, which is self-documented.
```
start-tunnel --help
```
## Web Interface
If you choose to enable the web interface (recommended in most cases), StartTunnel can be accessed as a website from the browser, or programmatically via API.
1. Initialize the web interface.
start-tunnel web init
1. When prompted, select the IP address at which to host the web interface. In many cases, there will be only one IP address.
1. When prompted, enter the port at which to host the web interface. The default is 8443, and we recommend using it. If you change the default, choose an uncommon port to avoid conflicts.
1. Select whether to autogenerate a self-signed certificate or provide your own certificate and key. If you choose to autogenerate, you will be asked to list all IP addresses and domains for which to sign the certificate. For example, if you intend to access your StartTunnel web UI at a domain, include the domain in the list.
1. You will receive a success message that the webserver is running at the chosen IP:port, as well as your SSL certificate and an autogenerated UI password.
1. If not already, trust the certificate in your system keychain and/or browser.
1. If you lose/forget your password, you can reset it using the CLI.

View File

@@ -1,5 +1,7 @@
#!/bin/bash
PROJECT=${PROJECT:-"startos"}
cd "$(dirname "${BASH_SOURCE[0]}")"
PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)"
@@ -16,4 +18,4 @@ if [ -n "$STARTOS_ENV" ]; then
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
fi
echo -n "startos-${VERSION_FULL}_${PLATFORM}"
echo -n "${PROJECT}-${VERSION_FULL}_${PLATFORM}"

View File

@@ -9,7 +9,6 @@ ca-certificates
cifs-utils
cryptsetup
curl
dnsutils
dmidecode
dnsutils
dosfstools
@@ -19,6 +18,8 @@ exfatprogs
flashrom
fuse3
grub-common
grub-efi
grub2-common
htop
httpdirfs
iotop
@@ -41,7 +42,6 @@ nvme-cli
nyx
openssh-server
podman
postgresql
psmisc
qemu-guest-agent
rfkill

View File

@@ -5,11 +5,15 @@ set -e
cd "$(dirname "${BASH_SOURCE[0]}")"
IFS="-" read -ra FEATURES <<< "$ENVIRONMENT"
FEATURES+=("${ARCH}")
if [ "$ARCH" != "$PLATFORM" ]; then
FEATURES+=("${PLATFORM}")
fi
feature_file_checker='
/^#/ { next }
/^\+ [a-z0-9-]+$/ { next }
/^- [a-z0-9-]+$/ { next }
/^\+ [a-z0-9.-]+$/ { next }
/^- [a-z0-9.-]+$/ { next }
{ exit 1 }
'
@@ -30,8 +34,8 @@ for type in conflicts depends; do
for feature in ${FEATURES[@]}; do
file="$feature.$type"
if [ -f $file ]; then
if grep "^- $pkg$" $file; then
SKIP=1
if grep "^- $pkg$" $file > /dev/null; then
SKIP=yes
fi
fi
done

View File

@@ -0,0 +1,12 @@
- grub-common
- grub-efi
- grub2-common
+ parted
+ raspberrypi-net-mods
+ raspberrypi-sys-mods
+ raspi-config
+ raspi-firmware
+ raspi-utils
+ rpi-eeprom
+ rpi-update
+ rpi.gpio-common

View File

@@ -0,0 +1 @@
+ grub-pc-bin

View File

@@ -1,34 +1,123 @@
#!/bin/sh
printf "\n"
printf "Welcome to\n"
cat << "ASCII"
███████
█ █ █
█ █ █ █
█ █ █ █
█ █ █ █
█ █ █ █
█ █
███████
parse_essential_db_info() {
DB_DUMP="/tmp/startos_db.json"
_____ __ ___ __ __
(_ | /\ |__) | / \(_
__) | / \| \ | \__/__)
ASCII
printf " v$(cat /usr/lib/startos/VERSION.txt)\n\n"
printf " %s (%s %s)\n" "$(uname -o)" "$(uname -r)" "$(uname -m)"
printf " Git Hash: $(cat /usr/lib/startos/GIT_HASH.txt)"
if [ -n "$(cat /usr/lib/startos/ENVIRONMENT.txt)" ]; then
printf " ~ $(cat /usr/lib/startos/ENVIRONMENT.txt)\n"
else
printf "\n"
if command -v start-cli >/dev/null 2>&1; then
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
else
return 1
fi
if command -v jq >/dev/null 2>&1 && [ -f "$DB_DUMP" ]; then
HOSTNAME=$(jq -r '.value.serverInfo.hostname // "unknown"' "$DB_DUMP" 2>/dev/null)
VERSION=$(jq -r '.value.serverInfo.version // "unknown"' "$DB_DUMP" 2>/dev/null)
RAM_BYTES=$(jq -r '.value.serverInfo.ram // 0' "$DB_DUMP" 2>/dev/null)
WAN_IP=$(jq -r '.value.serverInfo.network.gateways[].ipInfo.wanIp // "unknown"' "$DB_DUMP" 2>/dev/null | head -1)
NTP_SYNCED=$(jq -r '.value.serverInfo.ntpSynced // false' "$DB_DUMP" 2>/dev/null)
if [ "$RAM_BYTES" != "0" ] && [ "$RAM_BYTES" != "null" ]; then
RAM_GB=$(echo "scale=1; $RAM_BYTES / 1073741824" | bc 2>/dev/null || echo "unknown")
else
RAM_GB="unknown"
fi
RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.status.main == "running")] | length' "$DB_DUMP" 2>/dev/null)
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
rm -f "$DB_DUMP"
return 0
else
rm -f "$DB_DUMP" 2>/dev/null
return 1
fi
}
DB_INFO_AVAILABLE=0
if parse_essential_db_info; then
DB_INFO_AVAILABLE=1
fi
printf "\n"
printf " * Documentation: https://docs.start9.com\n"
printf " * Management: https://%s.local\n" "$(hostname)"
printf " * Support: https://start9.com/contact\n"
printf " * Source Code: https://github.com/Start9Labs/start-os\n"
printf " * License: MIT\n"
printf "\n"
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$VERSION" != "unknown" ]; then
version_display="v$VERSION"
else
version_display="v$(cat /usr/lib/startos/VERSION.txt 2>/dev/null || echo 'unknown')"
fi
printf "\n\033[1;37m ▄▄▀▀▀▀▀▄▄\033[0m\n"
printf "\033[1;37m ▄▀ ▄ ▀▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ \033[1;31m▄██████▄ ▄██████\033[0m\n"
printf "\033[1;37m █ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██ \033[0m\n"
printf "\033[1;37m█ █ █ █ ▀▄▄▄▄ █ █ █ █ ▄▄▄▀ █ \033[1;31m██ ██ ▀█████▄\033[0m\n"
printf "\033[1;37m█ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██\033[0m\n"
printf "\033[1;37m █ █ █ █ ▄▄▄▄▄▀ █ █ █ █ ▀▄ █ \033[1;31m▀██████▀ ██████▀\033[0m\n"
printf "\033[1;37m █ █\033[0m\n"
printf "\033[1;37m ▀▀▄▄▄▀▀ $version_display\033[0m\n\n"
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}' | sed 's/^ *//')
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$RAM_GB" != "unknown" ]; then
memory_used=$(free -m | awk 'NR==2{printf "%.0fMB", $3}')
memory_display="$memory_used / ${RAM_GB}GB"
else
memory_display=$(free -m | awk 'NR==2{printf "%.0fMB / %.0fMB", $3, $2}')
fi
root_usage=$(df -h / | awk 'NR==2{printf "%s (%s free)", $5, $4}')
if [ -d "/media/startos/data/package-data" ]; then
data_usage=$(df -h /media/startos/data/package-data | awk 'NR==2{printf "%s (%s free)", $5, $4}')
else
data_usage="N/A"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
services_text="$RUNNING_SERVICES/$TOTAL_SERVICES running"
else
services_text="Unknown"
fi
local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
if [ -z "$local_ip" ]; then local_ip="N/A"; fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$WAN_IP" != "unknown" ]; then
wan_ip="$WAN_IP"
else
wan_ip="N/A"
fi
printf " \033[1;37m┌─ SYSTEM STATUS ───────────────────────────────────────────────────┐\033[0m\n"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Uptime:" "$uptime_str" "Memory:" "$memory_display"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Root:" "$root_usage" "Data:" "$data_usage"
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
if [ "$RUNNING_SERVICES" -eq "$TOTAL_SERVICES" ] && [ "$TOTAL_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;32m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
elif [ "$RUNNING_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
else
printf " \033[1;37m│\033[0m %-8s \033[0;31m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
else
printf " \033[1;37m│\033[0m %-8s \033[0;37m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "true" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;32m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Synced"
elif [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "false" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;31m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Not Synced"
else
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;37m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Unknown"
fi
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m"
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$HOSTNAME" != "unknown" ]; then
web_url="https://$HOSTNAME.local"
else
web_url="https://$(hostname).local"
fi
printf "\n \033[1;37m┌──────────────────────────────────────────────────── QUICK ACCESS ─┐\033[0m\n"
printf " \033[1;37m│\033[0m Web Interface: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "$web_url"
printf " \033[1;37m│\033[0m Documentation: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://staging.docs.start9.com"
printf " \033[1;37m│\033[0m Support: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://start9.com/contact"
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m\n\n"

View File

@@ -64,9 +64,11 @@ user_pref("messaging-system.rsexperimentloader.enabled", false);
user_pref("network.allow-experiments", false);
user_pref("network.captive-portal-service.enabled", false);
user_pref("network.connectivity-service.enabled", false);
user_pref("network.proxy.autoconfig_url", "file:///usr/lib/startos/proxy.pac");
user_pref("network.proxy.socks", "10.0.3.1");
user_pref("network.proxy.socks_port", 9050);
user_pref("network.proxy.socks_version", 5);
user_pref("network.proxy.socks_remote_dns", true);
user_pref("network.proxy.type", 2);
user_pref("network.proxy.type", 1);
user_pref("privacy.resistFingerprinting", true);
//Enable letterboxing if we want the window size sent to the server to snap to common resolutions:
//user_pref("privacy.resistFingerprinting.letterboxing", true);

View File

@@ -1,26 +1,38 @@
#!/bin/bash
if [ -z "$iiface" ] || [ -z "$oiface" ] || [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
>&2 echo 'missing required env var'
exit 1
fi
kind="-A"
# Helper function to check if a rule exists
nat_rule_exists() {
iptables -t nat -C "$@" 2>/dev/null
}
# Helper function to add or delete a rule idempotently
# Usage: apply_rule [add|del] <iptables args...>
apply_nat_rule() {
local action="$1"
shift
if [ "$action" = "add" ]; then
# Only add if rule doesn't exist
if ! rule_exists "$@"; then
iptables -t nat -A "$@"
fi
elif [ "$action" = "del" ]; then
if rule_exists "$@"; then
iptables -t nat -D "$@"
fi
fi
}
if [ "$UNDO" = 1 ]; then
kind="-D"
action="del"
else
action="add"
fi
iptables -t nat "$kind" POSTROUTING -o $iiface -j MASQUERADE
iptables -t nat "$kind" PREROUTING -i $iiface -p tcp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" PREROUTING -i $iiface -p udp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" PREROUTING -i $oiface -s $dip/24 -d $sip -p tcp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" PREROUTING -i $oiface -s $dip/24 -d $sip -p udp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" POSTROUTING -o $oiface -s $dip/24 -d $dip/32 -p tcp --dport $dport -j SNAT --to-source $sip:$sport
iptables -t nat "$kind" POSTROUTING -o $oiface -s $dip/24 -d $dip/32 -p udp --dport $dport -j SNAT --to-source $sip:$sport
iptables -t nat "$kind" PREROUTING -i $iiface -s $sip/32 -d $sip -p tcp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" PREROUTING -i $iiface -s $sip/32 -d $sip -p udp --dport $sport -j DNAT --to-destination $dip:$dport
iptables -t nat "$kind" POSTROUTING -o $oiface -s $sip/32 -d $dip/32 -p tcp --dport $dport -j SNAT --to-source $sip:$sport
iptables -t nat "$kind" POSTROUTING -o $oiface -s $sip/32 -d $dip/32 -p udp --dport $dport -j SNAT --to-source $sip:$sport
apply_nat_rule "$action" PREROUTING -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport
apply_nat_rule "$action" OUTPUT -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport

View File

@@ -83,6 +83,7 @@ local_mount_root()
if [ -d "$image" ]; then
mount -r --bind $image /lower
elif [ -f "$image" ]; then
modprobe loop
modprobe squashfs
mount -r $image /lower
else

View File

@@ -18,7 +18,7 @@ if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" !=
docker run -d --rm --name os-compat --privileged --security-opt apparmor=unconfined -v "${project_pwd}:/root/start-os" -v /lib/modules:/lib/modules:ro start9/build-env
while ! docker exec os-compat systemctl is-active --quiet multi-user.target 2> /dev/null; do sleep .5; done
docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@
docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@
code=$?
docker stop os-compat
exit $code

View File

@@ -4,13 +4,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e
rm -rf web/dist/static
STATIC_DIR=web/dist/static/$1
RAW_DIR=web/dist/raw/$1
mkdir -p $STATIC_DIR
rm -rf $STATIC_DIR
if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
for file in $(find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br'); do
for file in $(find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br'); do
raw_size=$(du $file | awk '{print $1 * 512}')
gz_size=$(du $file.gz | awk '{print $1 * 512}')
br_size=$(du $file.br | awk '{print $1 * 512}')
@@ -23,4 +27,5 @@ if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
done
fi
cp -r web/dist/raw web/dist/static
cp -r $RAW_DIR $STATIC_DIR

View File

@@ -6,13 +6,9 @@ mkdir -p /run/systemd/resolve
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
apt-get update
apt-get install -y curl rsync qemu-user-static
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 22
ln -s $(which node) /usr/bin/node
apt-get install -y curl rsync qemu-user-static nodejs
sed -i '/\(^\|#\)DNSStubListener=/c\DNSStubListener=no' /etc/systemd/resolved.conf
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
@@ -20,4 +16,7 @@ sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.co
systemctl enable container-runtime.service
rm -rf /run/systemd
rm -rf /run/systemd
rm -f /etc/resolv.conf
echo "nameserver 10.0.3.1" > /etc/resolv.conf

View File

@@ -3,7 +3,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e
DISTRO=debian
VERSION=bookworm
VERSION=trixie
ARCH=${ARCH:-$(uname -m)}
FLAVOR=default

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.36",
"version": "0.4.0-beta.42",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
@@ -110,6 +110,7 @@
"integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@@ -1200,6 +1201,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.17"
@@ -2143,6 +2145,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -3990,6 +3993,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -6556,6 +6560,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -4,6 +4,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
if mountpoint -q tmp/combined; then sudo umount -R tmp/combined; fi
if mountpoint -q tmp/lower; then sudo umount tmp/lower; fi
sudo rm -rf tmp
@@ -39,8 +44,8 @@ sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runt
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service
sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container $@' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
sudo cp ../core/target/${RUST_ARCH}-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container "$@"' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
sudo chmod +x tmp/combined/usr/bin/start-cli
sudo chown 0:0 tmp/combined/usr/bin/start-container
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id

3998
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

2
core/Cross.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
pre-build = ["apt-get update && apt-get install -y rsync"]

View File

@@ -5,50 +5,52 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "${ARCH:-}" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
if [ -z "$KERNEL_NAME" ]; then
KERNEL_NAME=$(uname -s)
if [ -z "${KERNEL_NAME:-}" ]; then
KERNEL_NAME=$(uname -s)
fi
if [ -z "$TARGET" ]; then
if [ "$KERNEL_NAME" = "Linux" ]; then
TARGET="$ARCH-unknown-linux-musl"
elif [ "$KERNEL_NAME" = "Darwin" ]; then
TARGET="$ARCH-apple-darwin"
else
>&2 echo "unknown kernel $KERNEL_NAME"
exit 1
fi
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
if [ -z "${TARGET:-}" ]; then
if [ "$KERNEL_NAME" = "Linux" ]; then
TARGET="$ARCH-unknown-linux-musl"
elif [ "$KERNEL_NAME" = "Darwin" ]; then
TARGET="$ARCH-apple-darwin"
else
>&2 echo "unknown kernel $KERNEL_NAME"
exit 1
fi
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
# Ensure GIT_HASH.txt exists if not created by higher-level build steps
if [ ! -f GIT_HASH.txt ] && command -v git >/dev/null 2>&1; then
git rev-parse HEAD > GIT_HASH.txt || true
fi
if which zig > /dev/null && [ "$ENFORCE_USE_DOCKER" != 1 ]; do
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
RUSTFLAGS=$RUSTFLAGS sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET"
else
alias 'rust-zig-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/cargo-zigbuild'
RUSTFLAGS=$RUSTFLAGS rust-zig-builder sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET"
FEATURES="$(echo "${ENVIRONMENT:-}" | sed 's/-/,/g')"
FEATURE_ARGS="cli"
if [ -n "$FEATURES" ]; then
FEATURE_ARGS="$FEATURE_ARGS,$FEATURES"
fi
if [ "$(ls -nd core/target/$TARGET/release/start-cli | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
fi
RUSTFLAGS=""
if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
cross build --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features $FEATURE_ARGS --locked --bin start-cli --target=$TARGET

View File

@@ -5,6 +5,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
@@ -13,24 +18,19 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-container,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl"
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/containerbox | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross build --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features cli-container,$FEATURES --locked --bin containerbox --target=$RUST_ARCH-unknown-linux-musl

View File

@@ -5,6 +5,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
@@ -13,24 +18,19 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-registry,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl"
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/registrybox | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross build --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features cli-registry,registry,$FEATURES --locked --bin registrybox --target=$RUST_ARCH-unknown-linux-musl

View File

@@ -1,15 +1,15 @@
#!/bin/bash
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
@@ -18,24 +18,19 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build $BUILD_FLAGS --no-default-features --features cli,startd,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl"
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/${PROFILE}/startbox | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross build --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features cli,startd,$FEATURES --locked --bin startbox --target=$RUST_ARCH-unknown-linux-musl

View File

@@ -5,32 +5,30 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo test --release --features=test,$FEATURES 'export_bindings_' && chown \$UID:\$UID startos/bindings"
if [ "$(ls -nd core/startos/bindings | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID startos/bindings && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross test --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features test,$FEATURES --locked 'export_bindings_'

View File

@@ -5,6 +5,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
@@ -13,24 +18,19 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli-tunnel,tunnel,$FEATURES --locked --bin tunnelbox --target=$ARCH-unknown-linux-musl"
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/tunnelbox | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross build --manifest-path=./core/Cargo.toml $BUILD_FLAGS --no-default-features --features cli-tunnel,tunnel,$FEATURES --locked --bin tunnelbox --target=$RUST_ARCH-unknown-linux-musl

View File

@@ -1,24 +1,28 @@
[package]
edition = "2021"
name = "models"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
arti = ["arti-client"]
[dependencies]
arti-client = { version = "0.33", default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
arti-client = { version = "0.33", default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
axum = "0.8.4"
base64 = "0.22.1"
color-eyre = "0.6.2"
ed25519-dalek = { version = "2.0.0", features = ["serde"] }
gpt = "4.1.0"
lazy_static = "1.4"
lettre = { version = "0.11", default-features = false }
mbrman = "0.6.0"
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
"serde",
] }
gpt = "4.1.0"
ipnet = "2.8.0"
lazy_static = "1.4"
lettre = { version = "0.11", default-features = false }
mbrman = "0.6.0"
miette = "7.6.0"
num_enum = "0.7.1"
openssl = { version = "0.10.57", features = ["vendored"] }
patch-db = { version = "*", path = "../../patch-db/patch-db", features = [
@@ -32,9 +36,11 @@ rustls = "0.23"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
ssh-key = "0.6.2"
ts-rs = "9"
thiserror = "2.0"
tokio = { version = "1", features = ["full"] }
torut = "0.2.1"
tracing = "0.1.39"
ts-rs = "9"
typeid = "1"
yasi = { version = "0.1.6", features = ["serde", "ts-rs"] }
zbus = "5"

View File

@@ -1,5 +1,6 @@
use std::borrow::Cow;
use std::path::Path;
use std::str::FromStr;
use base64::Engine;
use color_eyre::eyre::eyre;
@@ -14,28 +15,26 @@ use crate::{mime, Error, ErrorKind, ResultExt};
#[derive(Clone, TS)]
#[ts(type = "string")]
pub struct DataUrl<'a> {
mime: InternedString,
data: Cow<'a, [u8]>,
pub mime: InternedString,
pub data: Cow<'a, [u8]>,
}
impl<'a> DataUrl<'a> {
pub const DEFAULT_MIME: &'static str = "application/octet-stream";
pub const MAX_SIZE: u64 = 100 * 1024;
// data:{mime};base64,{data}
pub fn to_string(&self) -> String {
fn to_string(&self) -> String {
use std::fmt::Write;
let mut res = String::with_capacity(self.data_url_len_without_mime() + self.mime.len());
let _ = write!(res, "data:{};base64,", self.mime);
base64::engine::general_purpose::STANDARD.encode_string(&self.data, &mut res);
let mut res = String::with_capacity(self.len());
write!(&mut res, "{self}").unwrap();
res
}
fn data_url_len_without_mime(&self) -> usize {
fn len_without_mime(&self) -> usize {
5 + 8 + (4 * self.data.len() / 3) + 3
}
pub fn data_url_len(&self) -> usize {
self.data_url_len_without_mime() + self.mime.len()
pub fn len(&self) -> usize {
self.len_without_mime() + self.mime.len()
}
pub fn from_slice(mime: &str, data: &'a [u8]) -> Self {
@@ -44,6 +43,10 @@ impl<'a> DataUrl<'a> {
data: Cow::Borrowed(data),
}
}
pub fn canonical_ext(&self) -> Option<&'static str> {
mime::unmime(&self.mime)
}
}
impl DataUrl<'static> {
pub async fn from_reader(
@@ -109,12 +112,57 @@ impl DataUrl<'static> {
}
}
impl<'a> std::fmt::Display for DataUrl<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"data:{};base64,{}",
self.mime,
base64::display::Base64Display::new(
&*self.data,
&base64::engine::general_purpose::STANDARD
)
)
}
}
impl<'a> std::fmt::Debug for DataUrl<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_string())
std::fmt::Display::fmt(self, f)
}
}
#[derive(Debug)]
pub struct DataUrlParseError;
impl std::fmt::Display for DataUrlParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid base64 url")
}
}
impl std::error::Error for DataUrlParseError {}
impl From<DataUrlParseError> for Error {
fn from(e: DataUrlParseError) -> Self {
Error::new(e, ErrorKind::ParseUrl)
}
}
impl FromStr for DataUrl<'static> {
type Err = DataUrlParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.strip_prefix("data:")
.and_then(|v| v.split_once(";base64,"))
.and_then(|(mime, data)| {
Some(DataUrl {
mime: InternedString::intern(mime),
data: Cow::Owned(
base64::engine::general_purpose::STANDARD
.decode(data)
.ok()?,
),
})
})
.ok_or(DataUrlParseError)
}
}
impl<'de> Deserialize<'de> for DataUrl<'static> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -130,21 +178,9 @@ impl<'de> Deserialize<'de> for DataUrl<'static> {
where
E: serde::de::Error,
{
v.strip_prefix("data:")
.and_then(|v| v.split_once(";base64,"))
.and_then(|(mime, data)| {
Some(DataUrl {
mime: InternedString::intern(mime),
data: Cow::Owned(
base64::engine::general_purpose::STANDARD
.decode(data)
.ok()?,
),
})
})
.ok_or_else(|| {
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid base64 data url")
})
v.parse().map_err(|_| {
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid base64 data url")
})
}
}
deserializer.deserialize_any(Visitor)
@@ -168,6 +204,6 @@ fn doesnt_reallocate() {
mime: InternedString::intern("png"),
data: Cow::Borrowed(&random[..i]),
};
assert_eq!(icon.to_string().capacity(), icon.data_url_len());
assert_eq!(icon.to_string().capacity(), icon.len());
}
}

View File

@@ -187,9 +187,9 @@ impl Display for ErrorKind {
}
}
#[derive(Debug)]
pub struct Error {
pub source: color_eyre::eyre::Error,
pub debug: Option<color_eyre::eyre::Error>,
pub kind: ErrorKind,
pub revision: Option<Revision>,
pub task: Option<JoinHandle<()>>,
@@ -197,13 +197,29 @@ pub struct Error {
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.kind.as_str(), self.source)
write!(f, "{}: {:#}", self.kind.as_str(), self.source)
}
}
impl Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {:?}",
self.kind.as_str(),
self.debug.as_ref().unwrap_or(&self.source)
)
}
}
impl Error {
pub fn new<E: Into<color_eyre::eyre::Error>>(source: E, kind: ErrorKind) -> Self {
pub fn new<E: Into<color_eyre::eyre::Error> + std::fmt::Debug + 'static>(
source: E,
kind: ErrorKind,
) -> Self {
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{source:?}"));
Error {
source: source.into(),
debug,
kind,
revision: None,
task: None,
@@ -211,11 +227,8 @@ impl Error {
}
pub fn clone_output(&self) -> Self {
Error {
source: ErrorData {
details: format!("{}", self.source),
debug: format!("{:?}", self.source),
}
.into(),
source: eyre!("{}", self.source),
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
kind: self.kind,
revision: self.revision.clone(),
task: None,
@@ -345,11 +358,17 @@ impl From<reqwest::Error> for Error {
Error::new(e, kind)
}
}
#[cfg(feature = "arti")]
impl From<arti_client::Error> for Error {
fn from(e: arti_client::Error) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<torut::control::ConnError> for Error {
fn from(e: torut::control::ConnError) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self {
Error::new(e, ErrorKind::DBus)
@@ -360,18 +379,6 @@ impl From<rustls::Error> for Error {
Error::new(e, ErrorKind::OpenSsl)
}
}
impl From<patch_db::value::Error> for Error {
fn from(value: patch_db::value::Error) -> Self {
match value.kind {
patch_db::value::ErrorKind::Serialization => {
Error::new(value.source, ErrorKind::Serialization)
}
patch_db::value::ErrorKind::Deserialization => {
Error::new(value.source, ErrorKind::Deserialization)
}
}
}
}
impl From<lettre::error::Error> for Error {
fn from(e: lettre::error::Error) -> Self {
Error::new(e, ErrorKind::Smtp)
@@ -387,6 +394,18 @@ impl From<lettre::address::AddressError> for Error {
Error::new(e, ErrorKind::Smtp)
}
}
impl From<patch_db::value::Error> for Error {
fn from(value: patch_db::value::Error) -> Self {
match value.kind {
patch_db::value::ErrorKind::Serialization => {
Error::new(value.source, ErrorKind::Serialization)
}
patch_db::value::ErrorKind::Deserialization => {
Error::new(value.source, ErrorKind::Deserialization)
}
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ErrorData {
@@ -556,25 +575,24 @@ where
impl<T, E> ResultExt<T, E> for Result<T, E>
where
color_eyre::eyre::Error: From<E>,
E: std::fmt::Debug + 'static,
{
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.into(),
kind,
revision: None,
task: None,
})
self.map_err(|e| Error::new(e, kind))
}
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{ctx}: {e:?}"));
let source = color_eyre::eyre::Error::from(e);
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(with_ctx);
Error {
kind,
source,
debug,
revision: None,
task: None,
}
@@ -595,25 +613,24 @@ where
}
impl<T> ResultExt<T, Error> for Result<T, Error> {
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.source,
kind,
revision: e.revision,
task: e.task,
})
self.map_err(|e| Error { kind, ..e })
}
fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let source = e.source;
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(with_ctx);
let debug = e.debug.map(|e| {
let with_ctx = format!("{ctx}: {e}");
e.wrap_err(with_ctx)
});
Error {
kind,
source,
revision: e.revision,
task: e.task,
debug,
..e
}
})
}

View File

@@ -14,12 +14,14 @@ impl GatewayId {
&*self.0
}
}
impl<T> From<T> for GatewayId
where
T: Into<InternedString>,
{
fn from(value: T) -> Self {
Self(value.into())
impl From<InternedString> for GatewayId {
fn from(value: InternedString) -> Self {
Self(value)
}
}
impl From<GatewayId> for InternedString {
fn from(value: GatewayId) -> Self {
value.0
}
}
impl FromStr for GatewayId {

View File

@@ -5,6 +5,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
@@ -22,15 +27,12 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
source ./core/builder-alias.sh
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "apt-get update && apt-get install -y rsync && cd core && cargo test --release --features=test,$FEATURES --workspace --locked --target=$ARCH-unknown-linux-musl -- --skip export_bindings_ && chown \$UID:\$UID target"
if [ "$(ls -nd core/target | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
cross test --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=test,$FEATURES --workspace --locked --target=$ARCH-unknown-linux-musl -- --skip export_bindings_

View File

@@ -4,18 +4,18 @@ description = "The core of StartOS"
documentation = "https://docs.rs/start-os"
edition = "2024"
keywords = [
"self-hosted",
"raspberry-pi",
"privacy",
"bitcoin",
"full-node",
"lightning",
"privacy",
"raspberry-pi",
"self-hosted",
]
license = "MIT"
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.10" # VERSION_BUMP
license = "MIT"
version = "0.4.0-alpha.12" # VERSION_BUMP
[lib]
name = "startos"
@@ -42,72 +42,88 @@ name = "tunnelbox"
path = "src/main.rs"
[features]
cli = ["cli-startd", "cli-registry", "cli-tunnel"]
arti = [
"arti-client",
"models/arti",
"safelog",
"tor-cell",
"tor-hscrypto",
"tor-hsservice",
"tor-keymgr",
"tor-llcrypto",
"tor-proto",
"tor-rtcompat",
]
cli = ["cli-registry", "cli-startd", "cli-tunnel"]
cli-container = ["procfs", "pty-process"]
cli-registry = []
cli-startd = []
cli-tunnel = []
default = ["cli", "startd", "registry", "cli-container", "tunnel"]
dev = []
console = ["console-subscriber", "tokio/tracing"]
default = ["cli", "cli-container", "registry", "startd", "tunnel"]
dev = ["backtrace-on-stack-overflow"]
docker = []
registry = []
startd = []
test = []
tunnel = []
unstable = ["console-subscriber", "tokio/tracing"]
unstable = ["backtrace-on-stack-overflow"]
[dependencies]
aes = { version = "0.7.5", features = ["ctr"] }
arti-client = { version = "0.33", features = [
"compression",
"ephemeral-keystore",
"experimental-api",
"onion-service-client",
"onion-service-service",
"rustls",
"static",
"tokio",
"ephemeral-keystore",
"onion-service-client",
"onion-service-service",
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
aes = { version = "0.7.5", features = ["ctr"] }
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls",
"use_tokio",
] }
async-compression = { version = "0.4.4", features = [
"gzip",
async-compression = { version = "0.4.32", features = [
"brotli",
"gzip",
"tokio",
"zstd",
] }
async-stream = "0.3.5"
async-trait = "0.1.74"
aws-lc-sys = { version = "0.32", features = ["bindgen"] }
axum = { version = "0.8.4", features = ["ws"] }
backtrace-on-stack-overflow = { version = "0.3.0", optional = true }
barrage = "0.2.3"
backhand = "0.23.0"
base32 = "0.5.0"
base64 = "0.22.1"
base64ct = "1.6.0"
basic-cookies = "0.1.4"
bech32 = "0.11.0"
blake3 = { version = "1.5.0", features = ["mmap", "rayon"] }
bytes = "1"
chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.12", features = ["string"] }
color-eyre = "0.6.2"
console = "0.16.0"
console = "0.15.7"
console-subscriber = { version = "0.4.1", optional = true }
const_format = "0.2.34"
cookie = "0.18.0"
cookie_store = "0.22.0"
cookie_store = "0.21.0"
curve25519-dalek = "4.1.3"
der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7"
divrem = "1.0.0"
dns-lookup = "3.0.0"
ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] }
dns-lookup = "2.1.0"
ed25519 = { version = "2.2.3", features = ["alloc", "pem", "pkcs8"] }
ed25519-dalek = { version = "2.2.0", features = [
"digest",
"hazmat",
"pkcs8",
"rand_core",
"serde",
"zeroize",
"rand_core",
"digest",
"pkcs8",
] }
ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" }
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
@@ -124,29 +140,29 @@ hickory-server = "0.25.2"
hmac = "0.12.1"
http = "1.0.0"
http-body-util = "0.1"
hyper = { version = "1.5", features = ["server", "http1", "http2"] }
hyper = { version = "1.5", features = ["http1", "http2", "server"] }
hyper-util = { version = "0.1.10", features = [
"http1",
"http2",
"server",
"server-auto",
"server-graceful",
"service",
"http1",
"http2",
"tokio",
] }
id-pool = { version = "0.2.2", default-features = false, features = [
"serde",
"u16",
] }
iddqd = "0.3.14"
imbl = { version = "6", features = ["serde", "small-chunks"] }
imbl-value = { version = "0.4.3", features = ["ts-rs"] }
include_dir = { version = "0.7.3", features = ["metadata"] }
indexmap = { version = "2.0.2", features = ["serde"] }
indicatif = { version = "0.18.0", features = ["tokio"] }
indicatif = { version = "0.17.7", features = ["tokio"] }
inotify = "0.11.0"
integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
ipnet = { version = "2.8.0", features = ["serde"] }
iroh = { version = "0.91.2", features = ["discovery-pkarr-dht"] }
isocountry = "0.3.2"
itertools = "0.14.0"
jaq-core = "0.10.1"
@@ -155,20 +171,21 @@ josekit = "0.10.3"
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
lazy_async_pool = "0.3.3"
lazy_format = "2.0"
lazy_static = "1.5.0"
lazy_static = "1.4.0"
lettre = { version = "0.11.18", default-features = false, features = [
"smtp-transport",
"pool",
"hostname",
"builder",
"tokio1-rustls",
"rustls-platform-verifier",
"aws-lc-rs",
"builder",
"hostname",
"pool",
"rustls-platform-verifier",
"smtp-transport",
"tokio1-rustls",
] }
libc = "0.2.149"
log = "0.4.20"
mio = "1"
mbrman = "0.6.0"
miette = { version = "7.6.0", features = ["fancy"] }
mio = "1"
models = { version = "*", path = "../models" }
new_mime_guess = "4"
nix = { version = "0.30.1", features = [
@@ -182,8 +199,8 @@ nix = { version = "0.30.1", features = [
] }
nom = "8.0.0"
num = "0.4.1"
num_enum = "0.7.0"
num_cpus = "1.16.0"
num_enum = "0.7.0"
once_cell = "1.19.0"
openssh-keys = "0.6.2"
openssl = { version = "0.10.57", features = ["vendored"] }
@@ -197,25 +214,25 @@ pkcs8 = { version = "0.10.2", features = ["std"] }
prettytable-rs = "0.10.0"
procfs = { version = "0.17.0", optional = true }
proptest = "1.3.1"
proptest-derive = "0.6.0"
proptest-derive = "0.5.0"
pty-process = { version = "0.5.1", optional = true }
qrcode = "0.14.1"
r3bl_tui = "0.7.6"
rand = "0.9.2"
regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] }
reqwest_cookie_store = "0.9.0"
reqwest = { version = "0.12.4", features = ["json", "socks", "stream"] }
reqwest_cookie_store = "0.8.0"
rpassword = "7.2.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
rust-argon2 = "3.0.0"
rustyline-async = "0.4.1"
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
rust-argon2 = "2.0.0"
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0"
serde_toml = { package = "toml", version = "0.9.5" }
serde_toml = { package = "toml", version = "0.8.2" }
serde_urlencoded = "0.7"
serde_with = { version = "3.4.0", features = ["macros", "json"] }
serde_with = { version = "3.4.0", features = ["json", "macros"] }
serde_yaml = { package = "serde_yml", version = "0.0.12" }
sha-crypt = "0.5.0"
sha2 = "0.10.2"
@@ -223,39 +240,40 @@ shell-words = "1"
signal-hook = "0.3.17"
simple-logging = "2.0.2"
socket2 = { version = "0.6.0", features = ["all"] }
socks5-impl = { version = "0.7.2", features = ["server"] }
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
sqlx = { version = "0.8.6", features = [
"runtime-tokio-rustls",
"postgres",
"runtime-tokio-rustls",
], default-features = false }
sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40"
termion = "4.0.5"
thiserror = "2.0.12"
textwrap = "0.16.1"
thiserror = "2.0.12"
tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.0"
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] }
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.27.0", features = ["native-tls", "url"] }
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] }
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hscrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-keymgr = { version = "0.33", features = [
"ephemeral-keystore",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-llcrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-rtcompat = { version = "0.33", features = [
"tokio",
"rustls",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
"tokio",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
torut = "0.2.1"
tower-service = "0.3.3"
tracing = "0.1.39"
tracing-error = "0.2.0"
@@ -268,10 +286,10 @@ unix-named-pipe = "0.2.0"
url = { version = "2.4.1", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.4.1", features = ["v4"] }
visit-rs = "0.1.1"
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
zbus = "5.1.1"
zeroize = "1.6.0"
rustls = "0.23.20"
rustls-pki-types = { version = "1.10.1", features = ["alloc"] }
[profile.test]
opt-level = 3

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::time::SystemTime;
use imbl_value::InternedString;
@@ -107,6 +108,7 @@ impl AccountInfo {
.map(|tor_key| tor_key.onion_address())
.collect(),
)?;
server_info.as_password_hash_mut().ser(&self.password)?;
db.as_private_mut().as_password_mut().ser(&self.password)?;
db.as_private_mut()
.as_ssh_privkey_mut()
@@ -119,12 +121,20 @@ impl AccountInfo {
key_store.as_onion_mut().insert_key(tor_key)?;
}
let cert_store = key_store.as_local_certs_mut();
cert_store
.as_root_key_mut()
.ser(Pem::new_ref(&self.root_ca_key))?;
cert_store
.as_root_cert_mut()
.ser(Pem::new_ref(&self.root_ca_cert))?;
if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
cert_store
.as_root_key_mut()
.ser(Pem::new_ref(&self.root_ca_key))?;
cert_store
.as_root_cert_mut()
.ser(Pem::new_ref(&self.root_ca_cert))?;
let int_key = crate::net::ssl::generate_key()?;
let int_cert =
crate::net::ssl::make_int_cert((&self.root_ca_key, &self.root_ca_cert), &int_key)?;
cert_store.as_int_key_mut().ser(&Pem(int_key))?;
cert_store.as_int_cert_mut().ser(&Pem(int_cert))?;
cert_store.as_leaves_mut().ser(&BTreeMap::new())?;
}
Ok(())
}

View File

@@ -52,6 +52,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ActionInput {
#[serde(default)]
pub event_id: Guid,
#[ts(type = "Record<string, unknown>")]
pub spec: Value,

View File

@@ -220,7 +220,7 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
pub struct LoginParams {
password: String,
#[ts(skip)]
#[serde(rename = "__auth_userAgent")] // from Auth middleware
#[serde(rename = "__Auth_userAgent")] // from Auth middleware
user_agent: Option<String>,
#[serde(default)]
ephemeral: bool,
@@ -279,7 +279,7 @@ pub async fn login_impl<C: AuthContext>(
#[command(rename_all = "kebab-case")]
pub struct LogoutParams {
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
#[serde(rename = "__Auth_session")] // from Auth middleware
session: InternedString,
}
@@ -373,7 +373,7 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) -> Resul
pub struct ListParams {
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
#[serde(rename = "__Auth_session")] // from Auth middleware
session: Option<InternedString>,
}
@@ -474,30 +474,19 @@ pub async fn reset_password_impl(
let old_password = old_password.unwrap_or_default().decrypt(&ctx)?;
let new_password = new_password.unwrap_or_default().decrypt(&ctx)?;
let mut account = ctx.account.write().await;
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword,
));
}
account.set_password(&new_password)?;
let account_password = &account.password;
let account = account.clone();
ctx.db
.mutate(|d| {
d.as_public_mut()
.as_server_info_mut()
.as_password_hash_mut()
.ser(account_password)?;
account.save(d)?;
Ok(())
})
.await
.result
let account = ctx.account.mutate(|account| {
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword,
));
}
account.set_password(&new_password)?;
Ok(account.clone())
})?;
ctx.db.mutate(|d| account.save(d)).await.result
}
#[instrument(skip_all)]

View File

@@ -317,7 +317,7 @@ async fn perform_backup(
.with_kind(ErrorKind::Filesystem)?;
os_backup_file
.write_all(&IoFormat::Json.to_vec(&OsBackup {
account: ctx.account.read().await.clone(),
account: ctx.account.peek(|a| a.clone()),
ui,
})?)
.await?;
@@ -342,7 +342,7 @@ async fn perform_backup(
let timestamp = Utc::now();
backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into();
backup_guard.unencrypted_metadata.hostname = ctx.account.read().await.hostname.clone();
backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.hostname.clone());
backup_guard.unencrypted_metadata.timestamp = timestamp.clone();
backup_guard.metadata.version = crate::version::Current::default().semver().into();
backup_guard.metadata.timestamp = Some(timestamp);

View File

@@ -11,14 +11,17 @@ use crate::context::config::ClientConfig;
use crate::net::web_server::{Acceptor, WebServer};
use crate::prelude::*;
use crate::registry::context::{RegistryConfig, RegistryContext};
use crate::registry::registry_router;
use crate::util::logger::LOGGER;
#[instrument(skip_all)]
async fn inner_main(config: &RegistryConfig) -> Result<(), Error> {
let server = async {
let ctx = RegistryContext::init(config).await?;
let mut server = WebServer::new(Acceptor::bind([ctx.listen]).await?);
server.serve_registry(ctx.clone());
let server = WebServer::new(
Acceptor::bind([ctx.listen]).await?,
registry_router(ctx.clone()),
);
let mut shutdown_recv = ctx.shutdown.subscribe();

View File

@@ -17,7 +17,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
if let Err(e) = CliApp::new(
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::expanded_api(),
crate::main_api(),
)
.run(args)
{

View File

@@ -11,7 +11,8 @@ use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
use crate::firmware::{check_for_firmware_update, update_firmware};
use crate::init::{InitPhases, STANDBY_MODE_PATH};
use crate::net::web_server::{UpgradableListener, WebServer};
use crate::net::gateway::UpgradableListener;
use crate::net::web_server::WebServer;
use crate::prelude::*;
use crate::progress::FullProgressTracker;
use crate::shutdown::Shutdown;
@@ -37,7 +38,7 @@ async fn setup_or_init(
let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10));
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
update_phase.start();
if let Err(e) = update_firmware(firmware).await {
@@ -93,7 +94,7 @@ async fn setup_or_init(
let ctx = InstallContext::init().await?;
server.serve_install(ctx.clone());
server.serve_ui_for(ctx.clone());
ctx.shutdown
.subscribe()
@@ -113,7 +114,7 @@ async fn setup_or_init(
{
let ctx = SetupContext::init(server, config)?;
server.serve_setup(ctx.clone());
server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe();
if let Some(shutdown) = shutdown.recv().await.expect("context dropped") {
@@ -149,7 +150,7 @@ async fn setup_or_init(
let init_phases = InitPhases::new(&handle);
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
async {
disk_phase.start();
@@ -247,7 +248,7 @@ pub async fn main(
e,
)?;
server.serve_diagnostic(ctx.clone());
server.serve_ui_for(ctx.clone());
let shutdown = ctx.shutdown.subscribe().recv().await.unwrap();

View File

@@ -12,8 +12,9 @@ use tracing::instrument;
use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, RpcContext};
use crate::net::gateway::SelfContainedNetworkInterfaceListener;
use crate::net::web_server::{Acceptor, UpgradableListener, WebServer};
use crate::net::gateway::{BindTcp, SelfContainedNetworkInterfaceListener, UpgradableListener};
use crate::net::static_server::refresher;
use crate::net::web_server::{Acceptor, WebServer};
use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task;
use crate::util::io::append_file;
@@ -38,7 +39,7 @@ async fn inner_main(
};
tokio::fs::write("/run/startos/initialized", "").await?;
server.serve_main(ctx.clone());
server.serve_ui_for(ctx.clone());
LOGGER.set_logfile(None);
handle.complete();
@@ -47,7 +48,7 @@ async fn inner_main(
let init_ctx = InitContext::init(config).await?;
let handle = init_ctx.progress.clone();
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx);
server.serve_ui_for(init_ctx);
let ctx = RpcContext::init(
&server.acceptor_setter(),
@@ -63,14 +64,14 @@ async fn inner_main(
)
.await?;
server.serve_main(ctx.clone());
server.serve_ui_for(ctx.clone());
handle.complete();
ctx
};
let (rpc_ctx, shutdown) = async {
crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?;
crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.clone())).await?;
let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
@@ -132,8 +133,6 @@ async fn inner_main(
.await?;
rpc_ctx.shutdown().await?;
tracing::info!("RPC Context is dropped");
Ok(shutdown)
}
@@ -149,9 +148,10 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
.build()
.expect("failed to initialize runtime");
let res = rt.block_on(async {
let mut server = WebServer::new(Acceptor::bind_upgradable(
SelfContainedNetworkInterfaceListener::bind(80),
));
let mut server = WebServer::new(
Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)),
refresher(),
);
match inner_main(&mut server, &config).await {
Ok(a) => {
server.shutdown().await;
@@ -179,7 +179,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
e,
)?;
server.serve_diagnostic(ctx.clone());
server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe();

View File

@@ -1,29 +1,110 @@
use std::ffi::OsString;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use futures::FutureExt;
use helpers::NonDetachingJoinHandle;
use rpc_toolkit::CliApp;
use tokio::signal::unix::signal;
use tracing::instrument;
use visit_rs::Visit;
use crate::context::CliContext;
use crate::context::config::ClientConfig;
use crate::net::web_server::{Acceptor, WebServer};
use crate::net::gateway::{Bind, BindTcp};
use crate::net::tls::TlsListener;
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
use crate::prelude::*;
use crate::tunnel::context::{TunnelConfig, TunnelContext};
use crate::tunnel::tunnel_router;
use crate::tunnel::web::TunnelCertHandler;
use crate::util::logger::LOGGER;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum WebserverListener {
Http,
Https(SocketAddr),
}
impl<V: MetadataVisitor> Visit<V> for WebserverListener {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
visitor.visit(self)
}
}
#[instrument(skip_all)]
async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
let server = async {
let ctx = TunnelContext::init(config).await?;
let mut server = WebServer::new(Acceptor::bind([ctx.listen]).await?);
server.serve_tunnel(ctx.clone());
let listen = ctx.listen;
let server = WebServer::new(
Acceptor::bind_map_dyn([(WebserverListener::Http, listen)]).await?,
tunnel_router(ctx.clone()),
);
let acceptor_setter = server.acceptor_setter();
let https_db = ctx.db.clone();
let https_thread: NonDetachingJoinHandle<()> = tokio::spawn(async move {
let mut sub = https_db.subscribe("/webserver".parse().unwrap()).await;
while {
while let Err(e) = async {
let webserver = https_db.peek().await.into_webserver();
if webserver.as_enabled().de()? {
let addr = webserver.as_listen().de()?.or_not_found("listen address")?;
acceptor_setter.send_if_modified(|a| {
let key = WebserverListener::Https(addr);
if !a.contains_key(&key) {
match (|| {
Ok::<_, Error>(TlsListener::new(
BindTcp.bind(addr)?,
TunnelCertHandler {
db: https_db.clone(),
crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
},
))
})() {
Ok(l) => {
a.retain(|k, _| *k == WebserverListener::Http);
a.insert(key, l.into_dyn());
true
}
Err(e) => {
tracing::error!("error adding ssl listener: {e}");
tracing::debug!("{e:?}");
false
}
}
} else {
false
}
});
} else {
acceptor_setter.send_if_modified(|a| {
let before = a.len();
a.retain(|k, _| *k == WebserverListener::Http);
a.len() != before
});
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("error updating webserver bind: {e}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
sub.recv().await.is_some()
} {}
})
.into();
let mut shutdown_recv = ctx.shutdown.subscribe();
let sig_handler_ctx = ctx;
let sig_handler = tokio::spawn(async move {
let sig_handler: NonDetachingJoinHandle<()> = tokio::spawn(async move {
use tokio::signal::unix::SignalKind;
futures::future::select_all(
[
@@ -48,14 +129,16 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
.send(())
.map_err(|_| ())
.expect("send shutdown signal");
});
})
.into();
shutdown_recv
.recv()
.await
.with_kind(crate::ErrorKind::Unknown)?;
sig_handler.abort();
sig_handler.wait_for_abort().await.with_kind(ErrorKind::Unknown)?;
https_thread.wait_for_abort().await.with_kind(ErrorKind::Unknown)?;
Ok::<_, Error>(server)
}

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore;
use http::HeaderMap;
use imbl_value::InternedString;
use josekit::jwk::Jwk;
use once_cell::sync::OnceCell;
@@ -26,7 +27,7 @@ use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path};
use crate::middleware::auth::AuthContext;
use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::tunnel::context::TunnelContext;
use crate::util::io::read_file_to_string;
#[derive(Debug)]
pub struct CliContextSeed {
@@ -159,7 +160,7 @@ impl CliContext {
continue;
}
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
&std::fs::read_to_string(&self.developer_key_path)?,
&std::fs::read_to_string(path)?,
)
.with_kind(crate::ErrorKind::Pem)?;
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
@@ -171,7 +172,7 @@ impl CliContext {
return Ok(secret.into())
}
Err(Error::new(
eyre!("Developer Key does not exist! Please run `start-cli init` before running this command."),
eyre!("Developer Key does not exist! Please run `start-cli init-key` before running this command."),
crate::ErrorKind::Uninitialized
))
})
@@ -233,23 +234,28 @@ impl CliContext {
&self,
method: &str,
params: Value,
) -> Result<Value, RpcError>
) -> Result<Value, Error>
where
Self: CallRemote<RemoteContext>,
{
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {})
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
}
pub async fn call_remote_with<RemoteContext, T>(
&self,
method: &str,
params: Value,
extra: T,
) -> Result<Value, RpcError>
) -> Result<Value, Error>
where
Self: CallRemote<RemoteContext, T>,
{
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
}
}
impl AsRef<Jwk> for CliContext {
@@ -279,9 +285,15 @@ impl Context for CliContext {
)
}
}
impl AsRef<Client> for CliContext {
fn as_ref(&self) -> &Client {
&self.client
}
}
impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
if let Ok(local) = std::fs::read_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH) {
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
.unwrap()
@@ -298,7 +310,8 @@ impl CallRemote<RpcContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -307,24 +320,11 @@ impl CallRemote<RpcContext> for CliContext {
}
impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
if let Ok(local) = std::fs::read_to_string(TunnelContext::LOCAL_AUTH_COOKIE_PATH) {
self.cookie_store
.lock()
.unwrap()
.insert_raw(
&Cookie::build(("local", local))
.domain("localhost")
.expires(Expiration::Session)
.same_site(SameSite::Strict)
.build(),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
}
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -336,7 +336,8 @@ impl CallRemote<InitContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -348,7 +349,8 @@ impl CallRemote<SetupContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
@@ -360,22 +362,11 @@ impl CallRemote<InstallContext> for CliContext {
crate::middleware::signature::call_remote(
self,
self.rpc_url.clone(),
self.rpc_url.host_str().or_not_found("rpc url hostname")?,
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
}
}
#[test]
fn test() {
let ctx = CliContext::init(ClientConfig::default()).unwrap();
ctx.runtime().unwrap().block_on(async {
reqwest::Client::new()
.get("http://example.com")
.send()
.await
.unwrap();
});
}

View File

@@ -25,10 +25,12 @@ impl InitContext {
#[instrument(skip_all)]
pub async fn init(cfg: &ServerConfig) -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let mut progress = FullProgressTracker::new();
progress.enable_logging(true);
Ok(Self(Arc::new(InitContextSeed {
config: cfg.clone(),
error: watch::channel(None).0,
progress: FullProgressTracker::new(),
progress,
shutdown,
rpc_continuations: RpcContinuations::new(),
})))

View File

@@ -1,11 +1,10 @@
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use chrono::{TimeDelta, Utc};
@@ -18,36 +17,37 @@ use models::{ActionId, PackageId};
use reqwest::{Client, Proxy};
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{CallRemote, Context, Empty};
use tokio::sync::{broadcast, oneshot, watch, RwLock};
use tokio::sync::{RwLock, broadcast, oneshot, watch};
use tokio::time::Instant;
use tracing::instrument;
use super::setup::CURRENT_SECRET;
use crate::DATA_DIR;
use crate::account::AccountInfo;
use crate::auth::Sessions;
use crate::context::config::ServerConfig;
use crate::db::model::package::TaskSeverity;
use crate::db::model::Database;
use crate::db::model::package::TaskSeverity;
use crate::disk::OsPartitionInfo;
use crate::init::{check_time_is_synchronized, InitResult};
use crate::init::{InitResult, check_time_is_synchronized};
use crate::install::PKG_ARCHIVE_DIR;
use crate::lxc::LxcManager;
use crate::net::gateway::UpgradableListener;
use crate::net::net_controller::{NetController, NetService};
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
use crate::net::utils::{find_eth_iface, find_wifi_iface};
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
use crate::net::web_server::WebServerAcceptorSetter;
use crate::net::wifi::WpaCli;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations};
use crate::service::ServiceMap;
use crate::service::action::update_tasks;
use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::ServiceMap;
use crate::shutdown::Shutdown;
use crate::util::io::delete_file;
use crate::util::lshw::LshwDevice;
use crate::util::sync::{SyncMutex, Watch};
use crate::{DATA_DIR, HOST_IP};
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
pub struct RpcContextSeed {
is_closed: AtomicBool,
@@ -58,7 +58,7 @@ pub struct RpcContextSeed {
pub ephemeral_sessions: SyncMutex<Sessions>,
pub db: TypedPatchDb<Database>,
pub sync_db: watch::Sender<u64>,
pub account: RwLock<AccountInfo>,
pub account: SyncRwLock<AccountInfo>,
pub net_controller: Arc<NetController>,
pub os_net_service: NetService,
pub s9pk_arch: Option<&'static str>,
@@ -76,6 +76,11 @@ pub struct RpcContextSeed {
pub start_time: Instant,
pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>,
}
impl Drop for RpcContextSeed {
fn drop(&mut self) {
tracing::info!("RpcContext is dropped");
}
}
pub struct Hardware {
pub devices: Vec<LshwDevice>,
@@ -220,7 +225,7 @@ impl RpcContext {
ephemeral_sessions: SyncMutex::new(Sessions::new()),
sync_db: watch::Sender::new(db.sequence().await),
db,
account: RwLock::new(account),
account: SyncRwLock::new(account),
callbacks: net_controller.callbacks.clone(),
net_controller,
os_net_service,
@@ -269,7 +274,7 @@ impl RpcContext {
self.crons.mutate(|c| std::mem::take(c));
self.services.shutdown_all().await?;
self.is_closed.store(true, Ordering::SeqCst);
tracing::info!("RPC Context is shutdown");
tracing::info!("RpcContext is shutdown");
Ok(())
}
@@ -478,6 +483,11 @@ impl RpcContext {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await
}
}
impl AsRef<Client> for RpcContext {
fn as_ref(&self) -> &Client {
&self.client
}
}
impl AsRef<Jwk> for RpcContext {
fn as_ref(&self) -> &Jwk {
&CURRENT_SECRET

View File

@@ -21,7 +21,8 @@ use crate::context::RpcContext;
use crate::context::config::ServerConfig;
use crate::disk::OsPartitionInfo;
use crate::hostname::Hostname;
use crate::net::web_server::{UpgradableListener, WebServer, WebServerAcceptorSetter};
use crate::net::gateway::UpgradableListener;
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
use crate::prelude::*;
use crate::progress::FullProgressTracker;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
@@ -86,6 +87,8 @@ impl SetupContext {
config: &ServerConfig,
) -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let mut progress = FullProgressTracker::new();
progress.enable_logging(true);
Ok(Self(Arc::new(SetupContextSeed {
webserver: webserver.acceptor_setter(),
config: config.clone(),
@@ -96,7 +99,7 @@ impl SetupContext {
)
})?,
disable_encryption: config.disable_encryption.unwrap_or(false),
progress: FullProgressTracker::new(),
progress,
task: OnceCell::new(),
result: OnceCell::new(),
disk_guid: OnceCell::new(),

View File

@@ -1,6 +1,7 @@
pub mod model;
pub mod prelude;
use std::panic::UnwindSafe;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -29,6 +30,26 @@ lazy_static::lazy_static! {
static ref PUBLIC: JsonPointer = "/public".parse().unwrap();
}
pub trait DbAccess<T>: Sized {
fn access<'a>(db: &'a Model<Self>) -> &'a Model<T>;
}
pub trait DbAccessMut<T>: DbAccess<T> {
fn access_mut<'a>(db: &'a mut Model<Self>) -> &'a mut Model<T>;
}
pub trait DbAccessByKey<T>: Sized {
type Key<'a>;
fn access_by_key<'a>(db: &'a Model<Self>, key: Self::Key<'_>) -> Option<&'a Model<T>>;
}
pub trait DbAccessMutByKey<T>: DbAccessByKey<T> {
fn access_mut_by_key<'a>(
db: &'a mut Model<Self>,
key: Self::Key<'_>,
) -> Option<&'a mut Model<T>>;
}
pub fn db<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
@@ -127,7 +148,7 @@ pub struct SubscribeParams {
#[ts(type = "string | null")]
pointer: Option<JsonPointer>,
#[ts(skip)]
#[serde(rename = "__auth_session")]
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
}

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use exver::{Version, VersionRange};
@@ -8,7 +9,6 @@ use imbl_value::InternedString;
use ipnet::IpNet;
use isocountry::CountryCode;
use itertools::Itertools;
use lazy_static::lazy_static;
use models::{GatewayId, PackageId};
use openssl::hash::MessageDigest;
use patch_db::{HasModel, Value};
@@ -16,9 +16,10 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::account::AccountInfo;
use crate::db::DbAccessByKey;
use crate::db::model::Database;
use crate::db::model::package::AllPackageData;
use crate::net::acme::AcmeProvider;
use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::host::Host;
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
use crate::net::utils::ipv6_is_local;
@@ -30,7 +31,7 @@ use crate::util::cpupower::Governor;
use crate::util::lshw::LshwDevice;
use crate::util::serde::MaybeUtf8String;
use crate::version::{Current, VersionT};
use crate::{ARCH, HOST_IP, PLATFORM};
use crate::{ARCH, PLATFORM};
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
@@ -122,11 +123,20 @@ impl Public {
kiosk,
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../web/patchdb-ui-seed.json"
)))
.with_kind(ErrorKind::Deserialization)?,
ui: {
#[cfg(feature = "startd")]
{
serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../web/patchdb-ui-seed.json"
)))
.with_kind(ErrorKind::Deserialization)?
}
#[cfg(not(feature = "startd"))]
{
Value::Null
}
},
})
}
}
@@ -202,8 +212,10 @@ pub struct NetworkInfo {
#[model = "Model<Self>"]
#[ts(export)]
pub struct DnsSettings {
pub dhcp_servers: Vec<SocketAddr>,
pub static_servers: Option<Vec<SocketAddr>>,
#[ts(type = "string[]")]
pub dhcp_servers: VecDeque<SocketAddr>,
#[ts(type = "string[] | null")]
pub static_servers: Option<VecDeque<SocketAddr>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
@@ -211,63 +223,12 @@ pub struct DnsSettings {
#[model = "Model<Self>"]
#[ts(export)]
pub struct NetworkInterfaceInfo {
pub name: Option<InternedString>,
pub public: Option<bool>,
pub secure: Option<bool>,
pub ip_info: Option<IpInfo>,
pub ip_info: Option<Arc<IpInfo>>,
}
impl NetworkInterfaceInfo {
pub fn loopback() -> (&'static GatewayId, &'static Self) {
lazy_static! {
static ref LO: GatewayId = GatewayId::from("lo");
static ref LOOPBACK: NetworkInterfaceInfo = NetworkInterfaceInfo {
public: Some(false),
secure: Some(true),
ip_info: Some(IpInfo {
name: "lo".into(),
scope_id: 1,
device_type: None,
subnets: [
IpNet::new(Ipv4Addr::LOCALHOST.into(), 8).unwrap(),
IpNet::new(Ipv6Addr::LOCALHOST.into(), 128).unwrap(),
]
.into_iter()
.collect(),
lan_ip: [
IpAddr::from(Ipv4Addr::LOCALHOST),
IpAddr::from(Ipv6Addr::LOCALHOST)
]
.into_iter()
.collect(),
wan_ip: None,
ntp_servers: Default::default(),
dns_servers: Default::default(),
}),
};
}
(&*LO, &*LOOPBACK)
}
pub fn lxc_bridge() -> (&'static GatewayId, &'static Self) {
lazy_static! {
static ref LXCBR0: GatewayId = GatewayId::from(START9_BRIDGE_IFACE);
static ref LXC_BRIDGE: NetworkInterfaceInfo = NetworkInterfaceInfo {
public: Some(false),
secure: Some(true),
ip_info: Some(IpInfo {
name: START9_BRIDGE_IFACE.into(),
scope_id: 0,
device_type: None,
subnets: [IpNet::new(HOST_IP.into(), 24).unwrap()]
.into_iter()
.collect(),
lan_ip: [IpAddr::from(HOST_IP)].into_iter().collect(),
wan_ip: None,
ntp_servers: Default::default(),
dns_servers: Default::default(),
}),
};
}
(&*LXCBR0, &*LXC_BRIDGE)
}
pub fn public(&self) -> bool {
self.public.unwrap_or_else(|| {
!self.ip_info.as_ref().map_or(true, |ip_info| {
@@ -302,7 +263,7 @@ impl NetworkInterfaceInfo {
self.secure.unwrap_or_else(|| {
self.ip_info.as_ref().map_or(false, |ip_info| {
ip_info.device_type == Some(NetworkInterfaceType::Wireguard)
})
}) && !self.public()
})
}
}
@@ -327,13 +288,15 @@ pub struct IpInfo {
pub dns_servers: OrdSet<IpAddr>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, TS)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "kebab-case")]
pub enum NetworkInterfaceType {
Ethernet,
Wireless,
Bridge,
Wireguard,
Loopback,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
@@ -343,6 +306,19 @@ pub enum NetworkInterfaceType {
pub struct AcmeSettings {
pub contact: Vec<String>,
}
impl DbAccessByKey<AcmeSettings> for Database {
type Key<'a> = &'a AcmeProvider;
fn access_by_key<'a>(
db: &'a Model<Self>,
key: Self::Key<'_>,
) -> Option<&'a Model<AcmeSettings>> {
db.as_public()
.as_server_info()
.as_network()
.as_acme()
.as_idx(key)
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]

View File

@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use imbl::OrdMap;
@@ -167,6 +168,21 @@ impl<T> Model<Option<T>> {
}
}
impl<T> Model<Arc<T>> {
pub fn deref(self) -> Model<T> {
use patch_db::ModelExt;
self.transmute(|a| a)
}
pub fn as_deref(&self) -> &Model<T> {
use patch_db::ModelExt;
self.transmute_ref(|a| a)
}
pub fn as_deref_mut(&mut self) -> &mut Model<T> {
use patch_db::ModelExt;
self.transmute_mut(|a| a)
}
}
pub trait Map: DeserializeOwned + Serialize {
type Key;
type Value;
@@ -193,10 +209,10 @@ where
A: serde::Serialize + serde::de::DeserializeOwned + Ord,
B: serde::Serialize + serde::de::DeserializeOwned,
{
type Key = A;
type Key = JsonKey<A>;
type Value = B;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
serde_json::to_string(key).with_kind(ErrorKind::Serialization)
serde_json::to_string(&key.0).with_kind(ErrorKind::Serialization)
}
}
@@ -216,13 +232,18 @@ impl<T: Map> Model<T>
where
T::Value: Serialize,
{
pub fn insert(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Error> {
pub fn insert_model(
&mut self,
key: &T::Key,
value: Model<T::Value>,
) -> Result<Option<Model<T::Value>>, Error> {
use patch_db::ModelExt;
use serde::ser::Error;
let v = patch_db::value::to_value(value)?;
let v = value.into_value();
match &mut self.value {
Value::Object(o) => {
o.insert(T::key_string(key)?, v);
Ok(())
let prev = o.insert(T::key_string(key)?, v);
Ok(prev.map(|v| Model::from_value(v)))
}
v => Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
@@ -231,6 +252,13 @@ where
.into()),
}
}
pub fn insert(
&mut self,
key: &T::Key,
value: &T::Value,
) -> Result<Option<Model<T::Value>>, Error> {
self.insert_model(key, Model::new(value)?)
}
pub fn upsert<F>(&mut self, key: &T::Key, value: F) -> Result<&mut Model<T::Value>, Error>
where
F: FnOnce() -> Result<T::Value, Error>,
@@ -257,22 +285,6 @@ where
.into()),
}
}
pub fn insert_model(&mut self, key: &T::Key, value: Model<T::Value>) -> Result<(), Error> {
use patch_db::ModelExt;
use serde::ser::Error;
let v = value.into_value();
match &mut self.value {
Value::Object(o) => {
o.insert(T::key_string(key)?, v);
Ok(())
}
v => Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
kind: patch_db::value::ErrorKind::Serialization,
}
.into()),
}
}
}
impl<T: Map> Model<T>
@@ -437,6 +449,12 @@ impl<T> std::ops::DerefMut for JsonKey<T> {
&mut self.0
}
}
impl<T: DeserializeOwned> FromStr for JsonKey<T> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).with_kind(ErrorKind::Deserialization)
}
}
impl<T: Serialize> Serialize for JsonKey<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -449,7 +467,7 @@ impl<T: Serialize> Serialize for JsonKey<T> {
}
}
// { "foo": "bar" } -> "{ \"foo\": \"bar\" }"
impl<'de, T: Serialize + DeserializeOwned> Deserialize<'de> for JsonKey<T> {
impl<'de, T: DeserializeOwned> Deserialize<'de> for JsonKey<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,

View File

@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::path::Path;
use imbl_value::InternedString;
use models::PackageId;
@@ -24,14 +25,57 @@ impl Map for Dependencies {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
#[derive(Clone, Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DepInfo {
pub description: Option<String>,
pub optional: bool,
pub s9pk: Option<PathOrUrl>,
#[serde(flatten)]
pub metadata: Option<MetadataSrc>,
}
impl TS for DepInfo {
type WithoutGenerics = Self;
fn decl() -> String {
format!("type {} = {}", Self::name(), Self::inline())
}
fn decl_concrete() -> String {
Self::decl()
}
fn name() -> String {
"DepInfo".into()
}
fn inline() -> String {
"{ description: string | null, optional: boolean } & MetadataSrc".into()
}
fn inline_flattened() -> String {
Self::inline()
}
fn visit_dependencies(v: &mut impl ts_rs::TypeVisitor)
where
Self: 'static,
{
v.visit::<MetadataSrc>()
}
fn output_path() -> Option<&'static std::path::Path> {
Some(Path::new("DepInfo.ts"))
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum MetadataSrc {
Metadata(Metadata),
S9pk(Option<PathOrUrl>), // backwards compatibility
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Metadata {
pub title: InternedString,
pub icon: PathOrUrl,
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]

View File

@@ -31,7 +31,7 @@ pub async fn write_developer_key(
secret_key: secret.to_bytes(),
public_key: Some(PublicKeyBytes(VerifyingKey::from(secret).to_bytes())),
};
let mut file = create_file_mod(path, 0o046).await?;
let mut file = create_file_mod(path, 0o640).await?;
file.write_all(
keypair_bytes
.to_pkcs8_pem(base64ct::LineEnding::default())

View File

@@ -80,23 +80,6 @@ impl<Fs: FileSystem> FileSystem for IdMapped<Fs> {
}
Ok(())
}
async fn mount<P: AsRef<Path> + Send>(
&self,
mountpoint: P,
mount_type: MountType,
) -> Result<(), Error> {
self.pre_mount(mountpoint.as_ref()).await?;
Command::new("mount.next")
.args(
default_mount_command(self, mountpoint, mount_type)
.await?
.get_args(),
)
.invoke(ErrorKind::Filesystem)
.await?;
Ok(())
}
async fn source_hash(
&self,
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {

View File

@@ -74,7 +74,7 @@ impl MountGuard {
}
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
if self.mounted {
unmount(&self.mountpoint, false).await?;
unmount(&self.mountpoint, !cfg!(feature = "unstable")).await?;
if delete_mountpoint {
match tokio::fs::remove_dir(&self.mountpoint).await {
Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty

View File

@@ -8,7 +8,7 @@ use const_format::formatcp;
use futures::{StreamExt, TryStreamExt};
use itertools::Itertools;
use models::ResultExt;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tracing::instrument;
@@ -17,15 +17,16 @@ use ts_rs::TS;
use crate::account::AccountInfo;
use crate::context::config::ServerConfig;
use crate::context::{CliContext, InitContext, RpcContext};
use crate::db::model::public::ServerStatus;
use crate::db::model::Database;
use crate::db::model::public::ServerStatus;
use crate::developer::OS_DEVELOPER_KEY_PATH;
use crate::hostname::Hostname;
use crate::middleware::auth::AuthContext;
use crate::net::gateway::UpgradableListener;
use crate::net::net_controller::{NetController, NetService};
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
use crate::net::utils::find_wifi_iface;
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
use crate::net::web_server::WebServerAcceptorSetter;
use crate::prelude::*;
use crate::progress::{
FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits,
@@ -34,10 +35,10 @@ use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
use crate::ssh::SSH_DIR;
use crate::system::{get_mem_info, sync_kiosk};
use crate::util::io::{open_file, IOHook};
use crate::util::io::{IOHook, open_file};
use crate::util::lshw::lshw;
use crate::util::net::WebSocketExt;
use crate::util::{cpupower, Invoke};
use crate::util::{Invoke, cpupower};
use crate::{Error, MAIN_DATA, PACKAGE_DATA};
pub const SYSTEM_REBUILD_PATH: &str = "/media/startos/config/system-rebuild";
@@ -247,7 +248,8 @@ pub async fn init(
Command::new("killall")
.arg("journalctl")
.invoke(crate::ErrorKind::Journald)
.await?;
.await
.log_err();
mount_logs.complete();
tokio::io::copy(
&mut open_file("/run/startos/init.log").await?,
@@ -495,14 +497,7 @@ pub async fn init_progress(ctx: InitContext) -> Result<InitProgressRes, Error> {
}
);
if let Err(e) = ws
.close_result(res.map(|_| "complete").map_err(|e| {
tracing::error!("error in init progress websocket: {e}");
tracing::debug!("{e:?}");
e
}))
.await
{
if let Err(e) = ws.close_result(res.map(|_| "complete")).await {
tracing::error!("error closing init progress websocket: {e}");
tracing::debug!("{e:?}");
}

View File

@@ -7,7 +7,7 @@ use clap::builder::ValueParserFactory;
use clap::{CommandFactory, FromArgMatches, Parser, value_parser};
use color_eyre::eyre::eyre;
use exver::VersionRange;
use futures::{AsyncWriteExt, StreamExt};
use futures::StreamExt;
use imbl_value::{InternedString, json};
use itertools::Itertools;
use models::{FromStrParser, VersionString};
@@ -15,7 +15,6 @@ use reqwest::Url;
use reqwest::header::{CONTENT_LENGTH, HeaderMap};
use rpc_toolkit::HandlerArgs;
use rpc_toolkit::yajrc::RpcError;
use rustyline_async::ReadlineEvent;
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
@@ -34,6 +33,7 @@ use crate::upload::upload;
use crate::util::Never;
use crate::util::io::open_file;
use crate::util::net::WebSocketExt;
use crate::util::tui::choose;
pub const PKG_ARCHIVE_DIR: &str = "package-data/archive";
pub const PKG_PUBLIC_DIR: &str = "package-data/public";
@@ -175,7 +175,7 @@ pub async fn install(
#[serde(rename_all = "camelCase")]
pub struct SideloadParams {
#[ts(skip)]
#[serde(rename = "__auth_session")]
#[serde(rename = "__Auth_session")]
session: Option<InternedString>,
}
@@ -483,47 +483,19 @@ pub async fn cli_install(
let version = if packages.best.len() == 1 {
packages.best.pop_first().map(|(k, _)| k).unwrap()
} else {
println!(
"Multiple flavors of {id} found. Please select one of the following versions to install:"
);
let version;
loop {
let (mut read, mut output) = rustyline_async::Readline::new("> ".into())
.with_kind(ErrorKind::Filesystem)?;
for (idx, version) in packages.best.keys().enumerate() {
output
.write_all(format!(" {}) {}\n", idx + 1, version).as_bytes())
.await?;
read.add_history_entry(version.to_string());
}
if let ReadlineEvent::Line(line) = read.readline().await? {
let trimmed = line.trim();
match trimmed.parse() {
Ok(v) => {
if let Some((k, _)) = packages.best.remove_entry(&v) {
version = k;
break;
}
}
Err(_) => match trimmed.parse::<usize>() {
Ok(i) if (1..=packages.best.len()).contains(&i) => {
version = packages.best.keys().nth(i - 1).unwrap().clone();
break;
}
_ => (),
},
}
eprintln!("invalid selection: {trimmed}");
println!("Please select one of the following versions to install:");
} else {
return Err(Error::new(
eyre!("Could not determine precise version to install"),
ErrorKind::InvalidRequest,
)
.into());
}
}
version
let versions = packages.best.keys().collect::<Vec<_>>();
let version = choose(
&format!(
concat!(
"Multiple flavors of {id} found. ",
"Please select one of the following versions to install:"
),
id = id
),
&versions,
)
.await?;
(*version).clone()
};
ctx.call_remote::<RpcContext>(
&method.join("."),

View File

@@ -80,17 +80,16 @@ use imbl_value::Value;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{
CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, from_fn, from_fn_async,
from_fn_blocking,
from_fn_async_local, from_fn_blocking,
};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::{
CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext,
};
use crate::context::{CliContext, DiagnosticContext, InitContext, RpcContext};
use crate::disk::fsck::RequiresReboot;
use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::system::kiosk;
use crate::tunnel::context::TunnelUrlParams;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
#[derive(Deserialize, Serialize, Parser, TS)]
@@ -139,6 +138,20 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"server",
server::<C>()
@@ -191,6 +204,19 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.no_cli(),
)
.subcommand(
"registry",
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
)
.subcommand(
"tunnel",
CallRemoteHandler::<RpcContext, _, _, TunnelUrlParams>::new(tunnel::api::tunnel_api())
.no_cli(),
)
.subcommand(
"tunnel",
tunnel::api::tunnel_api::<CliContext>().with_about("Commands related to StartTunnel"),
)
.subcommand(
"s9pk",
s9pk::rpc::s9pk().with_about("Commands for interacting with s9pk files"),
@@ -198,6 +224,29 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
.subcommand(
"util",
util::rpc::util::<C>().with_about("Command for calculating the blake3 hash of a file"),
)
.subcommand(
"init-key",
from_fn_async(developer::init)
.no_display()
.with_about("Create developer key if it doesn't exist"),
)
.subcommand(
"pubkey",
from_fn_blocking(developer::pubkey)
.with_about("Get public key for developer private key"),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<C>()
.with_about("Commands to display logs, restart the server, etc"),
)
.subcommand("init", init::init_api::<C>())
.subcommand("setup", setup::setup::<C>())
.subcommand(
"install",
os_install::install::<C>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
);
if &*PLATFORM != "raspberrypi" {
api = api.subcommand("kiosk", kiosk::<C>());
@@ -343,7 +392,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"install",
from_fn_async(install::cli_install)
from_fn_async_local(install::cli_install)
.no_display()
.with_about("Install a package from a marketplace or via sideloading"),
)
@@ -464,13 +513,6 @@ pub fn package<C: Context>() -> ParentHandler<C> {
backup::package_backup::<C>()
.with_about("Commands for restoring package(s) from backup"),
)
.subcommand("connect", from_fn_async(service::connect_rpc).no_cli())
.subcommand(
"connect",
from_fn_async(service::connect_rpc_cli)
.no_display()
.with_about("Connect to a LXC container"),
)
.subcommand(
"attach",
from_fn_async(service::attach)
@@ -484,127 +526,3 @@ pub fn package<C: Context>() -> ParentHandler<C> {
net::host::host_api::<C>().with_about("Manage network hosts for a package"),
)
}
pub fn diagnostic_api() -> ParentHandler<DiagnosticContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: DiagnosticContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<DiagnosticContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<DiagnosticContext>()
.with_about("Diagnostic commands i.e. logs, restart, rebuild"),
)
}
pub fn init_api() -> ParentHandler<InitContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: InitContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<InitContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_call_remote::<CliContext>(),
)
.subcommand(
"init",
init::init_api::<InitContext>()
.with_about("Commands to get logs or initialization progress"),
)
}
pub fn setup_api() -> ParentHandler<SetupContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: SetupContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<SetupContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand("setup", setup::setup::<SetupContext>())
}
pub fn install_api() -> ParentHandler<InstallContext> {
ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: InstallContext| version::git_info())
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",
from_fn(echo::<InstallContext>)
.with_about("Echo a message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"install",
os_install::install::<InstallContext>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
)
}
pub fn expanded_api() -> ParentHandler<CliContext> {
main_api()
.subcommand(
"init",
from_fn_async(developer::init)
.no_display()
.with_about("Create developer key if it doesn't exist"),
)
.subcommand(
"pubkey",
from_fn_blocking(developer::pubkey)
.with_about("Get public key for developer private key"),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<CliContext>()
.with_about("Commands to display logs, restart the server, etc"),
)
.subcommand("setup", setup::setup::<CliContext>())
.subcommand(
"install",
os_install::install::<CliContext>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
)
.subcommand(
"registry",
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
)
}

View File

@@ -551,8 +551,8 @@ pub async fn journalctl(
let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)?
.lines()
.map(serde_json::from_str::<JournalctlEntry>)
.collect::<Result<Vec<_>, _>>()
.with_kind(ErrorKind::Deserialization)?;
.filter_map(|e| e.ok())
.collect::<Vec<_>>();
if follow {
let mut follow_cmd = gen_journalctl_command(&id);
@@ -573,11 +573,8 @@ pub async fn journalctl(
let follow_deserialized_entries = journalctl_entries
.map_err(|e| Error::new(e, crate::ErrorKind::Journald))
.and_then(|s| {
futures::future::ready(
serde_json::from_str::<JournalctlEntry>(&s)
.with_kind(crate::ErrorKind::Deserialization),
)
.try_filter_map(|s| {
futures::future::ready(Ok(serde_json::from_str::<JournalctlEntry>(&s).ok()))
});
let entries = futures::stream::iter(deserialized_entries)

View File

@@ -10,7 +10,6 @@ use imbl_value::{InOMap, InternedString};
use models::{FromStrParser, InvalidId, PackageId};
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse};
use rustyline_async::{ReadlineEvent, SharedWriter};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
@@ -470,115 +469,6 @@ pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result<Guid,
Ok(guid)
}
pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> {
use futures::SinkExt;
use tokio_tungstenite::tungstenite::Message;
let mut ws = ctx.ws_continuation(guid).await?;
let (mut input, mut output) =
rustyline_async::Readline::new("> ".into()).with_kind(ErrorKind::Filesystem)?;
async fn handle_message(
msg: Option<Result<Message, tokio_tungstenite::tungstenite::Error>>,
output: &mut SharedWriter,
) -> Result<bool, Error> {
match msg {
None => return Ok(true),
Some(Ok(Message::Text(txt))) => match serde_json::from_str::<RpcResponse>(&txt) {
Ok(RpcResponse { result: Ok(a), .. }) => {
output
.write_all(
(serde_json::to_string(&a).with_kind(ErrorKind::Serialization)? + "\n")
.as_bytes(),
)
.await?;
}
Ok(RpcResponse { result: Err(e), .. }) => {
let e: Error = e.into();
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
Err(e) => {
tracing::error!("Error Parsing RPC response: {e}");
tracing::debug!("{e:?}");
}
},
Some(Ok(_)) => (),
Some(Err(e)) => {
return Err(Error::new(e, ErrorKind::Network));
}
};
Ok(false)
}
loop {
tokio::select! {
line = input.readline() => {
let line = line.with_kind(ErrorKind::Filesystem)?;
if let ReadlineEvent::Line(line) = line {
input.add_history_entry(line.clone());
if serde_json::from_str::<RpcRequest>(&line).is_ok() {
ws.send(Message::Text(line.into()))
.await
.with_kind(ErrorKind::Network)?;
} else {
match shell_words::split(&line) {
Ok(command) => {
if let Some((method, rest)) = command.split_first() {
let mut params = InOMap::new();
for arg in rest {
if let Some((name, value)) = arg.split_once('=') {
params.insert(InternedString::intern(name), if value.is_empty() {
Value::Null
} else if let Ok(v) = serde_json::from_str(value) {
v
} else {
Value::String(Arc::new(value.into()))
});
} else {
tracing::error!("argument without a value: {arg}");
tracing::debug!("help: set the value of {arg} with `{arg}=...`");
continue;
}
}
ws.send(Message::Text(match serde_json::to_string(&RpcRequest {
id: None,
method: GenericRpcMethod::new(method.into()),
params: Value::Object(params),
}) {
Ok(a) => a.into(),
Err(e) => {
tracing::error!("Error Serializing Request: {e}");
tracing::debug!("{e:?}");
continue;
}
})).await.with_kind(ErrorKind::Network)?;
if handle_message(ws.next().await, &mut output).await? {
break
}
}
}
Err(e) => {
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
}
}
} else {
ws.send(Message::Close(None)).await.with_kind(ErrorKind::Network)?;
}
}
msg = ws.next() => {
if handle_message(msg, &mut output).await? {
break;
}
}
}
}
Ok(())
}
pub async fn stats(ctx: RpcContext) -> Result<BTreeMap<PackageId, Option<ServiceStats>>, Error> {
let ids = ctx.db.peek().await.as_public().as_package_data().keys()?;

View File

@@ -1,3 +1,7 @@
fn main() {
#[cfg(feature = "backtrace-on-stack-overflow")]
unsafe {
backtrace_on_stack_overflow::enable()
};
startos::bins::startbox()
}

View File

@@ -27,14 +27,11 @@ use tokio::sync::Mutex;
use crate::auth::{Sessions, check_password, write_shadow};
use crate::context::RpcContext;
use crate::db::model::Database;
use crate::middleware::signature::{SignatureAuth, SignatureAuthContext};
use crate::prelude::*;
use crate::rpc_continuations::OpenAuthedContinuations;
use crate::sign::AnyVerifyingKey;
use crate::util::Invoke;
use crate::util::io::{create_file_mod, read_file_to_string};
use crate::util::iter::TransposeResultIterExt;
use crate::util::serde::BASE64;
use crate::util::sync::SyncMutex;
@@ -43,7 +40,7 @@ pub trait AuthContext: SignatureAuthContext {
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str;
fn init_auth_cookie() -> impl Future<Output = Result<(), Error>> + Send {
async {
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o046).await?;
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?;
file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes())
.await?;
file.sync_all().await?;
@@ -66,65 +63,6 @@ pub trait AuthContext: SignatureAuthContext {
}
}
impl SignatureAuthContext for RpcContext {
type Database = Database;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
let peek = self.db.peek().await;
self.account
.read()
.await
.hostnames()
.into_iter()
.map(Ok)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_public_domains()
.keys()
.map(|k| k.into_iter())
.transpose(),
)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_private_domains()
.de()
.map(|k| k.into_iter())
.transpose(),
)
.collect::<Vec<_>>()
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_private().as_auth_pubkeys().de()?.contains(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(&self, _: Self::CheckPubkeyRes, _: &RpcRequest) -> Result<(), Error> {
Ok(())
}
}
impl AuthContext for RpcContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos";
@@ -439,7 +377,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
));
}
if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) {
request.params["__auth_userAgent"] =
request.params["__Auth_userAgent"] =
Value::String(Arc::new(user_agent.to_owned()))
// TODO: will this panic?
}
@@ -458,7 +396,7 @@ impl<C: AuthContext> Middleware<C> for Auth {
{
match HasValidSession::from_header(self.cookie.as_ref(), context).await? {
HasValidSession(SessionType::Session(s)) if metadata.get_session => {
request.params["__auth_session"] =
request.params["__Auth_session"] =
Value::String(Arc::new(s.hashed().deref().to_owned()));
}
_ => (),

View File

@@ -0,0 +1,55 @@
use std::net::SocketAddr;
use axum::extract::Request;
use axum::response::Response;
use imbl_value::json;
use rpc_toolkit::Middleware;
use serde::Deserialize;
#[derive(Clone, Default)]
pub struct ConnectInfo {
peer_addr: Option<SocketAddr>,
local_addr: Option<SocketAddr>,
}
impl ConnectInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Deserialize)]
pub struct Metadata {
get_connect_info: bool,
}
impl<Context: Send + Sync + 'static> Middleware<Context> for ConnectInfo {
type Metadata = Metadata;
async fn process_http_request(
&mut self,
_: &Context,
request: &mut Request,
) -> Result<(), Response> {
if let Some(axum::extract::ConnectInfo((peer, local))) = request.extensions().get().cloned()
{
self.peer_addr = Some(peer);
self.local_addr = Some(local);
}
Ok(())
}
async fn process_rpc_request(
&mut self,
_: &Context,
metadata: Self::Metadata,
request: &mut rpc_toolkit::RpcRequest,
) -> Result<(), rpc_toolkit::RpcResponse> {
if metadata.get_connect_info {
if let Some(peer_addr) = self.peer_addr {
request.params["__ConnectInfo_peer_addr"] = json!(peer_addr);
}
if let Some(local_addr) = self.local_addr {
request.params["__ConnectInfo_local_addr"] = json!(local_addr);
}
}
Ok(())
}
}

View File

@@ -1,4 +1,5 @@
pub mod auth;
pub mod connect_info;
pub mod cors;
pub mod db;
pub mod signature;

View File

@@ -5,7 +5,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use axum::body::Body;
use axum::extract::Request;
use http::HeaderValue;
use http::{HeaderMap, HeaderValue};
use reqwest::Client;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse};
use serde::Deserialize;
@@ -13,13 +14,17 @@ use serde::de::DeserializeOwned;
use tokio::sync::Mutex;
use url::Url;
use crate::context::CliContext;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::prelude::*;
use crate::sign::commitment::Commitment;
use crate::sign::commitment::request::RequestCommitment;
use crate::sign::{AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme};
use crate::util::iter::TransposeResultIterExt;
use crate::util::serde::Base64;
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
pub trait SignatureAuthContext: Context {
type Database: HasModel<Model = Model<Self::Database>> + Send + Sync;
type AdditionalMetadata: DeserializeOwned + Send;
@@ -41,7 +46,82 @@ pub trait SignatureAuthContext: Context {
) -> impl Future<Output = Result<(), Error>> + Send;
}
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
impl SignatureAuthContext for RpcContext {
type Database = Database;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
let peek = self.db.peek().await;
self.account.peek(|a| {
a.hostnames()
.into_iter()
.map(Ok)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_public_domains()
.keys()
.map(|k| k.into_iter())
.transpose(),
)
.chain(
peek.as_public()
.as_server_info()
.as_network()
.as_host()
.as_private_domains()
.de()
.map(|k| k.into_iter())
.transpose(),
)
.collect::<Vec<_>>()
})
}
fn check_pubkey(
db: &Model<Self::Database>,
pubkey: Option<&AnyVerifyingKey>,
_: Self::AdditionalMetadata,
) -> Result<Self::CheckPubkeyRes, Error> {
if let Some(pubkey) = pubkey {
if db.as_private().as_auth_pubkeys().de()?.contains(pubkey) {
return Ok(());
}
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}
async fn post_auth_hook(&self, _: Self::CheckPubkeyRes, _: &RpcRequest) -> Result<(), Error> {
Ok(())
}
}
pub trait SigningContext {
fn signing_key(&self) -> Result<AnySigningKey, Error>;
}
impl SigningContext for CliContext {
fn signing_key(&self) -> Result<AnySigningKey, Error> {
Ok(AnySigningKey::Ed25519(self.developer_key()?.clone()))
}
}
impl SigningContext for RpcContext {
fn signing_key(&self) -> Result<AnySigningKey, Error> {
Ok(AnySigningKey::Ed25519(
self.account.peek(|a| a.developer_key.clone()),
))
}
}
#[derive(Deserialize)]
pub struct Metadata<Additional> {
@@ -203,7 +283,7 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
let signer = self.signer.take().transpose()?;
if metadata.get_signer {
if let Some(signer) = &signer {
request.params["__auth_signer"] = to_value(signer)?;
request.params["__Auth_signer"] = to_value(signer)?;
}
}
let db = context.db().peek().await;
@@ -216,10 +296,11 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
}
}
pub async fn call_remote(
ctx: &CliContext,
pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
ctx: &Ctx,
url: Url,
sig_context: &str,
headers: HeaderMap,
sig_context: Option<&str>,
method: &str,
params: Value,
) -> Result<Value, RpcError> {
@@ -235,16 +316,16 @@ pub async fn call_remote(
};
let body = serde_json::to_vec(&rpc_req)?;
let mut req = ctx
.client
.as_ref()
.request(Method::POST, url)
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.header(CONTENT_LENGTH, body.len());
if let Ok(key) = ctx.developer_key() {
.header(CONTENT_LENGTH, body.len())
.headers(headers);
if let (Some(sig_ctx), Ok(key)) = (sig_context, ctx.signing_key()) {
req = req.header(
AUTH_SIG_HEADER,
SignatureHeader::sign(&AnySigningKey::Ed25519(key.clone()), &body, sig_context)?
.to_header(),
SignatureHeader::sign(&key, &body, sig_ctx)?.to_header(),
);
}
let res = req.body(body).send().await?;

View File

@@ -1,9 +1,12 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
use async_acme::acme::Identifier;
use async_acme::acme::{ACME_TLS_ALPN_NAME, Identifier};
use clap::Parser;
use clap::builder::ValueParserFactory;
use futures::StreamExt;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{ErrorData, FromStrParser};
@@ -11,14 +14,209 @@ use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio_rustls::rustls::ServerConfig;
use tokio_rustls::rustls::crypto::CryptoProvider;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use tokio_rustls::rustls::server::ClientHello;
use tokio_rustls::rustls::sign::CertifiedKey;
use ts_rs::TS;
use url::Url;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::db::model::public::AcmeSettings;
use crate::db::{DbAccess, DbAccessByKey, DbAccessMut};
use crate::net::tls::{SingleCertResolver, TlsHandler};
use crate::net::web_server::Accept;
use crate::prelude::*;
use crate::util::serde::{Pem, Pkcs8Doc};
use crate::util::sync::{SyncMutex, Watch};
pub type AcmeTlsAlpnCache =
Arc<SyncMutex<BTreeMap<InternedString, Watch<Option<Arc<CertifiedKey>>>>>>;
pub struct AcmeTlsHandler<M: HasModel, S> {
pub db: TypedPatchDb<M>,
pub acme_cache: AcmeTlsAlpnCache,
pub crypto_provider: Arc<CryptoProvider>,
pub get_provider: S,
pub in_progress: Watch<BTreeSet<BTreeSet<InternedString>>>,
}
impl<M, S> AcmeTlsHandler<M, S>
where
for<'a> M: DbAccessByKey<AcmeSettings, Key<'a> = &'a AcmeProvider>
+ DbAccessMut<AcmeCertStore>
+ HasModel<Model = Model<M>>
+ Send
+ Sync,
S: GetAcmeProvider + Clone,
{
pub async fn get_cert(&self, san_info: &BTreeSet<InternedString>) -> Option<CertifiedKey> {
let provider = self.get_provider.get_provider(san_info).await?;
let provider = provider.as_ref();
loop {
let peek = self.db.peek().await;
let store = <M as DbAccess<AcmeCertStore>>::access(&peek);
if let Some(cert) = store
.as_certs()
.as_idx(&provider.0)
.and_then(|p| p.as_idx(JsonKey::new_ref(san_info)))
{
let cert = cert.de().log_err()?;
return Some(
CertifiedKey::from_der(
cert.fullchain
.into_iter()
.map(|c| Ok(CertificateDer::from(c.to_der()?)))
.collect::<Result<_, Error>>()
.log_err()?,
PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
cert.key.0.private_key_to_pkcs8().log_err()?,
)),
&*self.crypto_provider,
)
.log_err()?,
);
}
if !self.in_progress.send_if_modified(|x| {
if !x.contains(san_info) {
x.insert(san_info.clone());
true
} else {
false
}
}) {
self.in_progress
.clone()
.wait_for(|x| !x.contains(san_info))
.await;
continue;
}
let contact = <M as DbAccessByKey<AcmeSettings>>::access_by_key(&peek, &provider)?
.as_contact()
.de()
.log_err()?;
let identifiers: Vec<_> = san_info
.iter()
.map(|d| match d.parse::<IpAddr>() {
Ok(a) => Identifier::Ip(a),
_ => Identifier::Dns((&**d).into()),
})
.collect::<Vec<_>>();
let cache_entries = san_info
.iter()
.cloned()
.map(|d| (d, Watch::new(None)))
.collect::<BTreeMap<_, _>>();
self.acme_cache.mutate(|c| {
c.extend(cache_entries.iter().map(|(k, v)| (k.clone(), v.clone())));
});
let cert = async_acme::rustls_helper::order(
|identifier, cert| {
let domain = InternedString::from_display(&identifier);
if let Some(entry) = cache_entries.get(&domain) {
entry.send(Some(Arc::new(cert)));
}
Ok(())
},
provider.0.as_str(),
&identifiers,
Some(&AcmeCertCache(&self.db)),
&contact,
)
.await
.log_err()?;
self.acme_cache
.mutate(|c| c.retain(|c, _| !cache_entries.contains_key(c)));
self.in_progress.send_modify(|i| i.remove(san_info));
return Some(cert);
}
}
}
pub trait GetAcmeProvider {
fn get_provider<'a, 'b: 'a>(
&'b self,
san_info: &'a BTreeSet<InternedString>,
) -> impl Future<Output = Option<impl AsRef<AcmeProvider> + Send + 'b>> + Send + 'a;
}
impl<'a, A, M, S> TlsHandler<'a, A> for Arc<AcmeTlsHandler<M, S>>
where
A: Accept + 'a,
<A as Accept>::Metadata: Send + Sync,
for<'m> M: DbAccessByKey<AcmeSettings, Key<'m> = &'m AcmeProvider>
+ DbAccessMut<AcmeCertStore>
+ HasModel<Model = Model<M>>
+ Send
+ Sync,
S: GetAcmeProvider + Clone + Send + Sync,
{
async fn get_config(
&'a mut self,
hello: &'a ClientHello<'a>,
_: &'a <A as Accept>::Metadata,
) -> Option<ServerConfig> {
let domain = hello.server_name()?;
if hello
.alpn()
.into_iter()
.flatten()
.any(|a| a == ACME_TLS_ALPN_NAME)
{
let cert = self
.acme_cache
.peek(|c| c.get(domain).cloned())
.ok_or_else(|| {
Error::new(
eyre!("No challenge recv available for {domain}"),
ErrorKind::OpenSsl,
)
})
.log_err()?;
tracing::info!("Waiting for verification cert for {domain}");
let cert = cert
.filter(|c| futures::future::ready(c.is_some()))
.next()
.await
.flatten()?;
tracing::info!("Verification cert received for {domain}");
let mut cfg = ServerConfig::builder_with_provider(self.crypto_provider.clone())
.with_safe_default_protocol_versions()
.log_err()?
.with_no_client_auth()
.with_cert_resolver(Arc::new(SingleCertResolver(cert)));
cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()];
tracing::info!("performing ACME auth challenge");
return Some(cfg);
}
let domains: BTreeSet<InternedString> = [domain.into()].into_iter().collect();
let crypto_provider = self.crypto_provider.clone();
if let Some(cert) = self.get_cert(&domains).await {
return Some(
ServerConfig::builder_with_provider(crypto_provider)
.with_safe_default_protocol_versions()
.log_err()?
.with_no_client_auth()
.with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert)))),
);
}
None
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
@@ -32,29 +230,35 @@ impl AcmeCertStore {
}
}
impl DbAccess<AcmeCertStore> for Database {
fn access<'a>(db: &'a Model<Self>) -> &'a Model<AcmeCertStore> {
db.as_private().as_key_store().as_acme()
}
}
impl DbAccessMut<AcmeCertStore> for Database {
fn access_mut<'a>(db: &'a mut Model<Self>) -> &'a mut Model<AcmeCertStore> {
db.as_private_mut().as_key_store_mut().as_acme_mut()
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AcmeCert {
pub key: Pem<PKey<Private>>,
pub fullchain: Vec<Pem<X509>>,
}
pub struct AcmeCertCache<'a>(pub &'a TypedPatchDb<Database>);
pub struct AcmeCertCache<'a, M: HasModel>(pub &'a TypedPatchDb<M>);
#[async_trait::async_trait]
impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
impl<'a, M> async_acme::cache::AcmeCache for AcmeCertCache<'a, M>
where
M: HasModel<Model = Model<M>> + DbAccessMut<AcmeCertStore> + Send + Sync,
{
type Error = ErrorData;
async fn read_account(&self, contacts: &[&str]) -> Result<Option<Vec<u8>>, Self::Error> {
let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec());
let Some(account) = self
.0
.peek()
.await
.into_private()
.into_key_store()
.into_acme()
.into_accounts()
.into_idx(&contacts)
else {
let peek = self.0.peek().await;
let Some(account) = M::access(&peek).as_accounts().as_idx(&contacts) else {
return Ok(None);
};
Ok(Some(account.de()?.0.document.into_vec()))
@@ -68,9 +272,7 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
};
self.0
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_acme_mut()
M::access_mut(db)
.as_accounts_mut()
.insert(&contacts, &Pem::new(key))
})
@@ -96,16 +298,11 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
let directory_url = directory_url
.parse::<Url>()
.with_kind(ErrorKind::ParseUrl)?;
let Some(cert) = self
.0
.peek()
.await
.into_private()
.into_key_store()
.into_acme()
.into_certs()
.into_idx(&directory_url)
.and_then(|a| a.into_idx(&identifiers))
let peek = self.0.peek().await;
let Some(cert) = M::access(&peek)
.as_certs()
.as_idx(&directory_url)
.and_then(|a| a.as_idx(&identifiers))
else {
return Ok(None);
};
@@ -160,9 +357,7 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
};
self.0
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_acme_mut()
M::access_mut(db)
.as_certs_mut()
.upsert(&directory_url, || Ok(BTreeMap::new()))?
.insert(&identifiers, &cert)
@@ -235,6 +430,11 @@ impl AsRef<str> for AcmeProvider {
self.0.as_str()
}
}
impl AsRef<AcmeProvider> for AcmeProvider {
fn as_ref(&self) -> &AcmeProvider {
self
}
}
impl ValueParserFactory for AcmeProvider {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {

View File

@@ -1,20 +1,21 @@
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt};
use futures::{FutureExt, StreamExt};
use helpers::NonDetachingJoinHandle;
use hickory_client::client::Client;
use hickory_client::proto::DnsHandle;
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::tcp::TcpClientStream;
use hickory_client::proto::udp::UdpClientStream;
use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions};
use hickory_client::proto::xfer::DnsRequestOptions;
use hickory_server::ServerFuture;
use hickory_server::authority::MessageResponseBuilder;
use hickory_server::proto::op::{Header, ResponseCode};
@@ -24,6 +25,7 @@ use imbl::OrdMap;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{GatewayId, OptionExt, PackageId};
use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{
Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async, from_fn_blocking,
};
@@ -31,11 +33,12 @@ use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, UdpSocket};
use tracing::instrument;
use crate::context::RpcContext;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::file_string_stream;
use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::{SyncRwLock, Watch};
@@ -63,7 +66,36 @@ pub fn dns_api<C: Context>() -> ParentHandler<C> {
"set-static",
from_fn_async(set_static_dns)
.no_display()
.with_about("Set static DNS servers"),
.with_about("Set static DNS servers")
.with_call_remote::<CliContext>(),
)
.subcommand(
"dump-table",
from_fn_async(dump_table)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "FQDN", "DESTINATION"]);
for (hostname, destination) in res {
if let Some(ip) = destination {
table.add_row(row![hostname, ip]);
} else {
table.add_row(row![hostname, "SELF"]);
}
}
table.print_tty(false)?;
Ok(())
})
.with_about("Dump address resolution table")
.with_call_remote::<CliContext>(),
)
}
@@ -139,6 +171,38 @@ pub async fn set_static_dns(
.result
}
pub async fn dump_table(
ctx: RpcContext,
) -> Result<BTreeMap<InternedString, Option<IpAddr>>, Error> {
Ok(ctx
.net_controller
.dns
.resolve
.upgrade()
.or_not_found("DnsController")?
.peek(|map| {
map.private_domains
.iter()
.map(|(d, _)| (d.clone(), None))
.chain(map.services.iter().filter_map(|(svc, ip)| {
ip.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| {
(
svc.as_ref().map_or(
InternedString::from_static("startos"),
|svc| {
InternedString::from_display(&lazy_format!("{svc}.startos"))
},
),
Some(IpAddr::V4(*ip)),
)
})
}))
.collect()
}))
}
#[derive(Default)]
struct ResolveMap {
private_domains: BTreeMap<InternedString, Weak<()>>,
@@ -161,97 +225,146 @@ impl DnsClient {
Self {
client: client.clone(),
_thread: tokio::spawn(async move {
loop {
if let Err::<(), Error>(e) = async {
let mut stream = file_string_stream("/run/systemd/resolve/resolv.conf")
.filter_map(|a| futures::future::ready(a.transpose()))
.boxed();
let mut conf: String = stream
.next()
.await
.or_not_found("/run/systemd/resolve/resolv.conf")??;
let mut prev_nameservers = Vec::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let nameservers = conf
.lines()
.map(|l| l.trim())
.filter_map(|l| l.strip_prefix("nameserver "))
.skip(2)
.map(|n| {
n.parse::<SocketAddr>()
.or_else(|_| n.parse::<IpAddr>().map(|a| (a, 53).into()))
})
.collect::<Result<Vec<_>, _>>()?;
let static_nameservers = db
.mutate(|db| {
let dns = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)?;
dns.as_static_servers().de()
})
.await
.result?;
let nameservers = static_nameservers.unwrap_or(nameservers);
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
let (bg, mut runner) = BackgroundJobQueue::new();
runner
.run_while(async move {
let dhcp_ns_db = db.clone();
bg.add_job(async move {
loop {
if let Err(e) = async {
let mut stream =
file_string_stream("/run/systemd/resolve/resolv.conf")
.filter_map(|a| futures::future::ready(a.transpose()))
.boxed();
while let Some(conf) = stream.next().await {
let conf: String = conf?;
let mut nameservers = conf
.lines()
.map(|l| l.trim())
.filter_map(|l| l.strip_prefix("nameserver "))
.map(|n| {
n.parse::<SocketAddr>().or_else(|_| {
n.parse::<IpAddr>().map(|a| (a, 53).into())
})
})
.collect::<Result<VecDeque<_>, _>>()?;
if nameservers
.front()
.map_or(false, |addr| addr.ip().is_loopback())
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.boxed());
client
};
new.push((*addr, client));
nameservers.pop_front();
}
if nameservers.front().map_or(false, |addr| {
addr.ip() == IpAddr::from([1, 1, 1, 1])
}) {
nameservers.pop_front();
}
dhcp_ns_db
.mutate(|db| {
let dns = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)
})
.await
.result?
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
tokio::select! {
c = stream.next() => conf = c.or_not_found("/run/systemd/resolve/resolv.conf")??,
_ = futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
) => (),
});
loop {
if let Err::<(), Error>(e) = async {
let mut dns_changed = db
.subscribe(
"/public/serverInfo/network/dns"
.parse::<JsonPointer>()
.with_kind(ErrorKind::Database)?,
)
.await;
let mut prev_nameservers = VecDeque::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let dns = db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_dns();
let nameservers = dns
.as_static_servers()
.transpose_ref()
.unwrap_or_else(|| dns.as_dhcp_servers())
.de()?;
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.fuse().boxed());
client
};
new.push((*addr, client));
}
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
futures::future::select(
dns_changed.recv().boxed(),
futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
),
)
.await;
}
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
}
})
.await;
})
.into(),
}
@@ -269,22 +382,40 @@ impl DnsClient {
}
}
lazy_static::lazy_static! {
static ref LOCALHOST: Name = Name::from_ascii("localhost").unwrap();
static ref STARTOS: Name = Name::from_ascii("startos").unwrap();
static ref EMBASSY: Name = Name::from_ascii("embassy").unwrap();
}
struct Resolver {
client: DnsClient,
net_iface: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
resolve: Arc<SyncRwLock<ResolveMap>>,
}
impl Resolver {
fn resolve(&self, name: &Name, src: IpAddr) -> Option<Vec<IpAddr>> {
fn resolve(&self, name: &Name, mut src: IpAddr) -> Option<Vec<IpAddr>> {
if name.zone_of(&*LOCALHOST) {
return Some(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]);
}
src = match src {
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
IpAddr::V4(v4)
} else {
IpAddr::V6(v6)
}
}
a => a,
};
self.resolve.peek(|r| {
if r.private_domains
.get(&*name.to_lowercase().to_ascii())
.get(&*name.to_lowercase().to_utf8().trim_end_matches('.'))
.map_or(false, |d| d.strong_count() > 0)
{
if let Some(res) = self.net_iface.peek(|i| {
i.values()
.chain([NetworkInterfaceInfo::lxc_bridge().1])
.flat_map(|i| i.ip_info.as_ref())
.filter_map(|i| i.ip_info.as_ref())
.find(|i| i.subnets.iter().any(|s| s.contains(&src)))
.map(|ip_info| {
let mut res = ip_info.subnets.iter().collect::<Vec<_>>();
@@ -293,38 +424,34 @@ impl Resolver {
})
}) {
return Some(res);
} else {
tracing::warn!("Could not determine source interface of {src}");
}
}
match name.iter().next_back() {
Some(b"embassy") | Some(b"startos") => {
if let Some(pkg) = name.iter().rev().skip(1).next() {
if let Some(ip) = r.services.get(&Some(
std::str::from_utf8(pkg)
.unwrap_or_default()
.parse()
.unwrap_or_default(),
)) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
} else if let Some(ip) = r.services.get(&None) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
if STARTOS.zone_of(name) || EMBASSY.zone_of(name) {
let Ok(pkg) = name
.trim_to(2)
.iter()
.next()
.map(std::str::from_utf8)
.transpose()
.map_err(|_| ())
.and_then(|s| s.map(PackageId::from_str).transpose().map_err(|_| ()))
else {
return None;
};
if let Some(ip) = r.services.get(&pkg) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
_ => None,
} else {
None
}
})
}
@@ -343,10 +470,12 @@ impl RequestHandler for Resolver {
if let Some(ip) = self.resolve(query.name().borrow(), req.src.ip()) {
match query.query_type() {
RecordType::A => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
header,
&ip.into_iter()
.filter_map(|a| {
if let IpAddr::V4(a) = a { Some(a) } else { None }
@@ -367,10 +496,12 @@ impl RequestHandler for Resolver {
.await
}
RecordType::AAAA => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
header,
&ip.into_iter()
.filter_map(|a| {
if let IpAddr::V6(a) = a { Some(a) } else { None }
@@ -391,11 +522,12 @@ impl RequestHandler for Resolver {
.await
}
_ => {
let res = Header::response_from_request(request.header());
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
res.into(),
header.into(),
[],
[],
[],
@@ -407,16 +539,22 @@ impl RequestHandler for Resolver {
}
} else {
let query = query.original().clone();
let mut streams = self.client.lookup(query, DnsRequestOptions::default());
let mut opt = DnsRequestOptions::default();
opt.recursion_desired = request.recursion_desired();
let mut streams = self.client.lookup(query, opt);
let mut err = None;
for stream in streams.iter_mut() {
match tokio::time::timeout(Duration::from_secs(5), stream.next()).await {
Ok(Some(Err(e))) => err = Some(e),
Ok(Some(Ok(msg))) => {
let mut header = msg.header().clone();
header.set_id(request.id());
header.set_checking_disabled(request.checking_disabled());
header.set_recursion_available(true);
return response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
header,
msg.answers(),
msg.name_servers(),
&msg.soa().map(|s| s.to_owned().into_record_of_rdata()),
@@ -432,12 +570,13 @@ impl RequestHandler for Resolver {
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
let mut res = Header::response_from_request(request.header());
res.set_response_code(ResponseCode::ServFail);
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
header.set_response_code(ResponseCode::ServFail);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
res,
header,
[],
[],
[],
@@ -453,12 +592,13 @@ impl RequestHandler for Resolver {
Err(e) => {
tracing::error!("{}", e);
tracing::debug!("{:?}", e);
let mut res = Header::response_from_request(request.header());
res.set_response_code(ResponseCode::ServFail);
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
header.set_response_code(ResponseCode::ServFail);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
res,
header,
[],
[],
[],
@@ -466,7 +606,7 @@ impl RequestHandler for Resolver {
),
)
.await
.unwrap_or(res.into())
.unwrap_or(header.into())
}
}
}

View File

@@ -1,21 +1,25 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::{IpAddr, SocketAddr, SocketAddrV6};
use std::net::{IpAddr, SocketAddrV4};
use std::sync::{Arc, Weak};
use std::time::Duration;
use futures::channel::oneshot;
use helpers::NonDetachingJoinHandle;
use id_pool::IdPool;
use iddqd::{IdOrdItem, IdOrdMap};
use imbl::OrdMap;
use models::GatewayId;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio::sync::mpsc;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter};
use crate::net::utils::ipv6_is_link_local;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::Watch;
pub const START9_BRIDGE_IFACE: &str = "lxcbr0";
@@ -42,162 +46,397 @@ impl AvailablePorts {
}
}
struct ForwardRequest {
external: u16,
target: SocketAddr,
filter: DynInterfaceFilter,
pub fn forward_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand(
"dump-table",
from_fn_async(
|ctx: RpcContext| async move { ctx.net_controller.forward.dump_table().await },
)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "FROM", "TO", "FILTER"]);
for (external, target) in res.0 {
table.add_row(row![external, target.target, target.filter]);
}
table.print_tty(false)?;
Ok(())
})
.with_call_remote::<CliContext>(),
)
}
struct ForwardMapping {
source: SocketAddrV4,
target: SocketAddrV4,
rc: Weak<()>,
}
struct ForwardEntry {
external: u16,
target: SocketAddr,
prev_filter: DynInterfaceFilter,
forwards: BTreeMap<SocketAddr, GatewayId>,
rc: Weak<()>,
#[derive(Default)]
struct PortForwardState {
mappings: BTreeMap<SocketAddrV4, ForwardMapping>, // source -> target
}
impl ForwardEntry {
fn new(external: u16, target: SocketAddr, rc: Weak<()>) -> Self {
Self {
external,
target,
prev_filter: false.into_dyn(),
forwards: BTreeMap::new(),
rc,
}
}
fn take(&mut self) -> Self {
Self {
external: self.external,
target: self.target,
prev_filter: std::mem::replace(&mut self.prev_filter, false.into_dyn()),
forwards: std::mem::take(&mut self.forwards),
rc: self.rc.clone(),
}
}
async fn destroy(mut self) -> Result<(), Error> {
while let Some((source, interface)) = self.forwards.pop_first() {
unforward(interface.as_str(), source, self.target).await?;
}
Ok(())
}
async fn update(
impl PortForwardState {
async fn add_forward(
&mut self,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
filter: Option<DynInterfaceFilter>,
) -> Result<(), Error> {
if self.rc.strong_count() == 0 {
return self.take().destroy().await;
}
let filter_ref = filter.as_ref().unwrap_or(&self.prev_filter);
let mut keep = BTreeSet::<SocketAddr>::new();
for (iface, info) in ip_info
.iter()
.chain([NetworkInterfaceInfo::loopback()])
.filter(|(id, info)| filter_ref.filter(*id, *info))
{
if let Some(ip_info) = &info.ip_info {
for ipnet in &ip_info.subnets {
let addr = match ipnet.addr() {
IpAddr::V6(ip6) => SocketAddrV6::new(
ip6,
self.external,
0,
if ipv6_is_link_local(ip6) {
ip_info.scope_id
} else {
0
},
)
.into(),
ip => SocketAddr::new(ip, self.external),
};
keep.insert(addr);
if !self.forwards.contains_key(&addr) {
forward(iface.as_str(), addr, self.target).await?;
self.forwards.insert(addr, iface.clone());
}
source: SocketAddrV4,
target: SocketAddrV4,
) -> Result<Arc<()>, Error> {
if let Some(existing) = self.mappings.get_mut(&source) {
if existing.target == target {
if let Some(existing_rc) = existing.rc.upgrade() {
return Ok(existing_rc);
} else {
let rc = Arc::new(());
existing.rc = Arc::downgrade(&rc);
return Ok(rc);
}
} else {
// Different target, need to remove old and add new
if let Some(mapping) = self.mappings.remove(&source) {
unforward(mapping.source, mapping.target).await?;
}
}
}
let rm = self
.forwards
.keys()
.copied()
.filter(|a| !keep.contains(a))
.collect::<Vec<_>>();
for rm in rm {
if let Some((source, interface)) = self.forwards.remove_entry(&rm) {
unforward(interface.as_str(), source, self.target).await?;
let rc = Arc::new(());
forward(source, target).await?;
self.mappings.insert(
source,
ForwardMapping {
source,
target,
rc: Arc::downgrade(&rc),
},
);
Ok(rc)
}
async fn gc(&mut self) -> Result<(), Error> {
let to_remove: Vec<SocketAddrV4> = self
.mappings
.iter()
.filter(|(_, mapping)| mapping.rc.strong_count() == 0)
.map(|(source, _)| *source)
.collect();
for source in to_remove {
if let Some(mapping) = self.mappings.remove(&source) {
unforward(mapping.source, mapping.target).await?;
}
}
if let Some(filter) = filter {
self.prev_filter = filter;
}
Ok(())
}
async fn update_request(
&mut self,
ForwardRequest {
external,
target,
filter,
rc,
}: ForwardRequest,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
) -> Result<(), Error> {
if external != self.external || target != self.target {
self.take().destroy().await?;
*self = Self::new(external, target, rc);
self.update(ip_info, Some(filter)).await?;
} else {
if self.prev_filter != filter {
self.update(ip_info, Some(filter)).await?;
}
self.rc = rc;
}
Ok(())
fn dump(&self) -> BTreeMap<SocketAddrV4, SocketAddrV4> {
self.mappings
.iter()
.filter(|(_, mapping)| mapping.rc.strong_count() > 0)
.map(|(source, mapping)| (*source, mapping.target))
.collect()
}
}
impl Drop for ForwardEntry {
impl Drop for PortForwardState {
fn drop(&mut self) {
if !self.forwards.is_empty() {
let take = self.take();
if !self.mappings.is_empty() {
let mappings = std::mem::take(&mut self.mappings);
tokio::spawn(async move {
take.destroy().await.log_err();
for (_, mapping) in mappings {
unforward(mapping.source, mapping.target).await.log_err();
}
});
}
}
}
#[derive(Default)]
struct ForwardState {
state: BTreeMap<u16, ForwardEntry>,
enum PortForwardCommand {
AddForward {
source: SocketAddrV4,
target: SocketAddrV4,
respond: oneshot::Sender<Result<Arc<()>, Error>>,
},
Gc {
respond: oneshot::Sender<Result<(), Error>>,
},
Dump {
respond: oneshot::Sender<BTreeMap<SocketAddrV4, SocketAddrV4>>,
},
}
impl ForwardState {
pub struct PortForwardController {
req: mpsc::UnboundedSender<PortForwardCommand>,
_thread: NonDetachingJoinHandle<()>,
}
impl PortForwardController {
pub fn new() -> Self {
let (req_send, mut req_recv) = mpsc::unbounded_channel::<PortForwardCommand>();
let thread = NonDetachingJoinHandle::from(tokio::spawn(async move {
while let Err(e) = async {
Command::new("sysctl")
.arg("-w")
.arg("net.ipv4.ip_forward=1")
.invoke(ErrorKind::Network)
.await?;
if Command::new("iptables")
.arg("-t")
.arg("nat")
.arg("-C")
.arg("POSTROUTING")
.arg("-j")
.arg("MASQUERADE")
.invoke(ErrorKind::Network)
.await
.is_err()
{
Command::new("iptables")
.arg("-t")
.arg("nat")
.arg("-A")
.arg("POSTROUTING")
.arg("-j")
.arg("MASQUERADE")
.invoke(ErrorKind::Network)
.await?;
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("error initializing PortForwardController: {e:#}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
let mut state = PortForwardState::default();
while let Some(cmd) = req_recv.recv().await {
match cmd {
PortForwardCommand::AddForward {
source,
target,
respond,
} => {
let result = state.add_forward(source, target).await;
respond.send(result).ok();
}
PortForwardCommand::Gc { respond } => {
let result = state.gc().await;
respond.send(result).ok();
}
PortForwardCommand::Dump { respond } => {
respond.send(state.dump()).ok();
}
}
}
}));
Self {
req: req_send,
_thread: thread,
}
}
pub async fn add_forward(
&self,
source: SocketAddrV4,
target: SocketAddrV4,
) -> Result<Arc<()>, Error> {
let (send, recv) = oneshot::channel();
self.req
.send(PortForwardCommand::AddForward {
source,
target,
respond: send,
})
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)?
}
pub async fn gc(&self) -> Result<(), Error> {
let (send, recv) = oneshot::channel();
self.req
.send(PortForwardCommand::Gc { respond: send })
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)?
}
pub async fn dump(&self) -> Result<BTreeMap<SocketAddrV4, SocketAddrV4>, Error> {
let (send, recv) = oneshot::channel();
self.req
.send(PortForwardCommand::Dump { respond: send })
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)
}
}
struct InterfaceForwardRequest {
external: u16,
target: SocketAddrV4,
filter: DynInterfaceFilter,
rc: Arc<()>,
}
#[derive(Clone)]
struct InterfaceForwardEntry {
external: u16,
filter: BTreeMap<DynInterfaceFilter, (SocketAddrV4, Weak<()>)>,
// Maps source SocketAddr -> strong reference for the forward created in PortForwardController
forwards: BTreeMap<SocketAddrV4, Arc<()>>,
}
impl IdOrdItem for InterfaceForwardEntry {
type Key<'a> = u16;
fn key(&self) -> Self::Key<'_> {
self.external
}
iddqd::id_upcast!();
}
impl InterfaceForwardEntry {
fn new(external: u16) -> Self {
Self {
external,
filter: BTreeMap::new(),
forwards: BTreeMap::new(),
}
}
async fn update(
&mut self,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
port_forward: &PortForwardController,
) -> Result<(), Error> {
let mut keep = BTreeSet::<SocketAddrV4>::new();
for (iface, info) in ip_info.iter() {
if let Some(target) = self
.filter
.iter()
.filter(|(_, (_, rc))| rc.strong_count() > 0)
.find(|(filter, _)| filter.filter(iface, info))
.map(|(_, (target, _))| *target)
{
if let Some(ip_info) = &info.ip_info {
for addr in ip_info.subnets.iter().filter_map(|net| {
if let IpAddr::V4(ip) = net.addr() {
Some(SocketAddrV4::new(ip, self.external))
} else {
None
}
}) {
keep.insert(addr);
if !self.forwards.contains_key(&addr) {
let rc = port_forward.add_forward(addr, target).await?;
self.forwards.insert(addr, rc);
}
}
}
}
}
// Remove forwards that should no longer exist (drops the strong references)
self.forwards.retain(|addr, _| keep.contains(addr));
Ok(())
}
async fn update_request(
&mut self,
InterfaceForwardRequest {
external,
target,
filter,
mut rc,
}: InterfaceForwardRequest,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
port_forward: &PortForwardController,
) -> Result<Arc<()>, Error> {
if external != self.external {
return Err(Error::new(
eyre!("Mismatched external port in InterfaceForwardEntry"),
ErrorKind::InvalidRequest,
));
}
let entry = self
.filter
.entry(filter)
.or_insert_with(|| (target, Arc::downgrade(&rc)));
if entry.0 != target {
entry.0 = target;
entry.1 = Arc::downgrade(&rc);
}
if let Some(existing) = entry.1.upgrade() {
rc = existing;
} else {
entry.1 = Arc::downgrade(&rc);
}
self.update(ip_info, port_forward).await?;
Ok(rc)
}
async fn gc(
&mut self,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
port_forward: &PortForwardController,
) -> Result<(), Error> {
self.filter.retain(|_, (_, rc)| rc.strong_count() > 0);
self.update(ip_info, port_forward).await
}
}
struct InterfaceForwardState {
port_forward: PortForwardController,
state: IdOrdMap<InterfaceForwardEntry>,
}
impl InterfaceForwardState {
fn new(port_forward: PortForwardController) -> Self {
Self {
port_forward,
state: IdOrdMap::new(),
}
}
}
impl InterfaceForwardState {
async fn handle_request(
&mut self,
request: ForwardRequest,
request: InterfaceForwardRequest,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
) -> Result<(), Error> {
) -> Result<Arc<()>, Error> {
self.state
.entry(request.external)
.or_insert_with(|| ForwardEntry::new(request.external, request.target, Weak::new()))
.update_request(request, ip_info)
.or_insert_with(|| InterfaceForwardEntry::new(request.external))
.update_request(request, ip_info, &self.port_forward)
.await
}
async fn sync(
&mut self,
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
) -> Result<(), Error> {
for entry in self.state.values_mut() {
entry.update(ip_info, None).await?;
for mut entry in self.state.iter_mut() {
entry.gc(ip_info, &self.port_forward).await?;
}
self.state.retain(|_, fwd| !fwd.forwards.is_empty());
Ok(())
}
}
@@ -209,28 +448,88 @@ fn err_has_exited<T>(_: T) -> Error {
)
}
pub struct PortForwardController {
req: mpsc::UnboundedSender<(Option<ForwardRequest>, oneshot::Sender<Result<(), Error>>)>,
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTarget {
pub target: SocketAddrV4,
pub filter: String,
}
impl From<&InterfaceForwardState> for ForwardTable {
fn from(value: &InterfaceForwardState) -> Self {
Self(
value
.state
.iter()
.flat_map(|entry| {
entry
.filter
.iter()
.filter(|(_, (_, rc))| rc.strong_count() > 0)
.map(|(filter, (target, _))| {
(
entry.external,
ForwardTarget {
target: *target,
filter: format!("{:?}", filter),
},
)
})
})
.collect(),
)
}
}
enum InterfaceForwardCommand {
Forward(
InterfaceForwardRequest,
oneshot::Sender<Result<Arc<()>, Error>>,
),
Sync(oneshot::Sender<Result<(), Error>>),
DumpTable(oneshot::Sender<ForwardTable>),
}
#[test]
fn test() {
use crate::net::gateway::SecureFilter;
assert_ne!(
false.into_dyn(),
SecureFilter { secure: false }.into_dyn().into_dyn()
);
}
pub struct InterfacePortForwardController {
req: mpsc::UnboundedSender<InterfaceForwardCommand>,
_thread: NonDetachingJoinHandle<()>,
}
impl PortForwardController {
impl InterfacePortForwardController {
pub fn new(mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>) -> Self {
let (req_send, mut req_recv) = mpsc::unbounded_channel::<(
Option<ForwardRequest>,
oneshot::Sender<Result<(), Error>>,
)>();
let port_forward = PortForwardController::new();
let (req_send, mut req_recv) = mpsc::unbounded_channel::<InterfaceForwardCommand>();
let thread = NonDetachingJoinHandle::from(tokio::spawn(async move {
let mut state = ForwardState::default();
let mut state = InterfaceForwardState::new(port_forward);
let mut interfaces = ip_info.read_and_mark_seen();
loop {
tokio::select! {
msg = req_recv.recv() => {
if let Some((msg, re)) = msg {
if let Some(req) = msg {
re.send(state.handle_request(req, &interfaces).await).ok();
} else {
re.send(state.sync(&interfaces).await).ok();
}
if let Some(cmd) = msg {
match cmd {
InterfaceForwardCommand::Forward(req, re) => {
re.send(state.handle_request(req, &interfaces).await).ok()
}
InterfaceForwardCommand::Sync(re) => {
re.send(state.sync(&interfaces).await).ok()
}
InterfaceForwardCommand::DumpTable(re) => {
re.send((&state).into()).ok()
}
};
} else {
break;
}
@@ -238,49 +537,61 @@ impl PortForwardController {
_ = ip_info.changed() => {
interfaces = ip_info.read();
state.sync(&interfaces).await.log_err();
state.port_forward.gc().await.log_err();
}
}
}
}));
Self {
req: req_send,
_thread: thread,
}
}
pub async fn add(
&self,
external: u16,
filter: impl InterfaceFilter,
target: SocketAddr,
filter: DynInterfaceFilter,
target: SocketAddrV4,
) -> Result<Arc<()>, Error> {
let rc = Arc::new(());
let (send, recv) = oneshot::channel();
self.req
.send((
Some(ForwardRequest {
.send(InterfaceForwardCommand::Forward(
InterfaceForwardRequest {
external,
target,
filter: filter.into_dyn(),
rc: Arc::downgrade(&rc),
}),
filter,
rc,
},
send,
))
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)?.map(|_| rc)
recv.await.map_err(err_has_exited)?
}
pub async fn gc(&self) -> Result<(), Error> {
let (send, recv) = oneshot::channel();
self.req.send((None, send)).map_err(err_has_exited)?;
self.req
.send(InterfaceForwardCommand::Sync(send))
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)?
}
pub async fn dump_table(&self) -> Result<ForwardTable, Error> {
let (req, res) = oneshot::channel();
self.req
.send(InterfaceForwardCommand::DumpTable(req))
.map_err(err_has_exited)?;
res.await.map_err(err_has_exited)
}
}
async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
async fn forward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error> {
Command::new("/usr/lib/startos/scripts/forward-port")
.env("iiface", interface)
.env("oiface", START9_BRIDGE_IFACE)
.env("sip", source.ip().to_string())
.env("dip", target.ip().to_string())
.env("sport", source.port().to_string())
@@ -290,11 +601,9 @@ async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Res
Ok(())
}
async fn unforward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
async fn unforward(source: SocketAddrV4, target: SocketAddrV4) -> Result<(), Error> {
Command::new("/usr/lib/startos/scripts/forward-port")
.env("UNDO", "1")
.env("iiface", interface)
.env("oiface", START9_BRIDGE_IFACE)
.env("sip", source.ip().to_string())
.env("dip", target.ip().to_string())
.env("sport", source.port().to_string())

View File

@@ -3,10 +3,11 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6};
use std::sync::{Arc, Weak};
use std::task::Poll;
use std::task::{Poll, ready};
use std::time::Duration;
use clap::Parser;
use futures::future::Either;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use helpers::NonDetachingJoinHandle;
use imbl::{OrdMap, OrdSet};
@@ -19,13 +20,14 @@ use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::sync::oneshot;
use ts_rs::TS;
use visit_rs::{Visit, VisitFields};
use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream};
use zbus::zvariant::{
DICT_ENTRY_SIG_END_STR, DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType,
Value as ZValue,
DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType, Value as ZValue,
};
use zbus::{Connection, proxy};
@@ -35,7 +37,7 @@ use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceTyp
use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::gateway::device::DeviceProxy;
use crate::net::utils::ipv6_is_link_local;
use crate::net::web_server::Accept;
use crate::net::web_server::{Accept, AcceptStream, Acceptor, MetadataVisitor};
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::collections::OrdMapIterMut;
@@ -130,64 +132,61 @@ async fn list_interfaces(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct NetworkInterfaceSetPublicParams {
interface: GatewayId,
gateway: GatewayId,
public: Option<bool>,
}
async fn set_public(
ctx: RpcContext,
NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams,
NetworkInterfaceSetPublicParams { gateway, public }: NetworkInterfaceSetPublicParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_public(&interface, Some(public.unwrap_or(true)))
.set_public(&gateway, Some(public.unwrap_or(true)))
.await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct UnsetInboundParams {
interface: GatewayId,
struct UnsetPublicParams {
gateway: GatewayId,
}
async fn unset_public(
ctx: RpcContext,
UnsetInboundParams { interface }: UnsetInboundParams,
UnsetPublicParams { gateway }: UnsetPublicParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_public(&interface, None)
.set_public(&gateway, None)
.await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct ForgetInterfaceParams {
interface: GatewayId,
struct ForgetGatewayParams {
gateway: GatewayId,
}
async fn forget_iface(
ctx: RpcContext,
ForgetInterfaceParams { interface }: ForgetInterfaceParams,
ForgetGatewayParams { gateway }: ForgetGatewayParams,
) -> Result<(), Error> {
ctx.net_controller.net_iface.forget(&interface).await
ctx.net_controller.net_iface.forget(&gateway).await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct RenameInterfaceParams {
interface: GatewayId,
name: String,
struct RenameGatewayParams {
id: GatewayId,
name: InternedString,
}
async fn set_name(
ctx: RpcContext,
RenameInterfaceParams { interface, name }: RenameInterfaceParams,
RenameGatewayParams { id, name }: RenameGatewayParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_name(&interface, &name)
.await
ctx.net_controller.net_iface.set_name(&id, name).await
}
#[proxy(
@@ -247,12 +246,19 @@ mod active_connection {
default_service = "org.freedesktop.NetworkManager"
)]
trait ConnectionSettings {
fn delete(&self) -> Result<(), Error>;
fn get_settings(&self) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error>;
fn update2(
&self,
settings: HashMap<String, HashMap<String, ZValue<'_>>>,
settings: HashMap<String, HashMap<String, OwnedValue>>,
flags: u32,
args: HashMap<String, ZValue<'_>>,
) -> Result<(), Error>;
) -> Result<HashMap<String, OwnedValue>, Error>;
#[zbus(signal)]
fn updated(&self) -> Result<(), Error>;
}
#[proxy(
@@ -556,6 +562,12 @@ async fn watch_ip(
let active_connection_proxy =
active_connection::ActiveConnectionProxy::new(&connection, dac).await?;
let settings_proxy = ConnectionSettingsProxy::new(
&connection,
active_connection_proxy.connection().await?,
)
.await?;
let mut until = Until::new()
.with_stream(
active_connection_proxy
@@ -569,20 +581,18 @@ async fn watch_ip(
.receive_dhcp4_config_changed()
.await
.stub(),
);
)
.with_stream(settings_proxy.receive_updated().await?.into_inner().stub());
loop {
until
.run(async {
let external = active_connection_proxy.state_flags().await? & 0x80 != 0;
if external {
return Ok(());
}
let device_type = match device_proxy.device_type().await? {
1 => Some(NetworkInterfaceType::Ethernet),
2 => Some(NetworkInterfaceType::Wireless),
13 => Some(NetworkInterfaceType::Bridge),
29 => Some(NetworkInterfaceType::Wireguard),
32 => Some(NetworkInterfaceType::Loopback),
_ => None,
};
@@ -628,12 +638,10 @@ async fn watch_ip(
let lan_ip = [
Some(ip4_proxy.gateway().await?)
.filter(|g| !g.is_empty())
.map(|g| g.parse::<IpAddr>())
.transpose()?,
.and_then(|g| g.parse::<IpAddr>().log_err()),
Some(ip6_proxy.gateway().await?)
.filter(|g| !g.is_empty())
.map(|g| g.parse::<IpAddr>())
.transpose()?,
.and_then(|g| g.parse::<IpAddr>().log_err()),
]
.into_iter()
.filter_map(|a| a)
@@ -650,9 +658,11 @@ async fn watch_ip(
}
if let Some(dns) = dhcp.domain_name_servers {
dns_servers.extend(
dns.split(",")
.map(|s| s.trim().parse::<IpAddr>())
.collect::<Result<Vec<_>, _>>()?,
dns.split_ascii_whitespace()
.filter_map(|s| {
s.parse::<IpAddr>().log_err()
})
.collect::<Vec<_>>(),
);
}
}
@@ -662,7 +672,14 @@ async fn watch_ip(
.into_iter()
.map(IpNet::try_from)
.try_collect()?;
let wan_ip = if !subnets.is_empty() {
let wan_ip = if !subnets.is_empty()
&& !matches!(
device_type,
Some(
NetworkInterfaceType::Bridge
| NetworkInterfaceType::Loopback
)
) {
match get_wan_ipv4(iface.as_str()).await {
Ok(a) => a,
Err(e) => {
@@ -676,7 +693,7 @@ async fn watch_ip(
} else {
None
};
let ip_info = Some(IpInfo {
let mut ip_info = IpInfo {
name: name.clone(),
scope_id,
device_type,
@@ -685,22 +702,34 @@ async fn watch_ip(
wan_ip,
ntp_servers,
dns_servers,
});
};
write_to.send_if_modified(
|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
let (public, secure) = m
let (name, public, secure, prev_wan_ip) = m
.get(&iface)
.map_or((None, None), |i| (i.public, i.secure));
.map_or((None, None, None, None), |i| {
(
i.name.clone(),
i.public,
i.secure,
i.ip_info
.as_ref()
.and_then(|i| i.wan_ip),
)
});
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip);
let ip_info = Arc::new(ip_info);
m.insert(
iface.clone(),
NetworkInterfaceInfo {
name,
public,
secure,
ip_info: ip_info.clone(),
ip_info: Some(ip_info.clone()),
},
)
.filter(|old| &old.ip_info == &ip_info)
.filter(|old| &old.ip_info == &Some(ip_info))
.is_none()
},
);
@@ -805,7 +834,7 @@ impl NetworkInterfaceWatcher {
self.ip_info.read()
}
pub fn bind(&self, port: u16) -> Result<NetworkInterfaceListener, Error> {
pub fn bind<B: Bind>(&self, bind: B, port: u16) -> Result<NetworkInterfaceListener<B>, Error> {
let arc = Arc::new(());
self.listeners.mutate(|l| {
if l.get(&port).filter(|w| w.strong_count() > 0).is_some() {
@@ -821,17 +850,17 @@ impl NetworkInterfaceWatcher {
Ok(NetworkInterfaceListener {
_arc: arc,
ip_info,
listeners: ListenerMap::new(port),
listeners: ListenerMap::new(bind, port),
})
}
pub fn upgrade_listener(
pub fn upgrade_listener<B: Bind>(
&self,
SelfContainedNetworkInterfaceListener {
mut listener,
..
}: SelfContainedNetworkInterfaceListener,
) -> Result<NetworkInterfaceListener, Error> {
}: SelfContainedNetworkInterfaceListener<B>,
) -> Result<NetworkInterfaceListener<B>, Error> {
let port = listener.listeners.port;
let arc = &listener._arc;
self.listeners.mutate(|l| {
@@ -924,11 +953,12 @@ impl NetworkInterfaceController {
Ok(())
}
pub fn new(db: TypedPatchDb<Database>) -> Self {
let (seeded_send, seeded) = oneshot::channel();
let watcher = NetworkInterfaceWatcher::new(
{
let db = db.clone();
async move {
match db
let info = match db
.peek()
.await
.as_public()
@@ -948,21 +978,26 @@ impl NetworkInterfaceController {
tracing::debug!("{e:?}");
OrdMap::new()
}
}
};
let _ = seeded_send.send(info.clone());
info
}
},
[START9_BRIDGE_IFACE.into()],
[InternedString::from_static(START9_BRIDGE_IFACE).into()],
);
let mut ip_info = watcher.subscribe();
let mut ip_info_watch = watcher.subscribe();
ip_info_watch.mark_seen();
Self {
db: db.clone(),
watcher,
_sync: tokio::spawn(async move {
let res: Result<(), Error> = async {
let mut ip_info = seeded.await.ok();
loop {
if let Err(e) = async {
let ip_info = ip_info.read();
Self::sync(&db, &ip_info).boxed().await?;
if let Some(ip_info) = ip_info {
Self::sync(&db, &ip_info).boxed().await?;
}
Ok::<_, Error>(())
}
@@ -972,7 +1007,8 @@ impl NetworkInterfaceController {
tracing::debug!("{e:?}");
}
let _ = ip_info.changed().await;
let _ = ip_info_watch.changed().await;
ip_info = Some(ip_info_watch.read());
}
}
.await;
@@ -1060,7 +1096,7 @@ impl NetworkInterfaceController {
.ip_info
.peek(|ifaces| ifaces.get(interface).map(|i| i.ip_info.is_some()))
else {
return Ok(());
return self.forget(interface).await;
};
if has_ip_info {
@@ -1080,7 +1116,21 @@ impl NetworkInterfaceController {
let device_proxy = DeviceProxy::new(&connection, device).await?;
device_proxy.delete().await?;
let ac = device_proxy.active_connection().await?;
if &*ac == "/" {
return Err(Error::new(
eyre!("Cannot delete device without active connection"),
ErrorKind::InvalidRequest,
));
}
let ac_proxy = active_connection::ActiveConnectionProxy::new(&connection, ac).await?;
let settings =
ConnectionSettingsProxy::new(&connection, ac_proxy.connection().await?).await?;
settings.delete().await?;
ip_info
.wait_for(|ifaces| ifaces.get(interface).map_or(true, |i| i.ip_info.is_none()))
@@ -1092,109 +1142,37 @@ impl NetworkInterfaceController {
Ok(())
}
pub async fn set_name(&self, interface: &GatewayId, name: &str) -> Result<(), Error> {
let (dump, mut sub) = self
pub async fn set_name(&self, interface: &GatewayId, name: InternedString) -> Result<(), Error> {
let mut sub = self
.db
.dump_and_sub(
.subscribe(
"/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?
.join_end(interface.as_str())
.join_end("ipInfo")
.join_end("name"),
)
.await;
let change = dump.value.as_str().or_not_found(interface)? != name;
if !change {
return Ok(());
let changed = self.watcher.ip_info.send_if_modified(|i| {
i.get_mut(interface)
.map(|i| {
if i.name.as_ref() != Some(&name) {
i.name = Some(name);
true
} else {
false
}
})
.unwrap_or(false)
});
if changed {
sub.recv().await;
}
let connection = Connection::system().await?;
let netman_proxy = NetworkManagerProxy::new(&connection).await?;
let device = Some(
netman_proxy
.get_device_by_ip_iface(interface.as_str())
.await?,
)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("{interface} in NetworkManager"))?;
let device_proxy = DeviceProxy::new(&connection, device).await?;
let dac = Some(device_proxy.active_connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ActiveConnection for {interface}"))?;
let dac_proxy = active_connection::ActiveConnectionProxy::new(&connection, dac).await?;
let settings = Some(dac_proxy.connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ConnectionSettings for {interface}"))?;
let settings_proxy = ConnectionSettingsProxy::new(&connection, settings).await?;
settings_proxy
.update2(
[(
"connection".into(),
[("id".into(), zbus::zvariant::Value::Str(name.into()))]
.into_iter()
.collect(),
)]
.into_iter()
.collect(),
0x1,
HashMap::new(),
)
.await?;
sub.recv().await;
Ok(())
}
}
struct ListenerMap {
prev_filter: DynInterfaceFilter,
port: u16,
listeners: BTreeMap<SocketAddr, (TcpListener, Option<Ipv4Addr>)>,
}
impl ListenerMap {
fn from_listener(listener: impl IntoIterator<Item = TcpListener>) -> Result<Self, Error> {
let mut port = 0;
let mut listeners = BTreeMap::<SocketAddr, (TcpListener, Option<Ipv4Addr>)>::new();
for listener in listener {
let mut local = listener.local_addr().with_kind(ErrorKind::Network)?;
if let SocketAddr::V6(l) = &mut local {
if ipv6_is_link_local(*l.ip()) && l.scope_id() == 0 {
continue; // TODO determine scope id
}
}
if port != 0 && port != local.port() {
return Err(Error::new(
eyre!("Provided listeners are bound to different ports"),
ErrorKind::InvalidRequest,
));
}
port = local.port();
listeners.insert(local, (listener, None));
}
if port == 0 {
return Err(Error::new(
eyre!("Listener array cannot be empty"),
ErrorKind::InvalidRequest,
));
}
Ok(Self {
prev_filter: false.into_dyn(),
port,
listeners,
})
}
}
pub trait InterfaceFilter: Any + Clone + std::fmt::Debug + Eq + Ord + Send + Sync {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool;
fn eq(&self, other: &dyn Any) -> bool {
@@ -1222,6 +1200,14 @@ impl InterfaceFilter for bool {
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TypeFilter(pub NetworkInterfaceType);
impl InterfaceFilter for TypeFilter {
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
info.ip_info.as_ref().and_then(|i| i.device_type) == Some(self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct IdFilter(pub GatewayId);
impl InterfaceFilter for IdFilter {
@@ -1236,7 +1222,7 @@ pub struct PublicFilter {
}
impl InterfaceFilter for PublicFilter {
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
self.public || !info.public()
self.public == info.public()
}
}
@@ -1327,6 +1313,9 @@ impl InterfaceFilter for DynInterfaceFilter {
fn as_any(&self) -> &dyn Any {
self.0.as_any()
}
fn into_dyn(self) -> DynInterfaceFilter {
self
}
}
impl DynInterfaceFilter {
fn new<T: InterfaceFilter>(value: T) -> Self {
@@ -1350,10 +1339,17 @@ impl Ord for DynInterfaceFilter {
}
}
impl ListenerMap {
fn new(port: u16) -> Self {
struct ListenerMap<B: Bind> {
prev_filter: DynInterfaceFilter,
bind: B,
port: u16,
listeners: BTreeMap<SocketAddr, B::Accept>,
}
impl<B: Bind> ListenerMap<B> {
fn new(bind: B, port: u16) -> Self {
Self {
prev_filter: false.into_dyn(),
bind,
port,
listeners: BTreeMap::new(),
}
@@ -1368,10 +1364,6 @@ impl ListenerMap {
let mut keep = BTreeSet::<SocketAddr>::new();
for (_, info) in ip_info
.iter()
.chain([
NetworkInterfaceInfo::loopback(),
NetworkInterfaceInfo::lxc_bridge(),
])
.filter(|(id, info)| filter.filter(*id, *info))
{
if let Some(ip_info) = &info.ip_info {
@@ -1391,24 +1383,9 @@ impl ListenerMap {
ip => SocketAddr::new(ip, self.port),
};
keep.insert(addr);
if let Some((_, wan_ip)) = self.listeners.get_mut(&addr) {
*wan_ip = info.ip_info.as_ref().and_then(|i| i.wan_ip);
continue;
if !self.listeners.contains_key(&addr) {
self.listeners.insert(addr, self.bind.bind(addr)?);
}
self.listeners.insert(
addr,
(
TcpListener::from_std(
mio::net::TcpListener::bind(addr)
.with_ctx(|_| {
(ErrorKind::Network, lazy_format!("binding to {addr:?}"))
})?
.into(),
)
.with_kind(ErrorKind::Network)?,
info.ip_info.as_ref().and_then(|i| i.wan_ip),
),
);
}
}
}
@@ -1416,24 +1393,13 @@ impl ListenerMap {
self.prev_filter = filter.clone().into_dyn();
Ok(())
}
fn poll_accept(&self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
for (bind_addr, (listener, wan_ip)) in self.listeners.iter() {
if let Poll::Ready((stream, addr)) = listener.poll_accept(cx)? {
if let Err(e) = socket2::SockRef::from(&stream).set_tcp_keepalive(
&socket2::TcpKeepalive::new()
.with_time(Duration::from_secs(900))
.with_interval(Duration::from_secs(60))
.with_retries(5),
) {
tracing::error!("Failed to set tcp keepalive: {e}");
tracing::debug!("{e:?}");
}
return Poll::Ready(Ok(Accepted {
stream,
peer: addr,
wan_ip: *wan_ip,
bind: *bind_addr,
}));
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(SocketAddr, <B::Accept as Accept>::Metadata, AcceptStream), Error>> {
for (addr, listener) in self.listeners.iter_mut() {
if let Poll::Ready((metadata, stream)) = listener.poll_accept(cx)? {
return Poll::Ready(Ok((*addr, metadata, stream)));
}
}
Poll::Pending
@@ -1444,54 +1410,100 @@ pub fn lookup_info_by_addr(
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
addr: SocketAddr,
) -> Option<(&GatewayId, &NetworkInterfaceInfo)> {
ip_info
.iter()
.chain([
NetworkInterfaceInfo::loopback(),
NetworkInterfaceInfo::lxc_bridge(),
])
.find(|(_, i)| {
i.ip_info
.as_ref()
.map_or(false, |i| i.subnets.iter().any(|i| i.addr() == addr.ip()))
})
ip_info.iter().find(|(_, i)| {
i.ip_info
.as_ref()
.map_or(false, |i| i.subnets.iter().any(|i| i.addr() == addr.ip()))
})
}
pub struct NetworkInterfaceListener {
pub trait Bind {
type Accept: Accept;
fn bind(&mut self, addr: SocketAddr) -> Result<Self::Accept, Error>;
}
#[derive(Clone, Copy, Default)]
pub struct BindTcp;
impl Bind for BindTcp {
type Accept = TcpListener;
fn bind(&mut self, addr: SocketAddr) -> Result<Self::Accept, Error> {
TcpListener::from_std(
mio::net::TcpListener::bind(addr)
.with_kind(ErrorKind::Network)?
.into(),
)
.with_kind(ErrorKind::Network)
}
}
pub trait FromGatewayInfo {
fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self;
}
#[derive(Clone, Debug)]
pub struct GatewayInfo {
pub id: GatewayId,
pub info: NetworkInterfaceInfo,
}
impl<V: MetadataVisitor> Visit<V> for GatewayInfo {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
visitor.visit(self)
}
}
impl FromGatewayInfo for GatewayInfo {
fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self {
Self {
id: id.clone(),
info: info.clone(),
}
}
}
pub struct NetworkInterfaceListener<B: Bind = BindTcp> {
pub ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
listeners: ListenerMap,
listeners: ListenerMap<B>,
_arc: Arc<()>,
}
impl NetworkInterfaceListener {
impl<B: Bind> NetworkInterfaceListener<B> {
pub(super) fn new(
mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
bind: B,
port: u16,
) -> Self {
ip_info.mark_unseen();
Self {
ip_info,
listeners: ListenerMap::new(bind, port),
_arc: Arc::new(()),
}
}
pub fn port(&self) -> u16 {
self.listeners.port
}
#[cfg_attr(feature = "unstable", inline(never))]
pub fn poll_accept(
pub fn poll_accept<M: FromGatewayInfo>(
&mut self,
cx: &mut std::task::Context<'_>,
filter: &impl InterfaceFilter,
) -> Poll<Result<Accepted, Error>> {
) -> Poll<Result<(M, <B::Accept as Accept>::Metadata, AcceptStream), Error>> {
while self.ip_info.poll_changed(cx).is_ready()
|| !DynInterfaceFilterT::eq(&self.listeners.prev_filter, filter.as_any())
{
self.ip_info
.peek_and_mark_seen(|ip_info| self.listeners.update(ip_info, filter))?;
}
self.listeners.poll_accept(cx)
}
pub(super) fn new(
mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
port: u16,
) -> Self {
ip_info.mark_unseen();
Self {
ip_info,
listeners: ListenerMap::new(port),
_arc: Arc::new(()),
}
let (addr, inner, stream) = ready!(self.listeners.poll_accept(cx)?);
Poll::Ready(Ok((
self.ip_info
.peek(|ip_info| {
lookup_info_by_addr(ip_info, addr)
.map(|(id, info)| M::from_gateway_info(id, info))
})
.or_not_found(lazy_format!("gateway for {addr}"))?,
inner,
stream,
)))
}
pub fn change_ip_info_source(
@@ -1502,7 +1514,10 @@ impl NetworkInterfaceListener {
self.ip_info = ip_info;
}
pub async fn accept(&mut self, filter: &impl InterfaceFilter) -> Result<Accepted, Error> {
pub async fn accept<M: FromGatewayInfo>(
&mut self,
filter: &impl InterfaceFilter,
) -> Result<(M, <B::Accept as Accept>::Metadata, AcceptStream), Error> {
futures::future::poll_fn(|cx| self.poll_accept(cx, filter)).await
}
@@ -1518,33 +1533,84 @@ impl NetworkInterfaceListener {
}
}
pub struct Accepted {
pub stream: TcpStream,
pub peer: SocketAddr,
pub wan_ip: Option<Ipv4Addr>,
pub bind: SocketAddr,
#[derive(VisitFields)]
pub struct NetworkInterfaceListenerAcceptMetadata<B: Bind> {
pub inner: <B::Accept as Accept>::Metadata,
pub info: GatewayInfo,
}
impl<B: Bind> Clone for NetworkInterfaceListenerAcceptMetadata<B>
where
<B::Accept as Accept>::Metadata: Clone,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
info: self.info.clone(),
}
}
}
impl<B, V> Visit<V> for NetworkInterfaceListenerAcceptMetadata<B>
where
B: Bind,
<B::Accept as Accept>::Metadata: Visit<V> + Clone + Send + Sync + 'static,
V: MetadataVisitor,
{
fn visit(&self, visitor: &mut V) -> V::Result {
self.visit_fields(visitor).collect()
}
}
pub struct SelfContainedNetworkInterfaceListener {
_watch_thread: NonDetachingJoinHandle<()>,
listener: NetworkInterfaceListener,
impl<B: Bind> Accept for NetworkInterfaceListener<B> {
type Metadata = NetworkInterfaceListenerAcceptMetadata<B>;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
NetworkInterfaceListener::poll_accept(self, cx, &true).map(|res| {
res.map(|(info, inner, stream)| {
(
NetworkInterfaceListenerAcceptMetadata { inner, info },
stream,
)
})
})
}
}
impl SelfContainedNetworkInterfaceListener {
pub fn bind(port: u16) -> Self {
pub struct SelfContainedNetworkInterfaceListener<B: Bind = BindTcp> {
_watch_thread: NonDetachingJoinHandle<()>,
listener: NetworkInterfaceListener<B>,
}
impl<B: Bind> SelfContainedNetworkInterfaceListener<B> {
pub fn bind(bind: B, port: u16) -> Self {
let ip_info = Watch::new(OrdMap::new());
let _watch_thread =
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
Self {
_watch_thread,
listener: NetworkInterfaceListener::new(ip_info, port),
listener: NetworkInterfaceListener::new(ip_info, bind, port),
}
}
}
impl Accept for SelfContainedNetworkInterfaceListener {
impl<B: Bind> Accept for SelfContainedNetworkInterfaceListener<B> {
type Metadata = <NetworkInterfaceListener<B> as Accept>::Metadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<super::web_server::Accepted, Error>> {
) -> std::task::Poll<Result<(Self::Metadata, AcceptStream), Error>> {
Accept::poll_accept(&mut self.listener, cx)
}
}
pub type UpgradableListener<B = BindTcp> =
Option<Either<SelfContainedNetworkInterfaceListener<B>, NetworkInterfaceListener<B>>>;
impl<B> Acceptor<UpgradableListener<B>>
where
B: Bind + Send + Sync + 'static,
B::Accept: Send + Sync,
{
pub fn bind_upgradable(listener: SelfContainedNetworkInterfaceListener<B>) -> Self {
Self::new(Some(Either::Left(listener)))
}
}

View File

@@ -27,6 +27,7 @@ pub enum HostAddress {
Domain {
address: InternedString,
public: Option<PublicDomainConfig>,
private: bool,
},
}
@@ -70,11 +71,14 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
for onion in host.as_onions().de()? {
check_onion(&mut onions, onion)?;
}
for domain in host.as_public_domains().keys()? {
check_domain(&mut domains, domain)?;
let public = host.as_public_domains().keys()?;
for domain in &public {
check_domain(&mut domains, domain.clone())?;
}
for domain in host.as_private_domains().de()? {
check_domain(&mut domains, domain)?;
if !public.contains(&domain) {
check_domain(&mut domains, domain)?;
}
}
}
for host in not_in_use {
@@ -88,11 +92,14 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
for onion in host.as_onions().de()? {
check_onion(&mut onions, onion)?;
}
for domain in host.as_public_domains().keys()? {
check_domain(&mut domains, domain)?;
let public = host.as_public_domains().keys()?;
for domain in &public {
check_domain(&mut domains, domain.clone())?;
}
for domain in host.as_private_domains().de()? {
check_domain(&mut domains, domain)?;
if !public.contains(&domain) {
check_domain(&mut domains, domain)?;
}
}
}
Ok(())
@@ -198,16 +205,21 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
HostAddress::Domain {
address,
public: Some(PublicDomainConfig { gateway, acme }),
private,
} => {
table.add_row(row![
address,
&format!("YES ({gateway})"),
&format!(
"{} ({gateway})",
if *private { "YES" } else { "ONLY" }
),
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
]);
}
HostAddress::Domain {
address,
public: None,
..
} => {
table.add_row(row![address, &format!("NO"), "N/A"]);
}
@@ -345,15 +357,7 @@ pub async fn add_onion<Kind: HostApiKind>(
OnionParams { onion }: OnionParams,
inheritance: Kind::Inheritance,
) -> Result<(), Error> {
let onion = onion
.strip_suffix(".onion")
.ok_or_else(|| {
Error::new(
eyre!("onion hostname must end in .onion"),
ErrorKind::InvalidOnionAddress,
)
})?
.parse::<OnionAddress>()?;
let onion = onion.parse::<OnionAddress>()?;
ctx.db
.mutate(|db| {
db.as_private().as_key_store().as_onion().get_key(&onion)?;
@@ -376,15 +380,7 @@ pub async fn remove_onion<Kind: HostApiKind>(
OnionParams { onion }: OnionParams,
inheritance: Kind::Inheritance,
) -> Result<(), Error> {
let onion = onion
.strip_suffix(".onion")
.ok_or_else(|| {
Error::new(
eyre!("onion hostname must end in .onion"),
ErrorKind::InvalidOnionAddress,
)
})?
.parse::<OnionAddress>()?;
let onion = onion.parse::<OnionAddress>()?;
ctx.db
.mutate(|db| {
Kind::host_for(&inheritance, db)?

View File

@@ -1,11 +1,11 @@
use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use clap::Parser;
use clap::builder::ValueParserFactory;
use imbl::OrdSet;
use models::{FromStrParser, GatewayId, HostId};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -16,7 +16,7 @@ use crate::net::gateway::InterfaceFilter;
use crate::net::host::HostApiKind;
use crate::net::vhost::AlpnInfo;
use crate::prelude::*;
use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::serde::{HandlerExtSerde, display_serializable};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
#[ts(export)]
@@ -135,11 +135,12 @@ impl BindInfo {
}
impl InterfaceFilter for NetInfo {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
info.ip_info.is_some()
&& if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
}
}
@@ -169,8 +170,8 @@ pub struct AddSslOptions {
pub alpn: Option<AlpnInfo>,
}
pub fn binding<C: Context, Kind: HostApiKind>(
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
pub fn binding<C: Context, Kind: HostApiKind>()
-> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
.subcommand(
"list",

View File

@@ -56,14 +56,17 @@ impl Host {
.map(|(address, config)| HostAddress::Domain {
address: address.clone(),
public: Some(config.clone()),
private: self.private_domains.contains(address),
}),
)
.chain(
self.private_domains
.iter()
.filter(|a| !self.public_domains.contains_key(*a))
.map(|address| HostAddress::Domain {
address: address.clone(),
public: None,
private: true,
}),
)
}

View File

@@ -1,585 +0,0 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::{FutureExt, StreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use iroh::{Endpoint, NodeId, SecretKey};
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use crate::context::{CliContext, RpcContext};
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::ReadWriter;
use crate::util::serde::{
deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, Pem,
PemEncoding, WithIoFormat,
};
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
const HRP: bech32::Hrp = bech32::Hrp::parse_unchecked("iroh");
#[derive(Debug, Clone, Copy)]
pub struct IrohAddress(pub NodeId);
impl std::fmt::Display for IrohAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bech32::encode_lower_to_fmt::<bech32::Bech32m, _>(f, HRP, self.0.as_bytes())
.map_err(|_| std::fmt::Error)?;
write!(f, ".p2p.start9.to")
}
}
impl FromStr for IrohAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(b32) = s.strip_suffix(".p2p.start9.to") {
let (hrp, data) = bech32::decode(b32).with_kind(ErrorKind::ParseNetAddress)?;
ensure_code!(
hrp == HRP,
ErrorKind::ParseNetAddress,
"not an iroh address"
);
Ok(Self(
NodeId::from_bytes(&*<Box<[u8; 32]>>::try_from(data).map_err(|_| {
Error::new(eyre!("invalid length"), ErrorKind::ParseNetAddress)
})?)
.with_kind(ErrorKind::ParseNetAddress)?,
))
} else {
Err(Error::new(
eyre!("Invalid iroh address"),
ErrorKind::ParseNetAddress,
))
}
}
}
impl Serialize for IrohAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_display(self, serializer)
}
}
impl<'de> Deserialize<'de> for IrohAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_from_str(deserializer)
}
}
impl PartialEq for IrohAddress {
fn eq(&self, other: &Self) -> bool {
self.0.as_ref() == other.0.as_ref()
}
}
impl Eq for IrohAddress {}
impl PartialOrd for IrohAddress {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0.as_ref().partial_cmp(other.0.as_ref())
}
}
impl Ord for IrohAddress {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.as_ref().cmp(other.0.as_ref())
}
}
#[derive(Clone, Debug)]
pub struct IrohSecretKey(pub SecretKey);
impl IrohSecretKey {
pub fn iroh_address(&self) -> IrohAddress {
IrohAddress(self.0.public())
}
pub fn generate() -> Self {
Self(SecretKey::generate(
&mut ssh_key::rand_core::OsRng::default(),
))
}
}
impl PemEncoding for IrohSecretKey {
fn from_pem<E: serde::de::Error>(pem: &str) -> Result<Self, E> {
ed25519_dalek::SigningKey::from_pem(pem)
.map(From::from)
.map(Self)
}
fn to_pem<E: serde::ser::Error>(&self) -> Result<String, E> {
self.0.secret().to_pem()
}
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct IrohKeyStore(BTreeMap<IrohAddress, Pem<IrohSecretKey>>);
impl Map for IrohKeyStore {
type Key = IrohAddress;
type Value = Pem<IrohSecretKey>;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Self::key_string(key)
}
fn key_string(key: &Self::Key) -> Result<imbl_value::InternedString, Error> {
Ok(InternedString::from_display(key))
}
}
impl IrohKeyStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: IrohSecretKey) {
self.0.insert(key.iroh_address(), Pem::new(key));
}
}
impl Model<IrohKeyStore> {
pub fn new_key(&mut self) -> Result<IrohSecretKey, Error> {
let key = IrohSecretKey::generate();
self.insert(&key.iroh_address(), &Pem::new(key))?;
Ok(key)
}
pub fn insert_key(&mut self, key: &IrohSecretKey) -> Result<(), Error> {
self.insert(&key.iroh_address(), Pem::new_ref(key))
}
pub fn get_key(&self, address: &IrohAddress) -> Result<IrohSecretKey, Error> {
self.as_idx(address)
.or_not_found(lazy_format!("private key for {address}"))?
.de()
.map(|k| k.0)
}
}
pub fn iroh_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list-services",
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display the status of running iroh services")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("Manage the iroh service key store"),
)
}
pub fn key<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("Generate an iroh service key and add it to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("Add an iroh service key to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_keys)
.with_custom_display_fn(|_, res| {
for addr in res {
println!("{addr}");
}
Ok(())
})
.with_about("List iroh services with keys in the key store")
.with_call_remote::<CliContext>(),
)
}
pub async fn generate_key(ctx: RpcContext) -> Result<IrohAddress, Error> {
ctx.db
.mutate(|db| {
Ok(db
.as_private_mut()
.as_key_store_mut()
.as_iroh_mut()
.new_key()?
.iroh_address())
})
.await
.result
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddKeyParams {
pub key: Pem<IrohSecretKey>,
}
pub async fn add_key(
ctx: RpcContext,
AddKeyParams { key }: AddKeyParams,
) -> Result<IrohAddress, Error> {
ctx.db
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_iroh_mut()
.insert_key(&key.0)
})
.await
.result?;
Ok(key.iroh_address())
}
pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<IrohAddress>, Error> {
ctx.db
.peek()
.await
.into_private()
.into_key_store()
.into_iroh()
.keys()
}
pub fn display_services(
params: WithIoFormat<Empty>,
services: BTreeMap<IrohAddress, IrohServiceInfo>,
) -> Result<(), Error> {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, services);
}
let mut table = Table::new();
table.add_row(row![bc => "ADDRESS", "BINDINGS"]);
for (service, info) in services {
let row = row![
&service.to_string(),
&info
.bindings
.into_iter()
.map(|((subdomain, port), addr)| lazy_format!("{subdomain}:{port} -> {addr}"))
.join("; ")
];
table.add_row(row);
}
table.print_tty(false)?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IrohServiceInfo {
pub bindings: BTreeMap<(InternedString, u16), SocketAddr>,
}
pub async fn list_services(
ctx: RpcContext,
_: Empty,
) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
ctx.net_controller.iroh.list_services().await
}
#[derive(Clone)]
pub struct IrohController(Arc<IrohControllerInner>);
struct IrohControllerInner {
// client: Endpoint,
services: SyncMutex<BTreeMap<IrohAddress, IrohService>>,
}
impl IrohController {
pub fn new() -> Result<Self, Error> {
Ok(Self(Arc::new(IrohControllerInner {
services: SyncMutex::new(BTreeMap::new()),
})))
}
pub fn service(&self, key: IrohSecretKey) -> Result<IrohService, Error> {
self.0.services.mutate(|s| {
use std::collections::btree_map::Entry;
let addr = key.iroh_address();
match s.entry(addr) {
Entry::Occupied(e) => Ok(e.get().clone()),
Entry::Vacant(e) => Ok(e
.insert(IrohService::launch(self.0.client.clone(), key)?)
.clone()),
}
})
}
pub async fn gc(&self, addr: Option<IrohAddress>) -> Result<(), Error> {
if let Some(addr) = addr {
if let Some(s) = self.0.services.mutate(|s| {
let rm = if let Some(s) = s.get(&addr) {
!s.gc()
} else {
false
};
if rm {
s.remove(&addr)
} else {
None
}
}) {
s.shutdown().await
} else {
Ok(())
}
} else {
for s in self.0.services.mutate(|s| {
let mut rm = Vec::new();
s.retain(|_, s| {
if s.gc() {
true
} else {
rm.push(s.clone());
false
}
});
rm
}) {
s.shutdown().await?;
}
Ok(())
}
}
pub async fn list_services(&self) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
Ok(self
.0
.services
.peek(|s| s.iter().map(|(a, s)| (a.clone(), s.info())).collect()))
}
pub async fn connect_iroh(
&self,
addr: &IrohAddress,
port: u16,
) -> Result<Box<dyn ReadWriter + Unpin + Send + Sync + 'static>, Error> {
if let Some(target) = self.0.services.peek(|s| {
s.get(addr).and_then(|s| {
s.0.bindings.peek(|b| {
b.get(&port).and_then(|b| {
b.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(a, _)| *a)
})
})
})
}) {
Ok(Box::new(
TcpStream::connect(target)
.await
.with_kind(ErrorKind::Network)?,
))
} else {
todo!()
}
}
}
#[derive(Clone)]
pub struct IrohService(Arc<IrohServiceData>);
struct IrohServiceData {
service: Endpoint,
bindings: Arc<SyncRwLock<BTreeMap<(InternedString, u16), BTreeMap<SocketAddr, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>,
}
impl IrohService {
fn launch(
mut client: Watch<(usize, IrohClient<TokioRustlsRuntime>)>,
key: IrohSecretKey,
) -> Result<Self, Error> {
let service = Arc::new(SyncMutex::new(None));
let bindings = Arc::new(SyncRwLock::new(BTreeMap::<
u16,
BTreeMap<SocketAddr, Weak<()>>,
>::new()));
Ok(Self(Arc::new(IrohServiceData {
service: service.clone(),
bindings: bindings.clone(),
_thread: tokio::spawn(async move {
let (bg, mut runner) = BackgroundJobQueue::new();
runner
.run_while(async {
loop {
if let Err(e) = async {
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
let epoch = client.peek(|(e, c)| {
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Iroh, "client recycled");
Ok::<_, Error>(*e)
})?;
let (new_service, stream) = client.peek(|(_, c)| {
c.launch_onion_service_with_hsid(
IrohServiceConfigBuilder::default()
.nickname(
key.iroh_address()
.to_string()
.trim_end_matches(".onion")
.parse::<HsNickname>()
.with_kind(ErrorKind::Iroh)?,
)
.build()
.with_kind(ErrorKind::Iroh)?,
key.clone().0,
)
.with_kind(ErrorKind::Iroh)
})?;
let mut status_stream = new_service.status_events();
bg.add_job(async move {
while let Some(status) = status_stream.next().await {
// TODO: health daemon?
}
});
service.replace(Some(new_service));
let mut stream = tor_hsservice::handle_rend_requests(stream);
while let Some(req) = tokio::select! {
req = stream.next() => req,
_ = client.wait_for(|(e, _)| *e != epoch) => None
} {
bg.add_job({
let bg = bg.clone();
let bindings = bindings.clone();
async move {
if let Err(e) = async {
let IncomingStreamRequest::Begin(begin) =
req.request()
else {
return req
.reject(tor_cell::relaycell::msg::End::new_with_reason(
tor_cell::relaycell::msg::EndReason::DONE,
))
.await
.with_kind(ErrorKind::Iroh);
};
let Some(target) = bindings.peek(|b| {
b.get(&begin.port()).and_then(|a| {
a.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(addr, _)| *addr)
})
}) else {
return req
.reject(tor_cell::relaycell::msg::End::new_with_reason(
tor_cell::relaycell::msg::EndReason::DONE,
))
.await
.with_kind(ErrorKind::Iroh);
};
bg.add_job(async move {
if let Err(e) = async {
let mut outgoing =
TcpStream::connect(target)
.await
.with_kind(ErrorKind::Network)?;
let mut incoming = req
.accept(Connected::new_empty())
.await
.with_kind(ErrorKind::Iroh)?;
if let Err(e) =
tokio::io::copy_bidirectional(
&mut outgoing,
&mut incoming,
)
.await
{
tracing::error!("Iroh Stream Error: {e}");
tracing::debug!("{e:?}");
}
Ok::<_, Error>(())
}
.await
{
tracing::trace!("Iroh Stream Error: {e}");
tracing::trace!("{e:?}");
}
});
Ok::<_, Error>(())
}
.await
{
tracing::trace!("Iroh Request Error: {e}");
tracing::trace!("{e:?}");
}
}
});
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("Iroh Client Error: {e}");
tracing::debug!("{e:?}");
}
}
})
.await
})
.into(),
})))
}
pub fn proxy_all<Rcs: FromIterator<Arc<()>>>(
&self,
bindings: impl IntoIterator<Item = (InternedString, u16, SocketAddr)>,
) -> Rcs {
self.0.bindings.mutate(|b| {
bindings
.into_iter()
.map(|(subdomain, port, target)| {
let entry = b
.entry((subdomain, port))
.or_default()
.entry(target)
.or_default();
if let Some(rc) = entry.upgrade() {
rc
} else {
let rc = Arc::new(());
*entry = Arc::downgrade(&rc);
rc
}
})
.collect()
})
}
pub fn gc(&self) -> bool {
self.0.bindings.mutate(|b| {
b.retain(|_, targets| {
targets.retain(|_, rc| rc.strong_count() > 0);
!targets.is_empty()
});
!b.is_empty()
})
}
pub async fn shutdown(self) -> Result<(), Error> {
self.0.service.replace(None);
self.0._thread.abort();
Ok(())
}
pub fn state(&self) -> IrohServiceState {
self.0
.service
.peek(|s| s.as_ref().map(|s| s.status().state().into()))
.unwrap_or(IrohServiceState::Bootstrapping)
}
pub fn info(&self) -> IrohServiceInfo {
IrohServiceInfo {
state: self.state(),
bindings: self.0.bindings.peek(|b| {
b.iter()
.filter_map(|(port, b)| {
b.iter()
.find(|(_, rc)| rc.strong_count() > 0)
.map(|(addr, _)| (*port, *addr))
})
.collect()
}),
}
}
}

View File

@@ -2,17 +2,15 @@ use serde::{Deserialize, Serialize};
use crate::account::AccountInfo;
use crate::net::acme::AcmeCertStore;
use crate::net::iroh::IrohKeyStore;
use crate::net::ssl::CertStore;
use crate::net::tor::OnionKeyStore;
use crate::net::tor::OnionStore;
use crate::prelude::*;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
#[serde(rename_all = "camelCase")]
pub struct KeyStore {
pub onion: OnionKeyStore,
#[serde(default)]
pub iroh: IrohKeyStore,
pub onion: OnionStore,
pub local_certs: CertStore,
#[serde(default)]
pub acme: AcmeCertStore,
@@ -20,8 +18,7 @@ pub struct KeyStore {
impl KeyStore {
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
let mut res = Self {
onion: OnionKeyStore::new(),
iroh: IrohKeyStore::new(),
onion: OnionStore::new(),
local_certs: CertStore::new(account)?,
acme: AcmeCertStore::new(),
};

View File

@@ -5,7 +5,6 @@ pub mod dns;
pub mod forward;
pub mod gateway;
pub mod host;
pub mod iroh;
pub mod keys;
pub mod mdns;
pub mod net_controller;
@@ -13,6 +12,7 @@ pub mod service_interface;
pub mod socks;
pub mod ssl;
pub mod static_server;
pub mod tls;
pub mod tor;
pub mod tunnel;
pub mod utils;
@@ -34,6 +34,10 @@ pub fn net_api<C: Context>() -> ParentHandler<C> {
"dns",
dns::dns_api::<C>().with_about("Manage and query DNS"),
)
.subcommand(
"forward",
forward::forward_api::<C>().with_about("Manage port forwards"),
)
.subcommand(
"gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),

View File

@@ -1,48 +1,48 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::{Ipv4Addr, SocketAddr};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::{Arc, Weak};
use color_eyre::eyre::eyre;
use imbl::{vector, OrdMap};
use imbl::{OrdMap, vector};
use imbl_value::InternedString;
use ipnet::IpNet;
use models::{HostId, OptionExt, PackageId};
use models::{GatewayId, HostId, OptionExt, PackageId};
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio_rustls::rustls::ClientConfig as TlsClientConfig;
use tracing::instrument;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::HOST_IP;
use crate::db::model::Database;
use crate::db::model::public::NetworkInterfaceType;
use crate::error::ErrorCollection;
use crate::hostname::Hostname;
use crate::net::dns::DnsController;
use crate::net::forward::PortForwardController;
use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE};
use crate::net::gateway::{
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter,
PublicFilter, SecureFilter,
PublicFilter, SecureFilter, TypeFilter,
};
use crate::net::host::address::HostAddress;
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
use crate::net::host::{host_for, Host, Hosts};
use crate::net::iroh::IrohController;
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
use crate::net::host::{Host, Hosts, host_for};
use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname, OnionHostname};
use crate::net::socks::SocksController;
use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
use crate::net::utils::ipv6_is_local;
use crate::net::vhost::{AlpnInfo, TargetInfo, VHostController};
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
use crate::prelude::*;
use crate::service::effects::callbacks::ServiceCallbacks;
use crate::util::serde::MaybeUtf8String;
use crate::HOST_IP;
pub struct NetController {
pub(crate) db: TypedPatchDb<Database>,
pub(super) tor: TorController,
pub(super) iroh: IrohController,
pub(super) vhost: VHostController,
pub(super) tls_client_config: Arc<TlsClientConfig>,
pub(crate) net_iface: Arc<NetworkInterfaceController>,
pub(super) dns: DnsController,
pub(super) forward: PortForwardController,
pub(super) forward: InterfacePortForwardController,
pub(super) socks: SocksController,
pub(super) server_hostnames: Vec<Option<InternedString>>,
pub(crate) callbacks: Arc<ServiceCallbacks>,
@@ -56,15 +56,27 @@ impl NetController {
) -> Result<Self, Error> {
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
let tor = TorController::new()?;
let iroh = IrohController::new()?;
let socks = SocksController::new(socks_listen, tor.clone())?;
let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
let tls_client_config = Arc::new(crate::net::tls::client_config(
crypto_provider.clone(),
[&*db
.peek()
.await
.as_private()
.as_key_store()
.as_local_certs()
.as_root_cert()
.de()?
.0],
)?);
Ok(Self {
db: db.clone(),
tor,
iroh,
vhost: VHostController::new(db.clone(), net_iface.clone()),
vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider),
tls_client_config,
dns: DnsController::init(db, &net_iface.watcher).await?,
forward: PortForwardController::new(net_iface.watcher.subscribe()),
forward: InterfacePortForwardController::new(net_iface.watcher.subscribe()),
net_iface,
socks,
server_hostnames: vec![
@@ -137,8 +149,8 @@ impl NetController {
#[derive(Default, Debug)]
struct HostBinds {
forwards: BTreeMap<u16, (SocketAddr, DynInterfaceFilter, Arc<()>)>,
vhosts: BTreeMap<(Option<InternedString>, u16), (TargetInfo, Arc<()>)>,
forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter, Arc<()>)>,
vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>,
private_dns: BTreeMap<InternedString, Arc<()>>,
tor: BTreeMap<OnionAddress, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
}
@@ -229,8 +241,8 @@ impl NetServiceData {
}
async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> {
let mut forwards: BTreeMap<u16, (SocketAddr, DynInterfaceFilter)> = BTreeMap::new();
let mut vhosts: BTreeMap<(Option<InternedString>, u16), TargetInfo> = BTreeMap::new();
let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new();
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
let mut tor: BTreeMap<OnionAddress, (TorSecretKey, OrdMap<u16, SocketAddr>)> =
BTreeMap::new();
@@ -267,11 +279,13 @@ impl NetServiceData {
for hostname in ctrl.server_hostnames.iter().cloned() {
vhosts.insert(
(hostname, external),
TargetInfo {
ProxyTarget {
filter: bind.net.clone().into_dyn(),
acme: None,
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
}
@@ -282,31 +296,35 @@ impl NetServiceData {
if hostnames.insert(hostname.clone()) {
vhosts.insert(
(Some(hostname), external),
TargetInfo {
ProxyTarget {
filter: OrFilter(
IdFilter(
NetworkInterfaceInfo::loopback().0.clone(),
),
IdFilter(
NetworkInterfaceInfo::lxc_bridge().0.clone(),
),
TypeFilter(NetworkInterfaceType::Loopback),
IdFilter(GatewayId::from(InternedString::from(
START9_BRIDGE_IFACE,
))),
)
.into_dyn(),
acme: None,
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
); // TODO: wrap onion ssl stream directly in tor ctrl
}
}
HostAddress::Domain { address, public } => {
HostAddress::Domain {
address,
public,
private,
} => {
if hostnames.insert(address.clone()) {
let address = Some(address.clone());
if ssl.preferred_external_port == 443 {
if let Some(public) = &public {
vhosts.insert(
(address.clone(), 5443),
TargetInfo {
ProxyTarget {
filter: AndFilter(
bind.net.clone(),
AndFilter(
@@ -317,29 +335,42 @@ impl NetServiceData {
.into_dyn(),
acme: public.acme.clone(),
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
vhosts.insert(
(address.clone(), 443),
TargetInfo {
ProxyTarget {
filter: AndFilter(
bind.net.clone(),
OrFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: false },
),
if private {
OrFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: false },
)
.into_dyn()
} else {
AndFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: true },
)
.into_dyn()
},
)
.into_dyn(),
acme: public.acme.clone(),
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
} else {
vhosts.insert(
(address.clone(), 443),
TargetInfo {
ProxyTarget {
filter: AndFilter(
bind.net.clone(),
PublicFilter { public: false },
@@ -347,7 +378,9 @@ impl NetServiceData {
.into_dyn(),
acme: None,
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
}
@@ -355,25 +388,42 @@ impl NetServiceData {
if let Some(public) = public {
vhosts.insert(
(address.clone(), external),
TargetInfo {
ProxyTarget {
filter: AndFilter(
bind.net.clone(),
IdFilter(public.gateway.clone()),
if private {
OrFilter(
IdFilter(public.gateway.clone()),
PublicFilter { public: false },
)
.into_dyn()
} else {
IdFilter(public.gateway.clone())
.into_dyn()
},
)
.into_dyn(),
acme: public.acme.clone(),
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
} else {
vhosts.insert(
(address.clone(), external),
TargetInfo {
filter: bind.net.clone().into_dyn(),
ProxyTarget {
filter: AndFilter(
bind.net.clone(),
PublicFilter { public: false },
)
.into_dyn(),
acme: None,
addr,
connect_ssl: connect_ssl.clone(),
connect_ssl: connect_ssl
.clone()
.map(|_| ctrl.tls_client_config.clone()),
},
);
}
@@ -392,7 +442,7 @@ impl NetServiceData {
forwards.insert(
external,
(
(self.ip, *port).into(),
SocketAddrV4::new(self.ip, *port),
AndFilter(
SecureFilter {
secure: bind.options.secure.is_some(),
@@ -405,66 +455,96 @@ impl NetServiceData {
}
let mut bind_hostname_info: Vec<HostnameInfo> =
hostname_info.remove(port).unwrap_or_default();
for (interface, info) in net_ifaces
for (gateway_id, info) in net_ifaces
.iter()
.filter(|(_, info)| {
info.ip_info.as_ref().map_or(false, |i| {
!matches!(i.device_type, Some(NetworkInterfaceType::Bridge))
})
})
.filter(|(id, info)| bind.net.filter(id, info))
{
if !info.public() {
let gateway = GatewayInfo {
id: gateway_id.clone(),
name: info
.name
.clone()
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
.unwrap_or_else(|| gateway_id.clone().into()),
public: info.public(),
};
let port = bind.net.assigned_port.filter(|_| {
bind.options.secure.map_or(false, |s| {
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
})
});
if !info.public()
&& info.ip_info.as_ref().map_or(false, |i| {
i.device_type != Some(NetworkInterfaceType::Wireguard)
})
{
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
gateway: gateway.clone(),
public: false,
hostname: IpHostname::Local {
value: InternedString::from_display(&{
let hostname = &hostname;
lazy_format!("{hostname}.local")
}),
port: bind.net.assigned_port,
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
}
for address in host.addresses() {
if let HostAddress::Domain {
address, public, ..
address,
public,
private,
} = address
{
if bind
.options
.add_ssl
.as_ref()
.map_or(false, |ssl| ssl.preferred_external_port == 443)
{
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
public: public.is_some(),
hostname: IpHostname::Domain {
value: address.clone(),
port: None,
ssl_port: Some(443),
},
});
} else {
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
public: public.is_some(),
hostname: IpHostname::Domain {
value: address.clone(),
port: bind.net.assigned_port,
ssl_port: bind.net.assigned_ssl_port,
},
});
let private = private && !info.public();
let public =
public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
if public || private {
if bind
.options
.add_ssl
.as_ref()
.map_or(false, |ssl| ssl.preferred_external_port == 443)
{
bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(),
public,
hostname: IpHostname::Domain {
value: address.clone(),
port: None,
ssl_port: Some(443),
},
});
} else {
bind_hostname_info.push(HostnameInfo::Ip {
gateway: gateway.clone(),
public,
hostname: IpHostname::Domain {
value: address.clone(),
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
}
}
}
}
if let Some(ip_info) = &info.ip_info {
let public = info.public();
if let Some(wan_ip) = ip_info.wan_ip.filter(|_| public) {
if let Some(wan_ip) = ip_info.wan_ip {
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
public,
gateway: gateway.clone(),
public: true,
hostname: IpHostname::Ipv4 {
value: wan_ip,
port: bind.net.assigned_port,
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
@@ -474,11 +554,11 @@ impl NetServiceData {
IpNet::V4(net) => {
if !public {
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
gateway: gateway.clone(),
public,
hostname: IpHostname::Ipv4 {
value: net.addr(),
port: bind.net.assigned_port,
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
@@ -486,12 +566,12 @@ impl NetServiceData {
}
IpNet::V6(net) => {
bind_hostname_info.push(HostnameInfo::Ip {
gateway_id: interface.clone(),
gateway: gateway.clone(),
public: public && !ipv6_is_local(net.addr()),
hostname: IpHostname::Ipv6 {
value: net.addr(),
scope_id: ip_info.scope_id,
port: bind.net.assigned_port,
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
@@ -606,7 +686,10 @@ impl NetServiceData {
if let Some(prev) = prev {
prev
} else {
(target.clone(), ctrl.vhost.add(key.0, key.1, target)?)
(
target.clone(),
ctrl.vhost.add(key.0, key.1, DynVHostTarget::new(target))?,
)
},
);
} else {
@@ -641,7 +724,7 @@ impl NetServiceData {
.collect::<BTreeSet<_>>();
for onion in all {
let mut prev = binds.tor.remove(&onion);
if let Some((key, tor_binds)) = tor.remove(&onion) {
if let Some((key, tor_binds)) = tor.remove(&onion).filter(|(_, b)| !b.is_empty()) {
prev = prev.filter(|(b, _)| b == &tor_binds);
binds.tor.insert(
onion,

View File

@@ -12,8 +12,7 @@ use ts_rs::TS;
#[serde(tag = "kind")]
pub enum HostnameInfo {
Ip {
#[ts(type = "string")]
gateway_id: GatewayId,
gateway: GatewayInfo,
public: bool,
hostname: IpHostname,
},
@@ -30,6 +29,15 @@ impl HostnameInfo {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GatewayInfo {
pub id: GatewayId,
pub name: InternedString,
pub public: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]

View File

@@ -1,5 +1,6 @@
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc;
use std::time::Duration;
use helpers::NonDetachingJoinHandle;
use socks5_impl::protocol::{Address, Reply};
@@ -7,10 +8,10 @@ use socks5_impl::server::auth::NoAuth;
use socks5_impl::server::{AuthAdaptor, ClientConnection, Server};
use tokio::net::{TcpListener, TcpStream};
use crate::HOST_IP;
use crate::net::tor::TorController;
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::HOST_IP;
pub const DEFAULT_SOCKS_LISTEN: SocketAddr = SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(HOST_IP[0], HOST_IP[1], HOST_IP[2], HOST_IP[3]),
@@ -22,15 +23,21 @@ pub struct SocksController {
}
impl SocksController {
pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> {
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
let listener = TcpListener::from_std(
mio::net::TcpListener::bind(listen)
.with_kind(ErrorKind::Network)?
.into(),
)
.with_kind(ErrorKind::Network)?;
Ok(Self {
_thread: tokio::spawn(async move {
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
let listener;
loop {
if let Some(l) = TcpListener::bind(listen)
.await
.with_kind(ErrorKind::Network)
.log_err()
{
listener = l;
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
let (bg, mut runner) = BackgroundJobQueue::new();
runner
.run_while(async {
@@ -149,8 +156,8 @@ impl SocksController {
}
.await
{
tracing::error!("SOCKS5 Stream Error: {e}");
tracing::debug!("{e:?}");
tracing::trace!("SOCKS5 Stream Error: {e}");
tracing::trace!("{e:?}");
}
});
}

View File

@@ -2,6 +2,7 @@ use std::cmp::{Ordering, min};
use std::collections::{BTreeMap, BTreeSet};
use std::net::IpAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::FutureExt;
@@ -10,22 +11,43 @@ use libc::time_t;
use openssl::asn1::{Asn1Integer, Asn1Time, Asn1TimeRef};
use openssl::bn::{BigNum, MsbOption};
use openssl::ec::{EcGroup, EcKey};
use openssl::error::ErrorStack;
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::{PKey, Private};
use openssl::x509::{X509, X509Builder, X509Extension, X509NameBuilder};
use openssl::x509::extension::{
AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectAlternativeName,
SubjectKeyIdentifier,
};
use openssl::x509::{X509, X509Builder, X509NameBuilder};
use openssl::*;
use patch_db::HasModel;
use serde::{Deserialize, Serialize};
use tokio_rustls::rustls::ServerConfig;
use tokio_rustls::rustls::crypto::CryptoProvider;
use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
use tokio_rustls::rustls::server::ClientHello;
use tracing::instrument;
use visit_rs::Visit;
use crate::SOURCE_DATE;
use crate::account::AccountInfo;
use crate::db::model::Database;
use crate::db::{DbAccess, DbAccessMut};
use crate::hostname::Hostname;
use crate::init::check_time_is_synchronized;
use crate::net::gateway::GatewayInfo;
use crate::net::tls::TlsHandler;
use crate::net::web_server::{Accept, ExtractVisitor, TcpMetadata, extract};
use crate::prelude::*;
use crate::util::serde::Pem;
pub fn gen_nistp256() -> Result<PKey<Private>, ErrorStack> {
PKey::from_ec_key(EcKey::generate(&*EcGroup::from_curve_name(
Nid::X9_62_PRIME256V1,
)?)?)
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
#[serde(rename_all = "camelCase")]
@@ -96,9 +118,7 @@ impl Model<CertStore> {
} else {
PKeyPair {
ed25519: PKey::generate_ed25519()?,
nistp256: PKey::from_ec_key(EcKey::generate(&*EcGroup::from_curve_name(
Nid::X9_62_PRIME256V1,
)?)?)?,
nistp256: gen_nistp256()?,
}
};
let int_key = self.as_int_key().de()?.0;
@@ -125,6 +145,16 @@ impl Model<CertStore> {
})
}
}
impl DbAccess<CertStore> for Database {
fn access<'a>(db: &'a Model<Self>) -> &'a Model<CertStore> {
db.as_private().as_key_store().as_local_certs()
}
}
impl DbAccessMut<CertStore> for Database {
fn access_mut<'a>(db: &'a mut Model<Self>) -> &'a mut Model<CertStore> {
db.as_private_mut().as_key_store_mut().as_local_certs_mut()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct CertData {
@@ -300,22 +330,16 @@ pub fn make_root_cert(
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
let ctx = builder.x509v3_context(None, Some(&cfg));
// subjectKeyIdentifier = hash
let subject_key_identifier =
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
let subject_key_identifier = SubjectKeyIdentifier::new().build(&ctx)?;
// basicConstraints = critical, CA:true, pathlen:0
let basic_constraints = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::BASIC_CONSTRAINTS,
"critical,CA:true",
)?;
let basic_constraints = BasicConstraints::new().critical().ca().build()?;
// keyUsage = critical, digitalSignature, cRLSign, keyCertSign
let key_usage = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::KEY_USAGE,
"critical,digitalSignature,cRLSign,keyCertSign",
)?;
let key_usage = KeyUsage::new()
.critical()
.digital_signature()
.crl_sign()
.key_cert_sign()
.build()?;
builder.append_extension(subject_key_identifier)?;
builder.append_extension(basic_constraints)?;
builder.append_extension(key_usage)?;
@@ -350,30 +374,23 @@ pub fn make_int_cert(
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
let ctx = builder.x509v3_context(Some(&signer.1), Some(&cfg));
// subjectKeyIdentifier = hash
let subject_key_identifier =
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
let subject_key_identifier = SubjectKeyIdentifier::new().build(&ctx)?;
// authorityKeyIdentifier = keyid:always,issuer
let authority_key_identifier = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::AUTHORITY_KEY_IDENTIFIER,
"keyid:always,issuer",
)?;
let authority_key_identifier = AuthorityKeyIdentifier::new()
.keyid(false)
.issuer(true)
.build(&ctx)?;
// basicConstraints = critical, CA:true, pathlen:0
let basic_constraints = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::BASIC_CONSTRAINTS,
"critical,CA:true,pathlen:0",
)?;
let basic_constraints = BasicConstraints::new().critical().ca().pathlen(0).build()?;
// keyUsage = critical, digitalSignature, cRLSign, keyCertSign
let key_usage = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::KEY_USAGE,
"critical,digitalSignature,cRLSign,keyCertSign",
)?;
let key_usage = KeyUsage::new()
.critical()
.digital_signature()
.crl_sign()
.key_cert_sign()
.build()?;
builder.append_extension(subject_key_identifier)?;
builder.append_extension(authority_key_identifier)?;
builder.append_extension(basic_constraints)?;
@@ -423,6 +440,16 @@ impl SANInfo {
}
Self { dns, ips }
}
pub fn x509_extension(&self) -> SubjectAlternativeName {
let mut san = SubjectAlternativeName::new();
for h in &self.dns {
san.dns(&h.as_str());
}
for ip in &self.ips {
san.ip(&ip.to_string());
}
san
}
}
impl std::fmt::Display for SANInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -485,28 +512,20 @@ pub fn make_leaf_cert(
// Extensions
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
let ctx = builder.x509v3_context(Some(&signer.1), Some(&cfg));
// subjectKeyIdentifier = hash
let subject_key_identifier =
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
// authorityKeyIdentifier = keyid:always,issuer
let authority_key_identifier = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::AUTHORITY_KEY_IDENTIFIER,
"keyid,issuer:always",
)?;
let basic_constraints =
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::BASIC_CONSTRAINTS, "CA:FALSE")?;
let key_usage = X509Extension::new_nid(
Some(&cfg),
Some(&ctx),
Nid::KEY_USAGE,
"critical,digitalSignature,keyEncipherment",
)?;
let san_string = applicant.1.to_string();
let subject_alt_name =
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_ALT_NAME, &san_string)?;
let subject_key_identifier = SubjectKeyIdentifier::new().build(&ctx)?;
let authority_key_identifier = AuthorityKeyIdentifier::new()
.keyid(false)
.issuer(true)
.build(&ctx)?;
let subject_alt_name = applicant.1.x509_extension().build(&ctx)?;
let basic_constraints = BasicConstraints::new().build()?;
let key_usage = KeyUsage::new()
.critical()
.digital_signature()
.key_encipherment()
.build()?;
builder.append_extension(subject_key_identifier)?;
builder.append_extension(authority_key_identifier)?;
builder.append_extension(subject_alt_name)?;
@@ -518,3 +537,161 @@ pub fn make_leaf_cert(
let cert = builder.build();
Ok(cert)
}
#[instrument(skip_all)]
pub fn make_self_signed(applicant: (&PKey<Private>, &SANInfo)) -> Result<X509, Error> {
let mut builder = X509Builder::new()?;
builder.set_version(CERTIFICATE_VERSION)?;
let embargo = Asn1Time::from_unix(unix_time(SystemTime::now()) - 86400)?;
builder.set_not_before(&embargo)?;
// Google Apple and Mozilla reject certificate horizons longer than 398 days
// https://techbeacon.com/security/google-apple-mozilla-enforce-1-year-max-security-certifications
let expiration = Asn1Time::days_from_now(397)?;
builder.set_not_after(&expiration)?;
builder.set_serial_number(&*rand_serial()?)?;
let mut subject_name_builder = X509NameBuilder::new()?;
subject_name_builder.append_entry_by_text(
"CN",
applicant
.1
.dns
.first()
.map(MaybeWildcard::as_str)
.unwrap_or("localhost"),
)?;
subject_name_builder.append_entry_by_text("O", "Start9")?;
subject_name_builder.append_entry_by_text("OU", "StartOS")?;
let subject_name = subject_name_builder.build();
builder.set_subject_name(&subject_name)?;
builder.set_issuer_name(&subject_name)?;
builder.set_pubkey(&applicant.0)?;
// Extensions
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
let ctx = builder.x509v3_context(None, Some(&cfg));
let subject_key_identifier = SubjectKeyIdentifier::new().build(&ctx)?;
let authority_key_identifier = AuthorityKeyIdentifier::new()
.keyid(false)
.issuer(true)
.build(&ctx)?;
let subject_alt_name = applicant.1.x509_extension().build(&ctx)?;
let basic_constraints = BasicConstraints::new().build()?;
let key_usage = KeyUsage::new()
.critical()
.digital_signature()
.key_encipherment()
.build()?;
builder.append_extension(subject_key_identifier)?;
builder.append_extension(authority_key_identifier)?;
builder.append_extension(subject_alt_name)?;
builder.append_extension(basic_constraints)?;
builder.append_extension(key_usage)?;
builder.sign(&applicant.0, MessageDigest::sha256())?;
let cert = builder.build();
Ok(cert)
}
pub struct RootCaTlsHandler<M: HasModel> {
pub db: TypedPatchDb<M>,
pub crypto_provider: Arc<CryptoProvider>,
}
impl<M: HasModel> Clone for RootCaTlsHandler<M> {
fn clone(&self) -> Self {
Self {
db: self.db.clone(),
crypto_provider: self.crypto_provider.clone(),
}
}
}
impl<'a, A, M> TlsHandler<'a, A> for RootCaTlsHandler<M>
where
A: Accept + 'a,
<A as Accept>::Metadata: Visit<ExtractVisitor<TcpMetadata>>
+ Visit<ExtractVisitor<GatewayInfo>>
+ Clone
+ Send
+ Sync
+ 'static,
M: HasModel<Model = Model<M>> + DbAccessMut<CertStore> + Send + Sync,
{
async fn get_config(
&mut self,
hello: &ClientHello<'_>,
metadata: &<A as Accept>::Metadata,
) -> Option<ServerConfig> {
let hostnames: BTreeSet<InternedString> = hello
.server_name()
.map(InternedString::from)
.into_iter()
.chain(
extract::<TcpMetadata, _>(metadata)
.map(|m| m.local_addr.ip())
.as_ref()
.map(InternedString::from_display),
)
.chain(
extract::<GatewayInfo, _>(metadata)
.and_then(|i| i.info.ip_info)
.and_then(|i| i.wan_ip)
.as_ref()
.map(InternedString::from_display),
)
.collect();
let cert = self
.db
.mutate(|db| M::access_mut(db).cert_for(&hostnames))
.await
.result
.log_err()?;
let cfg = ServerConfig::builder_with_provider(self.crypto_provider.clone())
.with_safe_default_protocol_versions()
.log_err()?
.with_no_client_auth();
if hello
.signature_schemes()
.contains(&tokio_rustls::rustls::SignatureScheme::ED25519)
{
cfg.with_single_cert(
cert.fullchain_ed25519()
.into_iter()
.map(|c| {
Ok(tokio_rustls::rustls::pki_types::CertificateDer::from(
c.to_der()?,
))
})
.collect::<Result<_, Error>>()
.log_err()?,
PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
cert.leaf.keys.ed25519.private_key_to_pkcs8().log_err()?,
)),
)
} else {
cfg.with_single_cert(
cert.fullchain_nistp256()
.into_iter()
.map(|c| {
Ok(tokio_rustls::rustls::pki_types::CertificateDer::from(
c.to_der()?,
))
})
.collect::<Result<_, Error>>()
.log_err()?,
PrivateKeyDer::from(PrivatePkcs8KeyDer::from(
cert.leaf.keys.nistp256.private_key_to_pkcs8().log_err()?,
)),
)
}
.log_err()
}
}

View File

@@ -9,7 +9,7 @@ use async_compression::tokio::bufread::GzipEncoder;
use axum::Router;
use axum::body::Body;
use axum::extract::{self as x, Request};
use axum::response::{Redirect, Response};
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{any, get};
use base64::display::Base64Display;
use digest::Digest;
@@ -26,16 +26,19 @@ use models::PackageId;
use new_mime_guess::MimeGuess;
use openssl::hash::MessageDigest;
use openssl::x509::X509;
use rpc_toolkit::{Context, HttpServer, Server};
use rpc_toolkit::{Context, HttpServer, ParentHandler, Server};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader};
use tokio_util::io::ReaderStream;
use url::Url;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::hostname::Hostname;
use crate::main_api;
use crate::middleware::auth::{Auth, HasValidSession};
use crate::middleware::cors::Cors;
use crate::middleware::db::SyncDb;
use crate::net::gateway::GatewayInfo;
use crate::net::tls::TlsHandshakeInfo;
use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuations};
use crate::s9pk::S9pk;
@@ -46,7 +49,6 @@ use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::util::io::open_file;
use crate::util::net::SyncBody;
use crate::util::serde::BASE64;
use crate::{diagnostic_api, init_api, install_api, main_api, setup_api};
const NOT_FOUND: &[u8] = b"Not Found";
const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed";
@@ -55,26 +57,151 @@ const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error";
const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"];
#[cfg(all(feature = "startd", not(feature = "test")))]
const EMBEDDED_UIS: Dir<'_> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static");
#[cfg(not(all(feature = "startd", not(feature = "test"))))]
const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]);
pub const EMPTY_DIR: Dir<'_> = Dir::new("", &[]);
#[derive(Clone)]
pub enum UiMode {
Setup,
Install,
Main,
#[macro_export]
macro_rules! else_empty_dir {
($cfg:meta => $dir:expr) => {{
#[cfg(all($cfg, not(feature = "test")))]
{
$dir
}
#[cfg(not(all($cfg, not(feature = "test"))))]
{
crate::net::static_server::EMPTY_DIR
}
}};
}
impl UiMode {
fn path(&self, path: &str) -> PathBuf {
match self {
Self::Setup => Path::new("setup-wizard").join(path),
Self::Install => Path::new("install-wizard").join(path),
Self::Main => Path::new("ui").join(path),
const EMBEDDED_UI_ROOT: Dir<'_> = else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static")
);
pub trait UiContext: Context + AsRef<RpcContinuations> + Clone + Sized {
const UI_DIR: &'static Dir<'static>;
fn api() -> ParentHandler<Self>;
fn middleware(server: Server<Self>) -> HttpServer<Self>;
fn extend_router(self, router: Router) -> Router {
router
}
}
impl UiContext for RpcContext {
const UI_DIR: &'static Dir<'static> = &else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static/ui")
);
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server
.middleware(Cors::new())
.middleware(Auth::new())
.middleware(SyncDb::new())
}
fn extend_router(self, router: Router) -> Router {
async fn https_redirect_if_public_http(
req: Request,
next: axum::middleware::Next,
) -> Response {
if req
.extensions()
.get::<GatewayInfo>()
.map_or(false, |p| p.info.public())
&& req.extensions().get::<TlsHandshakeInfo>().is_none()
{
Redirect::temporary(&format!(
"https://{}{}",
req.headers()
.get(HOST)
.and_then(|s| s.to_str().ok())
.unwrap_or("localhost"),
req.uri()
))
.into_response()
} else {
next.run(req).await
}
}
router
.route("/proxy/{url}", {
let ctx = self.clone();
any(move |x::Path(url): x::Path<String>, request: Request| {
let ctx = ctx.clone();
async move {
proxy_request(ctx, request, url)
.await
.unwrap_or_else(server_error)
}
})
})
.nest("/s9pk", s9pk_router(self.clone()))
.route(
"/static/local-root-ca.crt",
get(move || {
let ctx = self.clone();
async move {
ctx.account
.peek(|account| cert_send(&account.root_ca_cert, &account.hostname))
}
}),
)
.layer(axum::middleware::from_fn(https_redirect_if_public_http))
}
}
impl UiContext for InitContext {
const UI_DIR: &'static Dir<'static> = &else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static/ui")
);
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for DiagnosticContext {
const UI_DIR: &'static Dir<'static> = &else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static/ui")
);
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for SetupContext {
const UI_DIR: &'static Dir<'static> = &else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static/setup-wizard")
);
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
impl UiContext for InstallContext {
const UI_DIR: &'static Dir<'static> = &else_empty_dir!(
feature = "startd" =>
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static/install-wizard")
);
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
@@ -111,24 +238,23 @@ pub fn rpc_router<C: Context + Clone + AsRef<RpcContinuations>>(
)
}
fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
fn serve_ui<C: UiContext>(req: Request) -> Result<Response, Error> {
let (request_parts, _body) = req.into_parts();
match &request_parts.method {
&Method::GET | &Method::HEAD => {
let uri_path = ui_mode.path(
request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path()),
);
let uri_path = request_parts
.uri
.path()
.strip_prefix('/')
.unwrap_or(request_parts.uri.path());
let file = EMBEDDED_UIS
.get_file(&*uri_path)
.or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html")));
let file = C::UI_DIR
.get_file(uri_path)
.or_else(|| C::UI_DIR.get_file("index.html"));
if let Some(file) = file {
FileData::from_embedded(&request_parts, file)?.into_response(&request_parts)
FileData::from_embedded(&request_parts, file, C::UI_DIR)?
.into_response(&request_parts)
} else {
Ok(not_found())
}
@@ -137,79 +263,15 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result<Response, Error> {
}
}
pub fn setup_ui_router(ctx: SetupContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Setup).unwrap_or_else(server_error)
}))
}
pub fn diagnostic_ui_router(ctx: DiagnosticContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
}
pub fn install_ui_router(ctx: InstallContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Install).unwrap_or_else(server_error)
}))
}
pub fn init_ui_router(ctx: InitContext) -> Router {
rpc_router(
ctx.clone(),
Server::new(move || ready(Ok(ctx.clone())), init_api()).middleware(Cors::new()),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
}
pub fn main_ui_router(ctx: RpcContext) -> Router {
rpc_router(ctx.clone(), {
let ctx = ctx.clone();
Server::new(move || ready(Ok(ctx.clone())), main_api::<RpcContext>())
.middleware(Cors::new())
.middleware(Auth::new())
.middleware(SyncDb::new())
})
.route("/proxy/{url}", {
let ctx = ctx.clone();
any(move |x::Path(url): x::Path<String>, request: Request| {
let ctx = ctx.clone();
async move {
proxy_request(ctx, request, url)
.await
.unwrap_or_else(server_error)
}
})
})
.nest("/s9pk", s9pk_router(ctx.clone()))
.route(
"/static/local-root-ca.crt",
get(move || {
let ctx = ctx.clone();
async move {
let account = ctx.account.read().await;
cert_send(&account.root_ca_cert, &account.hostname)
}
}),
)
.fallback(any(|request: Request| async move {
serve_ui(request, UiMode::Main).unwrap_or_else(server_error)
}))
pub fn ui_router<C: UiContext>(ctx: C) -> Router {
ctx.clone()
.extend_router(rpc_router(
ctx.clone(),
C::middleware(Server::new(move || ready(Ok(ctx.clone())), C::api())),
))
.fallback(any(|request: Request| async move {
serve_ui::<C>(request).unwrap_or_else(server_error)
}))
}
pub fn refresher() -> Router {
@@ -229,20 +291,6 @@ pub fn refresher() -> Router {
}))
}
pub fn redirecter() -> Router {
Router::new().fallback(get(|request: Request| async move {
Redirect::temporary(&format!(
"https://{}{}",
request
.headers()
.get(HOST)
.and_then(|s| s.to_str().ok())
.unwrap_or("localhost"),
request.uri()
))
}))
}
async fn proxy_request(ctx: RpcContext, request: Request, url: String) -> Result<Response, Error> {
if_authorized(&ctx, request, |mut request| async {
for header in PROXY_STRIP_HEADERS {
@@ -492,6 +540,7 @@ impl FileData {
fn from_embedded(
req: &RequestParts,
file: &'static include_dir::File<'static>,
ui_dir: &'static Dir<'static>,
) -> Result<Self, Error> {
let path = file.path();
let (encoding, data, len, content_range) = if let Some(range) = req.headers.get(RANGE) {
@@ -533,12 +582,12 @@ impl FileData {
.fold((None, file.contents()), |acc, e| {
if let Some(file) = (e == "br")
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display())))
.and_then(|_| ui_dir.get_file(format!("{}.br", path.display())))
{
(Some("br"), file.contents())
} else if let Some(file) = (e == "gzip" && acc.0 != Some("br"))
.then_some(())
.and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display())))
.and_then(|_| ui_dir.get_file(format!("{}.gz", path.display())))
{
(Some("gzip"), file.contents())
} else {

315
core/startos/src/net/tls.rs Normal file
View File

@@ -0,0 +1,315 @@
use std::sync::Arc;
use std::task::{Poll, ready};
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::{FutureExt, StreamExt};
use imbl_value::InternedString;
use openssl::x509::X509Ref;
use tokio::io::AsyncWriteExt;
use tokio_rustls::LazyConfigAcceptor;
use tokio_rustls::rustls::crypto::CryptoProvider;
use tokio_rustls::rustls::pki_types::CertificateDer;
use tokio_rustls::rustls::server::{Acceptor, ClientHello, ResolvesServerCert};
use tokio_rustls::rustls::sign::CertifiedKey;
use tokio_rustls::rustls::{ClientConfig, RootCertStore, ServerConfig};
use visit_rs::{Visit, VisitFields};
use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor};
use crate::prelude::*;
use crate::util::io::{BackTrackingIO, ReadWriter};
use crate::util::serde::MaybeUtf8String;
use crate::util::sync::SyncMutex;
#[derive(Debug, Clone, VisitFields)]
pub struct TlsMetadata<M> {
pub inner: M,
pub tls_info: TlsHandshakeInfo,
}
impl<V: MetadataVisitor<Result = ()>, M: Visit<V>> Visit<V> for TlsMetadata<M> {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
self.visit_fields(visitor).collect()
}
}
#[derive(Debug, Clone)]
pub struct TlsHandshakeInfo {
pub sni: Option<InternedString>,
pub alpn: Vec<MaybeUtf8String>,
}
impl<V: MetadataVisitor> Visit<V> for TlsHandshakeInfo {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
visitor.visit(self)
}
}
pub trait TlsHandler<'a, A: Accept> {
fn get_config(
&'a mut self,
hello: &'a ClientHello<'a>,
metadata: &'a A::Metadata,
) -> impl Future<Output = Option<ServerConfig>> + Send + 'a;
}
#[derive(Clone)]
pub struct ChainedHandler<H0, H1>(pub H0, pub H1);
impl<'a, A, H0, H1> TlsHandler<'a, A> for ChainedHandler<H0, H1>
where
A: Accept + 'a,
<A as Accept>::Metadata: Send + Sync,
H0: TlsHandler<'a, A> + Send,
H1: TlsHandler<'a, A> + Send,
{
async fn get_config(
&'a mut self,
hello: &'a ClientHello<'a>,
metadata: &'a <A as Accept>::Metadata,
) -> Option<ServerConfig> {
if let Some(config) = self.0.get_config(hello, metadata).await {
return Some(config);
}
self.1.get_config(hello, metadata).await
}
}
#[derive(Clone)]
pub struct TlsHandlerWrapper<I, W> {
pub inner: I,
pub wrapper: W,
}
pub trait WrapTlsHandler<A: Accept> {
fn wrap<'a>(
&'a mut self,
prev: ServerConfig,
hello: &'a ClientHello<'a>,
metadata: &'a <A as Accept>::Metadata,
) -> impl Future<Output = Option<ServerConfig>> + Send + 'a
where
Self: 'a;
}
impl<'a, A, I, W> TlsHandler<'a, A> for TlsHandlerWrapper<I, W>
where
A: Accept + 'a,
<A as Accept>::Metadata: Send + Sync,
I: TlsHandler<'a, A> + Send,
W: WrapTlsHandler<A> + Send,
{
async fn get_config(
&'a mut self,
hello: &'a ClientHello<'a>,
metadata: &'a <A as Accept>::Metadata,
) -> Option<ServerConfig> {
let prev = self.inner.get_config(hello, metadata).await?;
self.wrapper.wrap(prev, hello, metadata).await
}
}
#[derive(Debug)]
pub struct SingleCertResolver(pub Arc<CertifiedKey>);
impl ResolvesServerCert for SingleCertResolver {
fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
Some(self.0.clone())
}
}
pub struct TlsListener<A: Accept, H: for<'a> TlsHandler<'a, A>> {
pub accept: A,
pub tls_handler: H,
in_progress: SyncMutex<
FuturesUnordered<
BoxFuture<
'static,
(
H,
Result<Option<(TlsMetadata<A::Metadata>, AcceptStream)>, Error>,
),
>,
>,
>,
}
impl<A: Accept, H: for<'a> TlsHandler<'a, A>> TlsListener<A, H> {
pub fn new(accept: A, cert_handler: H) -> Self {
Self {
accept,
tls_handler: cert_handler,
in_progress: SyncMutex::new(FuturesUnordered::new()),
}
}
}
impl<A, H> Accept for TlsListener<A, H>
where
A: Accept + 'static,
A::Metadata: Send + 'static,
for<'a> H: TlsHandler<'a, A> + Clone + Send + 'static,
{
type Metadata = TlsMetadata<A::Metadata>;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
self.in_progress.mutate(|in_progress| {
loop {
if !in_progress.is_empty() {
if let Poll::Ready(Some((handler, res))) = in_progress.poll_next_unpin(cx) {
if let Some(res) = res.transpose() {
self.tls_handler = handler;
return Poll::Ready(res);
}
continue;
}
}
let (metadata, stream) = ready!(self.accept.poll_accept(cx)?);
let mut tls_handler = self.tls_handler.clone();
let mut fut = async move {
let res = async {
let mut acceptor = LazyConfigAcceptor::new(
Acceptor::default(),
BackTrackingIO::new(stream),
);
let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> =
match (&mut acceptor).await {
Ok(a) => a,
Err(e) => {
let mut stream =
acceptor.take_io().or_not_found("acceptor io")?;
let (_, buf) = stream.rewind();
if std::str::from_utf8(buf)
.ok()
.and_then(|buf| {
buf.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.next()
})
.map_or(false, |buf| {
regex::Regex::new("[A-Z]+ (.+) HTTP/1")
.unwrap()
.is_match(buf)
})
{
handle_http_on_https(stream).await.log_err();
return Ok(None);
} else {
return Err(e).with_kind(ErrorKind::Network);
}
}
};
let hello = mid.client_hello();
if let Some(cfg) = tls_handler.get_config(&hello, &metadata).await {
let metadata = TlsMetadata {
inner: metadata,
tls_info: TlsHandshakeInfo {
sni: hello.server_name().map(InternedString::intern),
alpn: hello
.alpn()
.into_iter()
.flatten()
.map(|a| MaybeUtf8String(a.to_vec()))
.collect(),
},
};
let buffered = mid.io.stop_buffering();
mid.io
.write_all(&buffered)
.await
.with_kind(ErrorKind::Network)?;
return Ok(Some((
metadata,
Box::pin(mid.into_stream(Arc::new(cfg)).await?) as AcceptStream,
)));
}
Ok(None)
}
.await;
(tls_handler, res)
}
.boxed();
match fut.poll_unpin(cx) {
Poll::Pending => {
in_progress.push(fut);
return Poll::Pending;
}
Poll::Ready((handler, res)) => {
if let Some(res) = res.transpose() {
self.tls_handler = handler;
return Poll::Ready(res);
}
}
};
}
})
}
}
async fn handle_http_on_https(stream: impl ReadWriter + Unpin + 'static) -> Result<(), Error> {
use axum::body::Body;
use axum::extract::Request;
use axum::response::Response;
use http::Uri;
use crate::net::static_server::server_error;
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
.serve_connection(
hyper_util::rt::TokioIo::new(stream),
hyper_util::service::TowerToHyperService::new(axum::Router::new().fallback(
axum::routing::method_routing::any(move |req: Request| async move {
match async move {
let host = req
.headers()
.get(http::header::HOST)
.and_then(|host| host.to_str().ok());
if let Some(host) = host {
let uri = Uri::from_parts({
let mut parts = req.uri().to_owned().into_parts();
parts.scheme = Some("https".parse()?);
parts.authority = Some(host.parse()?);
parts
})?;
Response::builder()
.status(http::StatusCode::TEMPORARY_REDIRECT)
.header(http::header::LOCATION, uri.to_string())
.body(Body::default())
} else {
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(Body::from("Host header required"))
}
}
.await
{
Ok(a) => a,
Err(e) => {
tracing::warn!("Error redirecting http request on ssl port: {e}");
tracing::error!("{e:?}");
server_error(Error::new(e, ErrorKind::Network))
}
}
}),
)),
)
.await
.map_err(|e| Error::new(color_eyre::eyre::Report::msg(e), ErrorKind::Network))
}
pub fn client_config<'a, I: IntoIterator<Item = &'a X509Ref>>(
crypto_provider: Arc<CryptoProvider>,
root_certs: I,
) -> Result<ClientConfig, Error> {
let mut certs = RootCertStore::empty();
for cert in root_certs {
certs
.add(CertificateDer::from_slice(&cert.to_der()?))
.with_kind(ErrorKind::OpenSsl)?;
}
Ok(ClientConfig::builder_with_provider(crypto_provider.clone())
.with_safe_default_protocol_versions()
.with_kind(ErrorKind::OpenSsl)?
.with_root_certificates(certs)
.with_no_client_auth())
}

View File

@@ -14,7 +14,7 @@ use futures::{FutureExt, StreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
@@ -34,8 +34,8 @@ use crate::util::actor::background::BackgroundJobQueue;
use crate::util::future::Until;
use crate::util::io::ReadWriter;
use crate::util::serde::{
deserialize_from_str, display_serializable, serialize_display, Base64, HandlerExtSerde,
WithIoFormat, BASE64,
BASE64, Base64, HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable,
serialize_display,
};
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
@@ -62,7 +62,7 @@ impl FromStr for OnionAddress {
Cow::Owned(format!("{s}.onion"))
}
.parse::<HsId>()
.with_kind(ErrorKind::ParseNetAddress)?,
.with_kind(ErrorKind::Tor)?,
))
}
}
@@ -165,8 +165,8 @@ impl<'de> Deserialize<'de> for TorSecretKey {
}
#[derive(Default, Deserialize, Serialize)]
pub struct OnionKeyStore(BTreeMap<OnionAddress, TorSecretKey>);
impl Map for OnionKeyStore {
pub struct OnionStore(BTreeMap<OnionAddress, TorSecretKey>);
impl Map for OnionStore {
type Key = OnionAddress;
type Value = TorSecretKey;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
@@ -176,7 +176,7 @@ impl Map for OnionKeyStore {
Ok(InternedString::from_display(key))
}
}
impl OnionKeyStore {
impl OnionStore {
pub fn new() -> Self {
Self::default()
}
@@ -184,7 +184,7 @@ impl OnionKeyStore {
self.0.insert(key.onion_address(), key);
}
}
impl Model<OnionKeyStore> {
impl Model<OnionStore> {
pub fn new_key(&mut self) -> Result<TorSecretKey, Error> {
let key = TorSecretKey::generate();
self.insert(&key.onion_address(), &key)?;
@@ -199,7 +199,7 @@ impl Model<OnionKeyStore> {
.de()
}
}
impl std::fmt::Debug for OnionKeyStore {
impl std::fmt::Debug for OnionStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
struct OnionStoreMap<'a>(&'a BTreeMap<OnionAddress, TorSecretKey>);
impl<'a> std::fmt::Debug for OnionStoreMap<'a> {
@@ -227,7 +227,7 @@ pub fn tor_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Show the status of running onion services")
.with_about("Display Tor V3 Onion Addresses")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -417,6 +417,7 @@ impl TorController {
0,
TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?)
.local_resource_timeout(Duration::from_secs(0))
.create_unbootstrapped()?,
));
let reset = Arc::new(Notify::new());
@@ -424,10 +425,10 @@ impl TorController {
let bootstrapper_client = client.clone();
let bootstrapper = tokio::spawn(async move {
loop {
let (epoch, client): (usize, _) = bootstrapper_client.read();
if let Err(e) = Until::new()
.with_async_fn(|| bootstrapper_reset.notified().map(Ok))
.run(async {
let (epoch, client): (usize, _) = bootstrapper_client.read();
let mut events = client.bootstrap_events();
let bootstrap_fut =
client.bootstrap().map(|res| res.with_kind(ErrorKind::Tor));
@@ -560,7 +561,7 @@ impl TorController {
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::error!("Tor Client Health Error: {e}");
tracing::debug!("{e:?}");
}
}
@@ -569,21 +570,7 @@ impl TorController {
HEALTH_CHECK_FAILURE_ALLOWANCE
);
}
if let Err::<(), Error>(e) = async {
tokio::time::sleep(RETRY_COOLDOWN).await;
bootstrapper_client.send((
epoch.wrapping_add(1),
TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?)
.create_unbootstrapped()?,
));
Ok(())
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::debug!("{e:?}");
}
Ok(())
})
.await
@@ -591,6 +578,24 @@ impl TorController {
tracing::error!("Tor Bootstrapper Error: {e}");
tracing::debug!("{e:?}");
}
if let Err::<(), Error>(e) = async {
tokio::time::sleep(RETRY_COOLDOWN).await;
bootstrapper_client.send((
epoch.wrapping_add(1),
TorClient::with_runtime(TokioRustlsRuntime::current()?)
.config(config.build().with_kind(ErrorKind::Tor)?)
.local_resource_timeout(Duration::from_secs(0))
.create_unbootstrapped_async()
.await?,
));
tracing::debug!("TorClient recycled");
Ok(())
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::debug!("{e:?}");
}
}
})
.into();
@@ -623,11 +628,7 @@ impl TorController {
} else {
false
};
if rm {
s.remove(&addr)
} else {
None
}
if rm { s.remove(&addr) } else { None }
}) {
s.shutdown().await
} else {
@@ -729,14 +730,15 @@ impl OnionService {
if let Err(e) = async {
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
let epoch = client.peek(|(e, c)| {
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "client recycled");
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "TorClient recycled");
Ok::<_, Error>(*e)
})?;
let addr = key.onion_address();
let (new_service, stream) = client.peek(|(_, c)| {
c.launch_onion_service_with_hsid(
OnionServiceConfigBuilder::default()
.nickname(
key.onion_address()
addr
.to_string()
.trim_end_matches(".onion")
.parse::<HsNickname>()
@@ -749,8 +751,20 @@ impl OnionService {
.with_kind(ErrorKind::Tor)
})?;
let mut status_stream = new_service.status_events();
let mut status = new_service.status();
if status.state().is_fully_reachable() {
tracing::debug!("{addr} is fully reachable");
} else {
tracing::debug!("{addr} is not fully reachable");
}
bg.add_job(async move {
while let Some(status) = status_stream.next().await {
while let Some(new_status) = status_stream.next().await {
if status.state().is_fully_reachable() && !new_status.state().is_fully_reachable() {
tracing::debug!("{addr} is no longer fully reachable");
} else if !status.state().is_fully_reachable() && new_status.state().is_fully_reachable() {
tracing::debug!("{addr} is now fully reachable");
}
status = new_status;
// TODO: health daemon?
}
});
@@ -806,8 +820,8 @@ impl OnionService {
)
.await
{
tracing::error!("Tor Stream Error: {e}");
tracing::debug!("{e:?}");
tracing::trace!("Tor Stream Error: {e}");
tracing::trace!("{e:?}");
}
Ok::<_, Error>(())
@@ -843,11 +857,11 @@ impl OnionService {
})))
}
pub fn proxy_all<Rcs: FromIterator<Arc<()>>>(
pub async fn proxy_all<Rcs: FromIterator<Arc<()>>>(
&self,
bindings: impl IntoIterator<Item = (u16, SocketAddr)>,
) -> Rcs {
self.0.bindings.mutate(|b| {
) -> Result<Rcs, Error> {
Ok(self.0.bindings.mutate(|b| {
bindings
.into_iter()
.map(|(port, target)| {
@@ -861,7 +875,7 @@ impl OnionService {
}
})
.collect()
})
}))
}
pub fn gc(&self) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
#[cfg(feature = "arti")]
mod arti;
#[cfg(not(feature = "arti"))]
mod ctor;
#[cfg(feature = "arti")]
pub use arti::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api};
#[cfg(not(feature = "arti"))]
pub use ctor::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api};

View File

@@ -1,13 +1,15 @@
use clap::Parser;
use imbl_value::InternedString;
use models::GatewayId;
use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceType;
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType};
use crate::net::host::all_hosts;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::io::{TmpDir, write_file_atomic};
@@ -32,7 +34,6 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct AddTunnelParams {
#[ts(type = "string")]
name: InternedString,
config: String,
public: bool,
@@ -46,26 +47,46 @@ pub async fn add_tunnel(
public,
}: AddTunnelParams,
) -> Result<GatewayId, Error> {
let existing = ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_gateways()
.keys()?;
let mut iface = GatewayId::from("wg0");
for id in 1.. {
if !existing.contains(&iface) {
break;
let ifaces = ctx.net_controller.net_iface.watcher.subscribe();
let mut iface = GatewayId::from(InternedString::intern("wg0"));
if !ifaces.send_if_modified(|i| {
for id in 1..256 {
if !i.contains_key(&iface) {
i.insert(
iface.clone(),
NetworkInterfaceInfo {
name: Some(name),
public: Some(public),
secure: None,
ip_info: None,
},
);
return true;
}
iface = InternedString::from_display(&lazy_format!("wg{id}")).into();
}
iface = InternedString::from_display(&lazy_format!("wg{id}")).into();
false
}) {
return Err(Error::new(
eyre!("too many wireguard interfaces"),
ErrorKind::InvalidRequest,
));
}
let mut sub = ctx
.db
.subscribe(
"/public/serverInfo/network/gateways"
.parse::<JsonPointer>()
.with_kind(ErrorKind::Database)?
.join_end(iface.as_str())
.join_end("ipInfo"),
)
.await;
let tmpdir = TmpDir::new().await?;
let conf = tmpdir.join(&iface).with_extension("conf");
write_file_atomic(&conf, &config).await?;
let mut ifaces = ctx.net_controller.net_iface.watcher.subscribe();
Command::new("nmcli")
.arg("connection")
.arg("import")
@@ -77,14 +98,7 @@ pub async fn add_tunnel(
.await?;
tmpdir.delete().await?;
ifaces.wait_for(|ifaces| ifaces.contains_key(&iface)).await;
ctx.net_controller
.net_iface
.set_public(&iface, Some(public))
.await?;
ctx.net_controller.net_iface.set_name(&iface, &name).await?;
sub.recv().await;
Ok(iface)
}
@@ -112,14 +126,44 @@ pub async fn remove_tunnel(
return Ok(());
};
if existing.as_device_type().de()? != Some(NetworkInterfaceType::Wireguard) {
if existing.as_deref().as_device_type().de()? != Some(NetworkInterfaceType::Wireguard) {
return Err(Error::new(
eyre!("network interface {id} is not a proxy"),
ErrorKind::InvalidRequest,
));
}
ctx.db
.mutate(|db| {
for host in all_hosts(db) {
let host = host?;
host.as_public_domains_mut()
.mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?;
}
Ok(())
})
.await
.result?;
ctx.net_controller.net_iface.delete_iface(&id).await?;
ctx.db
.mutate(|db| {
for host in all_hosts(db) {
let host = host?;
host.as_bindings_mut().mutate(|b| {
Ok(b.values_mut().for_each(|v| {
v.net.private_disabled.remove(&id);
v.net.public_enabled.remove(&id);
}))
})?;
}
Ok(())
})
.await
.result?;
Ok(())
}

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use std::path::Path;
@@ -7,13 +8,56 @@ use futures::stream::BoxStream;
use futures::{StreamExt, TryStreamExt};
use imbl_value::InternedString;
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use models::GatewayId;
use nix::net::if_::if_nametoindex;
use tokio::net::{TcpListener, TcpStream};
use tokio::process::Command;
use crate::db::model::public::{IpInfo, NetworkInterfaceType};
use crate::prelude::*;
use crate::util::Invoke;
pub async fn load_ip_info() -> Result<BTreeMap<GatewayId, IpInfo>, Error> {
let output = String::from_utf8(
Command::new("ip")
.arg("-o")
.arg("addr")
.arg("show")
.invoke(crate::ErrorKind::Network)
.await?,
)?;
let err_fn = || {
Error::new(
eyre!("malformed output from `ip`"),
crate::ErrorKind::Network,
)
};
let mut res = BTreeMap::<GatewayId, IpInfo>::new();
for line in output.lines() {
let split = line.split_ascii_whitespace().collect::<Vec<_>>();
let iface = GatewayId::from(InternedString::from(*split.get(1).ok_or_else(&err_fn)?));
let subnet: IpNet = split.get(3).ok_or_else(&err_fn)?.parse()?;
let ip_info = res.entry(iface.clone()).or_default();
ip_info.name = iface.into();
ip_info.scope_id = split
.get(0)
.ok_or_else(&err_fn)?
.strip_suffix(":")
.ok_or_else(&err_fn)?
.parse()?;
ip_info.subnets.insert(subnet);
}
for (id, ip_info) in res.iter_mut() {
ip_info.device_type = probe_iface_type(id.as_str()).await;
}
Ok(res)
}
pub fn ipv6_is_link_local(addr: Ipv6Addr) -> bool {
(addr.segments()[0] & 0xffc0) == 0xfe80
}
@@ -75,6 +119,22 @@ pub async fn get_iface_ipv6_addr(iface: &str) -> Result<Option<(Ipv6Addr, Ipv6Ne
.transpose()?)
}
pub async fn probe_iface_type(iface: &str) -> Option<NetworkInterfaceType> {
match tokio::fs::read_to_string(Path::new("/sys/class/net").join(iface).join("uevent"))
.await
.ok()?
.lines()
.find_map(|l| l.strip_prefix("DEVTYPE="))
{
Some("wlan") => Some(NetworkInterfaceType::Wireless),
Some("bridge") => Some(NetworkInterfaceType::Bridge),
Some("wireguard") => Some(NetworkInterfaceType::Wireguard),
None if iface_is_physical(iface).await => Some(NetworkInterfaceType::Ethernet),
None if iface_is_loopback(iface).await => Some(NetworkInterfaceType::Loopback),
_ => None,
}
}
pub async fn iface_is_physical(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("device"))
.await
@@ -87,6 +147,19 @@ pub async fn iface_is_wireless(iface: &str) -> bool {
.is_ok()
}
pub async fn iface_is_bridge(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("bridge"))
.await
.is_ok()
}
pub async fn iface_is_loopback(iface: &str) -> bool {
tokio::fs::read_to_string(Path::new("/sys/class/net").join(iface).join("type"))
.await
.ok()
.map_or(false, |x| x.trim() == "772")
}
pub fn list_interfaces() -> BoxStream<'static, Result<String, Error>> {
try_stream! {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,200 @@
use std::any::Any;
use std::collections::BTreeMap;
use std::future::Future;
use std::net::SocketAddr;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use std::task::{Poll, ready};
use std::time::Duration;
use axum::Router;
use futures::future::Either;
use futures::FutureExt;
use futures::{FutureExt, TryFutureExt};
use helpers::NonDetachingJoinHandle;
use http::Extensions;
use hyper_util::rt::{TokioIo, TokioTimer};
use tokio::net::{TcpListener, TcpStream};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
use visit_rs::{Visit, VisitFields, Visitor};
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::net::gateway::{
lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener,
};
use crate::net::static_server::{
diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, redirecter, refresher,
setup_ui_router,
};
use crate::net::static_server::{UiContext, ui_router};
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::ReadWriter;
use crate::util::sync::{SyncRwLock, Watch};
pub struct Accepted {
pub https_redirect: bool,
pub stream: TcpStream,
pub type AcceptStream = Pin<Box<dyn ReadWriter + Send + 'static>>;
pub trait MetadataVisitor: Visitor<Result = ()> {
fn visit<M: Clone + Send + Sync + 'static>(&mut self, metadata: &M) -> Self::Result;
}
pub struct ExtensionVisitor<'a>(&'a mut Extensions);
impl<'a> Visitor for ExtensionVisitor<'a> {
type Result = ();
}
impl<'a> MetadataVisitor for ExtensionVisitor<'a> {
fn visit<M: Clone + Send + Sync + 'static>(&mut self, metadata: &M) -> Self::Result {
self.0.insert(metadata.clone());
}
}
impl<'a> Visit<ExtensionVisitor<'a>>
for Box<dyn for<'x> Visit<ExtensionVisitor<'x>> + Send + Sync + 'static>
{
fn visit(
&self,
visitor: &mut ExtensionVisitor<'a>,
) -> <ExtensionVisitor<'a> as Visitor>::Result {
(&**self).visit(visitor)
}
}
pub struct ExtractVisitor<T>(Option<T>);
impl<T> Visitor for ExtractVisitor<T> {
type Result = ();
}
impl<T: Clone + Send + Sync + 'static> MetadataVisitor for ExtractVisitor<T> {
fn visit<M: Clone + Send + Sync + 'static>(&mut self, metadata: &M) -> Self::Result {
if let Some(matching) = (metadata as &dyn Any).downcast_ref::<T>() {
self.0 = Some(matching.clone());
}
}
}
pub fn extract<
T: Clone + Send + Sync + 'static,
M: Visit<ExtractVisitor<T>> + Clone + Send + Sync + 'static,
>(
metadata: &M,
) -> Option<T> {
let mut visitor = ExtractVisitor(None);
visitor.visit(metadata);
visitor.0
}
#[derive(Clone, Copy, Debug)]
pub struct TcpMetadata {
pub peer_addr: SocketAddr,
pub local_addr: SocketAddr,
}
impl<V: MetadataVisitor> Visit<V> for TcpMetadata {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
visitor.visit(self)
}
}
pub trait Accept {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>>;
type Metadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>>;
fn into_dyn(self) -> DynAccept
where
Self: Sized + Send + Sync + 'static,
for<'a> Self::Metadata: Visit<ExtensionVisitor<'a>> + Send + Sync + 'static,
{
DynAccept::new(self)
}
}
impl Accept for Vec<TcpListener> {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
for listener in &*self {
if let Poll::Ready((stream, _)) = listener.poll_accept(cx)? {
return Poll::Ready(Ok(Accepted {
https_redirect: false,
stream,
}));
impl Accept for TcpListener {
type Metadata = TcpMetadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
if let Poll::Ready((stream, peer_addr)) = TcpListener::poll_accept(self, cx)? {
if let Err(e) = socket2::SockRef::from(&stream).set_tcp_keepalive(
&socket2::TcpKeepalive::new()
.with_time(Duration::from_secs(900))
.with_interval(Duration::from_secs(60))
.with_retries(5),
) {
tracing::error!("Failed to set tcp keepalive: {e}");
tracing::debug!("{e:?}");
}
return Poll::Ready(Ok((
TcpMetadata {
local_addr: self.local_addr()?,
peer_addr,
},
Box::pin(stream),
)));
}
Poll::Pending
}
}
impl<A> Accept for Vec<A>
where
A: Accept,
{
type Metadata = A::Metadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
for listener in self {
if let Poll::Ready(accepted) = listener.poll_accept(cx)? {
return Poll::Ready(Ok(accepted));
}
}
Poll::Pending
}
}
impl Accept for NetworkInterfaceListener {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
NetworkInterfaceListener::poll_accept(self, cx, &true).map(|res| {
res.map(|a| {
let public = self
.ip_info
.peek(|i| lookup_info_by_addr(i, a.bind).map_or(true, |(_, i)| i.public()));
Accepted {
https_redirect: public,
stream: a.stream,
}
})
})
#[derive(Clone, VisitFields)]
pub struct MapListenerMetadata<K, M> {
pub inner: M,
pub key: K,
}
impl<K, M, V> Visit<V> for MapListenerMetadata<K, M>
where
V: MetadataVisitor,
K: Visit<V>,
M: Visit<V>,
{
fn visit(&self, visitor: &mut V) -> <V as Visitor>::Result {
self.visit_fields(visitor).collect()
}
}
impl<A: Accept, B: Accept> Accept for Either<A, B> {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
impl<K, A> Accept for BTreeMap<K, A>
where
K: Clone,
A: Accept,
{
type Metadata = MapListenerMetadata<K, A::Metadata>;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
for (key, listener) in self {
if let Poll::Ready((metadata, stream)) = listener.poll_accept(cx)? {
return Poll::Ready(Ok((
MapListenerMetadata {
inner: metadata,
key: key.clone(),
},
stream,
)));
}
}
Poll::Pending
}
}
impl<A, B> Accept for Either<A, B>
where
A: Accept,
B: Accept<Metadata = A::Metadata>,
{
type Metadata = A::Metadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
match self {
Either::Left(a) => a.poll_accept(cx),
Either::Right(b) => b.poll_accept(cx),
@@ -72,7 +202,11 @@ impl<A: Accept, B: Accept> Accept for Either<A, B> {
}
}
impl<A: Accept> Accept for Option<A> {
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
type Metadata = A::Metadata;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
match self {
None => Poll::Pending,
Some(a) => a.poll_accept(cx),
@@ -80,6 +214,68 @@ impl<A: Accept> Accept for Option<A> {
}
}
trait DynAcceptT: Send + Sync {
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<
Result<
(
Box<dyn for<'a> Visit<ExtensionVisitor<'a>> + Send + Sync>,
AcceptStream,
),
Error,
>,
>;
}
impl<A> DynAcceptT for A
where
A: Accept + Send + Sync,
for<'a> <A as Accept>::Metadata: Visit<ExtensionVisitor<'a>> + Send + Sync + 'static,
{
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<
Result<
(
Box<dyn for<'a> Visit<ExtensionVisitor<'a>> + Send + Sync>,
AcceptStream,
),
Error,
>,
> {
let (metadata, stream) = ready!(Accept::poll_accept(self, cx)?);
Poll::Ready(Ok((Box::new(metadata), stream)))
}
}
pub struct DynAccept(Box<dyn DynAcceptT>);
impl Accept for DynAccept {
type Metadata = Box<dyn for<'a> Visit<ExtensionVisitor<'a>> + Send + Sync>;
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
DynAcceptT::poll_accept(&mut *self.0, cx)
}
fn into_dyn(self) -> DynAccept
where
Self: Sized,
for<'a> Self::Metadata: Visit<ExtensionVisitor<'a>> + Send + Sync + 'static,
{
self
}
}
impl DynAccept {
pub fn new<A>(accept: A) -> Self
where
A: Accept + Send + Sync + 'static,
for<'a> <A as Accept>::Metadata: Visit<ExtensionVisitor<'a>> + Send + Sync + 'static,
{
Self(Box::new(accept))
}
}
#[pin_project::pin_project]
pub struct Acceptor<A: Accept> {
acceptor: Watch<A>,
@@ -95,12 +291,15 @@ impl<A: Accept + Send + Sync + 'static> Acceptor<A> {
self.acceptor.poll_changed(cx)
}
fn poll_accept(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<Accepted, Error>> {
let _ = self.poll_changed(cx);
fn poll_accept(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(A::Metadata, AcceptStream), Error>> {
while self.poll_changed(cx).is_ready() {}
self.acceptor.peek_mut(|a| a.poll_accept(cx))
}
async fn accept(&mut self) -> Result<Accepted, Error> {
async fn accept(&mut self) -> Result<(A::Metadata, AcceptStream), Error> {
std::future::poll_fn(|cx| self.poll_accept(cx)).await
}
}
@@ -111,20 +310,73 @@ impl Acceptor<Vec<TcpListener>> {
))
}
}
pub type UpgradableListener =
Option<Either<SelfContainedNetworkInterfaceListener, NetworkInterfaceListener>>;
impl Acceptor<UpgradableListener> {
pub fn bind_upgradable(listener: SelfContainedNetworkInterfaceListener) -> Self {
Self::new(Some(Either::Left(listener)))
impl Acceptor<Vec<DynAccept>> {
pub async fn bind_dyn(listen: impl IntoIterator<Item = SocketAddr>) -> Result<Self, Error> {
Ok(Self::new(
futures::future::try_join_all(
listen
.into_iter()
.map(TcpListener::bind)
.map(|f| f.map_ok(DynAccept::new)),
)
.await?,
))
}
}
impl<K> Acceptor<BTreeMap<K, TcpListener>>
where
K: Ord + Clone + Send + Sync + 'static,
{
pub async fn bind_map(
listen: impl IntoIterator<Item = (K, SocketAddr)>,
) -> Result<Self, Error> {
Ok(Self::new(
futures::future::try_join_all(listen.into_iter().map(|(key, addr)| async move {
Ok::<_, Error>((
key,
TcpListener::bind(addr)
.await
.with_kind(ErrorKind::Network)?,
))
}))
.await?
.into_iter()
.collect(),
))
}
}
impl<K> Acceptor<BTreeMap<K, DynAccept>>
where
K: Ord + Clone + Send + Sync + 'static,
{
pub async fn bind_map_dyn(
listen: impl IntoIterator<Item = (K, SocketAddr)>,
) -> Result<Self, Error> {
Ok(Self::new(
futures::future::try_join_all(listen.into_iter().map(|(key, addr)| async move {
Ok::<_, Error>((
key,
TcpListener::bind(addr)
.await
.with_kind(ErrorKind::Network)?,
))
}))
.await?
.into_iter()
.map(|(key, listener)| (key, listener.into_dyn()))
.collect(),
))
}
}
pub struct WebServerAcceptorSetter<A: Accept> {
acceptor: Watch<A>,
}
impl<A: Accept, B: Accept> WebServerAcceptorSetter<Option<Either<A, B>>> {
impl<A, B> WebServerAcceptorSetter<Option<Either<A, B>>>
where
A: Accept,
B: Accept<Metadata = A::Metadata>,
{
pub fn try_upgrade<F: FnOnce(A) -> Result<B, Error>>(&self, f: F) -> Result<(), Error> {
let mut res = Ok(());
self.acceptor.send_modify(|a| {
@@ -151,20 +403,24 @@ impl<A: Accept> Deref for WebServerAcceptorSetter<A> {
pub struct WebServer<A: Accept> {
shutdown: oneshot::Sender<()>,
router: Watch<Option<Router>>,
router: Watch<Router>,
acceptor: Watch<A>,
thread: NonDetachingJoinHandle<()>,
}
impl<A: Accept + Send + Sync + 'static> WebServer<A> {
impl<A> WebServer<A>
where
A: Accept + Send + Sync + 'static,
for<'a> A::Metadata: Visit<ExtensionVisitor<'a>> + Send + Sync + 'static,
{
pub fn acceptor_setter(&self) -> WebServerAcceptorSetter<A> {
WebServerAcceptorSetter {
acceptor: self.acceptor.clone(),
}
}
pub fn new(mut acceptor: Acceptor<A>) -> Self {
pub fn new(mut acceptor: Acceptor<A>, router: Router) -> Self {
let acceptor_send = acceptor.acceptor.clone();
let router = Watch::<Option<Router>>::new(None);
let router = Watch::new(router);
let service = router.clone_unseen();
let (shutdown, shutdown_recv) = oneshot::channel();
let thread = NonDetachingJoinHandle::from(tokio::spawn(async move {
@@ -187,8 +443,14 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
}
}
struct SwappableRouter(Watch<Option<Router>>, bool);
impl hyper::service::Service<hyper::Request<hyper::body::Incoming>> for SwappableRouter {
struct SwappableRouter<M> {
router: Watch<Router>,
metadata: M,
}
impl<M: for<'a> Visit<ExtensionVisitor<'a>> + Send + Sync + 'static>
hyper::service::Service<hyper::Request<hyper::body::Incoming>>
for SwappableRouter<M>
{
type Response = <Router as tower_service::Service<
hyper::Request<hyper::body::Incoming>,
>>::Response;
@@ -199,19 +461,13 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
hyper::Request<hyper::body::Incoming>,
>>::Future;
fn call(&self, req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
fn call(&self, mut req: hyper::Request<hyper::body::Incoming>) -> Self::Future {
use tower_service::Service;
if self.1 {
redirecter().call(req)
} else {
let router = self.0.read();
if let Some(mut router) = router {
router.call(req)
} else {
refresher().call(req)
}
}
self.metadata
.visit(&mut ExtensionVisitor(req.extensions_mut()));
self.router.read().call(req)
}
}
@@ -238,16 +494,16 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
let mut err = None;
for _ in 0..5 {
if let Err(e) = async {
let accepted = acceptor.accept().await?;
let (metadata, stream) = acceptor.accept().await?;
queue.add_job(
graceful.watch(
server
.serve_connection_with_upgrades(
TokioIo::new(accepted.stream),
SwappableRouter(
service.clone(),
accepted.https_redirect,
),
TokioIo::new(stream),
SwappableRouter {
router: service.clone(),
metadata,
},
)
.into_owned(),
),
@@ -258,6 +514,7 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
.await
{
err = Some(e);
tokio::time::sleep(Duration::from_millis(100)).await;
} else {
break;
}
@@ -299,26 +556,10 @@ impl<A: Accept + Send + Sync + 'static> WebServer<A> {
}
pub fn serve_router(&mut self, router: Router) {
self.router.send(Some(router))
self.router.send(router)
}
pub fn serve_main(&mut self, ctx: RpcContext) {
self.serve_router(main_ui_router(ctx))
}
pub fn serve_setup(&mut self, ctx: SetupContext) {
self.serve_router(setup_ui_router(ctx))
}
pub fn serve_diagnostic(&mut self, ctx: DiagnosticContext) {
self.serve_router(diagnostic_ui_router(ctx))
}
pub fn serve_install(&mut self, ctx: InstallContext) {
self.serve_router(install_ui_router(ctx))
}
pub fn serve_init(&mut self, ctx: InitContext) {
self.serve_router(init_ui_router(ctx))
pub fn serve_ui_for<C: UiContext>(&mut self, ctx: C) {
self.serve_router(ui_router(ctx))
}
}

View File

@@ -1017,6 +1017,31 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
.await?;
}
Command::new("ip")
.arg("rule")
.arg("add")
.arg("pref")
.arg("1000")
.arg("from")
.arg("all")
.arg("lookup")
.arg("main")
.invoke(ErrorKind::Network)
.await
.log_err();
Command::new("ip")
.arg("rule")
.arg("add")
.arg("pref")
.arg("1100")
.arg("from")
.arg("all")
.arg("lookup")
.arg("default")
.invoke(ErrorKind::Network)
.await
.log_err();
Command::new("systemctl")
.arg("restart")
.arg("NetworkManager")

View File

@@ -155,6 +155,16 @@ pub async fn remove(
for id in ids {
n.remove(&id)?;
}
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(())
})
.await
@@ -179,6 +189,16 @@ pub async fn remove_before(
for id in n.keys()?.range(..before) {
n.remove(&id)?;
}
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(())
})
.await
@@ -443,7 +463,8 @@ pub fn notify<T: NotificationType>(
data,
seen: false,
},
)
)?;
Ok(())
}
#[test]

View File

@@ -361,6 +361,7 @@ pub async fn execute<C: Context>(
match ARCH {
"x86_64" => install.arg("--target=x86_64-efi"),
"aarch64" => install.arg("--target=arm64-efi"),
"riscv64" => install.arg("--target=riscv64-efi"),
_ => &mut install,
};
}

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use chrono::Utc;
use clap::Parser;
use http::HeaderMap;
use imbl_value::InternedString;
use patch_db::PatchDb;
use reqwest::{Client, Proxy};
@@ -168,25 +169,38 @@ impl CallRemote<RegistryContext> for CliContext {
let url = if let Some(url) = self.registry_url.clone() {
url
} else if self.registry_hostname.is_some() {
format!(
let mut url: Url = format!(
"http://{}",
self.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN)
)
.parse()
.map_err(Error::from)?
.map_err(Error::from)?;
url.path_segments_mut()
.map_err(|_| Error::new(eyre!("cannot extend URL path"), ErrorKind::ParseUrl))?
.push("rpc")
.push("v0");
url
} else {
return Err(
Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest).into(),
);
};
method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = self
.registry_hostname
.clone()
.or(url.host().as_ref().map(InternedString::from_display))
.or_not_found("registry hostname")?;
.or_else(|| url.host().as_ref().map(InternedString::from_display));
crate::middleware::signature::call_remote(self, url, &sig_context, method, params).await
crate::middleware::signature::call_remote(
self,
url,
HeaderMap::new(),
sig_context.as_deref(),
method,
params,
)
.await
}
}
@@ -195,61 +209,32 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
&self,
mut method: &str,
params: Value,
RegistryUrlParams { registry }: RegistryUrlParams,
RegistryUrlParams { mut registry }: RegistryUrlParams,
) -> Result<Value, RpcError> {
use reqwest::Method;
use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
use rpc_toolkit::RpcResponse;
use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest};
let mut headers = HeaderMap::new();
headers.insert(
DEVICE_INFO_HEADER,
DeviceInfo::load(self).await?.to_header_value(),
);
registry
.path_segments_mut()
.map_err(|_| Error::new(eyre!("cannot extend URL path"), ErrorKind::ParseUrl))?
.push("rpc")
.push("v0");
let url = registry.join("rpc/v0")?;
method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from);
let rpc_req = RpcRequest {
id: Some(Id::Number(0.into())),
method: GenericRpcMethod::<_, _, Value>::new(method),
crate::middleware::signature::call_remote(
self,
registry,
headers,
sig_context.as_deref(),
method,
params,
};
let body = serde_json::to_vec(&rpc_req)?;
let res = self
.client
.request(Method::POST, url)
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.header(CONTENT_LENGTH, body.len())
.header(
DEVICE_INFO_HEADER,
DeviceInfo::load(self).await?.to_header_value(),
)
.body(body)
.send()
.await?;
if !res.status().is_success() {
let status = res.status();
let txt = res.text().await?;
let mut res = Err(Error::new(
eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())),
ErrorKind::Network,
));
if !txt.is_empty() {
res = res.with_ctx(|_| (ErrorKind::Network, txt));
}
return res.map_err(From::from);
}
match res
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
Some("application/json") => {
serde_json::from_slice::<RpcResponse>(&*res.bytes().await?)
.with_kind(ErrorKind::Deserialization)?
.result
}
_ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
}
)
.await
}
}

View File

@@ -175,7 +175,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
async move {
if metadata.get_device_info {
if let Some(device_info) = &self.device_info {
request.params["__device_info"] =
request.params["__DeviceInfo_device_info"] =
to_value(&DeviceInfo::from_header_value(device_info)?)?;
}
}

View File

@@ -141,9 +141,3 @@ pub fn registry_router(ctx: RegistryContext) -> Router {
}),
)
}
impl<A: Accept + Send + Sync + 'static> WebServer<A> {
pub fn serve_registry(&mut self, ctx: RegistryContext) {
self.serve_router(registry_router(ctx))
}
}

View File

@@ -83,7 +83,7 @@ pub struct AddAssetParams {
pub platform: InternedString,
#[ts(type = "string")]
pub url: Url,
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
#[ts(skip)]
pub signer: AnyVerifyingKey,
pub signature: AnySignature,
@@ -289,7 +289,7 @@ pub struct RemoveAssetParams {
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
#[ts(skip)]
pub signer: AnyVerifyingKey,
}

View File

@@ -56,7 +56,7 @@ pub struct SignAssetParams {
#[ts(type = "string")]
platform: InternedString,
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
signer: AnyVerifyingKey,
signature: AnySignature,
}

View File

@@ -68,7 +68,7 @@ pub struct AddVersionParams {
pub source_version: VersionRange,
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}
@@ -146,7 +146,7 @@ pub struct GetOsVersionParams {
platform: Option<InternedString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__device_info")]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
}

View File

@@ -31,7 +31,7 @@ pub struct AddPackageParams {
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub uploader: AnyVerifyingKey,
pub commitment: MerkleArchiveCommitment,
pub signature: AnySignature,
@@ -169,7 +169,7 @@ pub struct RemovePackageParams {
pub version: VersionString,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__auth_signer")]
#[serde(rename = "__Auth_signer")]
pub signer: Option<AnyVerifyingKey>,
}

View File

@@ -51,7 +51,7 @@ pub struct GetPackageParams {
pub source_version: Option<VersionString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__device_info")]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
#[serde(default)]
#[arg(default_value = "none")]

View File

@@ -3,12 +3,10 @@ use tokio::io::{AsyncSeek, AsyncWrite};
use crate::prelude::*;
use crate::util::io::TrackingIO;
#[async_trait::async_trait]
pub trait Sink: AsyncWrite + Unpin + Send {
async fn current_position(&mut self) -> Result<u64, Error>;
fn current_position(&mut self) -> impl Future<Output = Result<u64, Error>> + Send + '_;
}
#[async_trait::async_trait]
impl<S: AsyncWrite + AsyncSeek + Unpin + Send> Sink for S {
async fn current_position(&mut self) -> Result<u64, Error> {
use tokio::io::AsyncSeekExt;
@@ -17,7 +15,6 @@ impl<S: AsyncWrite + AsyncSeek + Unpin + Send> Sink for S {
}
}
#[async_trait::async_trait]
impl<W: AsyncWrite + Unpin + Send> Sink for TrackingIO<W> {
async fn current_position(&mut self) -> Result<u64, Error> {
Ok(self.position())

View File

@@ -8,7 +8,7 @@ use models::{ImageId, VolumeId};
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
use tokio::process::Command;
use crate::dependencies::{DepInfo, Dependencies};
use crate::dependencies::{DepInfo, Dependencies, MetadataSrc};
use crate::prelude::*;
use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
@@ -225,7 +225,7 @@ impl TryFrom<ManifestV1> for Manifest {
DepInfo {
description: value.description,
optional: !value.requirement.required(),
s9pk: None,
metadata: None,
},
)
})

Some files were not shown because too many files have changed in this diff Show More