5 Commits

Author SHA1 Message Date
1555419549 files: default mode 2023-11-27 16:08:09 +01:00
3f2cd997a0 better build 2023-11-27 14:40:15 +01:00
86d85f014c fix write files and deprecated calls 2023-11-27 14:08:44 +01:00
69cc01db9b move to clean crypt handling 2023-02-02 00:28:36 +01:00
3c7d56ae48 bootv2: bootstrap, vpn 2022-04-04 10:29:28 +02:00
24 changed files with 459 additions and 346 deletions

View File

@ -1 +1,3 @@
Dockerfile
tmp/**/*
dist/*

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
from golang:1.21.4-alpine3.18 as build
workdir /src
copy go.mod go.sum .
run go mod download
copy . .
run go test ./...
run go build -o /go/bin/init -trimpath .
# ------------------------------------------------------------------------
from alpine:3.18.4
workdir /layer
run wget -O- https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.4-x86_64.tar.gz |tar zxv
run apk add --no-cache -p . musl lvm2 lvm2-dmeventd udev cryptsetup e2fsprogs btrfs-progs lsblk
run rm -rf usr/share/apk var/cache/apk
copy --from=build /go/bin/init .
# check viability
run chroot /layer /init hello
entrypoint ["sh","-c","find |cpio -H newc -o |base64"]

View File

@ -1,12 +0,0 @@
# ------------------------------------------------------------------------
from alpine:3.15
add alpine-minirootfs-3.15.0-x86_64.tar.gz /layer/
workdir /layer
run apk update
run apk add -p . musl lvm2 lvm2-dmeventd udev cryptsetup e2fsprogs btrfs-progs
run rm -rf usr/share/apk var/cache/apk
entrypoint ["sh","-c","find |cpio -H newc -o |base64"]

View File

@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
)
@ -38,6 +39,8 @@ func askSecret(prompt string) []byte {
fatalf("failed to read from stdin: %v", err)
}
fmt.Println()
s = bytes.TrimRight(s, "\r\n")
return s
}

View File

@ -6,7 +6,8 @@ import (
"log"
"golang.org/x/crypto/ssh"
"novit.nc/direktil/initrd/config"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
var (

View File

@ -2,7 +2,6 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
@ -11,7 +10,7 @@ import (
"time"
yaml "gopkg.in/yaml.v2"
"novit.nc/direktil/pkg/sysfs"
"novit.tech/direktil/pkg/sysfs"
)
var loopOffset = 0
@ -58,7 +57,7 @@ func bootV1() {
}
func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) {
cfgBytes, err := ioutil.ReadFile(cfgPath)
cfgBytes, err := os.ReadFile(cfgPath)
if err != nil {
fatalf("failed to read %s: %v", cfgPath, err)
}
@ -134,7 +133,7 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) {
// - write configuration
log.Print("writing /config.yaml")
if err := ioutil.WriteFile("/system/config.yaml", cfgBytes, 0600); err != nil {
if err := os.WriteFile("/system/config.yaml", cfgBytes, 0600); err != nil {
fatal("failed: ", err)
}
@ -144,7 +143,19 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) {
filePath := filepath.Join("/system", fileDef.Path)
ioutil.WriteFile(filePath, []byte(fileDef.Content), fileDef.Mode)
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
log.Printf("failed to create dir %s: %v", filepath.Dir(fileDef.Path), err)
}
mode := fileDef.Mode
if mode == 0 {
mode = 0644
}
err = os.WriteFile(filePath, []byte(fileDef.Content), mode)
if err != nil {
fatalf("failed to write %s: %v", fileDef.Path, err)
}
}
return

View File

@ -7,7 +7,7 @@ import (
"gopkg.in/yaml.v3"
"novit.nc/direktil/initrd/config"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
func bootV2() {

View File

@ -2,13 +2,15 @@ package main
import (
"bytes"
"io/ioutil"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"novit.nc/direktil/initrd/config"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
func bootstrap(cfg *config.Config) {
@ -34,14 +36,58 @@ func bootstrap(cfg *config.Config) {
log.Printf("seeding bootstrap from %s", seed)
// TODO
err = os.MkdirAll(baseDir, 0700)
if err != nil {
fatalf("failed to create bootstrap dir: %v", err)
}
bootstrapFile := filepath.Join(baseDir, "bootstrap.tar")
err = func() (err error) {
resp, err := http.Get(seed)
if err != nil {
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("bad HTTP status: %s", resp.Status)
return
}
defer resp.Body.Close()
out, err := os.Create(bootstrapFile)
if err != nil {
return
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return
}()
if err != nil {
fatalf("seeding failed: %v", err)
}
log.Print("unpacking bootstrap file")
run("tar", "xvf", bootstrapFile, "-C", baseDir)
}
layersDir = baseDir
layersOverride["modules"] = "/modules.sqfs"
sysCfg := applyConfig(sysCfgPath, false)
// mounts are v2 only
localGenDir := filepath.Join(bsDir, "local-gen")
// vpns are v2+
for _, vpn := range sysCfg.VPNs {
setupVPN(vpn, localGenDir)
}
// mounts are v2+
for _, mount := range sysCfg.Mounts {
log.Print("mount ", mount.Dev, " to system's ", mount.Path)
@ -74,7 +120,7 @@ func bootstrap(cfg *config.Config) {
func setUserPass(user, passwordHash string) {
const fpath = "/system/etc/shadow"
ba, err := ioutil.ReadFile(fpath)
ba, err := os.ReadFile(fpath)
if err != nil {
fatalf("failed to read shadow: %v", err)
}
@ -97,7 +143,7 @@ func setUserPass(user, passwordHash string) {
buf.WriteByte('\n')
}
err = ioutil.WriteFile(fpath, buf.Bytes(), 0600)
err = os.WriteFile(fpath, buf.Bytes(), 0600)
if err != nil {
fatalf("failed to write shadow: %v", err)
}
@ -116,7 +162,7 @@ func setAuthorizedKeys(ak []string) {
fatalf("failed to create %s: %v", sshDir, err)
}
err = ioutil.WriteFile(filepath.Join(sshDir, "authorized_keys"), buf.Bytes(), 0600)
err = os.WriteFile(filepath.Join(sshDir, "authorized_keys"), buf.Bytes(), 0600)
if err != nil {
fatalf("failed to write authorized keys: %v", err)
}

View File

@ -1,26 +1,7 @@
package main
import (
nconfig "novit.nc/direktil/pkg/config"
nconfig "novit.tech/direktil/pkg/config"
)
type configV1 struct {
Layers []string `yaml:"layers"`
Files []nconfig.FileDef `yaml:"files"`
// v2 handles more
RootUser struct {
PasswordHash string `yaml:"password_hash"`
AuthorizedKeys []string `yaml:"authorized_keys"`
} `yaml:"root_user"`
Mounts []MountDef `yaml:"mounts"`
}
type MountDef struct {
Dev string
Path string
Options string
Type string
}
type configV1 = nconfig.Config

View File

@ -1,61 +0,0 @@
package config
type Config struct {
AntiPhishingCode string `json:"anti_phishing_code"`
Keymap string
Modules string
Auths []Auth
Networks []struct {
Name string
Interfaces []struct {
Var string
N int
Regexps []string
}
Script string
}
LVM []LvmVG
Bootstrap Bootstrap
}
type Auth struct {
Name string
SSHKey string `yaml:"sshKey"`
Password string `yaml:"password"`
}
type LvmVG struct {
VG string
PVs struct {
N int
Regexps []string
}
Defaults struct {
FS string
Raid *RaidConfig
}
LVs []struct {
Name string
Crypt string
FS string
Raid *RaidConfig
Size string
Extents string
}
}
type RaidConfig struct {
Mirrors int
Stripes int
}
type Bootstrap struct {
Dev string
Seed string
}

View File

@ -1,46 +0,0 @@
package config
import (
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"strings"
"golang.org/x/crypto/pbkdf2"
)
var (
encoding = base64.RawStdEncoding
)
func PasswordHashFromSeed(seed, pass []byte) string {
h := pbkdf2.Key(pass, seed, 2048, 32, sha512.New)
return encoding.EncodeToString(h)
}
func PasswordHash(pass []byte) (hashedPassWithSeed string) {
seed := make([]byte, 10) // 8 bytes min by the RFC recommendation
_, err := rand.Read(seed)
if err != nil {
panic(err) // we do not expect this to fail...
}
return JoinSeedAndHash(seed, PasswordHashFromSeed(seed, pass))
}
func JoinSeedAndHash(seed []byte, hash string) string {
return encoding.EncodeToString(seed) + ":" + hash
}
func CheckPassword(hashedPassWithSeed string, pass []byte) (ok bool) {
parts := strings.SplitN(hashedPassWithSeed, ":", 2)
encodedSeed := parts[0]
encodedHash := parts[1]
seed, err := encoding.DecodeString(encodedSeed)
if err != nil {
return false
}
return encodedHash == PasswordHashFromSeed(seed, pass)
}

View File

@ -1,12 +0,0 @@
package config
import "fmt"
func ExamplePasswordHash() {
seed := []byte("myseed")
hash := PasswordHashFromSeed(seed, []byte("mypass"))
fmt.Println(JoinSeedAndHash(seed, hash))
// Output:
// bXlzZWVk:HMSxrg1cYphaPuUYUbtbl/htep/tVYYIQAuvkNMVpw0
}

View File

@ -1,95 +0,0 @@
package cpiocat
import (
"io"
"os"
"github.com/cavaliergopher/cpio"
)
func Append(out io.Writer, in io.Reader, filesToAppend []string) (err error) {
cout := cpio.NewWriter(out)
cin := cpio.NewReader(in)
for {
var hdr *cpio.Header
hdr, err = cin.Next()
if err != nil {
if err == io.EOF {
break
}
return
}
mode := hdr.FileInfo().Mode()
if mode&os.ModeSymlink != 0 {
// symlink target must be written after
hdr.Size = int64(len(hdr.Linkname))
}
err = cout.WriteHeader(hdr)
if err != nil {
return
}
if mode.IsRegular() {
_, err = io.Copy(cout, cin)
} else if mode&os.ModeSymlink != 0 {
_, err = cout.Write([]byte(hdr.Linkname))
}
if err != nil {
return
}
}
for _, file := range filesToAppend {
err = func() (err error) {
stat, err := os.Lstat(file)
if err != nil {
return
}
link := ""
if stat.Mode()&os.ModeSymlink != 0 {
link, err = os.Readlink(file)
if err != nil {
return
}
}
hdr, err := cpio.FileInfoHeader(stat, link)
if err != nil {
return
}
hdr.Name = file
cout.WriteHeader(hdr)
if stat.Mode().IsRegular() {
var f *os.File
f, err = os.Open(file)
if err != nil {
return
}
defer f.Close()
_, err = io.Copy(cout, f)
} else if stat.Mode()&os.ModeSymlink != 0 {
_, err = cout.Write([]byte(link))
}
return
}()
if err != nil {
return
}
}
err = cout.Close()
return
}

27
go.mod
View File

@ -1,17 +1,28 @@
module novit.nc/direktil/initrd
require (
github.com/cavaliergopher/cpio v1.0.1
github.com/kr/pty v1.1.8
github.com/pkg/term v1.1.0
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/sys v0.0.0-20220209214540-3681064d5158
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/crypto v0.5.0
golang.org/x/sys v0.4.0
golang.org/x/term v0.4.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
novit.nc/direktil/pkg v0.0.0-20191211161950-96b0448b84c2
gopkg.in/yaml.v3 v3.0.1
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e
)
require github.com/creack/pty v1.1.17 // indirect
require (
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/genetlink v1.3.1 // indirect
github.com/mdlayher/netlink v1.7.1 // indirect
github.com/mdlayher/socket v0.4.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
)
go 1.18
go 1.21

52
go.sum
View File

@ -1,8 +1,12 @@
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -10,23 +14,47 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mdlayher/genetlink v1.3.1 h1:roBiPnual+eqtRkKX2Jb8UQN5ZPWnhDCGj/wR6Jlz2w=
github.com/mdlayher/genetlink v1.3.1/go.mod h1:uaIPxkWmGk753VVIzDtROxQ8+T+dkHqOI0vB1NA9S/Q=
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c h1:Okh6a1xpnJslG9Mn84pId1Mn+Q8cvpo4HCeeFWHo0cA=
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
novit.nc/direktil/pkg v0.0.0-20191211161950-96b0448b84c2 h1:LN3K19gAJ1GamJXkzXAQmjbl8xCV7utqdxTTrM89MMc=
novit.nc/direktil/pkg v0.0.0-20191211161950-96b0448b84c2/go.mod h1:zwTVO6U0tXFEaga73megQIBK7yVIKZJVePaIh/UtdfU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
novit.nc/direktil/pkg v0.0.0-20220221171542-fd3ce3a1491b/go.mod h1:zwTVO6U0tXFEaga73megQIBK7yVIKZJVePaIh/UtdfU=
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e h1:eQFbzcuB4wOSrnOhkcN30hFDCIack40VkIoqVRbWnWc=
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e/go.mod h1:2Mir5x1eT/e295WeFGzzXa4siunKX4z+rmNPfVsXS0k=

130
lvm.go
View File

@ -7,18 +7,32 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"novit.nc/direktil/initrd/config"
"novit.nc/direktil/initrd/lvm"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
func sortedKeys[T any](m map[string]T) (keys []string) {
keys = make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return
}
func setupLVM(cfg *config.Config) {
if len(cfg.LVM) == 0 {
log.Print("no LVM VG configured.")
return
}
// [dev] = filesystem
// eg: [/dev/sda1] = ext4
createdDevs := map[string]string{}
run("pvscan")
run("vgscan", "--mknodes")
@ -27,17 +41,21 @@ func setupLVM(cfg *config.Config) {
}
for _, vg := range cfg.LVM {
setupLVs(vg)
setupLVs(vg, createdDevs)
}
run("vgchange", "--sysinit", "-a", "ly")
for _, vg := range cfg.LVM {
setupCrypt(vg)
}
setupCrypt(cfg.Crypt, createdDevs)
for _, vg := range cfg.LVM {
setupFS(vg)
devs := make([]string, 0, len(createdDevs))
for k := range createdDevs {
devs = append(devs, k)
}
sort.Strings(devs)
for _, dev := range devs {
setupFS(dev, createdDevs[dev])
}
}
@ -113,7 +131,7 @@ func setupVG(vg config.LvmVG) {
}
}
func setupLVs(vg config.LvmVG) {
func setupLVs(vg config.LvmVG, createdDevs map[string]string) {
lvsRep := lvm.LVSReport{}
err := runJSON(&lvsRep, "lvs", "--reportformat", "json")
if err != nil {
@ -171,6 +189,12 @@ func setupLVs(vg config.LvmVG) {
dev := "/dev/" + vg.VG + "/" + lv.Name
zeroDevStart(dev)
fs := lv.FS
if fs == "" {
fs = vg.Defaults.FS
}
createdDevs[dev] = fs
}
}
@ -188,21 +212,52 @@ func zeroDevStart(dev string) {
}
}
func setupCrypt(vg config.LvmVG) {
cryptDevs := map[string]bool{}
func setupCrypt(devSpecs []config.CryptDev, createdDevs map[string]string) {
var password []byte
passwordVerified := false
for _, lv := range vg.LVs {
if lv.Crypt == "" {
// flat, expanded devices to open
devNames := make([]config.CryptDev, 0, len(devSpecs))
for _, devSpec := range devSpecs {
if devSpec.Dev == "" && devSpec.Prefix == "" {
fatalf("crypt: name %q: no dev or match set", devSpec.Name)
}
if devSpec.Dev != "" && devSpec.Prefix != "" {
fatalf("crypt: name %q: both dev (%q) and match (%q) are set", devSpec.Name, devSpec.Dev, devSpec.Prefix)
}
if devSpec.Dev != "" {
// already flat
devNames = append(devNames, devSpec)
continue
}
if cryptDevs[lv.Crypt] {
fatalf("duplicate crypt device name: %s", lv.Crypt)
matches, err := filepath.Glob(devSpec.Prefix + "*")
if err != nil {
fatalf("failed to search for device matches: %v", err)
}
cryptDevs[lv.Crypt] = true
for _, m := range matches {
suffix := m[len(devSpec.Prefix):]
devNames = append(devNames, config.CryptDev{Dev: m, Name: devSpec.Name + suffix})
}
}
cryptDevs := map[string]bool{}
for _, devName := range devNames {
name, dev := devName.Name, devName.Dev
if name == "" {
name = filepath.Base(dev)
}
if cryptDevs[name] {
fatalf("duplicate crypt device name: %s", name)
}
cryptDevs[name] = true
retryOpen:
if len(password) == 0 {
@ -213,7 +268,10 @@ func setupCrypt(vg config.LvmVG) {
}
}
dev := "/dev/" + vg.VG + "/" + lv.Name
fs := createdDevs[dev]
delete(createdDevs, dev)
tgtDev := "/dev/mapper/" + name
needFormat := !devInitialized(dev)
if needFormat {
@ -242,10 +300,20 @@ func setupCrypt(vg config.LvmVG) {
if err != nil {
fatalf("failed luksFormat: %v", err)
}
createdDevs[tgtDev] = fs
}
log.Print("openning encrypted device ", lv.Crypt, " from ", dev)
cmd := exec.Command("cryptsetup", "open", dev, lv.Crypt, "--key-file=-")
if len(password) == 0 {
password = askSecret("crypt password")
if len(password) == 0 {
fatalf("empty password given")
}
}
log.Print("openning encrypted device ", name, " from ", dev)
cmd := exec.Command("cryptsetup", "open", dev, name, "--key-file=-")
cmd.Stdin = bytes.NewBuffer(password)
cmd.Stdout = stdout
cmd.Stderr = stderr
@ -261,7 +329,7 @@ func setupCrypt(vg config.LvmVG) {
}
if needFormat {
zeroDevStart("/dev/mapper/" + lv.Crypt)
zeroDevStart(tgtDev)
}
passwordVerified = true
@ -294,33 +362,25 @@ func devInitialized(dev string) bool {
return false
}
func setupFS(vg config.LvmVG) {
for _, lv := range vg.LVs {
dev := "/dev/" + vg.VG + "/" + lv.Name
if lv.Crypt != "" {
dev = "/dev/mapper/" + lv.Crypt
}
func setupFS(dev, fs string) {
if devInitialized(dev) {
log.Print("device ", dev, " already formatted")
continue
return
}
if lv.FS == "" {
lv.FS = vg.Defaults.FS
if fs == "" {
fs = "ext4"
}
log.Print("formatting ", dev, " (", lv.FS, ")")
log.Print("formatting ", dev, " (", fs, ")")
args := make([]string, 0)
switch lv.FS {
switch fs {
case "btrfs":
args = append(args, "-f")
case "ext4":
args = append(args, "-F")
}
run("mkfs."+lv.FS, append(args, dev)...)
}
run("mkfs."+fs, append(args, dev)...)
}

20
main.go
View File

@ -41,8 +41,26 @@ func newPipe() (io.ReadCloser, io.WriteCloser) {
}
func main() {
switch baseName := filepath.Base(os.Args[0]); baseName {
case "init":
runInit()
default:
log.Fatal("unknown sub-command: ", baseName)
}
}
func runInit() {
if len(os.Args) > 1 && os.Args[1] == "hello" {
fmt.Println("hello world!")
os.Exit(0)
}
runtime.LockOSThread()
if pid := os.Getpid(); pid != 1 {
log.Fatal("init must be PID 1, not ", pid)
}
// move log to shio
go io.Copy(os.Stdout, stdout.NewReader())
log.SetOutput(stderr)
@ -134,8 +152,10 @@ mainLoop:
switch b[0] {
case 'o':
run("sync")
syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF)
case 'r':
run("sync")
syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART)
case 's':
for _, sh := range []string{"bash", "ash", "sh", "busybox"} {

View File

@ -6,7 +6,7 @@ import (
"os/exec"
"strings"
"novit.nc/direktil/initrd/config"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
func setupNetworks(cfg *config.Config) {

View File

@ -1,12 +1,12 @@
package main
import (
"io/ioutil"
"os"
"strings"
)
func param(name, defaultValue string) (value string) {
ba, err := ioutil.ReadFile("/proc/cmdline")
ba, err := os.ReadFile("/proc/cmdline")
if err != nil {
fatal("could not read /proc/cmdline: ", err)
}

31
ssh.go
View File

@ -4,7 +4,6 @@ import (
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
@ -16,7 +15,7 @@ import (
"github.com/kr/pty"
"golang.org/x/crypto/ssh"
"novit.nc/direktil/initrd/config"
config "novit.tech/direktil/pkg/bootstrapconfig"
)
func startSSH(cfg *config.Config) {
@ -24,7 +23,7 @@ func startSSH(cfg *config.Config) {
PublicKeyCallback: sshCheckPubkey,
}
pkBytes, err := ioutil.ReadFile("/id_rsa") // TODO configurable
pkBytes, err := os.ReadFile("/id_rsa") // TODO configurable
if err != nil {
fatalf("ssh: failed to load private key: %v", err)
}
@ -118,7 +117,7 @@ func sshHandleChannel(remoteAddr string, channel ssh.Channel, requests <-chan *s
}()
var once sync.Once
close := func() {
closeCh := func() {
channel.Close()
}
@ -130,11 +129,27 @@ func sshHandleChannel(remoteAddr string, channel ssh.Channel, requests <-chan *s
case "init":
go func() {
io.Copy(channel, stdout.NewReader())
once.Do(close)
once.Do(closeCh)
}()
go func() {
io.Copy(stdinPipe, channel)
once.Do(close)
once.Do(closeCh)
}()
req.Reply(true, nil)
case "bootstrap":
// extract a new bootstrap package
os.MkdirAll("/bootstrap/current", 0750)
cmd := exec.Command("/bin/tar", "xv", "-C", "/bootstrap/current")
cmd.Stdin = channel
cmd.Stdout = channel
cmd.Stderr = channel.Stderr()
go func() {
cmd.Run()
closeCh()
}()
req.Reply(true, nil)
@ -167,11 +182,11 @@ func sshHandleChannel(remoteAddr string, channel ssh.Channel, requests <-chan *s
go func() {
io.Copy(channel, ptyF)
once.Do(close)
once.Do(closeCh)
}()
go func() {
io.Copy(ptyF, channel)
once.Do(close)
once.Do(closeCh)
}()
req.Reply(true, nil)

View File

@ -26,8 +26,9 @@ networks:
- eno.*
- enp.*
script: |
ip a add 2001:41d0:306:168f::1337:2eed/64 dev $iface
ip li set $iface up
udhcpc $iface
#udhcpc $iface
lvm:
- vg: storage
@ -52,18 +53,28 @@ lvm:
lvs:
- name: bootstrap
crypt: bootstrap
size: 2g
- name: varlog
crypt: varlog
extents: 10%FREE
# size: 10g
- name: podman
extents: 10%FREE
# size: 10g
- name: dls
crypt: dls
extents: 100%FREE
# size: 10g
crypt:
- dev: /dev/storage/bootstrap
- dev: /dev/storage/dls
bootstrap:
dev: /dev/mapper/bootstrap
#seed: https://direktil.novit.io/bootstraps/dls
# TODO seed: https://direktil.novit.io/bootstraps/dls-crypt
seed: http://192.168.10.254:7606/hosts/m1/bootstrap.tar
# TODO seed_sign_key: "..."
# TODO load_and_close: true

View File

@ -5,7 +5,7 @@ import (
"log"
"os"
"novit.nc/direktil/initrd/cpiocat"
"novit.tech/direktil/pkg/cpiocat"
)
func main() {

125
vpn.go Normal file
View File

@ -0,0 +1,125 @@
package main
import (
"log"
"net"
"os"
"path/filepath"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"novit.tech/direktil/pkg/config"
)
func setupVPN(vpn config.VPNDef, localGenDir string) {
log.Printf("setting up VPN %s", vpn.Name)
vpnDir := filepath.Join(localGenDir, vpn.Name)
os.MkdirAll(vpnDir, 0750)
// public/private key
keyFile := filepath.Join(vpnDir, "key")
keyBytes, err := os.ReadFile(keyFile)
if os.IsNotExist(err) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
fatalf("failed to generate VPN key: %v", err)
}
keyBytes = []byte(key.String())
os.WriteFile(keyFile, keyBytes, 0600)
} else if err != nil {
fatalf("failed to read VPN key: %v", err)
}
key, err := wgtypes.ParseKey(string(keyBytes))
if err != nil {
fatalf("bad VPN key: %v", err)
}
log.Printf("VPN %s public key is %s", vpn.Name, key.PublicKey().String())
// pre-shared key
pskeyFile := filepath.Join(vpnDir, "pskey")
pskeyBytes, err := os.ReadFile(pskeyFile)
if os.IsNotExist(err) {
key, err := wgtypes.GenerateKey()
if err != nil {
fatalf("failed to generate VPN pre-shared key: %v", err)
}
pskeyBytes = []byte(key.String())
os.WriteFile(pskeyFile, pskeyBytes, 0600)
} else if err != nil {
fatalf("failed to read VPN pre-shared key: %v", err)
}
pskey, err := wgtypes.ParseKey(string(pskeyBytes))
if err != nil {
fatalf("bad VPN pre-shared key: %v", err)
}
log.Printf("VPN %s pre-shared key is %s", vpn.Name, key.String())
// setup interface
cfg := wgtypes.Config{
PrivateKey: &key,
ListenPort: vpn.ListenPort,
Peers: make([]wgtypes.PeerConfig, 0, len(vpn.Peers)),
}
for idx, vpnPeer := range vpn.Peers {
vpnPeer := vpnPeer
wgPeer := wgtypes.PeerConfig{
Endpoint: vpnPeer.Endpoint,
AllowedIPs: make([]net.IPNet, 0, len(vpnPeer.AllowedIPs)),
PersistentKeepaliveInterval: &vpnPeer.KeepAlive,
}
if vpnPeer.WithPreSharedKey {
wgPeer.PresharedKey = &pskey
}
pubkey, err := wgtypes.ParseKey(vpnPeer.PublicKey)
if err != nil {
fatalf("bad VPN peer[%d] public key: %v", idx, err)
}
wgPeer.PublicKey = pubkey
for _, ipnetStr := range vpnPeer.AllowedIPs {
_, ipnet, err := net.ParseCIDR(ipnetStr)
if err != nil {
fatalf("bad IP/net: %q: %v", ipnetStr, err)
}
wgPeer.AllowedIPs = append(wgPeer.AllowedIPs, *ipnet)
}
cfg.Peers = append(cfg.Peers, wgPeer)
}
wg, err := wgctrl.New()
if err != nil {
fatalf("failed to setup WireGuard client: %v", err)
}
defer wg.Close()
run("ip", "link", "add", vpn.Name, "type", "wireguard")
for _, ip := range vpn.IPs {
run("ip", "addr", "add", ip, "dev", vpn.Name)
}
err = wg.ConfigureDevice(vpn.Name, cfg)
if err != nil {
fatalf("failed to setup VPN %s: %v", vpn.Name, err)
}
run("ip", "link", "set", vpn.Name, "up")
}