baby
This commit is contained in:
commit
1a9b64d503
8 changed files with 1543 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
rootfs.ext4
|
||||||
|
rootfs.qcow2
|
||||||
|
fireactions
|
||||||
|
*.ext4
|
||||||
|
vmlinux.bin
|
117
agent/main.go
Normal file
117
agent/main.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sec, err := parseSecret()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
e.Use(auth{secret: sec}.middleware)
|
||||||
|
|
||||||
|
e.GET("/hello", hello)
|
||||||
|
e.POST("/run", run)
|
||||||
|
|
||||||
|
e.Logger.Fatal(e.Start(":8080"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hello(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "Hello, World!")
|
||||||
|
}
|
||||||
|
|
||||||
|
type runResp struct {
|
||||||
|
Stdout string
|
||||||
|
Stderr string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(c echo.Context) error {
|
||||||
|
f, err := os.CreateTemp("", "fireactions-agent-*")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(f, c.Request().Body); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = f.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Chmod(f.Name(), 0700); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(f.Name())
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err = cmd.Run()
|
||||||
|
errorr := ""
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
errorr = err.Error()
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, runResp{
|
||||||
|
Stdout: string(stdout.Bytes()),
|
||||||
|
Stderr: string(stderr.Bytes()),
|
||||||
|
Error: errorr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type auth struct {
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a auth) middleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
s := strings.Split(c.Request().Header.Get("Authorization"), " ")
|
||||||
|
if len(s) < 2 {
|
||||||
|
return c.String(http.StatusBadRequest, "fuck no")
|
||||||
|
}
|
||||||
|
sec := s[1]
|
||||||
|
if sec == a.secret {
|
||||||
|
return next(c)
|
||||||
|
} else {
|
||||||
|
return c.String(http.StatusUnauthorized, "wrong secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSecret() (string, error) {
|
||||||
|
byt, err := ioutil.ReadFile("/proc/cmdline")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
opts := strings.Split(string(byt), " ")
|
||||||
|
for _, opt := range opts {
|
||||||
|
s := strings.Split(opt, "=")
|
||||||
|
key := s[0]
|
||||||
|
val := s[1]
|
||||||
|
if key == "fireactions.secret" {
|
||||||
|
if len(val) < 5 {
|
||||||
|
return "", errors.New("secret too short")
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("no secret in cmdline")
|
||||||
|
}
|
54
build-rootfs.sh
Executable file
54
build-rootfs.sh
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
#!/bin/sh -xe
|
||||||
|
# https://github.com/firecracker-microvm/firecracker/blob/main/docs/rootfs-and-kernel-setup.md
|
||||||
|
|
||||||
|
dir="$(mktemp --tmpdir -d tmp.fireactions-rootfs.XXXXXXXXXX)"
|
||||||
|
podman run -it --rm -v "$dir:/rootfs:Z" docker.io/alpine:3.18 sh -c "
|
||||||
|
mkdir -p /rootfs/etc/apk
|
||||||
|
cp -r /etc/apk/keys /rootfs/etc/apk/
|
||||||
|
echo https://dl-cdn.alpinelinux.org/alpine/v3.18/main >> /rootfs/etc/apk/repositories
|
||||||
|
echo https://dl-cdn.alpinelinux.org/alpine/v3.18/community >> /rootfs/etc/apk/repositories
|
||||||
|
apk add --initdb --root /rootfs alpine-base dropbear util-linux dropbear-dbclient dhcpcd
|
||||||
|
"
|
||||||
|
|
||||||
|
mkdir -p "$dir"/usr/local/sbin
|
||||||
|
go build -tags=netgo -o "$dir"/usr/local/sbin/fireactions-agent ./agent
|
||||||
|
# https://github.com/OpenRC/openrc/blob/master/service-script-guide.md
|
||||||
|
echo "#!/sbin/openrc-run
|
||||||
|
pidfile=\"/run/\${RC_SVCNAME}.pid\"
|
||||||
|
command_background=true
|
||||||
|
command=/usr/local/sbin/fireactions-agent" > "$dir"/etc/init.d/fireactions-agent
|
||||||
|
chmod +x "$dir"/etc/init.d/fireactions-agent
|
||||||
|
ln -s /etc/init.d/fireactions-agent "$dir"/etc/runlevels/default/
|
||||||
|
|
||||||
|
ln -s /etc/init.d/devfs "$dir"/etc/runlevels/boot/
|
||||||
|
ln -s /etc/init.d/procfs "$dir"/etc/runlevels/boot/
|
||||||
|
ln -s /etc/init.d/sysfs "$dir"/etc/runlevels/boot/
|
||||||
|
ln -s /etc/init.d/networking "$dir"/etc/runlevels/default/
|
||||||
|
# ln -s /etc/init.d/dhcpcd "$dir"/etc/runlevels/default/
|
||||||
|
ln -s /etc/init.d/dropbear "$dir"/etc/runlevels/default/
|
||||||
|
|
||||||
|
echo ttyS0 > "$dir"/etc/securetty
|
||||||
|
ln -s agetty "$dir"/etc/init.d/agetty.ttyS0
|
||||||
|
ln -s /etc/init.d/agetty.ttyS0 "$dir"/etc/runlevels/default/
|
||||||
|
# ln -s /etc/init.d/local "$dir"/etc/runlevels/default/
|
||||||
|
|
||||||
|
mkdir -p "$dir"/etc/dropbear
|
||||||
|
for t in rsa dss ed25519 ecdsa; do
|
||||||
|
dropbearkey -t $t -f "$dir/etc/dropbear/dropbear_${t}_host_key"
|
||||||
|
done
|
||||||
|
|
||||||
|
sudo mkdir -p "$dir"/root/.ssh
|
||||||
|
echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEhrcTPMrhrOdwpgFkRFjTt8rdxt3gg16LJvBCZENRWa user@personal | sudo tee "$dir"/root/.ssh/authorized_keys
|
||||||
|
echo "auto lo
|
||||||
|
iface lo inet loopback" > "$dir"/etc/network/interfaces
|
||||||
|
|
||||||
|
rm rootfs.ext4
|
||||||
|
fallocate --length 1G rootfs.ext4
|
||||||
|
mkfs.ext4 rootfs.ext4
|
||||||
|
|
||||||
|
mkdir -p /tmp/rootfs
|
||||||
|
sudo mount rootfs.ext4 /tmp/rootfs
|
||||||
|
sudo cp -r "$dir"/* /tmp/rootfs/
|
||||||
|
sudo umount /tmp/rootfs
|
||||||
|
|
||||||
|
# sudo rm -rf "$dir"
|
18
fcnet.conflist
Normal file
18
fcnet.conflist
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "fcnet",
|
||||||
|
"cniVersion": "0.4.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"type": "ptp",
|
||||||
|
"ipMasq": true,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "192.168.127.0/24",
|
||||||
|
"resolvConf": "/etc/resolv.conf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tc-redirect-tap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
51
go.mod
Normal file
51
go.mod
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
module gitea.nulo.in/Nulo/fireactions
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
|
github.com/containerd/fifo v1.0.0 // indirect
|
||||||
|
github.com/containernetworking/cni v1.0.1 // indirect
|
||||||
|
github.com/containernetworking/plugins v1.0.1 // indirect
|
||||||
|
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/errors v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/loads v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/runtime v0.24.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/validate v0.22.0 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/jaevor/go-nanoid v1.3.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.8.3 // indirect
|
||||||
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
|
golang.org/x/net v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
|
golang.org/x/text v0.7.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
179
main.go
Normal file
179
main.go
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// https://stanislas.blog/2021/08/firecracker/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/firecracker-microvm/firecracker-go-sdk"
|
||||||
|
"github.com/firecracker-microvm/firecracker-go-sdk/client/models"
|
||||||
|
"github.com/jaevor/go-nanoid"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
|
||||||
|
e.GET("/", hello)
|
||||||
|
|
||||||
|
runVM()
|
||||||
|
|
||||||
|
// e.Logger.Fatal(e.Start(":8080"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hello(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "Hello, World!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVM() {
|
||||||
|
const socketPath = "/tmp/firecracker.sock"
|
||||||
|
|
||||||
|
nanid, err := nanoid.Standard(21)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
secret := nanid()
|
||||||
|
|
||||||
|
cfg := firecracker.Config{
|
||||||
|
SocketPath: socketPath,
|
||||||
|
KernelImagePath: "vmlinux.bin",
|
||||||
|
Drives: firecracker.NewDrivesBuilder("./rootfs.ext4").Build(),
|
||||||
|
NetworkInterfaces: []firecracker.NetworkInterface{{
|
||||||
|
CNIConfiguration: &firecracker.CNIConfiguration{
|
||||||
|
NetworkName: "fcnet",
|
||||||
|
IfName: "veth0-fire",
|
||||||
|
BinPath: []string{"/opt/cni/bin", "/usr/libexec/cni"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
MachineCfg: models.MachineConfiguration{
|
||||||
|
VcpuCount: firecracker.Int64(1),
|
||||||
|
MemSizeMib: firecracker.Int64(1024),
|
||||||
|
},
|
||||||
|
KernelArgs: "fireactions.secret=" + secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutPath := "/tmp/stdout.log"
|
||||||
|
stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create stdout file: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
stderrPath := "/tmp/stderr.log"
|
||||||
|
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create stderr file: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
// build our custom command that contains our two files to
|
||||||
|
// write to during process execution
|
||||||
|
cmd := firecracker.VMCommandBuilder{}.
|
||||||
|
WithBin("firecracker").
|
||||||
|
WithSocketPath(socketPath).
|
||||||
|
WithStdout(stdout).
|
||||||
|
WithStderr(stderr).
|
||||||
|
Build(ctx)
|
||||||
|
|
||||||
|
m, err := firecracker.NewMachine(ctx, cfg, firecracker.WithProcessRunner(cmd))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create new machine: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(cfg.SocketPath)
|
||||||
|
|
||||||
|
if err := m.Start(ctx); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to initialize machine: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := m.Cfg.NetworkInterfaces[0].StaticConfiguration.IPConfiguration.IPAddr.IP
|
||||||
|
log.Printf("IP: %s", ip.String())
|
||||||
|
|
||||||
|
defer m.StopVMM()
|
||||||
|
|
||||||
|
agent := agentConfig{ip: ip.String(), secret: secret}
|
||||||
|
|
||||||
|
if err := agent.waitForAgent(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := agent.run("#!/bin/sh\necho hola mundo"); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := agent.run("#!/bin/sh\nreboot"); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Wait(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentConfig struct {
|
||||||
|
ip string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) request() *http.Request {
|
||||||
|
req, err := http.NewRequest("GET", "http://"+a.ip+":8080/hello", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) run(script string) error {
|
||||||
|
req, err := http.NewRequest("POST", "http://"+a.ip+":8080/run", bytes.NewBuffer([]byte(script)))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println(res.Body)
|
||||||
|
byt, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println(string(byt))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) waitForAgent() error {
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: time.Millisecond * 50,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
log.Println("waiting for agent to come up...")
|
||||||
|
req := a.request()
|
||||||
|
req.Method = "GET"
|
||||||
|
req.URL.Path = "/hello"
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Println("Agent is failing", resp)
|
||||||
|
return errors.New("Agent is failing")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
6
readme.md
Normal file
6
readme.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
## setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo env GOBIN=/opt/cni/bin go install github.com/awslabs/tc-redirect-tap/cmd/tc-redirect-tap@latest
|
||||||
|
sudo dnf install containernetworking-plugins
|
||||||
|
```
|
Loading…
Reference in a new issue