Compare commits

..

37 Commits

Author SHA1 Message Date
Timmy Welch
b8fc58bd9f Add support for defining package capabilities
Some checks failed
gokrazy CI / CI (macos-latest) (push) Has been cancelled
gokrazy CI / CI (ubuntu-latest) (push) Has been cancelled
gokrazy CI / CI (windows-latest) (push) Has been cancelled
2025-12-28 14:51:38 -08:00
Michael Stapelberg
ba6a8936f4 packer: do not check for HTTP→HTTPS redirect
This check was broken: it tried to construct a http URL
by changing the updateBaseUrl schema instead of constructing
such a URL based on the configured HTTPPort.

I also don’t think this check is useful:
HTTPS will be used for updates regardless of the check.
Even if an attacker intercepted HTTP traffic and removed the redirect,
that has no bearing on the update, so why bother checking.

One thing the check (implicitly) did is the required fallback
on initial installation when --insecure is specified.
We now solve that by falling back from HTTPS to HTTP explicitly
(only when --insecure is specified, of course).

related to https://github.com/gokrazy/tools/pull/94
2025-12-09 17:11:22 +01:00
Michael Stapelberg
0daf1b1ae4 packer: stop using tlsflag global state 2025-12-08 21:32:59 +01:00
Michael Stapelberg
8320e69ccc packer: move error check closer to error assignment 2025-12-08 20:58:14 +01:00
Michael Stapelberg
0a82ebcb52 remove stale comment
git commit 87444dca50 accidentally
removed the code (partuuid = 0), but not the comment.
2025-12-08 20:57:45 +01:00
Michael Stapelberg
715673f4b5 packer: clean up sbomHook hack 2025-12-06 21:54:52 +01:00
Michael Stapelberg
2848fa1a69 packer: move findPackageFiles to packerprepare.go 2025-12-06 21:38:46 +01:00
Michael Stapelberg
ab66901132 packer: move find* to packerprepare.go 2025-12-06 21:37:13 +01:00
Michael Stapelberg
0bb33e2ae8 packer: move addToFileInfo to write.go 2025-12-06 21:35:56 +01:00
Michael Stapelberg
6ae03bee7a packer: move countingWriter to packerwrite.go 2025-12-06 21:32:21 +01:00
Michael Stapelberg
4fab9e7759 packer: move find* to packerprepare.go 2025-12-06 21:28:53 +01:00
Michael Stapelberg
cfba731eae packer: move partitionPath to packerwrite.go 2025-12-06 21:23:36 +01:00
Michael Stapelberg
bd1faa7647 packer: move overwrite{File,Device} to packerwrite.go 2025-12-06 21:21:00 +01:00
Michael Stapelberg
3def6ed054 packer: move printHowToInteract to packerwrite.go 2025-12-06 21:17:15 +01:00
Michael Stapelberg
8b448cc312 packer: move update into packerupdate.go 2025-12-06 21:16:38 +01:00
Michael Stapelberg
df53492c98 packer: move write into packerwrite.go 2025-12-06 21:14:44 +01:00
Michael Stapelberg
f5ddd27c7e packer: move build into packerbuild.go 2025-12-06 21:12:44 +01:00
Michael Stapelberg
23ac917f5b packer: move prepare into packerprepare.go 2025-12-06 21:11:31 +01:00
Michael Stapelberg
485405edac cleanup: remove unused parameters 2025-12-06 21:09:00 +01:00
Michael Stapelberg
45b2b940f6 cleanup: move write{Boot,Root}File next to write{Boot,Root} 2025-12-06 21:04:34 +01:00
Michael Stapelberg
67382a6dbe cleanup: move kernel GOARCH validation code into its own file 2025-12-06 20:59:01 +01:00
Michael Stapelberg
b513356080 refactor: split printHowToInteract into its own method
Also remove duplicate URL construction code
2025-12-06 20:55:30 +01:00
Michael Stapelberg
cbfacd97a6 refactor: split logicUpdate into a separate method 2025-12-06 20:42:09 +01:00
Michael Stapelberg
1921f918ee cleanup: inline programName now that the old packer is gone 2025-12-06 20:23:57 +01:00
Michael Stapelberg
9e3ab11076 packer: add test for losing HTTPS certificates
related to https://github.com/gokrazy/tools/pull/68
2025-12-06 08:49:22 +01:00
julienrbrt
9c9a33515b fix partuuid probing with --insecure after losing HTTPS certificates (#68) 2025-12-06 08:48:38 +01:00
Michael Stapelberg
52cab9f145 packer: stop using updateflag global state
related to https://github.com/gokrazy/tools/pull/68
2025-12-06 08:37:24 +01:00
Michael Stapelberg
dc8c88b368 gokupdate_test: refactor to use readConfig/writeConfig 2025-12-05 08:36:19 +01:00
Michael Stapelberg
91c487c959 packer: use HTTPS client despite -insecure (post-update) (+test)
While adding the integration test for
https://gokrazy.org/userguide/tls-for-untrusted-networks/,
I noticed that the packer does not actually successfully complete
the initial HTTPS deployment (where -insecure is used).
After writing the image to disk and rebooting, the packer was stuck at:

device not yet reachable: Get "https://localhost:9080/": http:
server gave HTTP response to HTTPS client

related to https://github.com/gokrazy/tools/pull/94
2025-11-29 12:39:22 +01:00
Michael Stapelberg
d1929f390f integration: enable QEMU hardware acceleration 2025-11-29 11:24:47 +01:00
Michael Stapelberg
2c805ed001 refactor cobra command initialization to avoid stale state
Before this commit, we held onto *cobra.Command objects,
but that is not actually supported: after the first Execute(),
commands like updateCmd are stuck on the first-ever provided ctx.

Instead, turn command initialization into functions.

I only noticed this when trying to do two 'gok update'
from within the same test, where the fake build timestamp
is injected via the context (the timestamp was always the same).
2025-11-29 10:47:48 +01:00
Michael Stapelberg
d588a72286 integration: start QEMU VM for 'gok update' test
related to https://github.com/gokrazy/tools/pull/94
2025-11-29 09:56:11 +01:00
Michael Stapelberg
50ceea79c7 internal/packer: apply BootloaderExtraEEPROM
related to https://github.com/gokrazy/gokrazy/issues/338
2025-11-16 19:09:52 +01:00
Michael Stapelberg
d57f04bf53 add internal/eeprom package (with test against other impls)
related to https://github.com/gokrazy/gokrazy/issues/338
2025-11-16 15:08:21 +01:00
Michael Stapelberg
4950fd73f6 add a flake.nix providing the two other rpi eeprom tools
see also: https://michael.stapelberg.ch/posts/2025-07-27-dev-shells-with-nix-4-quick-examples/

related to https://github.com/gokrazy/gokrazy/issues/338
2025-11-16 15:08:12 +01:00
Michael Stapelberg
57f559232c packer: eeprom: only write vl805-*.bin if present (Pi 4)
The rpi5-eeprom package no longer contains vl805, which is Pi 4-specific.

related to https://github.com/gokrazy/gokrazy/issues/332
2025-11-16 09:57:09 +01:00
Michael Stapelberg
2f0aac76a0 packer: include timestamp (ts: unixtime) in firmware .sig files
Otherwise the Pi 5 firmware will default to a timestamp of 0
and skip the update always.

related to https://github.com/gokrazy/gokrazy/issues/332
2025-11-16 09:08:03 +01:00
45 changed files with 4599 additions and 2153 deletions

View File

@@ -35,6 +35,10 @@ jobs:
run: |
go install -mod=mod ./cmd/...
- name: install qemu
if: matrix.os == 'ubuntu-latest'
run: sudo apt update && sudo apt-get install qemu-system-x86
- name: Run tests
if: matrix.os == 'ubuntu-latest'
# TestRelativeParentDir verifies breakglass.authorized_keys

78
flake.lock generated Normal file
View File

@@ -0,0 +1,78 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1763049705,
"narHash": "sha256-A5LS0AJZ1yDPTa2fHxufZN++n8MCmtgrJDtxFxrH4S8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3acb677ea67d4c6218f33de0db0955f116b7588c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rpi-eeprom-tools": "rpi-eeprom-tools"
}
},
"rpi-eeprom-tools": {
"flake": false,
"locked": {
"lastModified": 1758789610,
"narHash": "sha256-1aGazeN4Ou5GRMCRETie4I0E08mlAQFWp6cGURB1NIM=",
"owner": "info-beamer",
"repo": "rpi-eeprom-tools",
"rev": "9ae4d618a0a51789fbde8e9d667881a321cf6721",
"type": "github"
},
"original": {
"owner": "info-beamer",
"repo": "rpi-eeprom-tools",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

66
flake.nix Normal file
View File

@@ -0,0 +1,66 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
rpi-eeprom-tools = {
url = "github:info-beamer/rpi-eeprom-tools";
flake = false;
};
};
outputs =
{
self,
nixpkgs,
flake-utils,
rpi-eeprom-tools,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Python environment with pycryptodome for pi-eeprom-tools
pythonEnv = pkgs.python312.withPackages (
ps: with ps; [
pycryptodome
]
);
# Helper to create a wrapper for a pi-eeprom-* script
mkPiEepromTool =
name:
pkgs.writeShellApplication {
inherit name;
runtimeInputs = [
pythonEnv
];
text = ''
exec ${pythonEnv.interpreter} ${rpi-eeprom-tools}/${name} "$@"
'';
};
# All the pi-eeprom tools
pi-eeprom-extract = mkPiEepromTool "pi-eeprom-extract";
pi-eeprom-ls = mkPiEepromTool "pi-eeprom-ls";
pi-eeprom-recompress = mkPiEepromTool "pi-eeprom-recompress";
pi-eeprom-update = mkPiEepromTool "pi-eeprom-update";
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go
raspberrypi-eeprom
pi-eeprom-extract
pi-eeprom-ls
pi-eeprom-recompress
pi-eeprom-update
];
shellHook = ''
echo "Nix-based gokrazy/tools dev env (go: $(go version))"
'';
};
}
);
}

5
go.mod
View File

@@ -5,9 +5,10 @@ go 1.24.0
toolchain go1.24.6
require (
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c
github.com/google/renameio/v2 v2.0.0
github.com/mattn/go-isatty v0.0.20

17
go.sum
View File

@@ -1,12 +1,16 @@
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6 h1:zdaWj/ncXyzpPH3YqACvJXMrJxkkILrnWbjHojHBctc=
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6/go.mod h1:m5pN88x7ZnEDGXZldwg7RCX+EikR9qz/iSI2GzXq++Y=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8 h1:BvyzTtbpz1GCGD35Z3G/ZR0nK0j3Fh+dRCCso+w3RKE=
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8/go.mod h1:rVItujrJo0NpYZhFR5dYdzLDqMoMCtjEZkdxoCRDo+o=
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369 h1:aNni2iPwJbowfHW1SFapKLfY+ZPUIcBfFrJvYPAh3p4=
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4 h1:XFo3EqnHUbmAySp7zqms8ee/tU8bM9k+YzT7L4o5CcQ=
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4/go.mod h1:+StofDb/2cMb7vbA2znaNolgp9SadTYeyRIFtdhH1KQ=
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82 h1:4ghNfD9NaZLpFrqQiBF6mPVFeMYXJSky38ubVA4ic2E=
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c h1:j4/v9FR/cOy6nog5rmXUtauBsOU3mm+rTPn5IENUbmg=
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c/go.mod h1:EtAn+BPibqnAHnYGj3FW5e284xNsiOOMOL2dJiwu7H4=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
@@ -17,11 +21,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79 h1:WZWglxfb13JCTbJyKY1pk0V94spHxJzMAQW29INytRQ=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -34,4 +42,5 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -17,7 +17,7 @@ type Context struct {
}
func (c Context) Execute(ctx context.Context) error {
root := gok.RootCmd
root := gok.RootCmd()
if r := c.Stdin; r != nil {
root.SetIn(r)
}

View File

@@ -136,7 +136,7 @@ func TestGokRun(t *testing.T) {
// Testing the root command because individual cobra commands cannot be
// executed directly.
root := gok.RootCmd
root := gok.RootCmd()
root.SetContext(ctx)
logOutputFound := make(chan bool)
rd, wr := io.Pipe()

View File

@@ -2,41 +2,54 @@ package gokupdate_test
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"hash"
"hash/crc32"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/tlsflag"
"github.com/gokrazy/tools/gok"
"github.com/gokrazy/tools/internal/packer"
)
type gokrazyTestInstance struct {
name string
configDir string
}
func (inst *gokrazyTestInstance) writeConfig(t *testing.T, basename, content string) {
func (inst *gokrazyTestInstance) configPath() string {
return "gokrazy/" + inst.name + "/config.json"
}
func (inst *gokrazyTestInstance) readConfig(t *testing.T) config.Struct {
b, err := os.ReadFile(inst.configPath())
if err != nil {
t.Fatal(err)
}
var cfg config.Struct
if err := json.Unmarshal(b, &cfg); err != nil {
t.Fatal(err)
}
return cfg
}
func (inst *gokrazyTestInstance) writeConfig(t *testing.T, cfg config.Struct) {
t.Helper()
fn := filepath.Join(inst.configDir, basename)
if err := os.WriteFile(fn, []byte(content), 0600); err != nil {
t.Logf("Writing updated cfg = %+v", cfg)
b, err := cfg.FormatForFile()
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(inst.configPath(), b, 0644); err != nil {
t.Fatal(err)
}
}
func writeGokrazyInstance(t *testing.T) *gokrazyTestInstance {
func writeGokrazyInstance(t *testing.T, name string) *gokrazyTestInstance {
t.Helper()
// Redirect os.UserConfigDir() to a temporary directory under our
@@ -64,6 +77,7 @@ func writeGokrazyInstance(t *testing.T) *gokrazyTestInstance {
}
return &gokrazyTestInstance{
name: name,
configDir: configDir,
}
}
@@ -73,85 +87,17 @@ func TestGokUpdate(t *testing.T) {
// gokrazy/tools repository working copy.
t.Chdir(t.TempDir())
_ = writeGokrazyInstance(t)
// TODO: run the gokrazy instance in a VM instead of providing a fake
// implementation of the update protocol.
mux := http.NewServeMux()
mux.HandleFunc("/update/features", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
})
mux.HandleFunc("/update/", func(w http.ResponseWriter, r *http.Request) {
// accept whatever for now.
var hash hash.Hash
switch r.Header.Get("X-Gokrazy-Update-Hash") {
case "crc32":
hash = crc32.NewIEEE()
default:
hash = sha256.New()
}
if _, err := io.Copy(hash, r.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%x", hash.Sum(nil))
})
mux.HandleFunc("/reboot", func(w http.ResponseWriter, r *http.Request) {
// you got it, boss!
})
mux.HandleFunc("/uploadtemp/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("[HTTP] uploadtemp: %s", r.URL.Path)
})
mux.HandleFunc("/divert", func(w http.ResponseWriter, r *http.Request) {
log.Printf("[HTTP] divert: %s to %s",
r.FormValue("path"),
r.FormValue("diversion"))
})
mux.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
log.Printf("[HTTP] log: %s", r.FormValue("path"))
w.Header().Set("Content-type", "text/event-stream")
if r.FormValue("stream") == "stdout" {
const text = "Hello Sun"
line := fmt.Sprintf("data: %s\n", text)
if _, err := fmt.Fprintln(w, line); err != nil {
return
}
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
select {}
})
fakeBuildTimestamp := "fake-" + time.Now().Format(time.RFC3339)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") {
status := struct {
BuildTimestamp string `json:"BuildTimestamp"`
}{
BuildTimestamp: fakeBuildTimestamp,
}
b, err := json.Marshal(&status)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
return
}
http.Error(w, "handler not implemented", http.StatusNotImplemented)
})
srv := httptest.NewServer(mux)
u, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}
// create a new instance
const (
instanceName = "hello"
hostname = "localhost"
)
ti := writeGokrazyInstance(t, instanceName)
c := gok.Context{
Args: []string{
"--parent_dir", "gokrazy",
"-i", "hello",
"-i", instanceName,
"new",
},
}
@@ -160,33 +106,73 @@ func TestGokUpdate(t *testing.T) {
t.Fatalf("%v: %v", c.Args, err)
}
// update the instance config to speak to the test server
const configPath = "gokrazy/hello/config.json"
b, err := os.ReadFile(configPath)
// and update the (default) instance config for our test
{
cfg := ti.readConfig(t)
// use generic kernel, enable serial console
// TODO: use arm64 kernel when running on arm64
kernelPackage := "github.com/gokrazy/kernel.amd64"
cfg.KernelPackage = &kernelPackage
cfg.FirmwarePackage = &kernelPackage
cfg.SerialConsole = "ttyS0,115200"
cfg.Environment = []string{"GOOS=linux", "GOARCH=amd64"}
cfg.Hostname = hostname
cfg.Update.Hostname = hostname
cfg.Update.HTTPPort = "9080"
cfg.Update.HTTPSPort = "9443"
t.Logf("Updated cfg.Update = %+v", cfg.Update)
ti.writeConfig(t, cfg)
}
t.Logf("booting gokrazy instance in a VM")
qemu := Run(t, nil)
defer qemu.Kill()
// TODO: kill the test if this qemu process dies for any reason
// test by setting an aggressive QemuOptions.Timeout
// wait for this instance to become healthy
//
// TODO: include the actual build timestamp once gok overwrite returns it.
if err := qemu.ConsoleExpect("gokrazy build timestamp "); err != nil {
t.Fatal(err)
}
t.Logf("gokrazy VM booted up, waiting for network reachability")
// poll for reachability over the network
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(1 * time.Second) {
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
req, err := http.NewRequest("GET", "http://localhost:9080", nil)
if err != nil {
t.Fatal(err)
}
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Logf("VM not yet reachable: %v", err)
continue
}
if resp.StatusCode == http.StatusUnauthorized {
t.Logf("gokrazy VM became reachable over the network")
break
}
}
// TODO: make 'gok update' not change directory?
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var cfg config.Struct
if err := json.Unmarshal(b, &cfg); err != nil {
t.Fatal(err)
}
cfg.Update.Hostname = "localhost"
cfg.Update.HTTPPort = u.Port()
t.Logf("Updated cfg.Update = %+v", cfg.Update)
b, err = cfg.FormatForFile()
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(configPath, b, 0644); err != nil {
t.Fatal(err)
}
// verify overwrite works (i.e. locates extrafiles)
fakeBuildTimestamp := "fake-update-1"
ctx := context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
c = gok.Context{
Args: []string{
"--parent_dir", "gokrazy",
"-i", "hello",
"-i", instanceName,
"update",
},
}
@@ -194,4 +180,83 @@ func TestGokUpdate(t *testing.T) {
if err := c.Execute(ctx); err != nil {
t.Fatalf("%v: %v", c.Args, err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
// change to use self-signed TLS certificates
t.Logf("Setting Update.UseTLS = self-signed")
{
cfg := ti.readConfig(t)
cfg.Update.UseTLS = "self-signed"
t.Logf("Updated cfg.Update = %+v", cfg.Update)
ti.writeConfig(t, cfg)
}
fakeBuildTimestamp = "fake-update-2"
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
c = gok.Context{
Args: []string{
"--parent_dir", "gokrazy",
"-i", instanceName,
"update",
"--insecure", // only on first update after enabling self-signed TLS
},
}
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
if err := c.Execute(ctx); err != nil {
t.Fatalf("%v: %v", c.Args, err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
t.Logf("first update succeeded, doing another update without --insecure")
fakeBuildTimestamp = "fake-update-3"
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
c = gok.Context{
Args: []string{
"--parent_dir", "gokrazy",
"-i", instanceName,
"update",
},
}
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
if err := c.Execute(ctx); err != nil {
t.Fatalf("%v: %v", c.Args, err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
t.Logf("second update succeeded, doing another update after deleting the certificates (with --insecure)")
certPath, keyPath, err := tlsflag.CertificatePathsFor("self-signed", hostname)
if err != nil {
t.Fatal(err)
}
if err := os.Remove(certPath); err != nil {
t.Fatalf("deleting certificate: %v", err)
}
if err := os.Remove(keyPath); err != nil {
t.Fatalf("deleting certificate: %v", err)
}
fakeBuildTimestamp = "fake-update-4"
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
c = gok.Context{
Args: []string{
"--parent_dir", "gokrazy",
"-i", instanceName,
"update",
"--insecure", // because we deleted the certificate files
},
}
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
if err := c.Execute(ctx); err != nil {
t.Fatalf("%v: %v", c.Args, err)
}
}

View File

@@ -0,0 +1,97 @@
package gokupdate_test
import (
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"testing"
"time"
"github.com/anatol/vmtest"
)
func effectiveGOARCH() string {
goarch := os.Getenv("GOARCH")
if goarch != "" {
return goarch
}
return runtime.GOARCH
}
// TODO: move to a gokrazy/internal/integrationtest package
func Run(t *testing.T, qemuArgs []string) *vmtest.Qemu {
tempdir := t.TempDir()
diskImage := filepath.Join(tempdir, "gokrazy.img")
// diskImage := "/tmp/gokrazy.img" // for debugging
// TODO: use in-process gok overwrite
packer := exec.Command("gok",
"overwrite",
"--instance=hello",
"--parent_dir=gokrazy",
"--full="+diskImage,
"--target_storage_bytes="+strconv.Itoa(2*1024*1024*1024))
packer.Env = append(os.Environ(), "GOARCH=amd64")
packer.Stdout = os.Stdout
packer.Stderr = os.Stderr
log.Printf("%s", packer.Args)
if err := packer.Run(); err != nil {
t.Fatalf("%s: %v", packer.Args, err)
}
// Chosen to match internal/gok/vmrun.go
qemuArgs = append(qemuArgs,
//"-enable-kvm",
//"-cpu", "host",
"-nodefaults",
"-m", "1024",
// required! system gets stuck without -smp
"-smp", strconv.Itoa(max(runtime.NumCPU(), 2)),
"-device", "e1000,netdev=net0",
"-netdev", "user,id=net0,hostfwd=tcp::9080-:9080,hostfwd=tcp::9022-:22,hostfwd=tcp::9443-:9443",
// Use -drive instead of vmtest.QemuOptions.Disks because the latter
// results in wiring up the devices using SCSI in a way that the
// router7 kernel config does not support.
// TODO: update kernel config and switch to Disks:
"-boot", "order=d",
"-drive", "file="+diskImage+",format=raw",
)
// Do not use hardware acceleration on GitHub Actions,
// where there is no nested KVM available (by default).
if os.Getenv("GITHUB_ACTIONS") != "true" {
goarch := effectiveGOARCH()
if goarch == runtime.GOARCH {
// Hardware acceleration (in both cases) is only available for the
// native architecture, e.g. arm64 for M1 MacBooks.
switch runtime.GOOS {
case "linux":
qemuArgs = append(qemuArgs, "-accel", "kvm")
case "darwin":
qemuArgs = append(qemuArgs, "-accel", "hvf")
}
}
}
opts := vmtest.QemuOptions{
Architecture: vmtest.QEMU_X86_64,
OperatingSystem: vmtest.OS_LINUX,
Params: qemuArgs,
// Disks: []vmtest.QemuDisk{
// {
// Path: diskImage,
// Format: "raw",
// },
// },
Verbose: testing.Verbose(),
Timeout: 30 * time.Minute,
}
qemu, err := vmtest.NewQemu(&opts)
if err != nil {
t.Fatal(err)
}
return qemu
}

398
internal/cap/License Normal file
View File

@@ -0,0 +1,398 @@
/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-only */
Unless otherwise *explicitly* stated, the following text describes the
licensed conditions under which the contents of this libcap/cap release
may be used and distributed.
The licensed conditions are one or the other of these two Licenses:
- BSD 3-clause
- GPL v2.0
-------------------------------------------------------------------------
BSD 3-clause:
-------------
Redistribution and use in source and binary forms of libcap/cap, with
or without modification, are permitted provided that the following
conditions are met:
1. Redistributions of source code must retain any existing copyright
notice, and this entire permission notice in its entirety,
including the disclaimer of warranties.
2. Redistributions in binary form must reproduce all prior and current
copyright notices, this list of conditions, and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
3. The name of any author may not be used to endorse or promote
products derived from this software without their specific prior
written permission.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
-------------------------------------------------------------------------
GPL v2.0:
---------
ALTERNATIVELY, this product may be distributed under the terms of the
GNU General Public License (v2.0 - see below), in which case the
provisions of the GNU GPL are required INSTEAD OF the above
restrictions. (This clause is necessary due to a potential conflict
between the GNU GPL and the restrictions contained in a BSD-style
copyright.)
-------------------------
Full text of gpl-2.0.txt:
-------------------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

13
internal/cap/README Normal file
View File

@@ -0,0 +1,13 @@
This package is modified from the libcap project to allow
cross-platform marshalling to/from the kernel vfs format to write squashfs
xattr values and for parsing the text format used with the setcap command
Package cap is the libcap API for Linux Capabilities written in
Go. The official release announcement site for libcap is:
https://sites.google.com/site/fullycapable/
Like libcap, the cap package is distributed with a "you choose"
License. Specifically: BSD 3-clause, or GPL2. See the License file.
Andrew G. Morgan <morgan@kernel.org>

124
internal/cap/cap.go Normal file
View File

@@ -0,0 +1,124 @@
// Package cap
// Copyright (c) 2019-21 Andrew G. Morgan <morgan@kernel.org>
//
// The cap and psx packages are licensed with a (you choose) BSD
// 3-clause or GPL2. See LICENSE file for details.
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
package cap
import (
"errors"
"sync"
)
// Value is the type of a single capability (or permission) bit.
type Value uint
// Flag is the type of one of the three Value dimensions held in a
// Set. It is also used in the (*IAB).Fill() method for changing the
// Bounding and Ambient Vectors.
type Flag uint
// Effective, Permitted, Inheritable are the three Flags of Values
// held in a Set.
const (
Effective Flag = iota
Permitted
Inheritable
)
// String identifies a Flag value by its conventional "e", "p" or "i"
// string abbreviation.
func (f Flag) String() string {
switch f {
case Effective:
return "e"
case Permitted:
return "p"
case Inheritable:
return "i"
default:
return "<Error>"
}
}
// data holds a 32-bit slice of the compressed bitmaps of capability
// sets as understood by the kernel.
type data [Inheritable + 1]uint32
// Set is an opaque capabilities container for a set of system
// capbilities. It holds individually addressable capability Value's
// for the three capability Flag's. See GetFlag() and SetFlag() for
// how to adjust them individually, and Clear() and ClearFlag() for
// how to do bulk operations.
//
// For admin tasks associated with managing namespace specific file
// capabilities, Set can also support a namespace-root-UID value which
// defaults to zero. See GetNSOwner() and SetNSOwner().
type Set struct {
// mu protects all other members of a Set.
mu sync.RWMutex
// flat holds Flag Value bitmaps for all capabilities
// associated with this Set.
flat []data
// Linux specific
nsRoot int
}
// Various known kernel magic values.
const (
kv1 = 0x19980330 // First iteration of process capabilities (32 bits).
kv2 = 0x20071026 // First iteration of process and file capabilities (64 bits) - deprecated.
kv3 = 0x20080522 // Most recently supported process and file capabilities (64 bits).
)
var (
// startUp protects setting of the following values: magic,
// words, maxValues.
startUp sync.Once
// magic holds the preferred magic number for the kernel ABI.
magic uint32
// words holds the number of uint32's associated with each
// capability Flag for this session.
words int
// maxValues holds the number of bit values that are named by
// the running kernel. This is generally expected to match
// ValueCount which is autogenerated at packaging time.
maxValues uint
)
type header struct {
magic uint32
pid int32
}
// defines from uapi/linux/prctl.h
const (
prCapBSetRead = 23
prCapBSetDrop = 24
)
// NewSet returns an empty capability set.
func NewSet() *Set {
startUp.Do(cInit)
return &Set{
flat: make([]data, words),
}
}
// ErrBadSet indicates a nil pointer was used for a *Set, or the
// request of the Set is invalid in some way.
var ErrBadSet = errors.New("bad capability set")
// good confirms that c looks valid.
func (c *Set) good() error {
if c == nil || len(c.flat) == 0 {
return ErrBadSet
}
return nil
}

View File

@@ -0,0 +1,47 @@
//go:build linux
package cap
import (
"sort"
"syscall"
"unsafe"
)
// cInit performs the lazy identification of the capability vintage of
// the running system.
func cInit() {
h := &header{
magic: kv3,
}
_, _, _ = syscall.RawSyscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(h)), uintptr(0), 0)
magic = h.magic
switch magic {
case kv1:
words = 1
case kv2, kv3:
words = 2
default:
// Fall back to a known good version.
magic = kv3
words = 2
}
// Use the bounding set to evaluate which capabilities exist.
maxValues = uint(sort.Search(32*words, func(n int) bool {
_, err := GetBound(Value(n))
return err != nil
}))
if maxValues == 0 {
// Fall back to using the largest value defined at build time.
maxValues = NamedCount
}
}
func GetBound(val Value) (bool, error) {
r, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, prCapBSetRead, uintptr(val), 0)
if err != 0 {
return false, err
}
return int(r) > 0, nil
}

View File

@@ -0,0 +1,12 @@
//go:build !linux
package cap
// cInit performs the lazy identification of the capability vintage of
// the running system.
func cInit() {
if maxValues == 0 {
// Fall back to using the largest value defined at build time.
maxValues = NamedCount
}
}

174
internal/cap/file.go Normal file
View File

@@ -0,0 +1,174 @@
package cap
import (
"bytes"
"encoding/binary"
"errors"
"io"
"syscall"
)
// uapi/linux/xattr.h defined.
var (
xattrNameCaps, _ = syscall.BytePtrFromString("security.capability")
)
// uapi/linux/capability.h defined.
const (
vfsCapRevisionMask = uint32(0xff000000)
vfsCapFlagsMask = ^vfsCapRevisionMask
vfsCapFlagsEffective = uint32(1)
vfsCapRevision1 = uint32(0x01000000)
vfsCapRevision2 = uint32(0x02000000)
vfsCapRevision3 = uint32(0x03000000)
)
// Data types stored in little-endian order.
type vfsCaps1 struct {
MagicEtc uint32
Data [1]struct {
Permitted, Inheritable uint32
}
}
type vfsCaps2 struct {
MagicEtc uint32
Data [2]struct {
Permitted, Inheritable uint32
}
}
type vfsCaps3 struct {
MagicEtc uint32
Data [2]struct {
Permitted, Inheritable uint32
}
RootID uint32
}
// ErrBadSize indicates the loaded file capability has
// an invalid number of bytes in it.
var ErrBadSize = errors.New("filecap bad size")
// ErrBadMagic indicates that the kernel preferred magic number for
// capability Set values is not supported by this package. This
// generally implies you are using an exceptionally old
// "../libcap/cap" package. An upgrade is needed, or failing that see
// [the Fully Capable site] for the way to report or review a bug.
//
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
var ErrBadMagic = errors.New("unsupported magic")
// ErrBadPath indicates a failed attempt to set a file capability on
// an irregular (non-executable) file.
var ErrBadPath = errors.New("file is not a regular executable")
// ErrOutOfRange indicates an erroneous value for MinExtFlagSize.
var ErrOutOfRange = errors.New("flag length invalid for export")
// DigestFileCap unpacks a file capability and returns it in a *Set
// form.
func DigestFileCap(d []byte) (*Set, error) {
var (
err error
raw1 vfsCaps1
raw2 vfsCaps2
raw3 vfsCaps3
)
sz := len(d)
if sz < binary.Size(raw1) || sz > binary.Size(raw3) {
return nil, ErrBadSize
}
b := bytes.NewReader(d)
var magicEtc uint32
if err = binary.Read(b, binary.LittleEndian, &magicEtc); err != nil {
return nil, err
}
c := NewSet()
b.Seek(0, io.SeekStart)
switch magicEtc & vfsCapRevisionMask {
case vfsCapRevision1:
if err = binary.Read(b, binary.LittleEndian, &raw1); err != nil {
return nil, err
}
data := raw1.Data[0]
c.flat[0][Permitted] = data.Permitted
c.flat[0][Inheritable] = data.Inheritable
if raw1.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
c.flat[0][Effective] = data.Inheritable | data.Permitted
}
case vfsCapRevision2:
if err = binary.Read(b, binary.LittleEndian, &raw2); err != nil {
return nil, err
}
for i, data := range raw2.Data {
c.flat[i][Permitted] = data.Permitted
c.flat[i][Inheritable] = data.Inheritable
if raw2.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
c.flat[i][Effective] = data.Inheritable | data.Permitted
}
}
case vfsCapRevision3:
if err = binary.Read(b, binary.LittleEndian, &raw3); err != nil {
return nil, err
}
for i, data := range raw3.Data {
c.flat[i][Permitted] = data.Permitted
c.flat[i][Inheritable] = data.Inheritable
if raw3.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
c.flat[i][Effective] = data.Inheritable | data.Permitted
}
}
c.nsRoot = int(raw3.RootID)
default:
return nil, ErrBadMagic
}
return c, nil
}
// PackFileCap transforms a system capability into a VFS form. Because
// of the way Linux stores capabilities in the file extended
// attributes, the process is a little lossy with respect to effective
// bits.
func (c *Set) PackFileCap() ([]byte, error) {
c.mu.RLock()
defer c.mu.RUnlock()
var magic uint32
switch words {
case 1:
if c.nsRoot != 0 {
return nil, ErrBadSet // nsRoot not supported for single DWORD caps.
}
magic = vfsCapRevision1
case 2:
if c.nsRoot == 0 {
magic = vfsCapRevision2
break
}
magic = vfsCapRevision3
}
if magic == 0 {
return nil, ErrBadSize
}
eff := uint32(0)
for _, f := range c.flat {
eff |= (f[Permitted] | f[Inheritable]) & f[Effective]
}
if eff != 0 {
magic |= vfsCapFlagsEffective
}
b := new(bytes.Buffer)
binary.Write(b, binary.LittleEndian, magic)
for _, f := range c.flat {
binary.Write(b, binary.LittleEndian, f[Permitted])
binary.Write(b, binary.LittleEndian, f[Inheritable])
}
if c.nsRoot != 0 {
binary.Write(b, binary.LittleEndian, uint32(c.nsRoot))
}
return b.Bytes(), nil
}

134
internal/cap/flags.go Normal file
View File

@@ -0,0 +1,134 @@
package cap
import "errors"
// GetFlag determines if the requested Value is enabled in the
// specified Flag of the capability Set.
func (c *Set) GetFlag(vec Flag, val Value) (bool, error) {
if err := c.good(); err != nil {
// Checked this first, because otherwise we are sure
// cInit has been called.
return false, err
}
offset, mask, err := bitOf(vec, val)
if err != nil {
return false, err
}
c.mu.RLock()
defer c.mu.RUnlock()
return c.flat[offset][vec]&mask != 0, nil
}
// SetFlag sets the requested bits to the indicated enable state. This
// function does not perform any security checks, so values can be set
// out-of-order. Only when the Set is used to SetProc() etc., will the
// bits be checked for validity and permission by the kernel. If the
// function returns an error, the Set will not be modified.
func (c *Set) SetFlag(vec Flag, enable bool, val ...Value) error {
if err := c.good(); err != nil {
// Checked this first, because otherwise we are sure
// cInit has been called.
return err
}
c.mu.Lock()
defer c.mu.Unlock()
// Make a backup.
replace := make([]uint32, words)
for i := range replace {
replace[i] = c.flat[i][vec]
}
var err error
for _, v := range val {
offset, mask, err2 := bitOf(vec, v)
if err2 != nil {
err = err2
break
}
if enable {
c.flat[offset][vec] |= mask
} else {
c.flat[offset][vec] &= ^mask
}
}
if err == nil {
return nil
}
// Clean up.
for i, bits := range replace {
c.flat[i][vec] = bits
}
return err
}
// Clear fully clears a capability set.
func (c *Set) Clear() error {
if err := c.good(); err != nil {
return err
}
// startUp.Do(cInit) is not called here because c cannot be
// initialized except via this package and doing that will
// perform that call at least once (sic).
c.mu.Lock()
defer c.mu.Unlock()
c.flat = make([]data, words)
c.nsRoot = 0
return nil
}
// ErrBadValue indicates a bad capability value was specified.
var ErrBadValue = errors.New("bad capability value")
// bitOf converts from a Value into the offset and mask for a specific
// Value bit in the compressed (kernel ABI) representation of a
// capabilities. If the requested bit is unsupported, an error is
// returned.
func bitOf(vec Flag, val Value) (uint, uint32, error) {
if vec > Inheritable || val > Value(words*32) {
return 0, 0, ErrBadValue
}
u := uint(val)
return u / 32, uint32(1) << (u % 32), nil
}
// allMask returns the mask of valid bits in the all mask for index.
func allMask(index uint) (mask uint32) {
if maxValues == 0 {
panic("uninitialized package")
}
base := 32 * uint(index)
if maxValues <= base {
return
}
if maxValues >= 32+base {
mask = ^mask
return
}
mask = uint32((uint64(1) << (maxValues % 32)) - 1)
return
}
// forceFlag sets 'all' capability values (supported by the kernel) of
// a specified Flag to enable.
func (c *Set) forceFlag(vec Flag, enable bool) error {
if err := c.good(); err != nil {
return err
}
if vec > Inheritable {
return ErrBadSet
}
m := uint32(0)
if enable {
m = ^m
}
c.mu.Lock()
defer c.mu.Unlock()
for i := range c.flat {
c.flat[i][vec] = m & allMask(uint(i))
}
return nil
}
// ClearFlag clears all the Values associated with the specified Flag.
func (c *Set) ClearFlag(vec Flag) error {
return c.forceFlag(vec, false)
}

440
internal/cap/names.go Normal file
View File

@@ -0,0 +1,440 @@
package cap
/* ** DO NOT EDIT THIS FILE. IT WAS AUTO-GENERATED BY LIBCAP'S GO BUILDER (mknames.go) ** */
// NamedCount holds the number of capability values, with official
// names, known at the time this libcap/cap version was released. The
// "../libcap/cap" package is fully able to manipulate higher numbered
// capability values by numerical value. However, if you find
// cap.NamedCount < cap.MaxBits(), it is probably time to upgrade this
// package on your system.
//
// FWIW the userspace tool '/sbin/capsh' also contains a runtime check
// for the condition that libcap is behind the running kernel in this
// way.
const NamedCount = 41
// CHOWN etc., are the named capability values of the Linux
// kernel. The canonical source for each name is the
// "uapi/linux/capabilities.h" file. Some values may not be available
// (yet) where the kernel is older. The actual number of capabities
// supported by the running kernel can be obtained using the
// cap.MaxBits() function.
const (
// CHOWN allows a process to arbitrarily change the user and
// group ownership of a file.
CHOWN Value = iota
// DAC_OVERRIDE allows a process to override of all Discretionary
// Access Control (DAC) access, including ACL execute
// access. That is read, write or execute files that the
// process would otherwise not have access to. This
// excludes DAC access covered by cap.LINUX_IMMUTABLE.
DAC_OVERRIDE
// DAC_READ_SEARCH allows a process to override all DAC restrictions
// limiting the read and search of files and
// directories. This excludes DAC access covered by
// cap.LINUX_IMMUTABLE.
DAC_READ_SEARCH
// FOWNER allows a process to perform operations on files, even
// where file owner ID should otherwise need be equal to
// the UID, except where cap.FSETID is applicable. It
// doesn't override MAC and DAC restrictions.
//
// This capability permits the deletion of a file owned
// by another UID in a directory protected by the sticky
// (t) bit.
FOWNER
// FSETID allows a process to set the S_ISUID and S_ISUID bits of
// the file permissions, even when the process' effective
// UID or GID/supplementary GIDs do not match that of the
// file.
FSETID
// KILL allows a process to send a kill(2) signal to any other
// process - overriding the limitation that there be a
// [E]UID match between source and target process.
KILL
// SETGID allows a process to freely manipulate its own GIDs:
// - arbitrarily set the GID, EGID, REGID, RESGID values
// - arbitrarily set the supplementary GIDs
// - allows the forging of GID credentials passed over a
// socket
SETGID
// SETUID allows a process to freely manipulate its own UIDs:
// - arbitrarily set the UID, EUID, REUID and RESUID
// values
// - allows the forging of UID credentials passed over a
// socket
SETUID
// SETPCAP allows a process to freely manipulate its inheritable
// capabilities.
//
// Linux supports the POSIX.1e Inheritable set, the POXIX.1e (X
// vector) known in Linux as the Bounding vector, as well as
// the Linux extension Ambient vector.
//
// This capability permits dropping bits from the Bounding
// vector (ie. raising B bits in the libcap IAB
// representation). It also permits the process to raise
// Ambient vector bits that are both raised in the Permitted
// and Inheritable sets of the process. This capability cannot
// be used to raise Permitted bits, Effective bits beyond those
// already present in the process' permitted set, or
// Inheritable bits beyond those present in the Bounding
// vector.
//
// [Historical note: prior to the advent of file capabilities
// (2008), this capability was suppressed by default, as its
// unsuppressed behavior was not auditable: it could
// asynchronously grant its own Permitted capabilities to and
// remove capabilities from other processes arbitrarily. The
// former leads to undefined behavior, and the latter is better
// served by the kill system call.]
SETPCAP
// LINUX_IMMUTABLE allows a process to modify the S_IMMUTABLE and
// S_APPEND file attributes.
LINUX_IMMUTABLE
// NET_BIND_SERVICE allows a process to bind to privileged ports:
// - TCP/UDP sockets below 1024
// - ATM VCIs below 32
NET_BIND_SERVICE
// NET_BROADCAST allows a process to broadcast to the network and to
// listen to multicast.
NET_BROADCAST
// NET_ADMIN allows a process to perform network configuration
// operations:
// - interface configuration
// - administration of IP firewall, masquerading and
// accounting
// - setting debug options on sockets
// - modification of routing tables
// - setting arbitrary process, and process group
// ownership on sockets
// - binding to any address for transparent proxying
// (this is also allowed via cap.NET_RAW)
// - setting TOS (Type of service)
// - setting promiscuous mode
// - clearing driver statistics
// - multicasing
// - read/write of device-specific registers
// - activation of ATM control sockets
NET_ADMIN
// NET_RAW allows a process to use raw networking:
// - RAW sockets
// - PACKET sockets
// - binding to any address for transparent proxying
// (also permitted via cap.NET_ADMIN)
NET_RAW
// IPC_LOCK allows a process to lock shared memory segments for IPC
// purposes. Also enables mlock and mlockall system
// calls.
IPC_LOCK
// IPC_OWNER allows a process to override IPC ownership checks.
IPC_OWNER
// SYS_MODULE allows a process to initiate the loading and unloading
// of kernel modules. This capability can effectively
// modify kernel without limit.
SYS_MODULE
// SYS_RAWIO allows a process to perform raw IO:
// - permit ioper/iopl access
// - permit sending USB messages to any device via
// /dev/bus/usb
SYS_RAWIO
// SYS_CHROOT allows a process to perform a chroot syscall to change
// the effective root of the process' file system:
// redirect to directory "/" to some other location.
SYS_CHROOT
// SYS_PTRACE allows a process to perform a ptrace() of any other
// process.
SYS_PTRACE
// SYS_PACCT allows a process to configure process accounting.
SYS_PACCT
// SYS_ADMIN allows a process to perform a somewhat arbitrary
// grab-bag of privileged operations. Over time, this
// capability should weaken as specific capabilities are
// created for subsets of cap.SYS_ADMINs functionality:
// - configuration of the secure attention key
// - administration of the random device
// - examination and configuration of disk quotas
// - setting the domainname
// - setting the hostname
// - calling bdflush()
// - mount() and umount(), setting up new SMB connection
// - some autofs root ioctls
// - nfsservctl
// - VM86_REQUEST_IRQ
// - to read/write pci config on alpha
// - irix_prctl on mips (setstacksize)
// - flushing all cache on m68k (sys_cacheflush)
// - removing semaphores
// - Used instead of cap.CHOWN to "chown" IPC message
// queues, semaphores and shared memory
// - locking/unlocking of shared memory segment
// - turning swap on/off
// - forged pids on socket credentials passing
// - setting readahead and flushing buffers on block
// devices
// - setting geometry in floppy driver
// - turning DMA on/off in xd driver
// - administration of md devices (mostly the above, but
// some extra ioctls)
// - tuning the ide driver
// - access to the nvram device
// - administration of apm_bios, serial and bttv (TV)
// device
// - manufacturer commands in isdn CAPI support driver
// - reading non-standardized portions of PCI
// configuration space
// - DDI debug ioctl on sbpcd driver
// - setting up serial ports
// - sending raw qic-117 commands
// - enabling/disabling tagged queuing on SCSI
// controllers and sending arbitrary SCSI commands
// - setting encryption key on loopback filesystem
// - setting zone reclaim policy
SYS_ADMIN
// SYS_BOOT allows a process to initiate a reboot of the system.
SYS_BOOT
// SYS_NICE allows a process to maipulate the execution priorities
// of arbitrary processes:
// - those involving different UIDs
// - setting their CPU affinity
// - alter the FIFO vs. round-robin (realtime)
// scheduling for itself and other processes.
SYS_NICE
// SYS_RESOURCE allows a process to adjust resource related parameters
// of processes and the system:
// - set and override resource limits
// - override quota limits
// - override the reserved space on ext2 filesystem
// (this can also be achieved via cap.FSETID)
// - modify the data journaling mode on ext3 filesystem,
// which uses journaling resources
// - override size restrictions on IPC message queues
// - configure more than 64Hz interrupts from the
// real-time clock
// - override the maximum number of consoles for console
// allocation
// - override the maximum number of keymaps
SYS_RESOURCE
// SYS_TIME allows a process to perform time manipulation of clocks:
// - alter the system clock
// - enable irix_stime on MIPS
// - set the real-time clock
SYS_TIME
// SYS_TTY_CONFIG allows a process to manipulate tty devices:
// - configure tty devices
// - perform vhangup() of a tty
SYS_TTY_CONFIG
// MKNOD allows a process to perform privileged operations with
// the mknod() system call.
MKNOD
// LEASE allows a process to take leases on files.
LEASE
// AUDIT_WRITE allows a process to write to the audit log via a
// unicast netlink socket.
AUDIT_WRITE
// AUDIT_CONTROL allows a process to configure audit logging via a
// unicast netlink socket.
AUDIT_CONTROL
// SETFCAP allows a process to set capabilities on files.
// Permits a process to uid_map the uid=0 of the
// parent user namespace into that of the child
// namespace. Also, permits a process to override
// securebits locks through user namespace
// creation.
SETFCAP
// MAC_OVERRIDE allows a process to override Manditory Access Control
// (MAC) access. Not all kernels are configured with a MAC
// mechanism, but this is the capability reserved for
// overriding them.
MAC_OVERRIDE
// MAC_ADMIN allows a process to configure the Mandatory Access
// Control (MAC) policy. Not all kernels are configured
// with a MAC enabled, but if they are this capability is
// reserved for code to perform administration tasks.
MAC_ADMIN
// SYSLOG allows a process to configure the kernel's syslog
// (printk) behavior.
SYSLOG
// WAKE_ALARM allows a process to trigger something that can wake the
// system up.
WAKE_ALARM
// BLOCK_SUSPEND allows a process to block system suspends - prevent the
// system from entering a lower power state.
BLOCK_SUSPEND
// AUDIT_READ allows a process to read the audit log via a multicast
// netlink socket.
AUDIT_READ
// PERFMON allows a process to enable observability of privileged
// operations related to performance. The mechanisms
// include perf_events, i915_perf and other kernel
// subsystems.
PERFMON
// BPF allows a process to manipulate aspects of the kernel
// enhanced Berkeley Packet Filter (BPF) system. This is
// an execution subsystem of the kernel, that manages BPF
// programs. cap.BPF permits a process to:
// - create all types of BPF maps
// - advanced verifier features:
// - indirect variable access
// - bounded loops
// - BPF to BPF function calls
// - scalar precision tracking
// - larger complexity limits
// - dead code elimination
// - potentially other features
//
// Other capabilities can be used together with cap.BFP to
// further manipulate the BPF system:
// - cap.PERFMON relaxes the verifier checks as follows:
// - BPF programs can use pointer-to-integer
// conversions
// - speculation attack hardening measures can be
// bypassed
// - bpf_probe_read to read arbitrary kernel memory is
// permitted
// - bpf_trace_printk to print the content of kernel
// memory
// - cap.SYS_ADMIN permits the following:
// - use of bpf_probe_write_user
// - iteration over the system-wide loaded programs,
// maps, links BTFs and convert their IDs to file
// descriptors.
// - cap.PERFMON is required to load tracing programs.
// - cap.NET_ADMIN is required to load networking
// programs.
BPF
// CHECKPOINT_RESTORE allows a process to perform checkpoint
// and restore operations. Also permits
// explicit PID control via clone3() and
// also writing to ns_last_pid.
CHECKPOINT_RESTORE
)
var names = map[Value]string{
CHOWN: "cap_chown",
DAC_OVERRIDE: "cap_dac_override",
DAC_READ_SEARCH: "cap_dac_read_search",
FOWNER: "cap_fowner",
FSETID: "cap_fsetid",
KILL: "cap_kill",
SETGID: "cap_setgid",
SETUID: "cap_setuid",
SETPCAP: "cap_setpcap",
LINUX_IMMUTABLE: "cap_linux_immutable",
NET_BIND_SERVICE: "cap_net_bind_service",
NET_BROADCAST: "cap_net_broadcast",
NET_ADMIN: "cap_net_admin",
NET_RAW: "cap_net_raw",
IPC_LOCK: "cap_ipc_lock",
IPC_OWNER: "cap_ipc_owner",
SYS_MODULE: "cap_sys_module",
SYS_RAWIO: "cap_sys_rawio",
SYS_CHROOT: "cap_sys_chroot",
SYS_PTRACE: "cap_sys_ptrace",
SYS_PACCT: "cap_sys_pacct",
SYS_ADMIN: "cap_sys_admin",
SYS_BOOT: "cap_sys_boot",
SYS_NICE: "cap_sys_nice",
SYS_RESOURCE: "cap_sys_resource",
SYS_TIME: "cap_sys_time",
SYS_TTY_CONFIG: "cap_sys_tty_config",
MKNOD: "cap_mknod",
LEASE: "cap_lease",
AUDIT_WRITE: "cap_audit_write",
AUDIT_CONTROL: "cap_audit_control",
SETFCAP: "cap_setfcap",
MAC_OVERRIDE: "cap_mac_override",
MAC_ADMIN: "cap_mac_admin",
SYSLOG: "cap_syslog",
WAKE_ALARM: "cap_wake_alarm",
BLOCK_SUSPEND: "cap_block_suspend",
AUDIT_READ: "cap_audit_read",
PERFMON: "cap_perfmon",
BPF: "cap_bpf",
CHECKPOINT_RESTORE: "cap_checkpoint_restore",
}
var bits = map[string]Value{
"cap_chown": CHOWN,
"cap_dac_override": DAC_OVERRIDE,
"cap_dac_read_search": DAC_READ_SEARCH,
"cap_fowner": FOWNER,
"cap_fsetid": FSETID,
"cap_kill": KILL,
"cap_setgid": SETGID,
"cap_setuid": SETUID,
"cap_setpcap": SETPCAP,
"cap_linux_immutable": LINUX_IMMUTABLE,
"cap_net_bind_service": NET_BIND_SERVICE,
"cap_net_broadcast": NET_BROADCAST,
"cap_net_admin": NET_ADMIN,
"cap_net_raw": NET_RAW,
"cap_ipc_lock": IPC_LOCK,
"cap_ipc_owner": IPC_OWNER,
"cap_sys_module": SYS_MODULE,
"cap_sys_rawio": SYS_RAWIO,
"cap_sys_chroot": SYS_CHROOT,
"cap_sys_ptrace": SYS_PTRACE,
"cap_sys_pacct": SYS_PACCT,
"cap_sys_admin": SYS_ADMIN,
"cap_sys_boot": SYS_BOOT,
"cap_sys_nice": SYS_NICE,
"cap_sys_resource": SYS_RESOURCE,
"cap_sys_time": SYS_TIME,
"cap_sys_tty_config": SYS_TTY_CONFIG,
"cap_mknod": MKNOD,
"cap_lease": LEASE,
"cap_audit_write": AUDIT_WRITE,
"cap_audit_control": AUDIT_CONTROL,
"cap_setfcap": SETFCAP,
"cap_mac_override": MAC_OVERRIDE,
"cap_mac_admin": MAC_ADMIN,
"cap_syslog": SYSLOG,
"cap_wake_alarm": WAKE_ALARM,
"cap_block_suspend": BLOCK_SUSPEND,
"cap_audit_read": AUDIT_READ,
"cap_perfmon": PERFMON,
"cap_bpf": BPF,
"cap_checkpoint_restore": CHECKPOINT_RESTORE,
}

328
internal/cap/text.go Normal file
View File

@@ -0,0 +1,328 @@
package cap
import (
"bufio"
"errors"
"fmt"
"strconv"
"strings"
)
// String converts a capability Value into its canonical text
// representation.
func (v Value) String() string {
name, ok := names[v]
if ok {
return name
}
// Un-named capabilities are referred to numerically (in decimal).
return strconv.Itoa(int(v))
}
// FromName converts a named capability Value to its binary
// representation.
func FromName(name string) (Value, error) {
startUp.Do(cInit)
v, ok := bits[name]
if ok {
if v >= Value(words*32) {
return 0, ErrBadValue
}
return v, nil
}
i, err := strconv.Atoi(name)
if err != nil {
return 0, err
}
if i >= 0 && i < int(words*32) {
return Value(i), nil
}
return 0, ErrBadValue
}
const (
eBin uint = (1 << Effective)
pBin = (1 << Permitted)
iBin = (1 << Inheritable)
)
var combos = []string{"", "e", "p", "ep", "i", "ei", "ip", "eip"}
// histo generates a histogram of flag state combinations.
// Note: c is locked by or private to the caller.
func (c *Set) histo(bins []int, patterns []uint, from, limit Value) uint {
for v := from; v < limit; v++ {
b := uint(v & 31)
u, bit, err := bitOf(0, v)
if err != nil {
break
}
x := uint((c.flat[u][Effective]&bit)>>b) * eBin
x |= uint((c.flat[u][Permitted]&bit)>>b) * pBin
x |= uint((c.flat[u][Inheritable]&bit)>>b) * iBin
bins[x]++
patterns[uint(v)] = x
}
// Note, in the loop, we use >= to pick the smallest value for
// m with the highest bin value. That is ties break towards
// m=0.
m := uint(7)
for t := m; t > 0; {
t--
if bins[t] >= bins[m] {
m = t
}
}
return m
}
// String converts a full capability Set into a single short readable
// string representation (which may contain spaces). See the
// cap.FromText() function for an explanation of its return values.
//
// Note (*cap.Set).String() may evolve to generate more compact
// strings representing the a given Set over time, but it should
// maintain compatibility with the libcap:cap_to_text() function for
// any given release. Further, it will always be an inverse of
// cap.FromText().
func (c *Set) String() string {
if err := c.good(); err != nil {
return "<invalid>"
}
bins := make([]int, 8)
patterns := make([]uint, maxValues)
c.mu.RLock()
defer c.mu.RUnlock()
// Note, in order to have a *Set pointer, startUp.Do(cInit)
// must have been called which sets maxValues.
m := c.histo(bins, patterns, 0, Value(maxValues))
// Background state is the most popular of the named bits.
vs := []string{"=" + combos[m]}
for i := uint(8); i > 0; {
i--
if i == m || bins[i] == 0 {
continue
}
var list []string
for j, p := range patterns {
if p != i {
continue
}
list = append(list, Value(j).String())
}
x := strings.Join(list, ",")
var y, z string
if cf := i & ^m; cf != 0 {
op := "+"
if len(vs) == 1 && vs[0] == "=" {
// Special case "= foo+..." == "foo=...".
// Prefer because it
vs = nil
op = "="
}
y = op + combos[cf]
}
if cf := m & ^i; cf != 0 {
z = "-" + combos[cf]
}
vs = append(vs, x+y+z)
}
// The unnamed bits can only add to the above named ones since
// unnamed ones are always defaulted to lowered.
uBins := make([]int, 8)
uPatterns := make([]uint, 32*words)
c.histo(uBins, uPatterns, Value(maxValues), 32*Value(words))
for i := uint(7); i > 0; i-- {
if uBins[i] == 0 {
continue
}
var list []string
for j, p := range uPatterns {
if p != i {
continue
}
list = append(list, Value(j).String())
}
vs = append(vs, strings.Join(list, ",")+"+"+combos[i])
}
return strings.Join(vs, " ")
}
// ErrBadText is returned if the text for a capability set cannot be parsed.
var ErrBadText = errors.New("bad text")
// FromText converts the canonical text representation for a Set into
// a freshly allocated Set.
//
// The format follows the following pattern: a set of space separated
// sequences. Each sequence applies over the previous sequence to
// build up a Set. The format of a sequence is:
//
// [comma list of cap_values][[ops][flags]]*
//
// Examples:
//
// "all=ep"
// "cap_chown,cap_setuid=ip cap_setuid+e"
// "=p cap_setpcap-p+i"
//
// Here "all" refers to all named capabilities known to the hosting
// kernel, and "all" is assumed if no capabilities are listed before
// an "=".
//
// The ops values, "=", "+" and "-" imply "reset and raise", "raise"
// and "lower" respectively. The "e", "i" and "p" characters
// correspond to the capabilities of the corresponding Flag: "e"
// (Effective); "i" (Inheritable); "p" (Permitted).
//
// This syntax is overspecified and there are many ways of building
// the same final Set state. Any sequence that includes a '=' resets
// the accumulated state of all Flags ignoring earlier sequences. On
// each of the following lines we give three or more examples of ways
// to specify a common Set. The last entry on each line is the one
// generated by (*cap.Set).String() from that Set.
//
// "=p all+ei" "all=pie" "=pi all+e" "=eip"
//
// "cap_setuid=p cap_chown=i" "cap_chown=ip-p" "cap_chown=i"
//
// "cap_chown=-p" "all=" "cap_setuid=pie-pie" "="
//
// Note: FromText() is tested at release time to completely match the
// import ability of the libcap:cap_from_text() function.
func FromText(text string) (*Set, error) {
text = strings.ToLower(text)
c := NewSet()
scanner := bufio.NewScanner(strings.NewReader(text))
scanner.Split(bufio.ScanWords)
chunks := 0
for scanner.Scan() {
chunks++
// Parsing for xxx([-+=][eip]+)+
t := scanner.Text()
i := strings.IndexAny(t, "=+-")
if i < 0 {
return nil, fmt.Errorf("%w: needs one of [=+-]", ErrBadText)
}
var vs []Value
sep := t[i]
if vals := t[:i]; vals == "all" {
for v := Value(0); v < Value(maxValues); v++ {
vs = append(vs, v)
}
} else if vals != "" {
for name := range strings.SplitSeq(vals, ",") {
v, err := FromName(name)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrBadText, err)
}
vs = append(vs, v)
}
} else if sep != '=' {
if vals == "" {
// Only "=" supports ""=="all".
return nil, ErrBadText
}
} else if j := i + 1; j+1 < len(t) {
switch t[j] {
case '+':
sep = 'P'
i++
case '-':
sep = 'M'
i++
}
}
i++
// There are 5 ways to set: =, =+, =-, +, -. We call
// the 2nd and 3rd of these 'P' and 'M'.
for {
// read [eip]+ setting flags.
var fE, fP, fI bool
for ok := true; ok && i < len(t); i++ {
switch t[i] {
case 'e':
fE = true
case 'i':
fI = true
case 'p':
fP = true
default:
ok = false
}
if !ok {
break
}
}
if !(fE || fI || fP) {
if sep != '=' {
return nil, fmt.Errorf("%w: needs one of [eip]", ErrBadText)
}
}
switch sep {
case '=', 'P', 'M', '+':
if sep != '+' {
c.Clear()
if sep == 'M' {
break
}
}
if keep := len(vs) == 0; keep {
if sep != '=' {
return nil, fmt.Errorf("%w: expected an '='", ErrBadText)
}
c.forceFlag(Effective, fE)
c.forceFlag(Permitted, fP)
c.forceFlag(Inheritable, fI)
break
}
// =, + and P for specific values are left.
if fE {
c.SetFlag(Effective, true, vs...)
}
if fP {
c.SetFlag(Permitted, true, vs...)
}
if fI {
c.SetFlag(Inheritable, true, vs...)
}
case '-':
if fE {
c.SetFlag(Effective, false, vs...)
}
if fP {
c.SetFlag(Permitted, false, vs...)
}
if fI {
c.SetFlag(Inheritable, false, vs...)
}
}
if i == len(t) {
break
}
switch t[i] {
case '+', '-':
sep = t[i]
i++
default:
return nil, fmt.Errorf("%w: expected '+' or '-'", ErrBadText)
}
}
}
if chunks == 0 {
return nil, fmt.Errorf("%w: No capabilities found", ErrBadText)
}
return c, nil
}

129
internal/eeprom/eeprom.go Normal file
View File

@@ -0,0 +1,129 @@
// Package eeprom implements the Raspberry Pi EEPROM update file format.
package eeprom
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
)
// Other implementations:
//
// - https://github.com/raspberrypi/rpi-eeprom (Python)
// - https://github.com/info-beamer/rpi-eeprom-tools (Python)
const (
MAGIC = 0x55aaf00f
MAGIC_MASK = 0xfffff00f
FILE_MAGIC = 0x55aaf11f // id for modifiable files
FILENAME_LEN = 12
FILENAME_PADDING = 4
chunkHeaderLen = 4 + 4 // magic number + 32 bit offset
)
type Section struct {
img []byte
Magic uint32
Offset int
Length int
Filename string
}
func FileSection(offset int, name string, contents []byte) *Section {
if len(name) > FILENAME_LEN {
panic(fmt.Sprintf("BUG: file name %s exceeds max FILENAME_LEN = %d", name, FILENAME_LEN))
}
img := bytes.Repeat([]byte{0}, offset+chunkHeaderLen)
img = append(img, []byte(name)...)
img = append(img, bytes.Repeat([]byte{0}, FILENAME_PADDING+FILENAME_LEN-len(name))...)
img = append(img, contents...)
return &Section{
img: img,
Magic: FILE_MAGIC,
Offset: offset,
Length: len(contents) + FILENAME_LEN + FILENAME_PADDING,
Filename: name,
}
}
func (s *Section) WithoutImg() Section {
without := *s
without.img = nil
return without
}
func (s *Section) RawContent() []byte {
offset := s.Offset + chunkHeaderLen
length := s.Length
return s.img[offset : offset+length]
}
func (s *Section) FileContent() []byte {
offset := s.Offset + chunkHeaderLen
length := s.Length
if s.Magic == FILE_MAGIC {
const fileSkip = FILENAME_LEN + FILENAME_PADDING
offset += fileSkip
length -= fileSkip
}
return s.img[offset : offset+length]
}
func Analyze(img []byte) ([]*Section, error) {
// See https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L267
if len(img) != 512*1024 &&
len(img) != 2*1024*1024 {
return nil, fmt.Errorf("unexpected EEPROM size: got %d, want 512KB or 2MB", len(img))
}
var sections []*Section
for offset := 0; offset+chunkHeaderLen < len(img); {
magic := binary.BigEndian.Uint32(img[offset : offset+4])
length := binary.BigEndian.Uint32(img[offset+4 : offset+8])
if magic == 0 || magic == 0xffffffff {
break // end of file
}
if magic&MAGIC_MASK != MAGIC {
return nil, fmt.Errorf("EEPROM is corrupted: %x & %x != %x", magic, MAGIC_MASK, MAGIC)
}
sect := Section{
img: img,
Magic: magic,
Offset: offset,
Length: int(length),
}
if magic == FILE_MAGIC {
sect.Filename = string(img[offset+8 : offset+8+FILENAME_LEN])
sect.Filename = strings.ReplaceAll(sect.Filename, "\x00", "")
}
sections = append(sections, &sect)
offset += chunkHeaderLen + int(length)
offset = (offset + 7) &^ 7
}
if len(sections) == 0 {
return nil, fmt.Errorf("invalid EEPROM: no sections found")
}
if sections[len(sections)-1].Filename != "bootconf.txt" {
// “by convention bootconf.txt is the last section”, from:
// https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L373
return nil, fmt.Errorf("invalid EEPROM: bootconf.txt not the last section")
}
return sections, nil
}
func Assemble(sections []*Section) []byte {
output := bytes.Repeat([]byte{0xff}, len(sections[0].img))
offset := 0
for _, sect := range sections {
binary.BigEndian.PutUint32(output[offset:], sect.Magic)
binary.BigEndian.PutUint32(output[offset+4:], uint32(sect.Length))
copy(output[offset+8:], sect.RawContent())
offset += chunkHeaderLen + sect.Length
offset = (offset + 7) &^ 7
}
return output
}

View File

@@ -0,0 +1,110 @@
package eeprom_test
import (
"compress/gzip"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/gokrazy/tools/internal/eeprom"
)
func testAgainst(t *testing.T, verify func(eepromPath, needle string), pieeprom, bootUartNeedle string) {
verify(pieeprom, bootUartNeedle)
// Read + Write the EEPROM with our own code,
// verify that bootconf.txt can still be displayed.
img, err := os.ReadFile(pieeprom)
if err != nil {
t.Fatal(err)
}
sections, err := eeprom.Analyze(img)
if err != nil {
t.Fatal(err)
}
reassembleAndVerify := func(needle string) {
reassembled := filepath.Join(t.TempDir(), "reassembled.bin")
if err := os.WriteFile(reassembled, eeprom.Assemble(sections), 0644); err != nil {
t.Fatal(err)
}
verify(reassembled, needle)
}
reassembleAndVerify(bootUartNeedle)
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
// Re-create the section with the existing contents.
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, bc.FileContent())
reassembleAndVerify(bootUartNeedle)
// Change the bootconf.txt contents.
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, []byte("hello world"))
reassembleAndVerify("hello world")
}
func TestAgainst(t *testing.T) {
const fn = "testdata/pieeprom-2025-10-17.bin"
f, err := os.Open(fn + ".gz")
if err != nil {
t.Fatal(err)
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
t.Fatal(err)
}
bin, err := io.ReadAll(gz)
if err != nil {
t.Fatal(err)
}
pieeprom := filepath.Join(t.TempDir(), filepath.Base(fn))
if err := os.WriteFile(pieeprom, bin, 0644); err != nil {
t.Fatal(err)
}
t.Run("Infobeamer", func(t *testing.T) {
lsBin, err := exec.LookPath("pi-eeprom-ls")
if err != nil {
t.Skipf("pi-eeprom-ls not available (%v)", err)
}
verify := func(eepromPath, needle string) {
ls := exec.Command(lsBin, eepromPath)
ls.Stderr = os.Stderr
lsOut, err := ls.Output()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(lsOut), needle) {
t.Errorf("infobeamer pi-eeprom-ls output did not contain %q: \n%s", needle, string(lsOut))
}
}
const bootUartNeedle = "-[ bootconf.txt ]------------------\n[all]\nBOOT_UART=1"
testAgainst(t, verify, pieeprom, bootUartNeedle)
})
t.Run("Pi", func(t *testing.T) {
configBin, err := exec.LookPath("rpi-eeprom-config")
if err != nil {
t.Skipf("rpi-eeprom-config not available (%v)", err)
}
verify := func(eepromPath, needle string) {
extract := exec.Command(configBin, eepromPath)
extract.Dir = t.TempDir()
extract.Stderr = os.Stderr
extractOut, err := extract.Output()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(extractOut), needle) {
t.Errorf("rpi-eeprom-config output did not contain %q: \n%s", needle, string(extractOut))
}
}
const bootUartNeedle = "[all]\nBOOT_UART=1"
testAgainst(t, verify, pieeprom, bootUartNeedle)
})
}

Binary file not shown.

View File

@@ -18,12 +18,13 @@ import (
)
// addCmd is gok add.
var addCmd = &cobra.Command{
GroupID: "edit",
Use: "add [flags] importpath[@version]",
DisableFlagsInUseLine: true,
Short: "Add a Go package to a gokrazy instance",
Long: `Add a Go package to a gokrazy instance.
func addCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "edit",
Use: "add [flags] importpath[@version]",
DisableFlagsInUseLine: true,
Short: "Add a Go package to a gokrazy instance",
Long: `Add a Go package to a gokrazy instance.
This command creates the required build directory, runs go get, and adds
the specified package to the gokrazy instance configuration (Packages field).
@@ -42,26 +43,25 @@ Examples:
% gok -i scan2drive add /home/michael/projects/scanui/cmd/scanui
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() != 1 {
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() != 1 {
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
`)
return cmd.Usage()
}
return cmd.Usage()
}
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
},
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type addImplConfig struct{}
var addImpl addImplConfig
func init() {
instanceflag.RegisterPflags(addCmd.Flags())
}
type packageInfo struct {
// Dir is the directory on the local disk containing the package sources,
// e.g. /home/michael/projects/stapelberg/localmod/cmd/sup.

View File

@@ -13,32 +13,32 @@ import (
)
// editCmd is gok edit.
var editCmd = &cobra.Command{
GroupID: "edit",
Use: "edit",
Short: "Edit a gokrazy instance configuration interactively",
Long: `Edit a gokrazy instance configuration interactively.
func editCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "edit",
Use: "edit",
Short: "Edit a gokrazy instance configuration interactively",
Long: `Edit a gokrazy instance configuration interactively.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
`)
return cmd.Usage()
}
return cmd.Usage()
}
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type editImplConfig struct{}
var editImpl editImplConfig
func init() {
instanceflag.RegisterPflags(editCmd.Flags())
}
func (r *editImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
parentDir := instanceflag.ParentDir()
instance := instanceflag.Instance()

View File

@@ -11,17 +11,17 @@ import (
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/instanceflag"
"github.com/gokrazy/internal/updateflag"
"github.com/gokrazy/tools/packer"
"github.com/spf13/cobra"
)
// getCmd is gok get.
var getCmd = &cobra.Command{
GroupID: "edit",
Use: "get",
Short: "Update the version of your Go program(s) using `go get`",
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
func getCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "edit",
Use: "get",
Short: "Update the version of your Go program(s) using `go get`",
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
in your gokrazy instance.
Examples:
@@ -34,9 +34,13 @@ Examples:
# Update only gokrazy system packages
% gok -i scanner get gokrazy
`,
RunE: func(cmd *cobra.Command, args []string) error {
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type getImplConfig struct {
@@ -45,11 +49,6 @@ type getImplConfig struct {
var getImpl getImplConfig
func init() {
getCmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
instanceflag.RegisterPflags(getCmd.Flags())
}
func getGokrazySystemPackages(cfg *config.Struct) []string {
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
@@ -78,8 +77,6 @@ func (r *getImplConfig) run(ctx context.Context, args []string, stdout, stderr i
return err
}
updateflag.SetUpdate("yes")
packages := args
if r.updateAll {
if len(packages) > 0 {

View File

@@ -20,15 +20,20 @@ import (
)
// logsCmd is gok logs.
var logsCmd = &cobra.Command{
GroupID: "runtime",
Use: "logs",
Short: "Stream logs from a running gokrazy service",
Long: `Display the most recent 100 log lines from stdout and stderr each,
func logsCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "runtime",
Use: "logs",
Short: "Stream logs from a running gokrazy service",
Long: `Display the most recent 100 log lines from stdout and stderr each,
and any new lines the gokrazy service produces (cancel any time with Ctrl-C)`,
RunE: func(cmd *cobra.Command, args []string) error {
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type logsImplConfig struct {
@@ -37,11 +42,6 @@ type logsImplConfig struct {
var logsImpl logsImplConfig
func init() {
logsCmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
instanceflag.RegisterPflags(logsCmd.Flags())
}
func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
cfg, err := config.ApplyInstanceFlag()
if err != nil {
@@ -53,13 +53,11 @@ func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr
}
}
updateflag.SetUpdate("yes")
if l.service == "" {
return fmt.Errorf("the -service flag is empty, but required")
}
httpClient, _, logsUrl, err := httpclient.For(cfg)
httpClient, _, logsUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
if err != nil {
return err
}

View File

@@ -16,25 +16,30 @@ import (
)
// newCmd is gok new.
var newCmd = &cobra.Command{
GroupID: "edit",
Use: "new",
Short: "Create a new gokrazy instance",
Long: `Create a new gokrazy instance.
func newCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "edit",
Use: "new",
Short: "Create a new gokrazy instance",
Long: `Create a new gokrazy instance.
If you are unfamiliar with gokrazy, please follow:
https://gokrazy.org/quickstart/
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
`)
return cmd.Usage()
}
return cmd.Usage()
}
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
cmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
return cmd
}
type newImplConfig struct {
@@ -43,11 +48,6 @@ type newImplConfig struct {
var newImpl newImplConfig
func init() {
instanceflag.RegisterPflags(newCmd.Flags())
newCmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
}
func (r *newImplConfig) createBreakglassAuthorizedKeys(authorizedPath string, matches []string) error {
f, err := os.OpenFile(authorizedPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {

View File

@@ -15,11 +15,12 @@ import (
)
// overwriteCmd is gok overwrite.
var overwriteCmd = &cobra.Command{
GroupID: "deploy",
Use: "overwrite",
Short: "Build and deploy a gokrazy instance to a storage device",
Long: `Build and deploy a gokrazy instance to a storage device.
func overwriteCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "deploy",
Use: "overwrite",
Short: "Build and deploy a gokrazy instance to a storage device",
Long: `Build and deploy a gokrazy instance to a storage device.
You typically need to use the gok overwrite command only once,
when first deploying your gokrazy instance. Afterwards, you can
@@ -29,16 +30,27 @@ Examples:
# Overwrite the contents of the SD card sdx with gokrazy instance scan2drive:
% gok -i scan2drive overwrite --full=/dev/sdx
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
`)
return cmd.Usage()
}
return cmd.Usage()
}
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
cmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
cmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
cmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
cmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
cmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
cmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
cmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
cmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
return cmd
}
type overwriteImplConfig struct {
@@ -56,18 +68,6 @@ type overwriteImplConfig struct {
var overwriteImpl overwriteImplConfig
func init() {
instanceflag.RegisterPflags(overwriteCmd.Flags())
overwriteCmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
overwriteCmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
overwriteCmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
overwriteCmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
overwriteCmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
overwriteCmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
overwriteCmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
overwriteCmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
}
func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
if r.traceFile != "" {
out, err := os.Create(r.traceFile)
@@ -151,7 +151,7 @@ func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, st
Output: &output,
}
pack.Main(ctx, "gokrazy gok")
pack.Main(ctx)
return nil
}

View File

@@ -14,18 +14,22 @@ import (
)
// psCmd is gok ps.
var psCmd = &cobra.Command{
GroupID: "runtime",
Use: "ps",
Short: "list processes of a running gokrazy instance",
Long: `gok ps
func psCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "runtime",
Use: "ps",
Short: "list processes of a running gokrazy instance",
Long: `gok ps
Examples:
% gok -i scan2drive ps
`,
RunE: func(cmd *cobra.Command, args []string) error {
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type psImplConfig struct {
@@ -33,10 +37,6 @@ type psImplConfig struct {
var psImpl psImplConfig
func init() {
instanceflag.RegisterPflags(psCmd.Flags())
}
func (r *psImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
cfg, err := config.ApplyInstanceFlag()
if err != nil {

View File

@@ -15,11 +15,12 @@ import (
)
// pushCmd is gok push.
var pushCmd = &cobra.Command{
GroupID: "server",
Use: "push",
Short: "Push a gokrazy image to a remote GUS server",
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
func pushCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "server",
Use: "push",
Short: "Push a gokrazy image to a remote GUS server",
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
When the --json flag is specified, the server response is printed to stdout.
@@ -28,9 +29,15 @@ Examples:
% gok push --gaf /tmp/gokrazy.gaf --server https://gus.gokrazy.org
`,
RunE: func(cmd *cobra.Command, args []string) error {
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
cmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
cmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type pushConfig struct {
@@ -41,13 +48,6 @@ type pushConfig struct {
var pushImpl pushConfig
func init() {
pushCmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
pushCmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
pushCmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
instanceflag.RegisterPflags(pushCmd.Flags())
}
func (r *pushConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
// TODO: use an io.Reader that allows us to indicate progress
body, err := os.Open(r.gafPath)

View File

@@ -9,10 +9,11 @@ import (
"github.com/spf13/pflag"
)
var RootCmd = &cobra.Command{
Use: "gok",
Short: "top-level CLI entry point for all things gokrazy",
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
func RootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "gok",
Short: "top-level CLI entry point for all things gokrazy",
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
1. Create new gokrazy instances (gok new),
2. Deploy gokrazy instances to storage devices like SD cards (gok overwrite),
@@ -22,59 +23,58 @@ var RootCmd = &cobra.Command{
If you are unfamiliar with gokrazy, please follow:
https://gokrazy.org/quickstart/
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
versionVal, err := cmd.Flags().GetBool("version")
if err != nil {
return fmt.Errorf("BUG: version flag declared as non-bool")
}
if versionVal {
fmt.Println(version.Read())
return nil
}
return pflag.ErrHelp
},
}
func init() {
RootCmd.AddGroup(&cobra.Group{
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
versionVal, err := cmd.Flags().GetBool("version")
if err != nil {
return fmt.Errorf("BUG: version flag declared as non-bool")
}
if versionVal {
fmt.Println(version.Read())
return nil
}
return pflag.ErrHelp
},
}
rootCmd.AddGroup(&cobra.Group{
ID: "edit",
Title: "Commands to create and edit a gokrazy instance:",
})
RootCmd.AddGroup(&cobra.Group{
rootCmd.AddGroup(&cobra.Group{
ID: "deploy",
Title: "Commands to deploy and update a gokrazy instance:",
})
RootCmd.AddGroup(&cobra.Group{
rootCmd.AddGroup(&cobra.Group{
ID: "runtime",
Title: "Commands to work with a running gokrazy instance:",
})
RootCmd.AddGroup(&cobra.Group{
rootCmd.AddGroup(&cobra.Group{
ID: "server",
Title: "Commands to work with a remote GUS server:",
})
RootCmd.AddGroup(&cobra.Group{
rootCmd.AddGroup(&cobra.Group{
ID: "vm",
Title: "Commands to work with Virtual Machines (VMs):",
})
RootCmd.Flags().Bool("version", false, "print gok version")
rootCmd.Flags().Bool("version", false, "print gok version")
// Only defined so that it appears in documentation like --help.
//
// Cobra only parses local flags on the target command, but they can appear
// at any place in the command line (before or after the verb).
instanceflag.RegisterPflags(RootCmd.Flags())
RootCmd.AddCommand(runCmd)
RootCmd.AddCommand(logsCmd)
RootCmd.AddCommand(updateCmd)
RootCmd.AddCommand(overwriteCmd)
RootCmd.AddCommand(versionCmd)
RootCmd.AddCommand(newCmd)
RootCmd.AddCommand(editCmd)
RootCmd.AddCommand(addCmd)
RootCmd.AddCommand(getCmd)
RootCmd.AddCommand(sbomCmd)
RootCmd.AddCommand(pushCmd)
RootCmd.AddCommand(vmCmd)
RootCmd.AddCommand(psCmd)
instanceflag.RegisterPflags(rootCmd.Flags())
rootCmd.AddCommand(runCmd())
rootCmd.AddCommand(logsCmd())
rootCmd.AddCommand(updateCmd())
rootCmd.AddCommand(overwriteCmd())
rootCmd.AddCommand(versionCmd())
rootCmd.AddCommand(newCmd())
rootCmd.AddCommand(editCmd())
rootCmd.AddCommand(addCmd())
rootCmd.AddCommand(getCmd())
rootCmd.AddCommand(sbomCmd())
rootCmd.AddCommand(pushCmd())
rootCmd.AddCommand(vmCmd())
rootCmd.AddCommand(psCmd())
return rootCmd
}

View File

@@ -22,11 +22,12 @@ import (
)
// runCmd is gok run.
var runCmd = &cobra.Command{
GroupID: "runtime",
Use: "run",
Short: "`go install` and run on a running gokrazy instance",
Long: "gok run uses `go install` to build the Go program in the current directory," + `
func runCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "runtime",
Use: "run",
Short: "`go install` and run on a running gokrazy instance",
Long: "gok run uses `go install` to build the Go program in the current directory," + `
then it stores the program in RAM of a running gokrazy instance and runs the program.
This enables a quick feedback loop when working on a program that is running on gokrazy,
@@ -42,9 +43,13 @@ Examples:
# specify extra flags on the command line
% gok -i scan2drive run -- -tls_autocert_hosts=scan.example.com
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type runImplConfig struct {
@@ -53,11 +58,6 @@ type runImplConfig struct {
var runImpl runImplConfig
func init() {
runCmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
instanceflag.RegisterPflags(runCmd.Flags())
}
func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
cfg, err := config.ApplyInstanceFlag()
if err != nil {
@@ -69,8 +69,6 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
}
}
updateflag.SetUpdate("yes")
var tmp string
if r.keep {
tmp = os.TempDir()
@@ -122,7 +120,7 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
return err
}
httpClient, _, updateBaseUrl, err := httpclient.For(cfg)
httpClient, _, updateBaseUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
if err != nil {
return err
}

View File

@@ -10,17 +10,17 @@ import (
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/instanceflag"
"github.com/gokrazy/internal/updateflag"
"github.com/gokrazy/tools/internal/packer"
"github.com/spf13/cobra"
)
// sbomCmd is gok sbom.
var sbomCmd = &cobra.Command{
GroupID: "deploy",
Use: "sbom",
Short: "Print the Software Bill Of Materials of a gokrazy instance",
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
func sbomCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "deploy",
Use: "sbom",
Short: "Print the Software Bill Of Materials of a gokrazy instance",
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
Examples:
# print the hash and SBOM contents in JSON format
@@ -30,9 +30,13 @@ Examples:
% gok -i scanner sbom --format hash
`,
RunE: func(cmd *cobra.Command, args []string) error {
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
RunE: func(cmd *cobra.Command, args []string) error {
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type sbomConfig struct {
@@ -41,11 +45,6 @@ type sbomConfig struct {
var sbomImpl sbomConfig
func init() {
sbomCmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
instanceflag.RegisterPflags(sbomCmd.Flags())
}
func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
fileCfg, err := config.ApplyInstanceFlag()
if err != nil {
@@ -66,8 +65,6 @@ func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.W
return err
}
updateflag.SetUpdate("yes")
var buf bytes.Buffer
pack := &packer.Pack{
// Send all build output to stderr so that stdout

View File

@@ -13,22 +13,28 @@ import (
)
// updateCmd is gok update.
var updateCmd = &cobra.Command{
GroupID: "deploy",
Use: "update",
Short: "Build a gokrazy instance and update over the network",
Long: `Build a gokrazy instance and update over the network.
func updateCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "deploy",
Use: "update",
Short: "Build a gokrazy instance and update over the network",
Long: `Build a gokrazy instance and update over the network.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
`)
return cmd.Usage()
}
return cmd.Usage()
}
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
cmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
cmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
return cmd
}
type updateImplConfig struct {
@@ -38,12 +44,6 @@ type updateImplConfig struct {
var updateImpl updateImplConfig
func init() {
instanceflag.RegisterPflags(updateCmd.Flags())
updateCmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
updateCmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
}
func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
fileCfg, err := config.ApplyInstanceFlag()
if err != nil {
@@ -90,7 +90,7 @@ func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stder
Cfg: cfg,
}
pack.Main(ctx, "gokrazy gok")
pack.Main(ctx)
return nil
}

View File

@@ -10,13 +10,16 @@ import (
)
// versionCmd is gok version.
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print gok version",
Long: `Print gok version`,
RunE: func(cmd *cobra.Command, args []string) error {
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
func versionCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print gok version",
Long: `Print gok version`,
RunE: func(cmd *cobra.Command, args []string) error {
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
return cmd
}
type versionImplConfig struct{}

View File

@@ -5,11 +5,15 @@ import (
)
// vmCmd is the gok vm subcommand, which (only) has nested commands like run.
var vmCmd = &cobra.Command{
GroupID: "vm",
Use: "vm",
Short: "virtual machine",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Usage()
},
func vmCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "vm",
Use: "vm",
Short: "virtual machine",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Usage()
},
}
cmd.AddCommand(vmRunCmd())
return cmd
}

View File

@@ -18,10 +18,11 @@ import (
"github.com/spf13/cobra"
)
var vmRunCmd = &cobra.Command{
Use: "run",
Short: "run a virtual machine (using QEMU)",
Long: `gok run builds a gokrazy instance and runs it using QEMU.
func vmRunCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "run a virtual machine (using QEMU)",
Long: `gok run builds a gokrazy instance and runs it using QEMU.
Extra arguments are passed to QEMU as-is.
@@ -35,13 +36,20 @@ Examples:
# Directly specify QEMU USB flags
% gok vm run -- -usb -device usb-mouse
`,
RunE: func(cmd *cobra.Command, args []string) error {
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
func init() {
vmCmd.AddCommand(vmRunCmd)
RunE: func(cmd *cobra.Command, args []string) error {
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
const permSize = 512 * 1024 * 1024
cmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
cmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
cmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
cmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
cmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
cmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type vmRunConfig struct {
@@ -64,18 +72,6 @@ func (r *vmRunConfig) effectiveGoarch() string {
var vmRunImpl vmRunConfig
func init() {
vmRunCmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
const permSize = 512 * 1024 * 1024
vmRunCmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
vmRunCmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
vmRunCmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
vmRunCmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
vmRunCmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
vmRunCmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
instanceflag.RegisterPflags(vmRunCmd.Flags())
}
func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileCfg *config.Struct) error {
if r.arch != "" {
@@ -133,7 +129,7 @@ func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileC
Output: &output,
}
pack.Main(ctx, "gokrazy gok")
pack.Main(ctx)
return nil
}

View File

@@ -93,7 +93,7 @@ func generateAndStoreSelfSignedCertificate(cfg *config.Struct, hostConfigPath, c
}
func getCertificate(cfg *config.Struct) (string, string, error) {
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Hostname)
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Update.UseTLS, cfg.Hostname)
if err != nil {
var nycerr *tlsflag.ErrNotYetCreated
if errors.As(err, &nycerr) {

56
internal/packer/eeprom.go Normal file
View File

@@ -0,0 +1,56 @@
package packer
import (
"io"
"os"
"strings"
"github.com/gokrazy/tools/internal/eeprom"
)
func applyExtraEEPROM(pieeprom string, extraEEPROM []string) ([]byte, error) {
upstream, err := os.Open(pieeprom)
if err != nil {
return nil, err
}
upstreamBin, err := io.ReadAll(upstream)
if err != nil {
return nil, err
}
sections, err := eeprom.Analyze(upstreamBin)
if err != nil {
return nil, err
}
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
bootconfTxt := bc.FileContent()
applied := overwriteBootconf(bootconfTxt, extraEEPROM)
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, applied)
return eeprom.Assemble(sections), nil
}
func overwriteBootconf(bootconfTxt []byte, extraEEPROM []string) []byte {
bootconfLines := strings.Split(strings.TrimSpace(string(bootconfTxt)), "\n")
// For each property, there will be an entry from property to full line
extraByProp := make(map[string]string)
for _, line := range extraEEPROM {
prop, _, ok := strings.Cut(line, "=")
if !ok {
continue
}
extraByProp[prop] = line
}
var out strings.Builder
for _, line := range bootconfLines {
prop, _, ok := strings.Cut(line, "=")
if !ok {
out.WriteString(line + "\n")
continue
}
if extra, ok := extraByProp[prop]; ok {
out.WriteString(extra + "\n")
} else {
out.WriteString(line + "\n")
}
}
return []byte(out.String())
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,398 @@
package packer
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime/trace"
"strings"
"time"
"github.com/gokrazy/internal/updateflag"
"github.com/gokrazy/tools/packer"
)
func (pack *Pack) logicBuild(bindir string) error {
log := pack.Env.Logger()
cfg := pack.Cfg // for convenience
args := cfg.Packages
log.Printf("Building %d Go packages:", len(args))
log.Printf("")
for _, pkg := range args {
log.Printf(" %s", pkg)
for _, configFile := range packageConfigFiles[pkg] {
log.Printf(" will %s",
configFile.kind)
if configFile.path != "" {
log.Printf(" from %s",
configFile.path)
}
if !configFile.lastModified.IsZero() {
log.Printf(" last modified: %s (%s ago)",
configFile.lastModified.Format(time.RFC3339),
time.Since(configFile.lastModified).Round(1*time.Second))
}
}
log.Printf("")
}
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
pkgs = append(pkgs, cfg.Packages...)
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
noBuildPkgs := []string{
cfg.KernelPackageOrDefault(),
}
if fw := cfg.FirmwarePackageOrDefault(); fw != "" {
noBuildPkgs = append(noBuildPkgs, fw)
}
if e := cfg.EEPROMPackageOrDefault(); e != "" {
noBuildPkgs = append(noBuildPkgs, e)
}
setUmask()
buildEnv := &packer.BuildEnv{
BuildDir: packer.BuildDirOrMigrate,
}
var buildErr error
trace.WithRegion(context.Background(), "build", func() {
buildErr = buildEnv.Build(bindir, pkgs, pack.packageBuildFlags, pack.packageBuildTags, pack.packageBuildEnv, noBuildPkgs, pack.basenames)
})
if buildErr != nil {
return buildErr
}
log.Printf("")
var err error
trace.WithRegion(context.Background(), "validate", func() {
err = pack.validateTargetArchMatchesKernel()
})
if err != nil {
return err
}
var (
root *FileInfo
foundBins []foundBin
)
trace.WithRegion(context.Background(), "findbins", func() {
root, foundBins, err = findBins(cfg, buildEnv, bindir, pack.basenames)
})
if err != nil {
return err
}
pack.root = root
packageConfigFiles = make(map[string][]packageConfigFile)
var extraFiles map[string][]*FileInfo
trace.WithRegion(context.Background(), "findextrafiles", func() {
extraFiles, err = FindExtraFiles(cfg)
})
if err != nil {
return err
}
for _, packageExtraFiles := range extraFiles {
for _, ef := range packageExtraFiles {
for _, de := range ef.Dirents {
if de.Filename != "perm" {
continue
}
return fmt.Errorf("invalid ExtraFilePaths or ExtraFileContents: cannot write extra files to user-controlled /perm partition")
}
}
}
if len(packageConfigFiles) > 0 {
log.Printf("Including extra files for Go packages:")
log.Printf("")
for _, pkg := range args {
if len(packageConfigFiles[pkg]) == 0 {
continue
}
log.Printf(" %s", pkg)
for _, configFile := range packageConfigFiles[pkg] {
log.Printf(" will %s",
configFile.kind)
log.Printf(" from %s",
configFile.path)
log.Printf(" last modified: %s (%s ago)",
configFile.lastModified.Format(time.RFC3339),
time.Since(configFile.lastModified).Round(1*time.Second))
}
log.Printf("")
}
}
if cfg.InternalCompatibilityFlags.InitPkg == "" {
gokrazyInit := &gokrazyInit{
root: root,
flagFileContents: pack.flagFileContents,
envFileContents: pack.envFileContents,
buildTimestamp: pack.buildTimestamp,
dontStart: pack.dontStart,
waitForClock: pack.waitForClock,
basenames: pack.basenames,
}
if cfg.InternalCompatibilityFlags.OverwriteInit != "" {
return gokrazyInit.dump(cfg.InternalCompatibilityFlags.OverwriteInit)
}
var tmpdir string
trace.WithRegion(context.Background(), "buildinit", func() {
tmpdir, err = gokrazyInit.build()
})
if err != nil {
return err
}
pack.initTmp = tmpdir
initPath := filepath.Join(tmpdir, "init")
fileIsELFOrFatal(initPath)
gokrazy := root.mustFindDirent("gokrazy")
gokrazy.Dirents = append(gokrazy.Dirents, &FileInfo{
Filename: "init",
FromHost: initPath,
})
}
defaultPassword, updateHostname := updateflag.Value{
Update: cfg.InternalCompatibilityFlags.Update,
}.GetUpdateTarget(cfg.Hostname)
update, err := cfg.Update.WithFallbackToHostSpecific(cfg.Hostname)
if err != nil {
return err
}
if update.HTTPPort == "" {
update.HTTPPort = "80"
}
if update.HTTPSPort == "" {
update.HTTPSPort = "443"
}
if update.Hostname == "" {
update.Hostname = updateHostname
}
if update.HTTPPassword == "" && !update.NoPassword {
pw, err := ensurePasswordFileExists(updateHostname, defaultPassword)
if err != nil {
return err
}
update.HTTPPassword = pw
}
pack.update = update
for _, dir := range []string{"bin", "dev", "etc", "proc", "sys", "tmp", "perm", "lib", "run", "mnt"} {
root.Dirents = append(root.Dirents, &FileInfo{
Filename: dir,
})
}
root.Dirents = append(root.Dirents, &FileInfo{
Filename: "var",
SymlinkDest: "/perm/var",
})
mnt := root.mustFindDirent("mnt")
for _, md := range cfg.MountDevices {
if !strings.HasPrefix(md.Target, "/mnt/") {
continue
}
rest := strings.TrimPrefix(md.Target, "/mnt/")
rest = strings.TrimSuffix(rest, "/")
if strings.Contains(rest, "/") {
continue
}
mnt.Dirents = append(mnt.Dirents, &FileInfo{
Filename: rest,
})
}
// include lib/modules from kernelPackage dir, if present
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
if err != nil {
return err
}
pack.kernelDir = kernelDir
modulesDir := filepath.Join(kernelDir, "lib", "modules")
if _, err := os.Stat(modulesDir); err == nil {
log.Printf("Including loadable kernel modules from:")
log.Printf(" %s", modulesDir)
modules := &FileInfo{
Filename: "modules",
}
trace.WithRegion(context.Background(), "kernelmod", func() {
_, err = addToFileInfo(modules, modulesDir)
})
if err != nil {
return err
}
lib := root.mustFindDirent("lib")
lib.Dirents = append(lib.Dirents, modules)
}
etc := root.mustFindDirent("etc")
tmpdir, err := os.MkdirTemp("", "gokrazy")
if err != nil {
return err
}
defer os.RemoveAll(tmpdir)
hostLocaltime, err := hostLocaltime(tmpdir)
if err != nil {
return err
}
if hostLocaltime != "" {
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "localtime",
FromHost: hostLocaltime,
})
}
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "resolv.conf",
SymlinkDest: "/tmp/resolv.conf",
})
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "hosts",
FromLiteral: `127.0.0.1 localhost
::1 localhost
`,
})
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "hostname",
FromLiteral: cfg.Hostname,
})
ssl := &FileInfo{Filename: "ssl"}
ssl.Dirents = append(ssl.Dirents, &FileInfo{
Filename: "ca-bundle.pem",
FromLiteral: pack.systemCertsPEM,
})
pack.schema = "http"
if update.CertPEM == "" || update.KeyPEM == "" {
deployCertFile, deployKeyFile, err := getCertificate(cfg)
if err != nil {
return err
}
if deployCertFile != "" {
b, err := os.ReadFile(deployCertFile)
if err != nil {
return err
}
update.CertPEM = strings.TrimSpace(string(b))
b, err = os.ReadFile(deployKeyFile)
if err != nil {
return err
}
update.KeyPEM = strings.TrimSpace(string(b))
}
}
if update.CertPEM != "" && update.KeyPEM != "" {
// User requested TLS
pack.schema = "https"
ssl.Dirents = append(ssl.Dirents, &FileInfo{
Filename: "gokrazy-web.pem",
FromLiteral: update.CertPEM,
})
ssl.Dirents = append(ssl.Dirents, &FileInfo{
Filename: "gokrazy-web.key.pem",
FromLiteral: update.KeyPEM,
})
}
etc.Dirents = append(etc.Dirents, ssl)
if !update.NoPassword {
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "gokr-pw.txt",
Mode: 0400,
FromLiteral: update.HTTPPassword,
})
}
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "http-port.txt",
FromLiteral: update.HTTPPort,
})
etc.Dirents = append(etc.Dirents, &FileInfo{
Filename: "https-port.txt",
FromLiteral: update.HTTPSPort,
})
// GenerateSBOM() must be provided with a cfg
// that hasn't been modified by gok at runtime,
// as the SBOM should reflect whats going into gokrazy,
// not its internal implementation details
// (i.e. cfg.InternalCompatibilityFlags untouched).
var sbom []byte
var sbomWithHash SBOMWithHash
trace.WithRegion(context.Background(), "sbom", func() {
sbom, sbomWithHash, err = generateSBOM(pack.FileCfg, foundBins)
})
if err != nil {
return err
}
pack.sbom = sbom
pack.sbomWithHash = sbomWithHash
etcGokrazy := &FileInfo{Filename: "gokrazy"}
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
Filename: "sbom.json",
FromLiteral: string(sbom),
})
mountdevices, err := json.Marshal(cfg.MountDevices)
if err != nil {
return err
}
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
Filename: "mountdevices.json",
FromLiteral: string(mountdevices),
})
etc.Dirents = append(etc.Dirents, etcGokrazy)
empty := &FileInfo{Filename: ""}
if paths := getDuplication(root, empty); len(paths) > 0 {
return fmt.Errorf("root file system contains duplicate files: your config contains multiple packages that install %s", paths)
}
for pkg1, fs := range extraFiles {
for _, fs1 := range fs {
// check against root fs
if paths := getDuplication(root, fs1); len(paths) > 0 {
return fmt.Errorf("extra files of package %s collides with root file system: %v", pkg1, paths)
}
// check against other packages
for pkg2, fs := range extraFiles {
for _, fs2 := range fs {
if pkg1 == pkg2 {
continue
}
if paths := getDuplication(fs1, fs2); len(paths) > 0 {
return fmt.Errorf("extra files of package %s collides with package %s: %v", pkg1, pkg2, paths)
}
}
}
// add extra files to rootfs
if err := root.combine(fs1); err != nil {
return fmt.Errorf("failed to add extra files from package %s: %v", pkg1, err)
}
}
}
return nil
}

View File

@@ -0,0 +1,591 @@
package packer
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/deviceconfig"
"github.com/gokrazy/tools/internal/cap"
"github.com/gokrazy/tools/internal/version"
"github.com/gokrazy/tools/packer"
)
type filePathAndModTime struct {
path string
modTime time.Time
}
func findPackageFiles(fileType string) ([]filePathAndModTime, error) {
var packageFilePaths []filePathAndModTime
err := filepath.Walk(fileType, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info != nil && !info.Mode().IsRegular() {
return nil
}
if strings.HasSuffix(path, fmt.Sprintf("/%s.txt", fileType)) {
packageFilePaths = append(packageFilePaths, filePathAndModTime{
path: path,
modTime: info.ModTime(),
})
}
return nil
})
if err != nil {
if os.IsNotExist(err) {
return nil, nil // no fileType directory found
}
}
return packageFilePaths, nil
}
func (pack *Pack) logicPrepare(ctx context.Context) error {
log := pack.Env.Logger()
cfg := pack.Cfg
if cfg.InternalCompatibilityFlags.Update != "" &&
cfg.InternalCompatibilityFlags.Overwrite != "" {
return fmt.Errorf("both -update and -overwrite are specified; use either one, not both")
}
// Check early on if the destination is a device that is mounted
// so that the user does not get the impression that everything
// is fine and about to complete after a lengthy build phase.
// See also https://github.com/gokrazy/gokrazy/discussions/308
switch {
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
target := cfg.InternalCompatibilityFlags.Overwrite
st, err := os.Stat(target)
if err != nil && !os.IsNotExist(err) {
return err
}
if err == nil && st.Mode()&os.ModeDevice == os.ModeDevice {
if err := verifyNotMounted(target); err != nil {
return fmt.Errorf("cannot overwrite %s: %v (perhaps automatically?)\n please unmount all partitions and retry", target, err)
}
}
}
var mbrOnlyWithoutGpt bool
pack.firstPartitionOffsetSectors = deviceconfig.DefaultBootPartitionStartLBA
if cfg.DeviceType != "" {
if devcfg, ok := deviceconfig.GetDeviceConfigBySlug(cfg.DeviceType); ok {
pack.rootDeviceFiles = devcfg.RootDeviceFiles
mbrOnlyWithoutGpt = devcfg.MBROnlyWithoutGPT
if devcfg.BootPartitionStartLBA != 0 {
pack.firstPartitionOffsetSectors = devcfg.BootPartitionStartLBA
}
} else {
return fmt.Errorf("unknown device slug %q", cfg.DeviceType)
}
}
pack.Pack = packer.NewPackForHost(pack.firstPartitionOffsetSectors, cfg.Hostname)
newInstallation := cfg.InternalCompatibilityFlags.Update == ""
useGPT := newInstallation && !mbrOnlyWithoutGpt
pack.Pack.UsePartuuid = newInstallation
pack.Pack.UseGPTPartuuid = useGPT
pack.Pack.UseGPT = useGPT
if os.Getenv("GOKR_PACKER_FD") != "" { // partitioning child process
if _, err := pack.SudoPartition(cfg.InternalCompatibilityFlags.Overwrite); err != nil {
log.Printf("%s", err)
os.Exit(1)
}
os.Exit(0)
}
log.Printf("%s %s on GOARCH=%s GOOS=%s",
programName,
version.ReadBrief(),
runtime.GOARCH,
runtime.GOOS)
log.Printf("")
if cfg.InternalCompatibilityFlags.Update != "" {
// TODO: fix update URL:
log.Printf("Updating gokrazy installation on http://%s", cfg.Hostname)
log.Printf("")
}
log.Printf("Build target: %s", strings.Join(filterGoEnv(packer.Env()), " "))
pack.buildTimestamp = time.Now().Format(time.RFC3339)
if ts, ok := ctx.Value(BuildTimestampOverride).(string); ok {
pack.buildTimestamp = ts
}
log.Printf("Build timestamp: %s", pack.buildTimestamp)
systemCertsPEM, err := pack.findSystemCertsPEM()
if err != nil {
return err
}
pack.systemCertsPEM = systemCertsPEM
packageBuildFlags, err := pack.findBuildFlagsFiles(cfg)
if err != nil {
return err
}
pack.packageBuildFlags = packageBuildFlags
packageBuildTags, err := pack.findBuildTagsFiles(cfg)
if err != nil {
return err
}
pack.packageBuildTags = packageBuildTags
packageBuildEnv, err := findBuildEnv(cfg)
if err != nil {
return err
}
pack.packageBuildEnv = packageBuildEnv
flagFileContents, err := pack.findFlagFiles(cfg)
if err != nil {
return err
}
pack.flagFileContents = flagFileContents
envFileContents, err := pack.findEnvFiles(cfg)
if err != nil {
return err
}
pack.envFileContents = envFileContents
dontStart, err := pack.findDontStart(cfg)
if err != nil {
return err
}
pack.dontStart = dontStart
waitForClock, err := pack.findWaitForClock(cfg)
if err != nil {
return err
}
pack.waitForClock = waitForClock
basenames, err := findBasenames(cfg)
if err != nil {
return err
}
pack.basenames = basenames
capabilities, err := findCapabilities(cfg)
if err != nil {
return err
}
pack.xattrs = capabilities
return nil
}
func (pack *Pack) findFlagFiles(cfg *config.Struct) (map[string][]string, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string][]string)
for pkg, packageConfig := range cfg.PackageConfig {
if len(packageConfig.CommandLineFlags) == 0 {
continue
}
contents[pkg] = packageConfig.CommandLineFlags
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be started with command-line flags",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
flagFilePaths, err := findPackageFiles("flags")
if err != nil {
return nil, err
}
if len(flagFilePaths) == 0 {
return nil, nil // no flags.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string][]string)
for _, p := range flagFilePaths {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "flags/"), "/flags.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: flag file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be started with command-line flags",
path: p.path,
lastModified: p.modTime,
})
b, err := os.ReadFile(p.path)
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
contents[pkg] = lines
}
return contents, nil
}
func (pack *Pack) findBuildFlagsFiles(cfg *config.Struct) (map[string][]string, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string][]string)
for pkg, packageConfig := range cfg.PackageConfig {
if len(packageConfig.GoBuildFlags) == 0 {
continue
}
contents[pkg] = packageConfig.GoBuildFlags
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be compiled with build flags",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
buildFlagsFilePaths, err := findPackageFiles("buildflags")
if err != nil {
return nil, err
}
if len(buildFlagsFilePaths) == 0 {
return nil, nil // no flags.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string][]string)
for _, p := range buildFlagsFilePaths {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildflags/"), "/buildflags.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: buildflags file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be compiled with build flags",
path: p.path,
lastModified: p.modTime,
})
b, err := os.ReadFile(p.path)
if err != nil {
return nil, err
}
var buildFlags []string
sc := bufio.NewScanner(strings.NewReader(string(b)))
for sc.Scan() {
if flag := sc.Text(); flag != "" {
buildFlags = append(buildFlags, flag)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
// use full package path opposed to flags
contents[pkg] = buildFlags
}
return contents, nil
}
func findBuildEnv(cfg *config.Struct) (map[string][]string, error) {
contents := make(map[string][]string)
for pkg, packageConfig := range cfg.PackageConfig {
if len(packageConfig.GoBuildEnvironment) == 0 {
continue
}
contents[pkg] = packageConfig.GoBuildEnvironment
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be compiled with build environment variables",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
func (pack *Pack) findBuildTagsFiles(cfg *config.Struct) (map[string][]string, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string][]string)
for pkg, packageConfig := range cfg.PackageConfig {
if len(packageConfig.GoBuildTags) == 0 {
continue
}
contents[pkg] = packageConfig.GoBuildTags
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be compiled with build tags",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
buildTagsFiles, err := findPackageFiles("buildtags")
if err != nil {
return nil, err
}
if len(buildTagsFiles) == 0 {
return nil, nil // no flags.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string][]string)
for _, p := range buildTagsFiles {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildtags/"), "/buildtags.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: buildtags file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be compiled with build tags",
path: p.path,
lastModified: p.modTime,
})
b, err := os.ReadFile(p.path)
if err != nil {
return nil, err
}
var buildTags []string
sc := bufio.NewScanner(strings.NewReader(string(b)))
for sc.Scan() {
if flag := sc.Text(); flag != "" {
buildTags = append(buildTags, flag)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
// use full package path opposed to flags
contents[pkg] = buildTags
}
return contents, nil
}
func (pack *Pack) findEnvFiles(cfg *config.Struct) (map[string][]string, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string][]string)
for pkg, packageConfig := range cfg.PackageConfig {
if len(packageConfig.Environment) == 0 {
continue
}
contents[pkg] = packageConfig.Environment
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be started with environment variables",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
buildFlagsFilePaths, err := findPackageFiles("env")
if err != nil {
return nil, err
}
if len(buildFlagsFilePaths) == 0 {
return nil, nil // no flags.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string][]string)
for _, p := range buildFlagsFilePaths {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "env/"), "/env.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: environment variable file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be started with environment variables",
path: p.path,
lastModified: p.modTime,
})
b, err := os.ReadFile(p.path)
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
contents[pkg] = lines
}
return contents, nil
}
func (pack *Pack) findDontStart(cfg *config.Struct) (map[string]bool, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string]bool)
for pkg, packageConfig := range cfg.PackageConfig {
if !packageConfig.DontStart {
continue
}
contents[pkg] = packageConfig.DontStart
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "not be started at boot",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
dontStartPaths, err := findPackageFiles("dontstart")
if err != nil {
return nil, err
}
if len(dontStartPaths) == 0 {
return nil, nil // no dontstart.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string]bool)
for _, p := range dontStartPaths {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "dontstart/"), "/dontstart.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: dontstart.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "not be started at boot",
path: p.path,
lastModified: p.modTime,
})
contents[pkg] = true
}
return contents, nil
}
func (pack *Pack) findWaitForClock(cfg *config.Struct) (map[string]bool, error) {
log := pack.Env.Logger()
if len(cfg.PackageConfig) > 0 {
contents := make(map[string]bool)
for pkg, packageConfig := range cfg.PackageConfig {
if !packageConfig.WaitForClock {
continue
}
contents[pkg] = packageConfig.WaitForClock
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "wait for clock synchronization before start",
path: cfg.Meta.Path,
lastModified: cfg.Meta.LastModified,
})
}
return contents, nil
}
waitForClockPaths, err := findPackageFiles("waitforclock")
if err != nil {
return nil, err
}
if len(waitForClockPaths) == 0 {
return nil, nil // no waitforclock.txt files found
}
buildPackages := buildPackageMapFromFlags(cfg)
contents := make(map[string]bool)
for _, p := range waitForClockPaths {
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "waitforclock/"), "/waitforclock.txt")
if !buildPackages[pkg] {
log.Printf("WARNING: waitforclock.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
continue
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "wait for clock synchronization before start",
path: p.path,
lastModified: p.modTime,
})
contents[pkg] = true
}
return contents, nil
}
func findBasenames(cfg *config.Struct) (map[string]string, error) {
contents := make(map[string]string)
for pkg, packageConfig := range cfg.PackageConfig {
if packageConfig.Basename == "" {
continue
}
contents[pkg] = packageConfig.Basename
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be installed with the basename set to " + packageConfig.Basename,
})
}
return contents, nil
}
func findCapabilities(cfg *config.Struct) (map[string]map[string][]byte, error) {
contents := make(map[string]map[string][]byte)
for pkg, packageConfig := range cfg.PackageConfig {
if packageConfig.Capabilities == "" {
continue
}
set, err := cap.FromText(packageConfig.Capabilities)
if err != nil {
return nil, fmt.Errorf("Unable to parse capabilities: %s: %w", packageConfig.Capabilities, err)
}
xattrValue, err := set.PackFileCap()
if err != nil {
return nil, fmt.Errorf("Unable to pack capabilities: %s: %w", packageConfig.Capabilities, err) // This should basically never happen
}
set, err = cap.DigestFileCap(xattrValue)
if err != nil {
return nil, fmt.Errorf("Error checking packed capabilities: %s: %w", packageConfig.Capabilities, err) // This should also never happen
}
contents[pkg] = map[string][]byte{
"security.capability": xattrValue,
}
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
kind: "be installed with file capabilities " + set.String(),
})
}
return contents, nil
}

View File

@@ -0,0 +1,253 @@
package packer
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"syscall"
"time"
"github.com/gokrazy/internal/httpclient"
"github.com/gokrazy/internal/humanize"
"github.com/gokrazy/internal/progress"
"github.com/gokrazy/internal/updateflag"
"github.com/gokrazy/updater"
)
func (pack *Pack) logicUpdate(ctx context.Context, isDev bool, bootSize int64, rootSize int64, tmpMBR, tmpBoot, tmpRoot *os.File, updateBaseUrl *url.URL, target *updater.Target, updateHttpClient *http.Client) error {
log := pack.Env.Logger()
cfg := pack.Cfg // for convenience
update := pack.update // for convenience
var rootReader, bootReader, mbrReader io.Reader
// Determine where to read the boot, root and MBR images from.
switch {
case cfg.InternalCompatibilityFlags.Overwrite != "":
if isDev {
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "1")
if err != nil {
return err
}
bootReader = bootFile
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "2")
if err != nil {
return err
}
rootReader = rootFile
} else {
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
if err != nil {
return err
}
if _, err := bootFile.Seek(pack.firstPartitionOffsetSectors*512, io.SeekStart); err != nil {
return err
}
bootReader = &io.LimitedReader{
R: bootFile,
N: bootSize,
}
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
if err != nil {
return err
}
if _, err := rootFile.Seek(pack.firstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
return err
}
rootReader = &io.LimitedReader{
R: rootFile,
N: rootSize,
}
}
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
if err != nil {
return err
}
mbrReader = &io.LimitedReader{
R: mbrFile,
N: 446,
}
default:
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteBoot)
if err != nil {
return err
}
bootReader = bootFile
if cfg.InternalCompatibilityFlags.OverwriteMBR != "" {
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteMBR)
if err != nil {
return err
}
mbrReader = mbrFile
} else {
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
return err
}
mbrReader = tmpMBR
}
}
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteRoot)
if err != nil {
return err
}
rootReader = rootFile
}
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
if _, err := tmpBoot.Seek(0, io.SeekStart); err != nil {
return err
}
bootReader = tmpBoot
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
return err
}
mbrReader = tmpMBR
if _, err := tmpRoot.Seek(0, io.SeekStart); err != nil {
return err
}
rootReader = tmpRoot
}
}
updateBaseUrl.Path = "/"
log.Printf("Updating %s", updateBaseUrl.String())
progctx, canc := context.WithCancel(context.Background())
defer canc()
prog := &progress.Reporter{}
go prog.Report(progctx)
// Start with the root file system because writing to the non-active
// partition cannot break the currently running system.
if err := pack.updateWithProgress(prog, rootReader, target, "root file system", "root"); err != nil {
return err
}
for _, rootDeviceFile := range pack.rootDeviceFiles {
f, err := os.Open(filepath.Join(pack.kernelDir, rootDeviceFile.Name))
if err != nil {
return err
}
if err := pack.updateWithProgress(
prog, f, target, fmt.Sprintf("root device file %s", rootDeviceFile.Name),
filepath.Join("device-specific", rootDeviceFile.Name),
); err != nil {
if errors.Is(err, updater.ErrUpdateHandlerNotImplemented) {
log.Printf("target does not support updating device file %s yet, ignoring", rootDeviceFile.Name)
continue
}
return err
}
}
if err := pack.updateWithProgress(prog, bootReader, target, "boot file system", "boot"); err != nil {
return err
}
if err := target.StreamTo(ctx, "mbr", mbrReader); err != nil {
if err == updater.ErrUpdateHandlerNotImplemented {
log.Printf("target does not support updating MBR yet, ignoring")
} else {
return fmt.Errorf("updating MBR: %v", err)
}
}
if cfg.InternalCompatibilityFlags.Testboot {
if err := target.Testboot(ctx); err != nil {
return fmt.Errorf("enable testboot of non-active partition: %v", err)
}
} else {
if err := target.Switch(ctx); err != nil {
return fmt.Errorf("switching to non-active partition: %v", err)
}
}
// Stop progress reporting to not mess up the following logs output.
canc()
log.Printf("Triggering reboot")
if err := target.Reboot(ctx); err != nil {
if errors.Is(err, syscall.ECONNRESET) {
log.Printf("ignoring reboot error: %v", err)
} else {
return fmt.Errorf("reboot: %v", err)
}
}
const polltimeout = 5 * time.Minute
log.Printf("Updated, waiting %v for the device to become reachable (cancel with Ctrl-C any time)", polltimeout)
if update.CertPEM != "" && update.KeyPEM != "" {
// Use an HTTPS client (post-update),
// even when the --insecure flag was specified.
pack.schema = "https"
var err error
updateBaseUrl, err = updateflag.Value{
Update: "yes",
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
if err != nil {
return err
}
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, false /* insecure */, updateBaseUrl)
if err != nil {
return fmt.Errorf("getting http client by tls flag: %v", err)
}
}
pollctx, canc := context.WithTimeout(context.Background(), polltimeout)
defer canc()
for {
if err := pollctx.Err(); err != nil {
return fmt.Errorf("device did not become healthy after update (%v)", err)
}
if err := pollUpdated1(pollctx, updateHttpClient, updateBaseUrl.String(), pack.buildTimestamp); err != nil {
log.Printf("device not yet reachable: %v", err)
time.Sleep(1 * time.Second)
continue
}
log.Printf("Device ready to use!")
break
}
return nil
}
func (pack *Pack) updateWithProgress(prog *progress.Reporter, reader io.Reader, target *updater.Target, logStr string, stream string) error {
ctx := context.Background()
log := pack.Env.Logger()
start := time.Now()
prog.SetStatus(fmt.Sprintf("update %s", logStr))
prog.SetTotal(0)
if stater, ok := reader.(interface{ Stat() (os.FileInfo, error) }); ok {
if st, err := stater.Stat(); err == nil {
prog.SetTotal(uint64(st.Size()))
}
}
if err := target.StreamTo(ctx, stream, io.TeeReader(reader, &progress.Writer{})); err != nil {
return fmt.Errorf("updating %s: %w", logStr, err)
}
duration := time.Since(start)
transferred := progress.Reset()
log.Printf("\rTransferred %s (%s) at %.2f MiB/s (total: %v)",
logStr,
humanize.Bytes(transferred),
float64(transferred)/duration.Seconds()/1024/1024,
duration.Round(time.Second))
return nil
}

View File

@@ -0,0 +1,433 @@
package packer
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime/trace"
"strings"
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/deviceconfig"
"github.com/gokrazy/internal/httpclient"
"github.com/gokrazy/internal/updateflag"
"github.com/gokrazy/tools/packer"
"github.com/gokrazy/updater"
)
func (pack *Pack) logicWrite(dnsCheck chan error) error {
ctx := context.Background()
log := pack.Env.Logger()
var (
updateHttpClient *http.Client
updateBaseUrl *url.URL
target *updater.Target
)
newInstallation := pack.Cfg.InternalCompatibilityFlags.Update == ""
insecure := pack.Cfg.InternalCompatibilityFlags.Insecure
if !newInstallation {
update := pack.update // for convenience
var err error
updateBaseUrl, err = updateflag.Value{
Update: pack.Cfg.InternalCompatibilityFlags.Update,
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
if err != nil {
return err
}
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
if err != nil {
return fmt.Errorf("getting http client by tls flag: %v", err)
}
updateBaseUrl.Path = "/"
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
if err != nil {
if !insecure {
return fmt.Errorf("checking target partuuid support: %v", err)
}
log.Printf("Falling back to HTTP because of the --insecure flag")
updateBaseUrl, err = updateflag.Value{
Update: pack.Cfg.InternalCompatibilityFlags.Update,
}.BaseURL(update.HTTPPort, update.HTTPSPort, "http", update.Hostname, update.HTTPPassword)
if err != nil {
return err
}
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
if err != nil {
return fmt.Errorf("getting http client by tls flag: %v", err)
}
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
if err != nil {
return fmt.Errorf("checking target partuuid support: %v", err)
}
}
pack.UsePartuuid = target.Supports("partuuid")
pack.UseGPTPartuuid = target.Supports("gpt")
pack.UseGPT = target.Supports("gpt")
pack.ExistingEEPROM = target.InstalledEEPROM()
}
log.Printf("")
log.Printf("Feature summary:")
log.Printf(" use GPT: %v", pack.UseGPT)
log.Printf(" use PARTUUID: %v", pack.UsePartuuid)
log.Printf(" use GPT PARTUUID: %v", pack.UseGPTPartuuid)
cfg := pack.Cfg // for convenience
root := pack.root // for convenience
// Determine where to write the boot and root images to.
var (
isDev bool
tmpBoot, tmpRoot, tmpMBR *os.File
bootSize, rootSize int64
)
switch {
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
st, err := os.Stat(cfg.InternalCompatibilityFlags.Overwrite)
if err != nil && !os.IsNotExist(err) {
return err
}
isDev = err == nil && st.Mode()&os.ModeDevice == os.ModeDevice
if isDev {
if err := pack.overwriteDevice(cfg.InternalCompatibilityFlags.Overwrite, root, pack.rootDeviceFiles); err != nil {
return err
}
log.Printf("To boot gokrazy, plug the SD card into a supported device (see https://gokrazy.org/platforms/)")
log.Printf("")
} else {
lower := 1200*MB + int(pack.firstPartitionOffsetSectors)
if cfg.InternalCompatibilityFlags.TargetStorageBytes == 0 {
return fmt.Errorf("--target_storage_bytes is required (e.g. --target_storage_bytes=%d) when using overwrite with a file", lower)
}
if cfg.InternalCompatibilityFlags.TargetStorageBytes%512 != 0 {
return fmt.Errorf("--target_storage_bytes must be a multiple of 512 (sector size), use e.g. %d", lower)
}
if cfg.InternalCompatibilityFlags.TargetStorageBytes < lower {
return fmt.Errorf("--target_storage_bytes must be at least %d (for boot + 2 root file systems + 100 MB /perm)", lower)
}
bootSize, rootSize, err = pack.overwriteFile(root, pack.rootDeviceFiles, pack.firstPartitionOffsetSectors)
if err != nil {
return err
}
log.Printf("To boot gokrazy, copy %s to an SD card and plug it into a supported device (see https://gokrazy.org/platforms/)", cfg.InternalCompatibilityFlags.Overwrite)
log.Printf("")
}
case pack.Output != nil && pack.Output.Type == OutputTypeGaf && pack.Output.Path != "":
if err := pack.overwriteGaf(root, pack.sbom); err != nil {
return err
}
default:
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
mbrfn := cfg.InternalCompatibilityFlags.OverwriteMBR
if cfg.InternalCompatibilityFlags.OverwriteMBR == "" {
var err error
tmpMBR, err = os.CreateTemp("", "gokrazy")
if err != nil {
return err
}
defer os.Remove(tmpMBR.Name())
mbrfn = tmpMBR.Name()
}
if err := pack.writeBootFile(cfg.InternalCompatibilityFlags.OverwriteBoot, mbrfn); err != nil {
return err
}
}
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
var rootErr error
trace.WithRegion(context.Background(), "writeroot", func() {
rootErr = pack.writeRootFile(cfg.InternalCompatibilityFlags.OverwriteRoot, root)
})
if rootErr != nil {
return rootErr
}
}
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
var err error
tmpMBR, err = os.CreateTemp("", "gokrazy")
if err != nil {
return err
}
defer os.Remove(tmpMBR.Name())
tmpBoot, err = os.CreateTemp("", "gokrazy")
if err != nil {
return err
}
defer os.Remove(tmpBoot.Name())
if err := pack.writeBoot(tmpBoot, tmpMBR.Name()); err != nil {
return err
}
tmpRoot, err = os.CreateTemp("", "gokrazy")
if err != nil {
return err
}
defer os.Remove(tmpRoot.Name())
if err := pack.writeRoot(tmpRoot, root); err != nil {
return err
}
}
}
log.Printf("")
log.Printf("Build complete!")
if err := pack.printHowToInteract(cfg); err != nil {
return err
}
if err := <-dnsCheck; err != nil {
log.Printf("WARNING: if the above URL does not work, perhaps name resolution (DNS) is broken")
log.Printf("in your local network? Resolving your hostname failed: %v", err)
log.Printf("Did you maybe configure a DNS server other than your router?")
log.Printf("")
}
if newInstallation {
return nil
}
return pack.logicUpdate(ctx, isDev, bootSize, rootSize, tmpMBR, tmpBoot, tmpRoot, updateBaseUrl, target, updateHttpClient)
}
func (p *Pack) overwriteDevice(dev string, root *FileInfo, rootDeviceFiles []deviceconfig.RootFile) error {
log := p.Env.Logger()
if err := verifyNotMounted(dev); err != nil {
return err
}
parttable := "GPT + Hybrid MBR"
if !p.UseGPT {
parttable = "no GPT, only MBR"
}
log.Printf("partitioning %s (%s)", dev, parttable)
f, err := p.partition(p.Cfg.InternalCompatibilityFlags.Overwrite)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
return err
}
if err := p.writeBoot(f, ""); err != nil {
return err
}
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
return err
}
if _, err := f.Seek((p.FirstPartitionOffsetSectors+(100*MB/512))*512, io.SeekStart); err != nil {
return err
}
tmp, err := os.CreateTemp("", "gokr-packer")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
defer tmp.Close()
if err := p.writeRoot(tmp, root); err != nil {
return err
}
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := io.Copy(f, tmp); err != nil {
return err
}
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
log.Printf("If your applications need to store persistent data, unplug and re-plug the SD card, then create a file system using e.g.:")
log.Printf("")
partition := partitionPath(dev, "4")
if p.ModifyCmdlineRoot() {
partition = fmt.Sprintf("/dev/disk/by-partuuid/%s", p.PermUUID())
} else {
if target, err := filepath.EvalSymlinks(dev); err == nil {
partition = partitionPath(target, "4")
}
}
log.Printf("\tmkfs.ext4 %s", partition)
log.Printf("")
return nil
}
func partitionPath(base, num string) string {
if strings.HasPrefix(base, "/dev/mmcblk") ||
strings.HasPrefix(base, "/dev/loop") {
return base + "p" + num
} else if strings.HasPrefix(base, "/dev/disk") ||
strings.HasPrefix(base, "/dev/rdisk") {
return base + "s" + num
}
return base + num
}
type offsetReadSeeker struct {
io.ReadSeeker
offset int64
}
func (ors *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
if whence == io.SeekStart {
// github.com/gokrazy/internal/fat.Reader only uses io.SeekStart
return ors.ReadSeeker.Seek(offset+ors.offset, io.SeekStart)
}
return ors.ReadSeeker.Seek(offset, whence)
}
type countingWriter int64
func (cw *countingWriter) Write(p []byte) (n int, err error) {
*cw += countingWriter(len(p))
return len(p), nil
}
func (p *Pack) overwriteFile(root *FileInfo, rootDeviceFiles []deviceconfig.RootFile, firstPartitionOffsetSectors int64) (bootSize int64, rootSize int64, err error) {
log := p.Env.Logger()
f, err := os.Create(p.Cfg.InternalCompatibilityFlags.Overwrite)
if err != nil {
return 0, 0, err
}
if err := f.Truncate(int64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
return 0, 0, err
}
if err := p.Partition(f, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
return 0, 0, err
}
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
return 0, 0, err
}
var bs countingWriter
if err := p.writeBoot(io.MultiWriter(f, &bs), ""); err != nil {
return 0, 0, err
}
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
return 0, 0, err
}
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
return 0, 0, err
}
tmp, err := os.CreateTemp("", "gokr-packer")
if err != nil {
return 0, 0, err
}
defer os.Remove(tmp.Name())
defer tmp.Close()
if err := p.writeRoot(tmp, root); err != nil {
return 0, 0, err
}
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
return 0, 0, err
}
var rs countingWriter
if _, err := io.Copy(io.MultiWriter(f, &rs), tmp); err != nil {
return 0, 0, err
}
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
return 0, 0, err
}
log.Printf("If your applications need to store persistent data, create a file system using e.g.:")
log.Printf("\t/sbin/mkfs.ext4 -F -E offset=%v %s %v", p.FirstPartitionOffsetSectors*512+1100*MB, p.Cfg.InternalCompatibilityFlags.Overwrite, packer.PermSizeInKB(firstPartitionOffsetSectors, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)))
log.Printf("")
return int64(bs), int64(rs), f.Close()
}
func (pack *Pack) printHowToInteract(cfg *config.Struct) error {
log := pack.Env.Logger()
update := pack.update // for convenience
updateFlag := pack.Cfg.InternalCompatibilityFlags.Update
if updateFlag == "" {
updateFlag = "yes"
}
updateBaseUrl, err := updateflag.Value{
Update: updateFlag,
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
if err != nil {
return err
}
log.Printf("")
log.Printf("To interact with the device, gokrazy provides a web interface reachable at:")
log.Printf("")
log.Printf("\t%s", updateBaseUrl.String())
log.Printf("")
log.Printf("In addition, the following Linux consoles are set up:")
log.Printf("")
if cfg.SerialConsoleOrDefault() != "disabled" {
log.Printf("\t1. foreground Linux console on the serial port (115200n8, pin 6, 8, 10 for GND, TX, RX), accepting input")
log.Printf("\t2. secondary Linux framebuffer console on HDMI; shows Linux kernel message but no init system messages")
} else {
log.Printf("\t1. foreground Linux framebuffer console on HDMI")
}
if cfg.SerialConsoleOrDefault() != "disabled" {
log.Printf("")
log.Printf("Use -serial_console=disabled to make gokrazy not touch the serial port, and instead make the framebuffer console on HDMI the foreground console")
}
log.Printf("")
if pack.schema == "https" {
certObj, err := getCertificateFromString(update.CertPEM)
if err != nil {
return fmt.Errorf("error loading certificate: %v", err)
} else {
log.Printf("")
log.Printf("The TLS Certificate of the gokrazy web interface is located under")
log.Printf("\t%s", cfg.Meta.Path)
log.Printf("The fingerprint of the Certificate is")
log.Printf("\t%x", getCertificateFingerprintSHA1(certObj))
log.Printf("The certificate is valid until")
log.Printf("\t%s", certObj.NotAfter.String())
log.Printf("Please verify the certificate, before adding an exception to your browser!")
}
}
return nil
}

View File

@@ -0,0 +1,82 @@
package packer
import (
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"github.com/gokrazy/tools/packer"
)
// kernelGoarch returns the GOARCH value that corresponds to the provided
// vmlinuz header. It returns one of "arm", "arm64", "386", "amd64" or the empty
// string if not detected.
func kernelGoarch(hdr []byte) string {
// Some constants from the file(1) command's magic.
const (
// 32-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L238-L241
arm32Magic = 0x016f2818
arm32MagicOffset = 0x24
// 64-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L253-L254
arm64Magic = 0x644d5241
arm64MagicOffset = 0x38
// x86: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L137-L152
x86Magic = 0xaa55
x86MagicOffset = 0x1fe
x86XloadflagsOffset = 0x236
)
if len(hdr) >= arm64MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm64MagicOffset:]) == arm64Magic {
return "arm64"
}
if len(hdr) >= arm32MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm32MagicOffset:]) == arm32Magic {
return "arm"
}
if len(hdr) >= x86XloadflagsOffset+2 && binary.LittleEndian.Uint16(hdr[x86MagicOffset:]) == x86Magic {
// XLF0 in arch/x86/boot/header.S
if hdr[x86XloadflagsOffset]&1 != 0 {
return "amd64"
} else {
return "386"
}
}
return ""
}
// validateTargetArchMatchesKernel validates that the packer.TargetArch
// corresponds to the kernel's architecture.
//
// See https://github.com/gokrazy/gokrazy/issues/191 for background. Maybe the
// TargetArch will become automatic in the future but for now this is a safety
// net to prevent people from bricking their appliances with the wrong userspace
// architecture.
func (pack *Pack) validateTargetArchMatchesKernel() error {
cfg := pack.Cfg
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
if err != nil {
return err
}
kernelPath := filepath.Join(kernelDir, "vmlinuz")
k, err := os.Open(kernelPath)
if err != nil {
return err
}
defer k.Close()
hdr := make([]byte, 1<<10) // plenty
if _, err := io.ReadFull(k, hdr); err != nil {
return err
}
kernelArch := kernelGoarch(hdr)
if kernelArch == "" {
return fmt.Errorf("kernel %v architecture in %s not detected", cfg.KernelPackageOrDefault(), kernelPath)
}
targetArch := packer.TargetArch()
if kernelArch != targetArch {
return fmt.Errorf("target architecture %q (GOARCH) doesn't match the %s kernel type %q",
targetArch,
cfg.KernelPackageOrDefault(),
kernelArch)
}
return nil
}

View File

@@ -48,7 +48,7 @@ func copyFile(fw *fat.Writer, dest string, src fs.File, srcName string) error {
return src.Close()
}
func copyFileSquash(d *squashfs.Directory, dest, src string) error {
func copyFileSquash(d *squashfs.Directory, dest, src string, xattrs map[string][]byte) error {
f, err := os.Open(src)
if err != nil {
return err
@@ -58,7 +58,7 @@ func copyFileSquash(d *squashfs.Directory, dest, src string) error {
if err != nil {
return err
}
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm)
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm, xattrs)
if err != nil {
return err
}
@@ -202,6 +202,18 @@ func (p *Pack) copyGlobsToBoot(fw *fat.Writer, srcDir string, globs []string) er
return nil
}
func (p *Pack) writeBootFile(bootfilename, mbrfilename string) error {
f, err := os.Create(bootfilename)
if err != nil {
return err
}
defer f.Close()
if err := p.writeBoot(f, mbrfilename); err != nil {
return err
}
return f.Close()
}
func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
log := p.Env.Logger()
log.Printf("")
@@ -254,39 +266,25 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
}
}
// EEPROM update procedure. See also:
// https://news.ycombinator.com/item?id=21674550
writeEepromUpdateFile := func(globPattern, target string) (sig string, _ error) {
matches, err := filepath.Glob(globPattern)
if err != nil {
return "", err
}
if len(matches) == 0 {
return "", fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(globPattern))
}
// matches must be non-empty
bestMatch := func(matches []string) string {
// Select the EEPROM file that sorts last.
// This corresponds to most recent for the pieeprom-*.bin files,
// which contain the date in yyyy-mm-dd format.
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
f, err := os.Open(matches[0])
if err != nil {
return "", err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return "", err
}
return matches[0]
}
// EEPROM update procedure. See also:
// https://news.ycombinator.com/item?id=21674550
writeEepromUpdate := func(target string, modTime time.Time, r io.Reader) (sig string, _ error) {
// Copy the EEPROM file into the image and calculate its SHA256 hash
// while doing so:
w, err := fw.File(target, st.ModTime())
w, err := fw.File(target, modTime)
if err != nil {
return "", err
}
h := sha256.New()
if _, err := io.Copy(w, io.TeeReader(f, h)); err != nil {
if _, err := io.Copy(w, io.TeeReader(r, h)); err != nil {
return "", err
}
@@ -301,33 +299,81 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
sigFn := target
ext := filepath.Ext(sigFn)
if ext == "" {
return "", fmt.Errorf("BUG: cannot derive signature file name from matches[0]=%q", matches[0])
return "", fmt.Errorf("BUG: cannot derive signature file name from target=%q", target)
}
sigFn = strings.TrimSuffix(sigFn, ext) + ".sig"
w, err = fw.File(sigFn, st.ModTime())
w, err = fw.File(sigFn, modTime)
if err != nil {
return "", err
}
_, err = fmt.Fprintf(w, "%x\n", h.Sum(nil))
if err != nil {
return "", err
}
_, err = fmt.Fprintf(w, "ts: %d\n", modTime.Unix())
return fmt.Sprintf("%x", h.Sum(nil)), err
}
writeEepromUpdateFile := func(matches []string, target string) (sig string, _ error) {
f, err := os.Open(bestMatch(matches))
if err != nil {
return "", err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return "", err
}
return writeEepromUpdate(target, st.ModTime(), f)
}
var pieSig string
if eepromDir != "" {
log.Printf("EEPROM directory: %s", eepromDir)
log.Printf("(gokrazy config mtime: %v)", p.Cfg.Meta.LastModified)
log.Printf("EEPROM update summary:")
pieSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "pieeprom-*.bin"), "/pieeprom.upd")
eepromGlob := filepath.Join(eepromDir, "pieeprom-*.bin")
eepromMatches, err := filepath.Glob(eepromGlob)
if err != nil {
return err
}
vlSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "vl805-*.bin"), "/vl805.bin")
if len(eepromMatches) == 0 {
return fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(eepromGlob))
}
if ee := p.Cfg.BootloaderExtraEEPROM; len(ee) > 0 {
updated, err := applyExtraEEPROM(bestMatch(eepromMatches), ee)
if err != nil {
return err
}
pieSig, err = writeEepromUpdate("/pieeprom.upd", p.Cfg.Meta.LastModified, bytes.NewReader(updated))
if err != nil {
return err
}
} else {
pieSig, err = writeEepromUpdateFile(eepromMatches, "/pieeprom.upd")
if err != nil {
return err
}
}
vl805Glob := filepath.Join(eepromDir, "vl805-*.bin")
vl805Matches, err := filepath.Glob(vl805Glob)
if err != nil {
return err
}
var vlSig string
if len(vl805Matches) > 0 {
vlSig, err = writeEepromUpdateFile(vl805Matches, "/vl805.bin")
if err != nil {
return err
}
}
targetFilename := "/recovery.bin"
if pieSig == p.ExistingEEPROM.PieepromSHA256 &&
vlSig == p.ExistingEEPROM.VL805SHA256 {
log.Printf(" installing recovery.bin as RECOVERY.000 (EEPROM already up-to-date)")
targetFilename = "/RECOVERY.000"
}
if _, err := writeEepromUpdateFile(filepath.Join(eepromDir, "recovery.bin"), targetFilename); err != nil {
if _, err := writeEepromUpdateFile([]string{filepath.Join(eepromDir, "recovery.bin")}, targetFilename); err != nil {
return err
}
}
@@ -397,6 +443,7 @@ type FileInfo struct {
FromHost string
FromLiteral string
SymlinkDest string
Xattrs map[string][]byte
Dirents []*FileInfo
}
@@ -459,12 +506,79 @@ func (fi *FileInfo) mustFindDirent(path string) *FileInfo {
return nil
}
func addToFileInfo(parent *FileInfo, path string) (time.Time, error) {
entries, err := os.ReadDir(path)
if err != nil {
if os.IsNotExist(err) {
return time.Time{}, nil
}
return time.Time{}, err
}
var latestTime time.Time
for _, entry := range entries {
filename := entry.Name()
// get existing file info
var fi *FileInfo
for _, ent := range parent.Dirents {
if ent.Filename == filename {
fi = ent
break
}
}
info, err := entry.Info()
if err != nil {
return time.Time{}, err
}
if info.Mode()&os.ModeSymlink != 0 {
info, err = os.Stat(filepath.Join(path, filename))
if err != nil {
return time.Time{}, err
}
}
if latestTime.Before(info.ModTime()) {
latestTime = info.ModTime()
}
// or create if not exist
if fi == nil {
fi = &FileInfo{
Filename: filename,
Mode: info.Mode(),
}
parent.Dirents = append(parent.Dirents, fi)
} else {
// file overwrite is not supported -> return error
if !info.IsDir() || fi.FromHost != "" || fi.FromLiteral != "" {
return time.Time{}, fmt.Errorf("file already exists in filesystem: %s", filepath.Join(path, filename))
}
}
// add content
if info.IsDir() {
modTime, err := addToFileInfo(fi, filepath.Join(path, filename))
if err != nil {
return time.Time{}, err
}
if latestTime.Before(modTime) {
latestTime = modTime
}
} else {
fi.FromHost = filepath.Join(path, filename)
}
}
return latestTime, nil
}
type foundBin struct {
gokrazyPath string
hostPath string
}
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string) (*FileInfo, []foundBin, error) {
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string, xattrs map[string]map[string][]byte) (*FileInfo, []foundBin, error) {
var found []foundBin
result := FileInfo{Filename: ""}
@@ -519,15 +633,20 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
}
user := FileInfo{Filename: "user"}
for _, pkg := range mainPkgs {
xattr := make(map[string][]byte)
basename := pkg.Basename()
if basenameOverride, ok := basenames[pkg.ImportPath]; ok {
basename = basenameOverride
}
if xattrsOverride, ok := xattrs[pkg.ImportPath]; ok {
xattr = xattrsOverride
}
binPath := filepath.Join(bindir, basename)
fileIsELFOrFatal(binPath)
user.Dirents = append(user.Dirents, &FileInfo{
Filename: basename,
FromHost: binPath,
Xattrs: xattr,
})
found = append(found, foundBin{
gokrazyPath: "/user/" + basename,
@@ -540,14 +659,14 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
if fi.FromHost != "" { // copy a regular file
return copyFileSquash(dir, fi.Filename, fi.FromHost)
return copyFileSquash(dir, fi.Filename, fi.FromHost, fi.Xattrs)
}
if fi.FromLiteral != "" { // write a regular file
mode := fi.Mode
if mode == 0 {
mode = 0444
}
w, err := dir.File(fi.Filename, time.Now(), mode)
w, err := dir.File(fi.Filename, time.Now(), mode, fi.Xattrs)
if err != nil {
return err
}
@@ -578,6 +697,18 @@ func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
return d.Flush()
}
func (p *Pack) writeRootFile(filename string, root *FileInfo) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if err := p.writeRoot(f, root); err != nil {
return err
}
return f.Close()
}
func (p *Pack) writeRoot(f io.WriteSeeker, root *FileInfo) error {
log := p.Env.Logger()